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_HOST=127.0.0.1 # localhost (for docker use redis)
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=<placehoder>
|
||||
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||
|
||||
# Express
|
||||
# Fastify
|
||||
SERVER_PORT=<placeholder>
|
||||
CLIENT_URL=<placeholder>
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:<placeholder>/$
|
|||
REDIS_HOST=redis
|
||||
|
||||
REDIS_PORT=<placeholder>
|
||||
REDIS_PASSWORD=<placeholder>
|
||||
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||
|
||||
# Express
|
||||
# Fastify
|
||||
SERVER_PORT=<placeholder>
|
||||
|
||||
# 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-fastify": "^10.3.1",
|
||||
"@nestjs/swagger": "^7.2.0",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"nestjs-zod": "^3.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
@ -41,9 +51,12 @@
|
|||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
|
@ -52,6 +65,7 @@
|
|||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"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 { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
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 { PostModule } from "./post/post.module";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
controllers: [AppController],
|
||||
imports: [
|
||||
UserModule,
|
||||
PostModule,
|
||||
AuthModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
AppService,
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ZodValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
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 { AppModule } from "./app.module";
|
||||
import { patchNestJsSwagger } from "nestjs-zod";
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from "@nestjs/platform-fastify";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({ logger: true }),
|
||||
);
|
||||
|
||||
patchNestJsSwagger();
|
||||
|
||||
|
@ -14,15 +21,27 @@ async function bootstrap() {
|
|||
.setTitle("Project Knedita")
|
||||
.setDescription("An open-source social media")
|
||||
.setVersion("1.0")
|
||||
.addTag("User")
|
||||
.addTag("Post")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
name: "JWT",
|
||||
description: "Enter JWT Token",
|
||||
in: "header",
|
||||
},
|
||||
"JWT",
|
||||
)
|
||||
.addTag("Auth")
|
||||
.addTag("Comment")
|
||||
.addTag("Post")
|
||||
.addTag("User")
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
SwaggerModule.setup("/", app, document);
|
||||
|
||||
await app.listen(3000);
|
||||
await app.listen(3000, "0.0.0.0");
|
||||
}
|
||||
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(
|
||||
/^[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
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Invalid email"),
|
||||
password: z
|
||||
.string({
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.regex(
|
||||
/^(?=.*[0-9])(?=.*[!@#$%^&*_])[a-zA-Z0-9!@#$%^&*_]{8,}$/,
|
||||
"Password must have at least 8 characters, one number and one special character.",
|
||||
),
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.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 { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Body, Controller, Get, Post, Request } from "@nestjs/common";
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiCreatedResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { UserService } from "./user.service";
|
||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||
import { Public } from "src/public.decorator";
|
||||
|
||||
@ApiTags("User")
|
||||
@Controller("user")
|
||||
export class UserController {
|
||||
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")
|
||||
@ApiOperation({ summary: "Creates a new account" })
|
||||
@ApiResponse({ status: 200, description: "Account created successfully" })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
@ApiCreatedResponse({ description: "Account created successfully" })
|
||||
@ApiBadRequestResponse({
|
||||
description:
|
||||
"Missing field / Invalid username / Invalid email / Weak password",
|
||||
})
|
||||
async create(@Body() createUserDTO: CreateUserDTO) {
|
||||
return this.userService.create(createUserDTO);
|
||||
}
|
||||
|
||||
// PUT
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UserController } from "./user.controller";
|
||||
import { UserService } from "./user.service";
|
||||
import { PrismaService } from "src/prisma.service";
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
providers: [UserService]
|
||||
providers: [UserService, PrismaService],
|
||||
exports: [UserService],
|
||||
})
|
||||
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";
|
||||
|
||||
// TODO: Add prisma client
|
||||
import { PrismaService } from "src/prisma.service";
|
||||
import { UserModel } from "./models/user.model";
|
||||
import * as bcrypt from "bcrypt";
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
create(user: CreateUserDTO): string {
|
||||
console.log(user);
|
||||
return "Created successfully";
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
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