mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 17:41:15 +00:00
feat: added fastify, add authentication with passport
This commit is contained in:
parent
7d3afa3dcd
commit
b5911c712c
32 changed files with 1743 additions and 158 deletions
|
@ -16,9 +16,9 @@ DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_HOST=127.0.0.1 # localhost (for docker use redis)
|
REDIS_HOST=127.0.0.1 # localhost (for docker use redis)
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=<placehoder>
|
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||||
|
|
||||||
# Express
|
# Fastify
|
||||||
SERVER_PORT=<placeholder>
|
SERVER_PORT=<placeholder>
|
||||||
CLIENT_URL=<placeholder>
|
CLIENT_URL=<placeholder>
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:<placeholder>/$
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
|
|
||||||
REDIS_PORT=<placeholder>
|
REDIS_PORT=<placeholder>
|
||||||
REDIS_PASSWORD=<placeholder>
|
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||||
|
|
||||||
# Express
|
# Fastify
|
||||||
SERVER_PORT=<placeholder>
|
SERVER_PORT=<placeholder>
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
|
|
1300
package-lock.json
generated
1300
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -28,11 +28,21 @@
|
||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/static": "^6.12.0",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/platform-fastify": "^10.3.1",
|
||||||
"@nestjs/swagger": "^7.2.0",
|
"@nestjs/swagger": "^7.2.0",
|
||||||
|
"@prisma/client": "^5.8.1",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"nestjs-zod": "^3.0.0",
|
"nestjs-zod": "^3.0.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
|
@ -41,9 +51,12 @@
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/passport-jwt": "^4.0.0",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
@ -52,6 +65,7 @@
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
|
"prisma": "^5.8.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
describe('AppController', () => {
|
|
||||||
let appController: AppController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [AppController],
|
|
||||||
providers: [AppService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('root', () => {
|
|
||||||
it('should return "Hello World!"', () => {
|
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Controller, Get } from '@nestjs/common';
|
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AppController {
|
|
||||||
constructor(private readonly appService: AppService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
getHello(): string {
|
|
||||||
return this.appService.getHello();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,30 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { AppController } from "./app.controller";
|
|
||||||
import { AppService } from "./app.service";
|
|
||||||
import { UserModule } from "./user/user.module";
|
import { UserModule } from "./user/user.module";
|
||||||
import { APP_PIPE } from "@nestjs/core";
|
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
||||||
import { ZodValidationPipe } from "nestjs-zod";
|
import { ZodValidationPipe } from "nestjs-zod";
|
||||||
|
import { PostModule } from "./post/post.module";
|
||||||
|
import { AuthModule } from "./auth/auth.module";
|
||||||
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule],
|
imports: [
|
||||||
controllers: [AppController],
|
UserModule,
|
||||||
|
PostModule,
|
||||||
|
AuthModule,
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
|
||||||
{
|
{
|
||||||
provide: APP_PIPE,
|
provide: APP_PIPE,
|
||||||
useClass: ZodValidationPipe,
|
useClass: ZodValidationPipe,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: JwtAuthGuard,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
35
src/auth/auth.controller.ts
Normal file
35
src/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Request,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
Body,
|
||||||
|
HttpCode,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { LocalAuthGuard } from "./local-auth.guard";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { LoginUserDTO } from "./dto/login.dto";
|
||||||
|
import { Public } from "src/public.decorator";
|
||||||
|
|
||||||
|
@ApiTags("Auth")
|
||||||
|
@Controller("auth")
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@UseGuards(LocalAuthGuard)
|
||||||
|
@Post("/login")
|
||||||
|
@ApiOperation({ summary: "Authenticates a user" })
|
||||||
|
@ApiOkResponse({ status: 200, description: "Authenticated successfully" })
|
||||||
|
@ApiUnauthorizedResponse({ description: "Wrong username or password" })
|
||||||
|
@HttpCode(200)
|
||||||
|
async login(@Request() req, @Body() _: LoginUserDTO) {
|
||||||
|
return this.authService.login(req.user);
|
||||||
|
}
|
||||||
|
}
|
22
src/auth/auth.module.ts
Normal file
22
src/auth/auth.module.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { PassportModule } from "@nestjs/passport";
|
||||||
|
import { LocalStrategy } from "./local.strategy";
|
||||||
|
import { UserModule } from "src/user/user.module";
|
||||||
|
import { AuthController } from "./auth.controller";
|
||||||
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
import { JwtStrategy } from "./jwt.strategy";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AuthController],
|
||||||
|
imports: [
|
||||||
|
UserModule,
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_ACCESS_SECRET,
|
||||||
|
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
46
src/auth/auth.service.ts
Normal file
46
src/auth/auth.service.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { UserService } from "src/user/user.service";
|
||||||
|
import * as bcrypt from "bcrypt";
|
||||||
|
import { UserModel } from "src/user/models/user.model";
|
||||||
|
import { JwtService } from "@nestjs/jwt";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async validateUser(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<UserModel | null> {
|
||||||
|
const user = await this.userService.search(username);
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (user && validation) {
|
||||||
|
const { password, ...result } = user;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(user: UserModel): Promise<{ token: string }> {
|
||||||
|
const payload = {
|
||||||
|
displayName: user.displayName,
|
||||||
|
username: user.username,
|
||||||
|
profileImage: user.profileImage,
|
||||||
|
sub: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: this.jwtService.sign(payload),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
29
src/auth/dto/login.dto.ts
Normal file
29
src/auth/dto/login.dto.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { createZodDto } from "nestjs-zod";
|
||||||
|
import { z } from "nestjs-zod/z";
|
||||||
|
|
||||||
|
export const LoginUserSchema = z
|
||||||
|
.object({
|
||||||
|
username: z
|
||||||
|
.string({
|
||||||
|
required_error: "Username is required",
|
||||||
|
})
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||||
|
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||||
|
)
|
||||||
|
.toLowerCase(),
|
||||||
|
password: z
|
||||||
|
.password({
|
||||||
|
required_error: "Password is required",
|
||||||
|
})
|
||||||
|
.min(8)
|
||||||
|
.max(32)
|
||||||
|
.atLeastOne("digit")
|
||||||
|
.atLeastOne("uppercase")
|
||||||
|
.atLeastOne("lowercase")
|
||||||
|
.atLeastOne("special")
|
||||||
|
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export class LoginUserDTO extends createZodDto(LoginUserSchema) {}
|
25
src/auth/jwt-auth.guard.ts
Normal file
25
src/auth/jwt-auth.guard.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { IS_PUBLIC_KEY } from "src/public.decorator";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
28
src/auth/jwt.strategy.ts
Normal file
28
src/auth/jwt.strategy.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||||
|
|
||||||
|
type Payload = {
|
||||||
|
displayName: string;
|
||||||
|
username: string;
|
||||||
|
sub: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: process.env.JWT_ACCESS_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: Payload) {
|
||||||
|
return {
|
||||||
|
displayName: payload.displayName,
|
||||||
|
username: payload.username,
|
||||||
|
id: payload.sub,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
5
src/auth/local-auth.guard.ts
Normal file
5
src/auth/local-auth.guard.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard("local") {}
|
20
src/auth/local.strategy.ts
Normal file
20
src/auth/local.strategy.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
|
import { Strategy } from "passport-local";
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { UserModel } from "src/user/models/user.model";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(private authService: AuthService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(username: string, password: string): Promise<UserModel> {
|
||||||
|
const user = await this.authService.validateUser(username, password);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException("Wrong username or password");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
27
src/main.ts
27
src/main.ts
|
@ -2,9 +2,16 @@ import { NestFactory } from "@nestjs/core";
|
||||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
import { patchNestJsSwagger } from "nestjs-zod";
|
import { patchNestJsSwagger } from "nestjs-zod";
|
||||||
|
import {
|
||||||
|
FastifyAdapter,
|
||||||
|
NestFastifyApplication,
|
||||||
|
} from "@nestjs/platform-fastify";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
|
AppModule,
|
||||||
|
new FastifyAdapter({ logger: true }),
|
||||||
|
);
|
||||||
|
|
||||||
patchNestJsSwagger();
|
patchNestJsSwagger();
|
||||||
|
|
||||||
|
@ -14,15 +21,27 @@ async function bootstrap() {
|
||||||
.setTitle("Project Knedita")
|
.setTitle("Project Knedita")
|
||||||
.setDescription("An open-source social media")
|
.setDescription("An open-source social media")
|
||||||
.setVersion("1.0")
|
.setVersion("1.0")
|
||||||
.addTag("User")
|
.addBearerAuth(
|
||||||
.addTag("Post")
|
{
|
||||||
|
type: "http",
|
||||||
|
scheme: "bearer",
|
||||||
|
bearerFormat: "JWT",
|
||||||
|
name: "JWT",
|
||||||
|
description: "Enter JWT Token",
|
||||||
|
in: "header",
|
||||||
|
},
|
||||||
|
"JWT",
|
||||||
|
)
|
||||||
|
.addTag("Auth")
|
||||||
.addTag("Comment")
|
.addTag("Comment")
|
||||||
|
.addTag("Post")
|
||||||
|
.addTag("User")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const document = SwaggerModule.createDocument(app, config);
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
|
||||||
SwaggerModule.setup("/", app, document);
|
SwaggerModule.setup("/", app, document);
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000, "0.0.0.0");
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
1
src/post/dto/create-post.dto.ts
Normal file
1
src/post/dto/create-post.dto.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export class CreatePostDto {}
|
4
src/post/dto/update-post.dto.ts
Normal file
4
src/post/dto/update-post.dto.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreatePostDto } from './create-post.dto';
|
||||||
|
|
||||||
|
export class UpdatePostDto extends PartialType(CreatePostDto) {}
|
1
src/post/entities/post.entity.ts
Normal file
1
src/post/entities/post.entity.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export class Post {}
|
44
src/post/post.controller.ts
Normal file
44
src/post/post.controller.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { PostService } from "./post.service";
|
||||||
|
import { CreatePostDto } from "./dto/create-post.dto";
|
||||||
|
import { UpdatePostDto } from "./dto/update-post.dto";
|
||||||
|
import { ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
@ApiTags("Post")
|
||||||
|
@Controller("post")
|
||||||
|
export class PostController {
|
||||||
|
constructor(private readonly postService: PostService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createPostDto: CreatePostDto) {
|
||||||
|
return this.postService.create(createPostDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.postService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
findOne(@Param("id") id: string) {
|
||||||
|
return this.postService.findOne(+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
update(@Param("id") id: string, @Body() updatePostDto: UpdatePostDto) {
|
||||||
|
return this.postService.update(+id, updatePostDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
remove(@Param("id") id: string) {
|
||||||
|
return this.postService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
9
src/post/post.module.ts
Normal file
9
src/post/post.module.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PostService } from './post.service';
|
||||||
|
import { PostController } from './post.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [PostController],
|
||||||
|
providers: [PostService],
|
||||||
|
})
|
||||||
|
export class PostModule {}
|
26
src/post/post.service.ts
Normal file
26
src/post/post.service.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { CreatePostDto } from "./dto/create-post.dto";
|
||||||
|
import { UpdatePostDto } from "./dto/update-post.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PostService {
|
||||||
|
create(createPostDto: CreatePostDto) {
|
||||||
|
return "This action adds a new post";
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return "This action returns all post";
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number) {
|
||||||
|
return `This action returns a #${id} post`;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: number, updatePostDto: UpdatePostDto) {
|
||||||
|
return `This action updates a #${id} post`;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
return `This action removes a #${id} post`;
|
||||||
|
}
|
||||||
|
}
|
9
src/prisma.service.ts
Normal file
9
src/prisma.service.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
}
|
4
src/public.decorator.ts
Normal file
4
src/public.decorator.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { SetMetadata } from "@nestjs/common";
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = "isPublic";
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
@ -9,21 +9,25 @@ export const CreateUserSchema = z
|
||||||
})
|
})
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||||
"The username must have alphanumerics characters (uppercase and lowercase words), underscore, dots and it must be between 5 and 15 characters",
|
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||||
),
|
)
|
||||||
|
.toLowerCase(),
|
||||||
email: z
|
email: z
|
||||||
.string({
|
.string({
|
||||||
required_error: "Email is required",
|
required_error: "Email is required",
|
||||||
})
|
})
|
||||||
.email("Invalid email"),
|
.email("Invalid email"),
|
||||||
password: z
|
password: z
|
||||||
.string({
|
.password({
|
||||||
required_error: "Password is required",
|
required_error: "Password is required",
|
||||||
})
|
})
|
||||||
.regex(
|
.min(8)
|
||||||
/^(?=.*[0-9])(?=.*[!@#$%^&*_])[a-zA-Z0-9!@#$%^&*_]{8,}$/,
|
.max(32)
|
||||||
"Password must have at least 8 characters, one number and one special character.",
|
.atLeastOne("digit")
|
||||||
),
|
.atLeastOne("uppercase")
|
||||||
|
.atLeastOne("lowercase")
|
||||||
|
.atLeastOne("special")
|
||||||
|
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|
18
src/user/models/user.model.ts
Normal file
18
src/user/models/user.model.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { createZodDto } from "nestjs-zod";
|
||||||
|
import { z } from "nestjs-zod/z";
|
||||||
|
|
||||||
|
// TODO: Add posts, liked_posts, liked_comments, followers, following, post_comments and notifications field
|
||||||
|
|
||||||
|
export const UserSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
displayName: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.password(),
|
||||||
|
profileImage: z.string().url(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export class UserModel extends createZodDto(UserSchema) {}
|
|
@ -1,18 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { UserController } from './user.controller';
|
|
||||||
|
|
||||||
describe('UserController', () => {
|
|
||||||
let controller: UserController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [UserController],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<UserController>(UserController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,22 +1,43 @@
|
||||||
import { Body, Controller, Post, UsePipes } from "@nestjs/common";
|
import { Body, Controller, Get, Post, Request } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiCreatedResponse,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
ApiUnauthorizedResponse,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
import { UserService } from "./user.service";
|
import { UserService } from "./user.service";
|
||||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||||
|
import { Public } from "src/public.decorator";
|
||||||
|
|
||||||
@ApiTags("User")
|
@ApiTags("User")
|
||||||
@Controller("user")
|
@Controller("user")
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) {}
|
||||||
|
// GET
|
||||||
|
@Get("/me")
|
||||||
|
@ApiOperation({ summary: "Returns information about the logged user" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
@ApiUnauthorizedResponse({
|
||||||
|
description: "Not authenticated / Invalid JWT Token",
|
||||||
|
})
|
||||||
|
async me(@Request() req) {
|
||||||
|
return req.user; // TODO: Add typing to req.user
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST
|
||||||
|
@Public()
|
||||||
@Post("/signup")
|
@Post("/signup")
|
||||||
@ApiOperation({ summary: "Creates a new account" })
|
@ApiOperation({ summary: "Creates a new account" })
|
||||||
@ApiResponse({ status: 200, description: "Account created successfully" })
|
@ApiCreatedResponse({ description: "Account created successfully" })
|
||||||
@ApiResponse({
|
@ApiBadRequestResponse({
|
||||||
status: 400,
|
|
||||||
description:
|
description:
|
||||||
"Missing field / Invalid username / Invalid email / Weak password",
|
"Missing field / Invalid username / Invalid email / Weak password",
|
||||||
})
|
})
|
||||||
async create(@Body() createUserDTO: CreateUserDTO) {
|
async create(@Body() createUserDTO: CreateUserDTO) {
|
||||||
return this.userService.create(createUserDTO);
|
return this.userService.create(createUserDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { UserController } from './user.controller';
|
import { UserController } from "./user.controller";
|
||||||
import { UserService } from './user.service';
|
import { UserService } from "./user.service";
|
||||||
|
import { PrismaService } from "src/prisma.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService]
|
providers: [UserService, PrismaService],
|
||||||
|
exports: [UserService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { UserService } from "./user.service";
|
|
||||||
|
|
||||||
describe("UserService", () => {
|
|
||||||
let service: UserService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [UserService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<UserService>(UserService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be defined", () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +1,66 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||||
|
import { PrismaService } from "src/prisma.service";
|
||||||
// TODO: Add prisma client
|
import { UserModel } from "./models/user.model";
|
||||||
|
import * as bcrypt from "bcrypt";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
create(user: CreateUserDTO): string {
|
constructor(private prisma: PrismaService) {}
|
||||||
console.log(user);
|
|
||||||
return "Created successfully";
|
async create({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}: CreateUserDTO): Promise<
|
||||||
|
Pick<UserModel, "displayName" | "username" | "createdAt">
|
||||||
|
> {
|
||||||
|
if ((await this.prisma.user.findFirst({ where: { username } })) != null) {
|
||||||
|
throw new BadRequestException("Username already in use");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await this.prisma.user.findFirst({ where: { email } })) != null) {
|
||||||
|
throw new BadRequestException("Email already in use");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password encryption
|
||||||
|
const salt = await bcrypt.genSalt(15);
|
||||||
|
const hash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const user = await this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password: hash,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
displayName: true,
|
||||||
|
username: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(username: string): Promise<UserModel> {
|
||||||
|
const user = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
profileImage: true,
|
||||||
|
displayName: true,
|
||||||
|
username: true,
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue