issue #39
Some checks failed
Test CI / build (push) Failing after 28s

- 로그인 이후 access/refresh token 생성 및 반환 로직 구현
This commit is contained in:
2025-11-30 18:19:39 +09:00
parent 810b4c1fb0
commit 5c79aa18f4
20 changed files with 435 additions and 34 deletions

View File

@@ -1,13 +1,14 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from '@nestjs/config';
import dotenv from 'dotenv';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
'.env.common',
`.env.${process.env.NODE_ENV}`
`.env.${process.env.NODE_ENV}`,
'.env.common'
]
})
]

View File

@@ -1,8 +1,5 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import dotenv from 'dotenv';
dotenv.config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@@ -25,7 +22,7 @@ async function bootstrap() {
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000);
await app.listen(process.env.PORT ?? 3000, () => { process.env.NODE_ENV !== 'prod' && console.log(`servier is running on ${process.env.PORT}`) });
}
bootstrap();

View File

@@ -0,0 +1,24 @@
import { forwardRef, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AccountModule } from 'src/modules/account/account.module';
@Module({
imports: [
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET')!,
signOptions: { expiresIn: '1h' }
})
}),
forwardRef(() => AccountModule)
],
providers: [AuthService, JwtStrategy],
exports: [AuthService]
})
export class AuthModule{}

View File

@@ -0,0 +1,23 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
generateTokens(payload: any) {
const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
return { accessToken, refreshToken };
}
refreshTokens(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken);
return this.generateTokens(payload);
} catch (e) {
throw new UnauthorizedException('Invalid Refresh Token');
}
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}

View File

@@ -0,0 +1,27 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AccountRepo } from 'src/modules/account/account.repo';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly accountRepo: AccountRepo
, private readonly configService: ConfigService
, private readonly jwtService: JwtService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET')!
});
}
async validate(payload: any) {
const account = await this.accountRepo.findById(payload.id);
if (!account || account.length < 1) throw new UnauthorizedException();
return account[0];
}
}

View File

@@ -36,8 +36,14 @@ export class AccountController {
}
@Post('signup')
async signup(@Body() body: SignupRequest): Promise<LoginResponse> {
async signup(@Body() body: SignupRequest): Promise<SignupResponse> {
const result = await this.accountService.signup(body);
return result;
}
@Post('login')
async login(@Body() body: LoginRequest): Promise<LoginResponse> {
const result = await this.accountService.login(body);
return result;
}
}

View File

@@ -1,9 +1,10 @@
import { Module } from "@nestjs/common";
import { forwardRef, Module } from "@nestjs/common";
import { AccountController } from "./account.controller";
import { AccountRepo } from "./account.repo";
import { AccountService } from "./account.service";
import { AuthModule } from 'src/middleware/auth/auth.module';
@Module({
imports: [forwardRef(() => AuthModule)],
controllers: [AccountController],
providers: [AccountService, AccountRepo],
exports: [AccountService, AccountRepo]

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from "@nestjs/common";
import * as schema from "drizzle/schema";
import { countDistinct, and, eq } from 'drizzle-orm';
import { countDistinct, and, eq, not } from 'drizzle-orm';
import { NodePgDatabase } from "drizzle-orm/node-postgres";
@Injectable()
@@ -38,5 +38,36 @@ export class AccountRepo {
});
}
async
async login(
type: 'email' | 'accountId'
, id: string
) {
const condition = type === 'email'
? eq(schema.account.email, id)
: eq(schema.account.accountId, id);
return this
.db
.select()
.from(schema.account)
.where(
and(
condition,
eq(schema.account.isDeleted, false),
eq(schema.account.status, 'active')
)
);
}
async findById(id: string) {
return await this
.db
.select()
.from(schema.account)
.where(
and(
eq(schema.account.id, id),
eq(schema.account.isDeleted, false)
)
)
}
}

View File

@@ -5,12 +5,14 @@ import { MailerService } from "src/util/mailer/mailer.service";
import { Generator } from "src/util/generator";
import Redis from "ioredis";
import { Converter } from "src/util/converter";
import { AuthService } from "src/middleware/auth/auth.service";
@Injectable()
export class AccountService {
constructor(
private readonly accountRepo: AccountRepo
, private readonly mailerService: MailerService
, private readonly authService: AuthService
, @Inject("REDIS") private readonly redis: Redis
) {}
@@ -70,4 +72,41 @@ export class AccountService {
}
}
async login(data: DTO.LoginRequest): Promise<DTO.LoginResponse> {
const { type, id, password } = data;
const queryResult = await this.accountRepo.login(type, id);
const typeValue = type === 'email' ? '이메일' : '아이디';
console.log(queryResult);
if (!queryResult || (queryResult.length < 1)) {
return {
success: false,
message: `존재하지 않는 ${typeValue} 입니다.`
};
}
const hashedPassword = queryResult[0].password;
const isPasswordMatch = Converter.comparePassword(password, hashedPassword);
if (!isPasswordMatch) {
return {
success: false,
message: `비밀번호가 맞지 않습니다.`
};
}
{
const { id, accountId, name, nickname, email, status, isDeleted, birthday } = queryResult[0];
const payload = {
id, accountId, name, nickname, email, status, isDeleted, birthday
};
const { accessToken, refreshToken } = this.authService.generateTokens(payload);
return {
success: true,
accessToken: accessToken,
refreshToken: refreshToken
};
}
}
}

View File

@@ -1,5 +1,7 @@
export class LoginResponseDto {
success: boolean;
accessToken?: string;
refreshToken?: string;
message?: string;
error?: string;
}