diff --git a/packages/backend/package.json b/packages/backend/package.json index 5db955b3..bdd206db 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,6 +40,7 @@ "nest-winston": "^1.9.7", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", + "passport-local": "^1.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1", @@ -58,6 +59,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/passport-google-oauth20": "^2.0.16", + "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@types/unzipper": "^0.10.10", "@types/ws": "^8.5.13", diff --git a/packages/backend/src/auth/auth.module.ts b/packages/backend/src/auth/auth.module.ts index f513d613..c32261b7 100644 --- a/packages/backend/src/auth/auth.module.ts +++ b/packages/backend/src/auth/auth.module.ts @@ -1,13 +1,23 @@ import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; import { GoogleAuthController } from '@/auth/google/googleAuth.controller'; import { GoogleAuthService } from '@/auth/google/googleAuth.service'; import { GoogleStrategy } from '@/auth/google/strategy/google.strategy'; import { SessionSerializer } from '@/auth/session/session.serializer'; +import { TesterStrategy } from '@/auth/tester/strategy/tester.strategy'; +import { TesterAuthController } from '@/auth/tester/testerAuth.controller'; +import { TesterAuthService } from '@/auth/tester/testerAuth.service'; import { UserModule } from '@/user/user.module'; @Module({ - imports: [UserModule], - controllers: [GoogleAuthController], - providers: [GoogleStrategy, GoogleAuthService, SessionSerializer], + imports: [UserModule, PassportModule.register({ session: true })], + controllers: [GoogleAuthController, TesterAuthController], + providers: [ + GoogleStrategy, + GoogleAuthService, + SessionSerializer, + TesterAuthService, + TesterStrategy, + ], }) export class AuthModule {} diff --git a/packages/backend/src/auth/google/strategy/google.strategy.ts b/packages/backend/src/auth/google/strategy/google.strategy.ts index b87b7945..28295a2d 100644 --- a/packages/backend/src/auth/google/strategy/google.strategy.ts +++ b/packages/backend/src/auth/google/strategy/google.strategy.ts @@ -1,9 +1,8 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy, VerifyCallback } from 'passport-google-oauth20'; import { GoogleAuthService } from '@/auth/google/googleAuth.service'; import { OauthType } from '@/user/domain/ouathType'; -import { Logger } from 'winston'; export interface OauthUserInfo { type: OauthType; @@ -15,7 +14,7 @@ export interface OauthUserInfo { @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy) { - constructor(private readonly googleAuthService: GoogleAuthService, @Inject('winston') private readonly logger: Logger) { + constructor(private readonly googleAuthService: GoogleAuthService) { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, diff --git a/packages/backend/src/auth/tester/guard/tester.guard.ts b/packages/backend/src/auth/tester/guard/tester.guard.ts new file mode 100644 index 00000000..46f461d3 --- /dev/null +++ b/packages/backend/src/auth/tester/guard/tester.guard.ts @@ -0,0 +1,16 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class TestAuthGuard extends AuthGuard('local') { + constructor() { + super(); + } + + async canActivate(context: ExecutionContext) { + const isActivate = (await super.canActivate(context)) as boolean; + const request = context.switchToHttp().getRequest(); + await super.logIn(request); + return isActivate; + } +} diff --git a/packages/backend/src/auth/tester/strategy/tester.strategy.ts b/packages/backend/src/auth/tester/strategy/tester.strategy.ts new file mode 100644 index 00000000..2c6939ca --- /dev/null +++ b/packages/backend/src/auth/tester/strategy/tester.strategy.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; +import { TesterAuthService } from '@/auth/tester/testerAuth.service'; + +@Injectable() +export class TesterStrategy extends PassportStrategy(Strategy) { + constructor(private readonly testerAuthService: TesterAuthService) { + super(); + } + + async validate(username: string, password: string, done: CallableFunction) { + const user = await this.testerAuthService.attemptAuthentication(); + done(null, user); + } +} diff --git a/packages/backend/src/auth/tester/testerAuth.controller.ts b/packages/backend/src/auth/tester/testerAuth.controller.ts new file mode 100644 index 00000000..4ee02553 --- /dev/null +++ b/packages/backend/src/auth/tester/testerAuth.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; +import { + ApiOkResponse, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; +import { Request, Response } from 'express'; +import { TestAuthGuard } from '@/auth/tester/guard/tester.guard'; + +@ApiTags('Auth') +@Controller('auth/tester') +export class TesterAuthController { + constructor() {} + + @ApiOperation({ + summary: '테스터 로그인 api', + description: '테스터로 로그인합니다.', + }) + @ApiQuery({ + name: 'username', + required: true, + description: '테스터 아이디(값만 넣으면 됨)', + }) + @ApiQuery({ + name: 'password', + required: true, + description: '테스터 비밀번호(값만 넣으면 됨)', + }) + @Get('/login') + @UseGuards(TestAuthGuard) + async handleLogin(@Res() response: Response) { + response.redirect('/'); + } + + @ApiOperation({ + summary: '로그인 상태 확인', + description: '로그인 상태를 확인합니다.', + }) + @ApiOkResponse({ + description: '로그인된 상태', + example: { message: 'Authenticated' }, + }) + @Get('/status') + async user(@Req() request: Request) { + if (request.user) { + return { message: 'Authenticated' }; + } + return { message: 'Not Authenticated' }; + } +} diff --git a/packages/backend/src/auth/tester/testerAuth.service.ts b/packages/backend/src/auth/tester/testerAuth.service.ts new file mode 100644 index 00000000..0982c4b0 --- /dev/null +++ b/packages/backend/src/auth/tester/testerAuth.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { UserService } from '@/user/user.service'; + +@Injectable() +export class TesterAuthService { + constructor(private readonly userService: UserService) {} + + async attemptAuthentication() { + return await this.userService.registerTester(); + } +} diff --git a/packages/backend/src/user/user.service.ts b/packages/backend/src/user/user.service.ts index c1eebd52..dbef92dd 100644 --- a/packages/backend/src/user/user.service.ts +++ b/packages/backend/src/user/user.service.ts @@ -6,6 +6,7 @@ import { import { DataSource, EntityManager } from 'typeorm'; import { OauthType } from './domain/ouathType'; import { User } from './domain/user.entity'; +import { status, subject } from '@/user/constants/randomNickname'; type RegisterRequest = Required< Pick @@ -27,22 +28,25 @@ export class UserService { }); } + async registerTester() { + return await this.dataSource.transaction(async (manager) => { + return await manager.save(User, { + nickname: this.generateRandomNickname(), + email: 'tester@nav', + type: OauthType.LOCAL, + oauthId: String( + (await this.getMaxOauthId(OauthType.LOCAL, manager)) + 1, + ), + }); + }); + } + async findUserByOauthIdAndType(oauthId: string, type: OauthType) { return await this.dataSource.manager.findOne(User, { where: { oauthId, type }, }); } - private async validateUserExists( - type: OauthType, - oauthId: string, - manager: EntityManager, - ) { - if (await manager.exists(User, { where: { oauthId, type } })) { - throw new BadRequestException('user already exists'); - } - } - async updateUserTheme(userId: number, isLight?: boolean): Promise { return await this.dataSource.transaction(async (manager) => { if (isLight === undefined) { @@ -72,4 +76,30 @@ export class UserService { return user.isLight; } + + private generateRandomNickname() { + const statusName = status[Math.floor(Math.random() * status.length)]; + const subjectName = subject[Math.floor(Math.random() * subject.length)]; + return `${statusName}${subjectName}`; + } + + private async getMaxOauthId(oauthType: OauthType, manager: EntityManager) { + const result = await manager + .createQueryBuilder(User, 'user') + .select('MAX(user.oauthId)', 'max') + .where('user.type = :oauthType', { oauthType }) + .getRawOne(); + + return result ? Number(result.max) : 1; + } + + private async validateUserExists( + type: OauthType, + oauthId: string, + manager: EntityManager, + ) { + if (await manager.exists(User, { where: { oauthId, type } })) { + throw new BadRequestException('user already exists'); + } + } } diff --git a/yarn.lock b/yarn.lock index 624dd31a..5d71942f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,6 +2156,15 @@ "@types/passport" "*" "@types/passport-oauth2" "*" +"@types/passport-local@^1.0.38": + version "1.0.38" + resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.38.tgz#8073758188645dde3515808999b1c218a6fe7141" + integrity sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-strategy" "*" + "@types/passport-oauth2@*": version "1.4.17" resolved "https://registry.yarnpkg.com/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz#d5d54339d44f6883d03e69dc0cc0e2114067abb4" @@ -2165,6 +2174,14 @@ "@types/oauth" "*" "@types/passport" "*" +"@types/passport-strategy@*": + version "0.2.38" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" + integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport@*": version "1.0.17" resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" @@ -7046,6 +7063,13 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" +passport-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow== + dependencies: + passport-strategy "1.x.x" + passport-oauth2@1.x.x: version "1.8.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8"