diff --git a/backend-api/.gitignore b/backend-api/.gitignore index 164168a..4d1dc85 100644 --- a/backend-api/.gitignore +++ b/backend-api/.gitignore @@ -6,6 +6,8 @@ config.serverless.yml vanillameta bigquery-key.json test-connect-info.json +cert.pem +key.pem . # compiled output /dist diff --git a/backend-api/package-lock.json b/backend-api/package-lock.json index 7055499..79f7b73 100644 --- a/backend-api/package-lock.json +++ b/backend-api/package-lock.json @@ -2822,11 +2822,25 @@ "uuid": "9.0.0" } }, + "@nestjs/jwt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", + "integrity": "sha512-ZsXGY/wMYKzEhymw2+dxiwrHTRKIKrGszx6r2EjQqNLypdXMQu0QrujwZJ8k3+XQV4snmuJwwNakQoA2ILfq8w==", + "requires": { + "@types/jsonwebtoken": "8.5.8", + "jsonwebtoken": "8.5.1" + } + }, "@nestjs/mapped-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.1.0.tgz", "integrity": "sha512-+2kSly4P1QI+9eGt+/uGyPdEG1hVz7nbpqPHWZVYgoqz8eOHljpXPag+UCVRw9zo2XCu4sgNUIGe8Uk0+OvUQg==" }, + "@nestjs/passport": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", + "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==" + }, "@nestjs/platform-express": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.1.1.tgz", @@ -4052,6 +4066,14 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/jsonwebtoken": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz", + "integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==", + "requires": { + "@types/node": "*" + } + }, "@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -4122,6 +4144,47 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/passport": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.11.tgz", + "integrity": "sha512-pz1cx9ptZvozyGKKKIPLcVDVHwae4hrH5d6g5J+DkMRRjR3cVETb4jMabhXAUbg3Ov7T22nFHEgaK2jj+5CBpw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-jwt": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.7.tgz", + "integrity": "sha512-qRQ4qlww1Yhs3IaioDKrsDNmKy6gLDLgFsGwpCnc2YqWovO2Oxu9yCQdWHMJafQ7UIuOba4C4/TNXcGkQfEjlQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-local": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz", + "integrity": "sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/prettier": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz", @@ -13676,6 +13739,38 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-jwt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz", + "integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==", + "requires": { + "jsonwebtoken": "^8.2.0", + "passport-strategy": "^1.0.0" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -13802,6 +13897,11 @@ "integrity": "sha512-TX+cz8Jk+ta7IvRy2FAej8rdlbrP0+uBIkP/5DTODez/AuL/vSb30KuAdDxGVREXzn8QfAiu5mJYJ1XjbOhEPA==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", diff --git a/backend-api/package.json b/backend-api/package.json index 77c10aa..91f7946 100644 --- a/backend-api/package.json +++ b/backend-api/package.json @@ -29,7 +29,9 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^9.0.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^6.1.2", "@nestjs/typeorm": "^9.0.1", @@ -56,6 +58,9 @@ "nest-winston": "^1.7.0", "odbc": "^2.4.6", "oracledb": "^5.5.0", + "passport": "^0.6.0", + "passport-jwt": "^4.0.0", + "passport-local": "^1.0.0", "pg": "^8.8.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -76,6 +81,8 @@ "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/node": "^16.0.0", + "@types/passport-jwt": "^3.0.7", + "@types/passport-local": "^1.0.34", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/backend-api/src/app.module.ts b/backend-api/src/app.module.ts index 9500913..98d236a 100644 --- a/backend-api/src/app.module.ts +++ b/backend-api/src/app.module.ts @@ -11,6 +11,8 @@ import { TemplateModule } from './template/template.module'; import { CommonModule } from './common/common.module'; import { ComponentModule } from './component/component.module'; import { ConnectionModule } from './connection/connection.module'; +import { UserModule } from './user/user.module'; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -39,6 +41,9 @@ import { ConnectionModule } from './connection/connection.module'; CommonModule, ComponentModule, ConnectionModule, + UserModule, + AuthModule, + UserModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend-api/src/auth/auth.module.ts b/backend-api/src/auth/auth.module.ts new file mode 100644 index 0000000..34ab5d4 --- /dev/null +++ b/backend-api/src/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { UserModule } from 'src/user/user.module'; +import { AuthService } from './auth.service'; +import { JwtModule, JwtService } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../user/entities/user.entity.js'; +import { UserService } from 'src/user/user.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + PassportModule, + JwtModule.register({ + secret: process.env.JWT_ACCESS_SECRET, + signOptions: { expiresIn: '30s'} + }) + ], + providers: [AuthService] +}) +export class AuthModule {} diff --git a/backend-api/src/auth/auth.service.spec.ts b/backend-api/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/backend-api/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend-api/src/auth/auth.service.ts b/backend-api/src/auth/auth.service.ts new file mode 100644 index 0000000..cbb16d9 --- /dev/null +++ b/backend-api/src/auth/auth.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { UserService } from 'src/user/user.service'; +import { NestFactory } from '@nestjs/core'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from 'src/user/entities/user.entity'; + +@Injectable() +export class AuthService { + constructor( + private jwtService: JwtService, + @InjectRepository(User) private readonly userRepository: Repository ){} + + async generateAccessToken(email: string) { + console.log(email) + const accesstoken = await this.jwtService.sign({email: email}, { + secret: process.env.ACCESS_SECRET, + expiresIn: `30s` + }) + return accesstoken + // accesstoken이 없을때 + } + + async generateRefreshToken(email: string) { + const refreshtoken = await this.jwtService.sign({email: email}, { secret: process.env.REFRESH_SECRET, expiresIn: "3600s" }) + return refreshtoken + // accesstoken이 없을때 + } + + async setRefreshKey(){ + + } + + async validateUser(email: string, pass: string) { + const user = await this.userRepository.findOne({ where: { email: email } }); + if (user && user.password === pass) { + delete user.password; + return user; + } + } +} diff --git a/backend-api/src/auth/guards/jwt-auth.guard.ts b/backend-api/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/backend-api/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/backend-api/src/auth/strategies/jwt.strategy.ts b/backend-api/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..8ad22ce --- /dev/null +++ b/backend-api/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: process.env.JWT_ACCESS_SECRET + }); + console.log(process.env.JWT_ACCESS_SCRET) + } + + async validate(payload: any) { + return { userId: payload.sub, username: payload.username }; + } +} \ No newline at end of file diff --git a/backend-api/src/user/dto/create-user.dto.ts b/backend-api/src/user/dto/create-user.dto.ts new file mode 100644 index 0000000..d4c31e6 --- /dev/null +++ b/backend-api/src/user/dto/create-user.dto.ts @@ -0,0 +1,16 @@ +import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class CreateUserDto { + + @IsString() + @IsNotEmpty() + username: string; + + @IsString() + @IsNotEmpty() + password: string; + + @IsString() + @IsNotEmpty() + email: string; +} diff --git a/backend-api/src/user/dto/update-user.dto.ts b/backend-api/src/user/dto/update-user.dto.ts new file mode 100644 index 0000000..78ab602 --- /dev/null +++ b/backend-api/src/user/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/backend-api/src/user/entities/user.entity.ts b/backend-api/src/user/entities/user.entity.ts new file mode 100644 index 0000000..8a6d1bb --- /dev/null +++ b/backend-api/src/user/entities/user.entity.ts @@ -0,0 +1,21 @@ +import { IsNotEmpty, IsOptional } from 'class-validator'; +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn} from 'typeorm'; +import { BaseEntity } from '../../common/entities/base.entity'; +@Entity() +export class User extends BaseEntity{ + + @PrimaryGeneratedColumn() + id: number; + + @IsNotEmpty() + @Column() + username: string; + + @IsNotEmpty() + @Column() + email: string; + + @IsNotEmpty() + @Column() + password: string; +} \ No newline at end of file diff --git a/backend-api/src/user/user.controller.spec.ts b/backend-api/src/user/user.controller.spec.ts new file mode 100644 index 0000000..1f38440 --- /dev/null +++ b/backend-api/src/user/user.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [UserService], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend-api/src/user/user.controller.ts b/backend-api/src/user/user.controller.ts new file mode 100644 index 0000000..fcd2c8d --- /dev/null +++ b/backend-api/src/user/user.controller.ts @@ -0,0 +1,64 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, UsePipes, ValidationPipe, Res } from '@nestjs/common'; +import { UserService } from './user.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { AuthService } from 'src/auth/auth.service'; + + +@Controller('user') +export class UserController { + constructor( private readonly userService: UserService, + private authService: AuthService) {} + + + @Post('signin') + async login(@Res() res, @Body() loginDto: UpdateUserDto){ + + const findUser = await this.userService.signin(loginDto) + const accessToken = await this.authService.generateAccessToken( findUser.email ); + const refreshToken = await this.authService.generateRefreshToken( findUser.email ); + + res.cookie('jwt_ac', accessToken, { + httpOnly: true, + saemSite: 'none', + secure: true + }); + res.cookie('jwt_re', refreshToken, { + httpOnly: true, + saemSite: 'none', + secure: true + }) + return res.status(201).send('ok') + } + + @UsePipes(ValidationPipe) + @Post('signup') + create(@Body() createUserDto: CreateUserDto) { + return this.userService.create(createUserDto); + } + + @UseGuards(JwtAuthGuard) + @Get('userinfo/:email') + findOne(@Param('email') email: string) { + return this.userService.findOne(email); + } + + @UseGuards(JwtAuthGuard) + @Patch('change-password') + updatePassword(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.userService.updatePassword(+id, updateUserDto); + } + + @UseGuards(JwtAuthGuard) + @Patch('change-username') + updateUsername(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.userService.updateUsername(+id, updateUserDto); + } + + @UseGuards(JwtAuthGuard) + @Delete('delete/:id') + deleteUser(@Param('id') id: string) { + return this.userService.deleteUser(+id); + } +} diff --git a/backend-api/src/user/user.module.ts b/backend-api/src/user/user.module.ts new file mode 100644 index 0000000..affa311 --- /dev/null +++ b/backend-api/src/user/user.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { User } from './entities/user.entity.js'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthService } from 'src/auth/auth.service'; +import { AuthModule } from 'src/auth/auth.module'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [TypeOrmModule.forFeature([User]), AuthModule, JwtModule], + controllers: [UserController], + providers: [UserService, AuthService] +}) +export class UserModule {} diff --git a/backend-api/src/user/user.service.spec.ts b/backend-api/src/user/user.service.spec.ts new file mode 100644 index 0000000..873de8a --- /dev/null +++ b/backend-api/src/user/user.service.spec.ts @@ -0,0 +1,18 @@ +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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend-api/src/user/user.service.ts b/backend-api/src/user/user.service.ts new file mode 100644 index 0000000..0c28571 --- /dev/null +++ b/backend-api/src/user/user.service.ts @@ -0,0 +1,86 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { AuthService } from '../auth/auth.service.js'; +import { Repository } from 'typeorm'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { User } from './entities/user.entity'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) private readonly userRepository: Repository, + private authService: AuthService, + ) {} + + async signin( loginDto: UpdateUserDto ) { + const { email, password } = loginDto; + const findUser = await this.authService.validateUser(email, password); + if (!findUser) { + throw new UnauthorizedException(`No exist user ${email}`); + } + return findUser + } + + + async create(createUserDto: CreateUserDto) { + const userInfo = await this.userRepository.findOne({ + where: { email: createUserDto.email } + }); + if(!userInfo){ + const { email, password, username } = createUserDto; + await this.userRepository.save({ + email: email, + password: password, + user_name: username + }); + } + return 'This action adds a new user'; + } + + async findOne(email: string) { + const userData = await this.userRepository.findOne({ + where: { email: email } + }); + if(!userData){ + return null + } else { + delete userData.password; + return userData + } + } + + async updatePassword(id: number, updateUserDto: UpdateUserDto) { + const updateData = await this.userRepository.findOne({ where: { id: id }}); + if(!updateData){ + return 'not exist user'; + } else { + updateData.password = String(updateUserDto.password); + await this.userRepository.save(updateData) + return `This action update_password a #${id} user`; + } + } + + async updateUsername(id: number, updateUserDto: UpdateUserDto) { + const updateData = await this.userRepository.findOne({ where: { id: id }}); + if(!updateData){ + return 'not exist user'; + } else { + updateData.username = String(updateUserDto.username); + await this.userRepository.save(updateData) + return `This action update_name a #${id} user`; + } + } + + async deleteUser(id: number) { + const findUser = await this.userRepository.findOne({ + where: { id: id } + }); + if(!findUser){ + return 'not exist user' + } else { + await this.userRepository.delete(findUser.id) + return `This action removes a #${id} user`; + } + } +} diff --git a/backend-api/test/util/get-test-mysql.module.ts b/backend-api/test/util/get-test-mysql.module.ts index b2ebe21..cd1d153 100644 --- a/backend-api/test/util/get-test-mysql.module.ts +++ b/backend-api/test/util/get-test-mysql.module.ts @@ -19,7 +19,7 @@ export function getTestMysqlModule(): DynamicModule { autoLoadEntities: true, entities: [entityUrl], synchronize: false, - logging: process.env.NODE_ENV == 'dev', + logging: true, retryAttempts: 1, }); }