Skip to content

Commit

Permalink
feat : mock auth
Browse files Browse the repository at this point in the history
  • Loading branch information
kangjuhyup committed Oct 27, 2024
1 parent d7256f9 commit fe534c6
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 5 deletions.
18 changes: 17 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LetterModule } from './domain/letter/letter.module';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { plainToClass } from 'class-transformer';
import { Enviroments } from './domain/dto/env';
import { validateSync } from 'class-validator';
import { StorageModule } from '@storage/storage.module';
import { DatabaseModule } from '@database/database.module';
import { RedisClientModule } from './redis/redis.module';

const routers = [LetterModule];

Expand All @@ -27,6 +28,21 @@ const modules = [
}),
StorageModule,
DatabaseModule,
RedisClientModule.forRootAsync({
project : 'invite-service',
isGlobal : true,
imports : [
ConfigModule,
],
useFactory: (config: ConfigService) => {
return {
host: config.get<string>('REDIS_HOST'),
port: config.get<number>('REDIS_PORT'),
password: config.get<string>('REDIS_PWD'),
};
},
inject : [ConfigService]
})
].filter(Boolean);

@Module({
Expand Down
55 changes: 55 additions & 0 deletions src/domain/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Body, Controller, Delete, Post, Req, Res, UseGuards } from "@nestjs/common";
import { SignRequest } from "./dto/sign";
import { AuthService } from "./auth.service";
import { UserGuard } from "@app/jwt/guard/user.guard";
import { Response } from 'express';

@Controller('auth')
export class AuthController {

constructor(
private readonly authService : AuthService
) {}

@Post('signUp')
async signUp(
@Body() dto : SignRequest,
@Res({passthrough : true}) res:Response
) {
const data = await this.authService.signUp(dto.phone,dto.password);
res.setHeader('Authorization',`Bearer ${data.access}`)
return {
result : true
}
}

@Post('signIn')
async signIn(
@Body() dto : SignRequest,
@Res({passthrough : true}) res:Response
) {
const data = await this.authService.signIn(dto.phone,dto.password);
res.setHeader('Authorization',`Bearer ${data.access}`)
return {
result : true
}
}

@Post('signOut')
@UseGuards(UserGuard)
async signOut(
@Req() req
) {
return {
result : true
}
}

@Delete('account')
@UseGuards(UserGuard)
async deleteAccount(@Body() dto: SignRequest) {
return {
result : true
}
}
}
35 changes: 35 additions & 0 deletions src/domain/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DynamicModule, Module } from "@nestjs/common";
import { UserService } from "../user/user.service";
import { PassportModule } from "@nestjs/passport";
import { JwtModule } from "@nestjs/jwt";
import { UserStrategy } from "./strategy/user.strategy";
import { UserModule } from "../user/user.module";
import { AuthController } from "./auth.controller";

@Module({})
export class AuthModule {
static forRootAsync(options : { secret : string, expiresIn : string }) : DynamicModule {
return {
module : AuthModule,
imports : [
UserModule,
PassportModule,
JwtModule.register({
secret : options.secret,
signOptions : {
expiresIn : options.expiresIn
}
})
],
controllers : [
AuthController,
],
providers : [
{
provide : UserStrategy,
useFactory : (user:UserService) => new UserStrategy(user,options.secret),
inject : [UserService]
}],
}
}
}
29 changes: 29 additions & 0 deletions src/domain/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { UserService } from "../user/user.service";

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}

async signIn(phone: string, pwd: string) {
const user = await this.userService.getUser(phone);
if(user.pwd !== pwd) throw new UnauthorizedException('잘못된 비밀번호 입니다.')
return this._generateAccessToken(user.userId)
}

async signUp(phone: string, pwd:string) {
const user = await this.userService.saveUser(phone,pwd);
return this._generateAccessToken(user.userId);
}

private _generateAccessToken(id:string) {
const payload = { id };
return {
access: this.jwtService.sign(payload),
};
}
}
11 changes: 11 additions & 0 deletions src/domain/auth/dto/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from "class-validator"

export class SignRequest {
@IsNotEmpty()
@IsPhoneNumber()
phone : string

@IsNotEmpty()
@IsString()
password : string
}
44 changes: 40 additions & 4 deletions src/domain/letter/letter.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { StorageService } from '@storage/storage.service';
import { Injectable } from '@nestjs/common';
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { Enviroments } from '../dto/env';
import { v4 as uuidv4 } from 'uuid';
import { PrepareRequest } from './dto/request/prepare';
import { PrepareResponse } from './dto/response/prepare';
import { AddLetterRequest } from './dto/request/add.letter';
import { AddLetterResponse } from './dto/response/add.letter';
import { RedisClientService } from '@redis/redis.client.service';
import { User } from '@jwt/user';
import { randomString } from '@util/random';

@Injectable()
export class LetterService {
Expand All @@ -13,7 +18,7 @@ export class LetterService {
private readonly componentBucket;
private readonly letterBucket;
private readonly urlExpires = 60;
constructor(private readonly storage: StorageService) {
constructor(private readonly storage: StorageService, private readonly redis : RedisClientService) {
const env = plainToInstance(Enviroments, process.env, {
enableImplicitConversion: true,
});
Expand All @@ -25,13 +30,18 @@ export class LetterService {

async prepareAddLetter({
componentCount,
}: PrepareRequest): Promise<PrepareResponse> {
}: PrepareRequest, user:User): Promise<PrepareResponse> {
const uuid = uuidv4();
const thumbnailUrl = await this.storage.generateUploadPresignedUrl({
bucket: this.thumbnailBucket,
key: uuid,
expires: this.urlExpires,
});
const letterUrl = await this.storage.generateUploadPresignedUrl({
bucket : this.letterBucket,
key : uuid,
expires : this.urlExpires,
})
const backgroundUrl = await this.storage.generateUploadPresignedUrl({
bucket: this.backGroundBucket,
key: uuid,
Expand All @@ -41,17 +51,43 @@ export class LetterService {
for (let i = 0; i < componentCount; i++) {
componentUrls.push(
await this.storage.generateUploadPresignedUrl({
bucket: this.backGroundBucket,
bucket: this.componentBucket,
key: uuid + '-' + i,
expires: this.urlExpires,
}),
);
}
const sessionKey = randomString(5);
await this.redis.set(this.redis.generateKey(LetterService.name,`add-${user.id}`),{
sessionKey : sessionKey,
objectKey : uuid,
},70)
return {
thumbnailUrl,
letterUrl,
backgroundUrl,
componentUrls,
expires: this.urlExpires,
sessionKey
};
}

async addLetter(dto : AddLetterRequest,user:User) : Promise<AddLetterResponse> {
//1. 세션키 획득
const session = await this.redis.get<{sessionKey:string,objectKey:string}>(this.redis.generateKey(LetterService.name,`add-${user.id}`))
if(!session) throw new BadRequestException('필수 요청이 누락되었습니다.')
//2. 메타데이터 조회
const { sessionKey,objectKey } = session;
const thubnailMeta = await this.storage.getObjectMetadata({bucket:this.thumbnailBucket,key:objectKey})
const letterMeta = await this.storage.getObjectMetadata({bucket:this.letterBucket,key:objectKey})
const backgroundMeta = await this.storage.getObjectMetadata({bucket:this.backGroundBucket,key:objectKey})
if(!thubnailMeta || !letterMeta || !backgroundMeta) throw new ForbiddenException('파일을 찾을 수 없습니다.')
if(thubnailMeta['x-amz-session-key'] !== sessionKey ||letterMeta['x-amz-session-key'] !== sessionKey ||backgroundMeta['x-amz-session-key'] !== sessionKey ) {
//TODO: 업로드된 파일 제거
}
//3. 데이터 저장
return {
letterId : 0
}
}
}
10 changes: 10 additions & 0 deletions src/domain/user/user.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";

@Module({
imports : [],
controllers : [],
providers : [UserService],
exports : [UserService]
})
export class UserModule {}
31 changes: 31 additions & 0 deletions src/domain/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { randomString } from "@util/random";
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class UserService {
private readonly mock = [
{
userId : 'ddab41a0-0fc7-4602-927b-40a681021ace',
nickName : 'tester',
phone : '01012341234',
pwd : '97385f8ee138c77ecbd815a3dda29bc40ecbfc16945d5bb9d5e65480aca3c9bc'
}
]

async getUser(phone:string) {
const user = this.mock.find((u) => u.phone === phone)
if(!user) throw new UnauthorizedException('존재하지 않는 회원입니다.')
return user;
}

async saveUser(phone:string,pwd:string) {
const newUser = {
userId : uuidv4(),
nickName : randomString(),
phone,
pwd
}
this.mock.push()
return newUser;
}
}
46 changes: 46 additions & 0 deletions src/jwt/guard/user.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';

@Injectable()
export class UserGuard extends AuthGuard('jwt') {
constructor(private readonly jwtService: JwtService) {
super();
}

async canActivate(context: ExecutionContext): Promise<boolean> {
const isActivated = (await super.canActivate(context)) as boolean;
if (!isActivated) {
throw new UnauthorizedException('Invalid token');
}

const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);

if (!token) {
throw new UnauthorizedException('토큰이 없습니다.');
}

try {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
} catch (error) {
throw new UnauthorizedException('토큰 검증 실패');
}

return true;
}

private extractTokenFromHeader(request: Request): string | null {
const authHeader = request.headers.authorization;
if (!authHeader) return null;

const [type, token] = authHeader.split(' ');
return type === 'Bearer' ? token : null;
}
}
27 changes: 27 additions & 0 deletions src/jwt/strategy/user.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../../user/user.service';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class UserStrategy extends PassportStrategy(Strategy) {
constructor(private userService: UserService, private secret : string) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}

async validate(phone: string, password: string): Promise<any> {
const user = await this.userService.getUser(phone);
if (!user) {
throw new UnauthorizedException();
}
return {
id : user.userId,
nick_name : user.nickName,
};
}
}
3 changes: 3 additions & 0 deletions src/jwt/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class User {
id : string;
}
8 changes: 8 additions & 0 deletions src/util/random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const randomString = ( legnth : number = 10) : string => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}

0 comments on commit fe534c6

Please sign in to comment.