diff --git a/.gitignore b/.gitignore index a4b6410..bcbd307 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,9 @@ lerna-debug.log* .env.dev !.example.env -test/graphql-schema.gql \ No newline at end of file +test/graphql-schema.gql + +# key +private.key +public.key +refresh_private.key diff --git a/docker-compose.yml b/docker-compose.yml index 04cd11f..f5d6ed9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,57 @@ +# services: +# app: +# build: +# context: . +# ports: +# - '3000:3000' +# depends_on: +# - postgres +# volumes: +# - .:/app +# - node_modules:/app/node_modules +# environment: +# - NODE_ENV=${NODE_ENV} +# # command: > +# # sh -c "npm run migration:${NODE_ENV}:run && npm run ${NODE_ENV}" +# command: > +# sh -c "npm run migration:docker:generate && npm run migration:${NODE_ENV}:run && npm run ${NODE_ENV}" + +# postgres: +# image: postgres +# container_name: postgres +# restart: unless-stopped +# environment: +# POSTGRES_USER: ${DB_USER} +# POSTGRES_PASSWORD: ${DB_PASSWORD} +# POSTGRES_DB: ${DB_NAME} +# TZ: 'UTC' +# PGTZ: 'UTC' +# ports: +# - '5432:5432' +# healthcheck: +# test: ['CMD', 'pg_isready', '-U', 'postgres'] +# interval: 5s +# timeout: 5s +# retries: 5 + +# volumes: +# node_modules: + services: app: build: context: . ports: - '3000:3000' - depends_on: - - postgres volumes: - .:/app - node_modules:/app/node_modules environment: - NODE_ENV=${NODE_ENV} + # command: > + # sh -c "npm run migration:docker:generate && npm run migration:${NODE_ENV}:run && npm run ${NODE_ENV}" command: > - sh -c "npm run migration:${NODE_ENV}:run && npm run ${NODE_ENV}" - - postgres: - image: postgres - container_name: postgres - restart: unless-stopped - environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} - TZ: 'UTC' - PGTZ: 'UTC' - ports: - - '5432:5432' - healthcheck: - test: ['CMD', 'pg_isready', '-U', 'postgres'] - interval: 5s - timeout: 5s - retries: 5 + sh -c "npm run ${NODE_ENV}" volumes: node_modules: diff --git a/nest-cli.json b/nest-cli.json index 0a947a5..bddb8ca 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,6 +3,12 @@ "sourceRoot": "src", "compilerOptions": { "builder": "swc", - "typeCheck": true + "typeCheck": true, + "assets": [ + { + "include": "common/templates/**/*", + "outDir": "dist" + } + ] } } diff --git a/package-lock.json b/package-lock.json index 13cd7c3..d863f3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,8 @@ "rimraf": "^6.0.1", "rxjs": "^7.8.1", "typeorm": "^0.3.20", + "typeorm-naming-strategies": "^4.1.0", + "ulid": "^3.0.0", "uuid": "^11.0.5", "winston": "^3.17.0" }, @@ -9995,7 +9997,8 @@ "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debug": { "version": "4.4.0", @@ -19402,6 +19405,15 @@ } } }, + "node_modules/typeorm-naming-strategies": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz", + "integrity": "sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==", + "license": "MIT", + "peerDependencies": { + "typeorm": "^0.2.0 || ^0.3.0" + } + }, "node_modules/typeorm/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -19554,6 +19566,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ulid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.0.tgz", + "integrity": "sha512-yvZYdXInnJve6LdlPIuYmURdS2NP41ZoF4QW7SXwbUKYt53+0eDAySO+rGSvM2O/ciuB/G+8N7GQrZ1mCJpuqw==", + "license": "MIT", + "bin": { + "ulid": "dist/cli.js" + } + }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", diff --git a/package.json b/package.json index f7babdc..409e9d0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "api", + "name": "ResumeLog", "version": "0.0.1", "description": "", "author": "", @@ -17,6 +17,7 @@ "development": "export NODE_ENV=development && nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "start:dev": "export NODE_ENV=development && nest start --watch", "lint": "eslint \"{src,test}/**/*.ts\" --fix", "lint:fix": "eslint -c ./eslint.config.mjs \"{src,test}/**/*.ts\" --fix", "test": "jest", @@ -32,10 +33,10 @@ "typeorm": "export NODE_ENV=production&& ts-node ./node_modules/typeorm/cli.js -d ./src/common/config/ormconfig.ts", "typeorm:development": "export NODE_ENV=development && ts-node ./node_modules/typeorm/cli.js -d ./src/common/config/ormconfig.ts", "migration:create": "typeorm-ts-node-commonjs migration:create", - "migration:generate": "npm run typeorm:development migration:generate", + "migration:generate": "npm run typeorm:development migration:generate src/common/database/migrations/CreateInitialTables", "migration:show": "npm run typeorm:development migration:show", "migration:run": "npm run build && npm run typeorm migration:run", - "migration:development:run": "npm run build && npm run typeorm:development migration:run", + "migration:development:run": "npm run build && npm run typeorm:development", "migration:revert": "npm run typeorm:development migration:revert", "docker:up": "docker-compose up --build", "docker:development:up": "docker-compose -f docker-compose.yml --env-file .development.env up --build", @@ -84,6 +85,8 @@ "rimraf": "^6.0.1", "rxjs": "^7.8.1", "typeorm": "^0.3.20", + "typeorm-naming-strategies": "^4.1.0", + "ulid": "^3.0.0", "uuid": "^11.0.5", "winston": "^3.17.0" }, diff --git a/src/app.module.ts b/src/app.module.ts index 81e16b6..fe3e3ea 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { getEnvPath } from './common/helper/env.helper'; import { envValidation } from './common/helper/env.validation'; import { LoggingModule } from './common/logging/logging.module'; import { HealthModule } from './health/health.module'; +import { ResumeModule } from './resume/resume.module'; import { UploadModule } from './upload/upload.module'; import { UserModule } from './user/user.module'; @@ -37,6 +38,7 @@ import { UserModule } from './user/user.module'; HealthModule, CustomCacheModule.forRoot(), LoggingModule, + ResumeModule, ], }) export class AppModule {} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index c9a9554..2d9ca77 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,28 +2,20 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { EnvironmentVariables } from '../common/helper/env.validation'; import { CustomMailerModule } from '../common/modules/mailer.module'; +import { TypeOrmExModule } from '../common/modules/typeorm.module'; import { TransactionModule } from '../common/transaction/transaction.module'; import { UtilModule } from '../common/util/util.module'; import { UserModule } from '../user/user.module'; -import { EmailVerification } from './entities/email-verification.entity'; import { EmailVerificationRepository } from './repositories/email-verification.repository'; import { AuthVerificationResolver } from './resolvers/auth-verification.resolver'; import { AuthResolver } from './resolvers/auth.resolver'; import { AuthMailService } from './services/auth-mail.serivice'; import { AuthVerificationService } from './services/auth-verification.service'; import { AuthService } from './services/auth.service'; -// import { AppleAuthResolver } from './resolvers/apple-auth.resolver'; -// 이메일 로그인, 회원가입 - -// // 소셜 로그인 Resolver들 -// import { KakaoAuthResolver } from './resolvers/kakao-auth.resolver'; -// import { NaverAuthResolver } from './resolvers/naver-auth.resolver'; -import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy'; -// 로그인 전략 +// import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy'; import { JwtStrategy } from './strategies/jwt.strategy'; import { LocalStrategy } from './strategies/local.strategy'; @@ -34,36 +26,28 @@ import { LocalStrategy } from './strategies/local.strategy'; imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ - privateKey: configService.get('JWT_PRIVATE_KEY'), - publicKey: configService.get('JWT_PUBLIC_KEY'), + secret: configService.get('JWT_SECRET'), signOptions: { - algorithm: 'RS256', + algorithm: 'HS256', expiresIn: '1d', }, - verifyOptions: { - algorithms: ['RS256'], - }, }), }), ConfigModule, UserModule, UtilModule, CustomMailerModule, - TypeOrmModule.forFeature([EmailVerification]), + TypeOrmExModule.forCustomRepository([EmailVerificationRepository]), TransactionModule, ], providers: [ AuthService, AuthResolver, // 이메일/비밀번호 로그인 - // KakaoAuthResolver, // 소셜: Kakao - // AppleAuthResolver, // 소셜: Apple - // NaverAuthResolver, // 소셜: Naver JwtStrategy, - JwtRefreshStrategy, + // JwtRefreshStrategy, LocalStrategy, AuthMailService, AuthVerificationService, - EmailVerificationRepository, AuthVerificationResolver, ], }) diff --git a/src/auth/entities/auth._entity.ts b/src/auth/entities/auth._entity.ts index d0550d8..585a21d 100644 --- a/src/auth/entities/auth._entity.ts +++ b/src/auth/entities/auth._entity.ts @@ -1,12 +1,22 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { User } from '../../user/entities/user.entity'; +@ObjectType() +export class UserResponse { + @Field(() => String) + id: string; + + @Field(() => String) + email: string; + + @Field(() => String) + username: string; +} @ObjectType() export class JwtWithUser { @Field(() => String) jwt: string; - @Field(() => User) - user: User; + @Field(() => UserResponse) + user: UserResponse; } diff --git a/src/auth/entities/email-verification.entity.ts b/src/auth/entities/email-verification.entity.ts index 2beb177..50eec91 100644 --- a/src/auth/entities/email-verification.entity.ts +++ b/src/auth/entities/email-verification.entity.ts @@ -1,15 +1,14 @@ -// src/auth/entities/email-verification.entity.ts -import { - Column, - CreateDateColumn, - Entity, - PrimaryGeneratedColumn, -} from 'typeorm'; +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; +import { ulid } from 'ulid'; + +@ObjectType() @Entity('email_verifications') export class EmailVerification { - @PrimaryGeneratedColumn('uuid') - id: string; + @Field(() => ID) + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Column() email: string; diff --git a/src/auth/inputs/auth.input.ts b/src/auth/inputs/auth.input.ts index ebfd1af..0dd9aa2 100644 --- a/src/auth/inputs/auth.input.ts +++ b/src/auth/inputs/auth.input.ts @@ -32,9 +32,12 @@ export class SignUpInput { @IsNotEmpty() confirmPassword: string; + @Field({ nullable: true }) + nickname?: string; + @Field(() => String) @IsNotEmpty() - nickname: string; + username: string; @Field(() => Boolean) @IsNotEmpty() diff --git a/src/auth/repositories/email-verification.repository.ts b/src/auth/repositories/email-verification.repository.ts index cd4ef7b..d9a63cd 100644 --- a/src/auth/repositories/email-verification.repository.ts +++ b/src/auth/repositories/email-verification.repository.ts @@ -1,16 +1,12 @@ -import { Injectable } from '@nestjs/common'; - import dayjs from 'dayjs'; -import { DataSource, EntityManager, Repository } from 'typeorm'; +import { EntityManager } from 'typeorm'; +import { CustomRepository } from '../../common/decorators/typeorm.decorator'; +import { ExtendedRepository } from '../../common/graphql/customExtended'; import { EmailVerification } from '../entities/email-verification.entity'; -@Injectable() -export class EmailVerificationRepository extends Repository { - constructor(dataSource: DataSource) { - super(EmailVerification, dataSource.createEntityManager()); - } - +@CustomRepository(EmailVerification) +export class EmailVerificationRepository extends ExtendedRepository { async createCode(email: string, code: string) { const entity = this.create({ email, code }); return this.save(entity); @@ -21,7 +17,7 @@ export class EmailVerificationRepository extends Repository { code: string, manager: EntityManager, ): Promise { - const record = await this.findOne({ + const record = await manager.findOne(EmailVerification, { where: { email, code, isVerified: false }, order: { createdAt: 'DESC' }, lock: { mode: 'pessimistic_write' }, @@ -37,7 +33,7 @@ export class EmailVerificationRepository extends Repository { } record.isVerified = true; - await this.save(record); + await manager.save(EmailVerification, record); return true; } diff --git a/src/auth/resolvers/auth.resolver.ts b/src/auth/resolvers/auth.resolver.ts index 004fd6e..a5714fe 100644 --- a/src/auth/resolvers/auth.resolver.ts +++ b/src/auth/resolvers/auth.resolver.ts @@ -1,7 +1,6 @@ import { BadRequestException, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; -// import { SocialLoginInput } from 'src/auth/inputs/social-login.input'; import { Public } from '../../common/decorators/social.decorator'; import { CurrentUser } from '../../common/decorators/user.decorator'; import { RefreshGuard } from '../../common/guards/graphql-refresh.guard'; @@ -49,15 +48,7 @@ export class AuthResolver { @Mutation(() => JwtWithUser) @UseGuards(RefreshGuard) refreshAccessToken(@CurrentUser() user: User) { - const jwt = this.authService.generateAccessToken(user, user.refreshToken); - + const jwt = this.authService.generateAccessToken(user); return { jwt, user }; } - - // @Public() - // @Mutation(() => JwtWithUser) - // async socialLogin(@Args('input') input: SocialLoginInput) { - // const socialUser = await this.authService.validateSocialLogin(input); - // return this.authService.socialLogin(socialUser); - // } } diff --git a/src/auth/services/auth-mail.serivice.ts b/src/auth/services/auth-mail.serivice.ts index b841256..70bd560 100644 --- a/src/auth/services/auth-mail.serivice.ts +++ b/src/auth/services/auth-mail.serivice.ts @@ -10,7 +10,7 @@ export class AuthMailService { async sendVerificationCode(email: string, code: string) { await this.mailerService.sendMail({ to: email, - subject: '[YourApp] 이메일 인증 코드', + subject: '[ResumeLog] 이메일 인증 코드', template: 'verify-email', context: { code }, }); diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 9597960..bd84527 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -1,61 +1,29 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { SignInInput, SignUpInput } from '../../auth/inputs/auth.input'; -import { EnvironmentVariables } from '../../common/helper/env.validation'; import { TransactionService } from '../../common/transaction/transaction.service'; import { UtilService } from '../../common/util/util.service'; import { User } from '../../user/entities/user.entity'; import { UserService } from '../../user/user.service'; import { JwtWithUser } from '../entities/auth._entity'; +import { AuthVerificationService } from '../services/auth-verification.service'; @Injectable() export class AuthService { constructor( private readonly userService: UserService, private readonly jwtService: JwtService, - private readonly configService: ConfigService, private readonly utilService: UtilService, private readonly transactionService: TransactionService, + private readonly authVerificationService: AuthVerificationService, ) {} - private async generateRefreshToken(userId: string) { - const refreshToken = this.jwtService.sign( - { id: userId }, - { - secret: this.configService.get('JWT_REFRESH_TOKEN_PRIVATE_KEY'), - expiresIn: '7d', - }, - ); - await this.userService.update(userId, { refreshToken }); - - return refreshToken; - } - - async verifyRefreshToken( - userId: string, - refreshToken: string, - ): Promise { - try { - this.jwtService.verify(refreshToken, { - secret: this.configService.get('JWT_REFRESH_TOKEN_PRIVATE_KEY'), - }); - return await this.userService.getOne(userId); - } catch (err) { - if (err.message === 'jwt expired') { - await this.userService.update(userId, { refreshToken: null }); - } - return null; - } - } - - generateAccessToken(user: User, refreshToken: string) { + generateAccessToken(user: User) { return this.jwtService.sign({ ...this.utilService.pick(user, ['id', 'role']), - refreshToken, }); } @@ -66,41 +34,35 @@ export class AuthService { throw new BadRequestException('email already exists'); } + // 이메일 인증 여부 확인 + await this.authVerificationService.assertVerified(input.email); + const user = await this.userService.create( { ...input, role: 'user' }, manager, ); - - const refreshToken = this.jwtService.sign( - { id: user.id }, - { - secret: this.configService.get('JWT_REFRESH_TOKEN_PRIVATE_KEY'), - expiresIn: '7d', + const jwt = this.generateAccessToken(user); + return { + jwt, + user: { + id: user.id, + email: user.email, + username: user.username, }, - ); - - await this.userService.update(user.id, { refreshToken }, manager); - - const jwt = this.generateAccessToken(user, refreshToken); - return { jwt, user }; + }; }); } async signIn(user: User): Promise { - return this.transactionService.runInTransaction(async (manager) => { - const refreshToken = this.jwtService.sign( - { id: user.id }, - { - secret: this.configService.get('JWT_REFRESH_TOKEN_PRIVATE_KEY'), - expiresIn: '7d', - }, - ); - - await this.userService.update(user.id, { refreshToken }, manager); - - const jwt = this.generateAccessToken(user, refreshToken); - return { jwt, user }; - }); + const jwt = this.generateAccessToken(user); + return { + jwt, + user: { + id: user.id, + email: user.email, + username: user.username, + }, + }; } async validateUser(input: SignInInput): Promise { @@ -110,111 +72,4 @@ export class AuthService { const isValid = await bcrypt.compare(input.password, user.password); return isValid ? user : null; } - - // async socialLogin(socialUser: SocialLoginInput): Promise { - // const { email, nickname, provider, providerId } = socialUser; - - // // 1. provider + providerId로 찾기 - // let user = await this.userService.getOne({ - // where: { - // provider, - // providerId, - // }, - // }); - - // if (!user) { - // // 2. 이메일로 이미 가입된 사용자인지 확인 - // const existingByEmail = await this.userService.getOne({ - // where: { email: email }, - // }); - - // if (existingByEmail) { - // if (existingByEmail.provider !== provider) { - // throw new BadRequestException( - // `해당 이메일은 ${existingByEmail.provider} 계정으로 이미 가입되어 있습니다.`, - // ); - // } - - // // 같은 provider인데 providerId가 아직 등록 안 된 상태일 수도 있음 → 업데이트 - // user = await this.userService.update(existingByEmail.id, { - // providerId, - // }); - // } else { - // // 3. 새로운 사용자 생성 - // user = await this.userService.create({ - // email: email, - // nickname: nickname || email.split('@')[0], - // provider, - // providerId, - // role: 'user', - // }); - // } - // } - - // return this.signIn(user); - // } - - // async validateSocialLogin( - // input: SocialLoginInput, - // ): Promise { - // // 각 소셜 로그인 제공자의 토큰 검증 및 사용자 정보 가져오기 - // switch (input.provider) { - // case 'apple': - // // Apple 토큰 검증 로직 - // break; - // case 'kakao': - // // Kakao 토큰 검증 로직 - // break; - // case 'naver': - // // Naver 토큰 검증 로직 - // break; - // default: - // throw new BadRequestException('Invalid provider'); - // } - - // // 임시로 에러 throw (실제 구현 필요) - // throw new Error('Not implemented'); - // } - - // async validateKakao(accessToken: string): Promise { - // const res = await axios.get('https://kapi.kakao.com/v2/user/me', { - // headers: { Authorization: `Bearer ${accessToken}` }, - // }); - - // const profile = res.data; - // return { - // provider: SocialProvider.KAKAO, - // providerId: profile.id, - // email: profile.kakao_account?.email, - // nickname: profile.properties?.nickname, - // }; - // } - - // async validateNaver(accessToken: string): Promise { - // const res = await axios.get('https://openapi.naver.com/v1/nid/me', { - // headers: { Authorization: `Bearer ${accessToken}` }, - // }); - - // const profile = res.data.response; - // return { - // provider: SocialProvider.NAVER, - // providerId: profile.id, - // email: profile.email, - // nickname: profile.nickname, - // }; - // } - - // async validateApple( - // idToken: string, - // user?: string, - // ): Promise { - // const decoded = this.jwtService.decode(idToken); // 실제 검증 로직은 별도 필요 - - // return { - // provider: SocialProvider.APPLE, - // providerId: decoded.sub, - // email: decoded.email ?? `${decoded.sub}@apple.com`, - // nickname: user ?? null, - // }; - // } } diff --git a/src/auth/strategies/jwt-refresh.strategy.ts b/src/auth/strategies/jwt-refresh.strategy.ts index ae0b547..4c9c1eb 100644 --- a/src/auth/strategies/jwt-refresh.strategy.ts +++ b/src/auth/strategies/jwt-refresh.strategy.ts @@ -1,40 +1,40 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { PassportStrategy } from '@nestjs/passport'; +// import { Injectable, UnauthorizedException } from '@nestjs/common'; +// import { ConfigService } from '@nestjs/config'; +// import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'; +// import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'; -import { EnvironmentVariables } from '../../common/helper/env.validation'; -import { AuthService } from '../services/auth.service'; +// import { EnvironmentVariables } from '../../common/helper/env.validation'; +// import { AuthService } from '../services/auth.service'; -@Injectable() -export class JwtRefreshStrategy extends PassportStrategy( - Strategy, - 'jwt-refresh', -) { - constructor( - private readonly configService: ConfigService, - private readonly authService: AuthService, - ) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: configService.get('JWT_PUBLIC_KEY'), - }); - } +// @Injectable() +// export class JwtRefreshStrategy extends PassportStrategy( +// Strategy, +// 'jwt-refresh', +// ) { +// constructor( +// private readonly configService: ConfigService, +// private readonly authService: AuthService, +// ) { +// super({ +// jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), +// secretOrKey: configService.get('JWT_PUBLIC_KEY'), +// }); +// } - async validate( - payload: { id: string; refreshToken: string }, - done: VerifiedCallback, - ) { - try { - const userData = await this.authService.verifyRefreshToken( - payload.id, - payload.refreshToken, - ); +// async validate( +// payload: { id: string; refreshToken: string }, +// done: VerifiedCallback, +// ) { +// try { +// const userData = await this.authService.verifyRefreshToken( +// payload.id, +// payload.refreshToken, +// ); - done(null, userData); - } catch (err) { - throw new UnauthorizedException('Error', err.message); - } - } -} +// done(null, userData); +// } catch (err) { +// throw new UnauthorizedException('Error', err.message); +// } +// } +// } diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts index e5f56d9..4a2e3d8 100644 --- a/src/auth/strategies/jwt.strategy.ts +++ b/src/auth/strategies/jwt.strategy.ts @@ -15,7 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: configService.get('JWT_PUBLIC_KEY'), + secretOrKey: configService.get('JWT_SECRET'), }); } diff --git a/src/common/config/graphql-config.service.ts b/src/common/config/graphql-config.service.ts index 31c1cb5..01f5ad9 100644 --- a/src/common/config/graphql-config.service.ts +++ b/src/common/config/graphql-config.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { GqlOptionsFactory } from '@nestjs/graphql'; +import { ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default'; import GraphQLJSON from 'graphql-type-json'; import { join } from 'path'; import { cwd } from 'process'; @@ -32,14 +33,15 @@ export class GraphqlConfigService // 개발 환경 설정 ...(isDev && { introspection: true, - playground: false, + playground: true, + csrfPrevention: nodeEnv === 'production', }), // 프로덕션 환경에서만 랜딩 페이지 플러그인 사용 plugins: !isDev ? [ // 필요한 경우에만 프로덕션 랜딩 페이지 추가 - // ApolloServerPluginLandingPageProductionDefault() + ApolloServerPluginLandingPageProductionDefault(), ] : [], }; diff --git a/src/common/config/ormconfig.ts b/src/common/config/ormconfig.ts index 41906ec..bb8b7fd 100644 --- a/src/common/config/ormconfig.ts +++ b/src/common/config/ormconfig.ts @@ -4,6 +4,7 @@ import { config } from 'dotenv'; import { join } from 'path'; import { cwd, env } from 'process'; import { DataSource, DataSourceOptions } from 'typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { getEnvPath } from '../helper/env.helper'; @@ -23,6 +24,10 @@ export const setTypeormConfig = ( ? (key: string) => conf.get(key) ?? defaultValues[key] : (key: string) => conf[key] ?? defaultValues[key]; + const isTsRuntime = ['development', 'test'].includes( + getConfigValue('NODE_ENV'), + ); + return { type: 'postgres', host: getConfigValue('DB_HOST'), @@ -30,17 +35,23 @@ export const setTypeormConfig = ( username: getConfigValue('DB_USER'), password: getConfigValue('DB_PASSWORD'), database: getConfigValue('DB_NAME'), - entities: - getConfigValue('NODE_ENV') === 'test' - ? [join(cwd(), 'src', '**', '*.entity.{ts,js}')] - : [join(cwd(), 'dist', '**', '*.entity.js')], - synchronize: getConfigValue('NODE_ENV') !== 'production', - dropSchema: getConfigValue('NODE_ENV') === 'test', + + // 환경별 설정 + dropSchema: getConfigValue('NODE_ENV') === 'test', // 테스트 환경에서만 스키마 삭제 + + // entities: [join(cwd(), 'dist', '**', '*.entity.js')], + entities: [join(cwd(), 'dist', '**', '*.entity.js')], + migrations: [ - join(cwd(), 'dist', 'common', 'database', 'migrations', '*{.ts,.js}'), + join(cwd(), 'dist', 'common', 'database', 'migrations', '*.js'), ], + + // migration 디렉토리 설정 (migration 생성 시 테스트 확인용) + // migrationsTableName: 'typeorm_migrations', + synchronize: isTsRuntime, migrationsRun: false, - logging: false, + logging: getConfigValue('NODE_ENV') === 'development' ? true : false, + namingStrategy: new SnakeNamingStrategy(), }; }; diff --git a/src/common/database/migrations/1748951601567-CreateInitialTables.ts b/src/common/database/migrations/1748951601567-CreateInitialTables.ts new file mode 100644 index 0000000..6848a0b --- /dev/null +++ b/src/common/database/migrations/1748951601567-CreateInitialTables.ts @@ -0,0 +1,155 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateInitialTables1748951601567 implements MigrationInterface { + name = 'CreateInitialTables1748951601567'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."user_provider_enum" AS ENUM('local', 'apple', 'kakao', 'naver')`, + ); + await queryRunner.query( + `CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "username" character varying NOT NULL, "password" character varying, "nickname" character varying NOT NULL, "role" character varying NOT NULL, "provider" "public"."user_provider_enum" NOT NULL DEFAULT 'local', "providerId" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "refreshToken" character varying, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "public"."career_industry_enum" AS ENUM('IT', 'Finance', 'Manufacturing', 'Education', 'Healthcare', 'Retail', 'Logistics', 'Entertainment', 'Telecom', 'Government', 'Construction', 'Energy', 'Consulting', 'Automotive', 'Other')`, + ); + await queryRunner.query( + `CREATE TYPE "public"."career_position_enum" AS ENUM('backend', 'frontend', 'fullstack', 'mobile', 'devops', 'infra', 'data', 'ai_ml', 'security', 'design', 'pm', 'po', 'qa', 'etc')`, + ); + await queryRunner.query( + `CREATE TABLE "career" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid, "companyName" character varying NOT NULL, "industry" "public"."career_industry_enum", "position" "public"."career_position_enum", "startDate" date NOT NULL, "endDate" date, "summary" text, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_5f694c0aa9babcae2c4ad61c7d0" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "public"."project_type_enum" AS ENUM('work', 'personal')`, + ); + await queryRunner.query( + `CREATE TABLE "project" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" text NOT NULL, "type" "public"."project_type_enum" NOT NULL DEFAULT 'work', "careerId" uuid, "userId" uuid NOT NULL, "startDate" date NOT NULL, "endDate" date, "techStack" text, "responsibility" text, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_4d68b1358bb5b766d3e78f32f57" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "education" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "schoolName" character varying NOT NULL, "major" character varying, "degree" character varying, "startDate" date NOT NULL, "endDate" date, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_bf3d38701b3030a8ad634d43bd6" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "user_profile" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying, "name" character varying, "email" character varying, "phone" character varying, "github" character varying, "blog" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "userId" uuid, CONSTRAINT "PK_f44d0cd18cfd80b0fed7806c3b7" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "resume_download" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "isDownloaded" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "isDeleted" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_ae9985985e4163c75a3f10f36db" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "public"."daily_log_usage_type_enum" AS ENUM('summary', 'tag', 'area', 'title')`, + ); + await queryRunner.query( + `CREATE TABLE "daily_log_usage" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "type" "public"."daily_log_usage_type_enum" NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_5d229e8674dfd0df4b9be05dcb8" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "certificate" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "name" character varying NOT NULL, "issuer" character varying, "issuedDate" date NOT NULL, "expiryDate" date, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8daddfc65f59e341c2bbc9c9e43" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "title" character varying NOT NULL, "organization" character varying, "startDate" date NOT NULL, "endDate" date, "description" text, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TYPE "public"."daily_log_tag_enum" AS ENUM('development', 'debug', 'testing', 'review', 'documentation', 'meeting', 'deployment', 'planning', 'refactoring', 'study')`, + ); + await queryRunner.query( + `CREATE TYPE "public"."daily_log_area_enum" AS ENUM('backend', 'frontend', 'fullstack', 'mobile', 'devops', 'infra', 'data', 'ai_ml', 'security', 'design', 'pm', 'po', 'qa', 'etc')`, + ); + await queryRunner.query( + `CREATE TABLE "daily_log" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "projectId" uuid, "careerId" uuid, "content" character varying NOT NULL, "tag" "public"."daily_log_tag_enum" NOT NULL DEFAULT 'development', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "area" "public"."daily_log_area_enum" NOT NULL DEFAULT 'backend', CONSTRAINT "PK_688636c7fe6fdfce901613c20b6" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "email_verifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "code" character varying NOT NULL, "isVerified" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_c1ea2921e767f83cd44c0af203f" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD CONSTRAINT "FK_e03cdf15acee0efae133fb8afef" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD CONSTRAINT "FK_638174fd5f3847f4d5a41de710b" FOREIGN KEY ("careerId") REFERENCES "career"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD CONSTRAINT "FK_723e67bde13b73c5404305feb14" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD CONSTRAINT "FK_f0d4ac1626368960b634417e7ab" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD CONSTRAINT "FK_e2a943705ec58b0559d229d9684" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD CONSTRAINT "FK_52422eba9e5b9d779d3e173a25d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD CONSTRAINT "FK_162e70e47e7abcedf127ccd9246" FOREIGN KEY ("projectId") REFERENCES "project"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD CONSTRAINT "FK_9b3f028a723752b9e03aebc31ce" FOREIGN KEY ("careerId") REFERENCES "career"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "daily_log" DROP CONSTRAINT "FK_9b3f028a723752b9e03aebc31ce"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" DROP CONSTRAINT "FK_162e70e47e7abcedf127ccd9246"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" DROP CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1"`, + ); + await queryRunner.query( + `ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP CONSTRAINT "FK_52422eba9e5b9d779d3e173a25d"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP CONSTRAINT "FK_e2a943705ec58b0559d229d9684"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP CONSTRAINT "FK_f0d4ac1626368960b634417e7ab"`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`, + ); + await queryRunner.query( + `ALTER TABLE "education" DROP CONSTRAINT "FK_723e67bde13b73c5404305feb14"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP CONSTRAINT "FK_638174fd5f3847f4d5a41de710b"`, + ); + await queryRunner.query( + `ALTER TABLE "career" DROP CONSTRAINT "FK_e03cdf15acee0efae133fb8afef"`, + ); + await queryRunner.query(`DROP TABLE "email_verifications"`); + await queryRunner.query(`DROP TABLE "daily_log"`); + await queryRunner.query(`DROP TYPE "public"."daily_log_area_enum"`); + await queryRunner.query(`DROP TYPE "public"."daily_log_tag_enum"`); + await queryRunner.query(`DROP TABLE "activity"`); + await queryRunner.query(`DROP TABLE "certificate"`); + await queryRunner.query(`DROP TABLE "daily_log_usage"`); + await queryRunner.query(`DROP TYPE "public"."daily_log_usage_type_enum"`); + await queryRunner.query(`DROP TABLE "resume_download"`); + await queryRunner.query(`DROP TABLE "user_profile"`); + await queryRunner.query(`DROP TABLE "education"`); + await queryRunner.query(`DROP TABLE "project"`); + await queryRunner.query(`DROP TYPE "public"."project_type_enum"`); + await queryRunner.query(`DROP TABLE "career"`); + await queryRunner.query(`DROP TYPE "public"."career_position_enum"`); + await queryRunner.query(`DROP TYPE "public"."career_industry_enum"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TYPE "public"."user_provider_enum"`); + } +} diff --git a/src/common/database/migrations/1749007077813-CreateInitialTables.ts b/src/common/database/migrations/1749007077813-CreateInitialTables.ts new file mode 100644 index 0000000..4825e70 --- /dev/null +++ b/src/common/database/migrations/1749007077813-CreateInitialTables.ts @@ -0,0 +1,513 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateInitialTables1749007077813 implements MigrationInterface { + name = 'CreateInitialTables1749007077813'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "career" DROP CONSTRAINT "FK_e03cdf15acee0efae133fb8afef"`, + ); + await queryRunner.query( + `ALTER TABLE "education" DROP CONSTRAINT "FK_723e67bde13b73c5404305feb14"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP CONSTRAINT "FK_52422eba9e5b9d779d3e173a25d"`, + ); + await queryRunner.query( + `ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`, + ); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "providerId"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "refreshToken"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "companyName"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "startDate"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "endDate"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "startDate"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "endDate"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "techStack"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "updatedAt"`); + await queryRunner.query( + `ALTER TABLE "user_profile" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" DROP COLUMN "updatedAt"`, + ); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "schoolName"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "startDate"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "endDate"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "updatedAt"`); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "isDownloaded"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "isDeleted"`, + ); + await queryRunner.query(`ALTER TABLE "certificate" DROP COLUMN "userId"`); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "issuedDate"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "expiryDate"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "updatedAt"`, + ); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "startDate"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "endDate"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "updatedAt"`); + await queryRunner.query( + `ALTER TABLE "email_verifications" DROP COLUMN "isVerified"`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "provider_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "refresh_token" character varying`, + ); + await queryRunner.query(`ALTER TABLE "career" ADD "user_id" uuid`); + await queryRunner.query( + `ALTER TABLE "career" ADD "company_name" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD "start_date" date NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "career" ADD "end_date" date`); + await queryRunner.query( + `ALTER TABLE "career" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "career_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "user_id" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "start_date" date NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "project" ADD "end_date" date`); + await queryRunner.query(`ALTER TABLE "project" ADD "tech_stack" text`); + await queryRunner.query( + `ALTER TABLE "project" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "user_id" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "school_name" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "start_date" date NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "education" ADD "end_date" date`); + await queryRunner.query( + `ALTER TABLE "education" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "user_id" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "is_downloaded" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "is_deleted" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "user_id" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "issued_date" date NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "certificate" ADD "expiry_date" date`); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "user_id" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "start_date" date NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "activity" ADD "end_date" date`); + await queryRunner.query( + `ALTER TABLE "activity" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "user_id" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "project_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "career_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" ADD "is_verified" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD "user_id" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD "created_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63"`, + ); + await queryRunner.query( + `ALTER TABLE "project" ALTER COLUMN "userId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP CONSTRAINT "FK_f0d4ac1626368960b634417e7ab"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ALTER COLUMN "userId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" DROP CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ALTER COLUMN "userId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP CONSTRAINT "FK_e2a943705ec58b0559d229d9684"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ALTER COLUMN "userId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD CONSTRAINT "FK_254cdb8160a28e680566752bcc1" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD CONSTRAINT "FK_5bfcef10ecdda36d2ee68aa2049" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD CONSTRAINT "FK_f0d4ac1626368960b634417e7ab" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD CONSTRAINT "FK_88977bc339155e25ec30c68a9b3" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD CONSTRAINT "FK_10bf0c2dd4736190070e8475119" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD CONSTRAINT "FK_e2a943705ec58b0559d229d9684" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP CONSTRAINT "FK_e2a943705ec58b0559d229d9684"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" DROP CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1"`, + ); + await queryRunner.query( + `ALTER TABLE "activity" DROP CONSTRAINT "FK_10bf0c2dd4736190070e8475119"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP CONSTRAINT "FK_88977bc339155e25ec30c68a9b3"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP CONSTRAINT "FK_f0d4ac1626368960b634417e7ab"`, + ); + await queryRunner.query( + `ALTER TABLE "education" DROP CONSTRAINT "FK_5bfcef10ecdda36d2ee68aa2049"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63"`, + ); + await queryRunner.query( + `ALTER TABLE "career" DROP CONSTRAINT "FK_254cdb8160a28e680566752bcc1"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ALTER COLUMN "userId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD CONSTRAINT "FK_e2a943705ec58b0559d229d9684" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ALTER COLUMN "userId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD CONSTRAINT "FK_ecf384c95be97cafb4570bfb9b1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ALTER COLUMN "userId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD CONSTRAINT "FK_f0d4ac1626368960b634417e7ab" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "project" ALTER COLUMN "userId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD CONSTRAINT "FK_7c4b0d3b77eaf26f8b4da879e63" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" DROP COLUMN "user_id"`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" DROP COLUMN "is_verified"`, + ); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "career_id"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "project_id"`); + await queryRunner.query(`ALTER TABLE "daily_log" DROP COLUMN "user_id"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "end_date"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "start_date"`); + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "user_id"`); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "expiry_date"`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" DROP COLUMN "issued_date"`, + ); + await queryRunner.query(`ALTER TABLE "certificate" DROP COLUMN "user_id"`); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "is_deleted"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "created_at"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "is_downloaded"`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" DROP COLUMN "user_id"`, + ); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "end_date"`); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "start_date"`); + await queryRunner.query( + `ALTER TABLE "education" DROP COLUMN "school_name"`, + ); + await queryRunner.query(`ALTER TABLE "education" DROP COLUMN "user_id"`); + await queryRunner.query( + `ALTER TABLE "user_profile" DROP COLUMN "updated_at"`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" DROP COLUMN "created_at"`, + ); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "tech_stack"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "end_date"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "start_date"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "user_id"`); + await queryRunner.query(`ALTER TABLE "project" DROP COLUMN "career_id"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "end_date"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "start_date"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "company_name"`); + await queryRunner.query(`ALTER TABLE "career" DROP COLUMN "user_id"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "refresh_token"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "provider_id"`); + await queryRunner.query( + `ALTER TABLE "daily_log_usage" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "email_verifications" ADD "isVerified" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "daily_log" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "activity" ADD "endDate" date`); + await queryRunner.query( + `ALTER TABLE "activity" ADD "startDate" date NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD "userId" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "certificate" ADD "expiryDate" date`); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "issuedDate" date NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD "userId" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "isDeleted" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "resume_download" ADD "isDownloaded" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "education" ADD "endDate" date`); + await queryRunner.query( + `ALTER TABLE "education" ADD "startDate" date NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "schoolName" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD "userId" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user_profile" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "project" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "project" ADD "techStack" text`); + await queryRunner.query(`ALTER TABLE "project" ADD "endDate" date`); + await queryRunner.query( + `ALTER TABLE "project" ADD "startDate" date NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query(`ALTER TABLE "career" ADD "endDate" date`); + await queryRunner.query( + `ALTER TABLE "career" ADD "startDate" date NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD "companyName" character varying NOT NULL`, + ); + await queryRunner.query(`ALTER TABLE "career" ADD "userId" uuid`); + await queryRunner.query( + `ALTER TABLE "user" ADD "refreshToken" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "providerId" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "certificate" ADD CONSTRAINT "FK_52422eba9e5b9d779d3e173a25d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "education" ADD CONSTRAINT "FK_723e67bde13b73c5404305feb14" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "career" ADD CONSTRAINT "FK_e03cdf15acee0efae133fb8afef" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } +} diff --git a/src/common/database/migrations/1749020441928-CreateInitialTables.ts b/src/common/database/migrations/1749020441928-CreateInitialTables.ts new file mode 100644 index 0000000..469152b --- /dev/null +++ b/src/common/database/migrations/1749020441928-CreateInitialTables.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateInitialTables1749020441928 implements MigrationInterface { + name = 'CreateInitialTables1749020441928'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "password" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "nickname" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "nickname" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL`, + ); + } +} diff --git a/src/common/database/migrations/1749020634055-CreateInitialTables.ts b/src/common/database/migrations/1749020634055-CreateInitialTables.ts new file mode 100644 index 0000000..dadf669 --- /dev/null +++ b/src/common/database/migrations/1749020634055-CreateInitialTables.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateInitialTables1749020634055 implements MigrationInterface { + name = 'CreateInitialTables1749020634055'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "password" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "nickname" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "nickname" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL`, + ); + } +} diff --git a/src/common/external/external.module.ts b/src/common/external/external.module.ts index a39e2d7..b566ea7 100644 --- a/src/common/external/external.module.ts +++ b/src/common/external/external.module.ts @@ -2,18 +2,10 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ChatGPTDailyTextService } from './chat/chatgpt-daily-text.service'; -import { ChatGPTDailyVoiceService } from './whisper/chatgpt-daily-voice.service'; - -// @Module({ -// imports: [ConfigModule], -// providers: [ChatGPTDailyVoiceService, ChatGPTDailyTextService], -// exports: [ChatGPTDailyVoiceService, ChatGPTDailyTextService], -// }) -// export class ChatGPTDailyExternalModule {} @Module({ imports: [ConfigModule], - providers: [ChatGPTDailyTextService, ChatGPTDailyVoiceService], - exports: [ChatGPTDailyTextService, ChatGPTDailyVoiceService], + providers: [ChatGPTDailyTextService], + exports: [ChatGPTDailyTextService], }) export class ChatGPTDailyExternalModule {} diff --git a/src/common/helper/env.validation.ts b/src/common/helper/env.validation.ts index 595db40..d107503 100644 --- a/src/common/helper/env.validation.ts +++ b/src/common/helper/env.validation.ts @@ -46,15 +46,7 @@ export class EnvironmentVariables { @IsString() @IsNotEmpty() - JWT_PRIVATE_KEY: string; - - @IsString() - @IsNotEmpty() - JWT_PUBLIC_KEY: string; - - @IsString() - @IsNotEmpty() - JWT_REFRESH_TOKEN_PRIVATE_KEY: string; + JWT_SECRET: string; @IsString() @IsOptional() diff --git a/src/common/logging/logging.module.ts b/src/common/logging/logging.module.ts index fad80b0..e6007b1 100644 --- a/src/common/logging/logging.module.ts +++ b/src/common/logging/logging.module.ts @@ -7,7 +7,7 @@ import { LoggingLevelInterceptor } from './logging.interface'; const baseFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format.label({ label: 'hhplus-concert' }), + winston.format.label({ label: 'RECODE' }), winston.format.errors({ stack: true }), ); diff --git a/src/common/modules/mailer.module.ts b/src/common/modules/mailer.module.ts index f5fcc8c..9ea3f61 100644 --- a/src/common/modules/mailer.module.ts +++ b/src/common/modules/mailer.module.ts @@ -1,4 +1,3 @@ -// src/common/modules/mailer.module.ts import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -6,31 +5,52 @@ import { MailerModule } from '@nestjs-modules/mailer'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import * as path from 'path'; +// import { cwd, env } from 'process'; +// import { config } from 'dotenv'; + +// import { getEnvPath } from '../helper/env.helper'; + +// config({ +// path: getEnvPath(cwd()), +// }); + @Module({ imports: [ MailerModule.forRootAsync({ inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - transport: { + useFactory: (config: ConfigService) => { + const mailConfig = { host: config.get('MAIL_HOST'), port: config.get('MAIL_PORT'), - secure: true, - auth: { - user: config.get('MAIL_USER'), - pass: config.get('MAIL_PASS'), + user: config.get('MAIL_USER'), + pass: config.get('MAIL_PASS'), + fromName: config.get('MAIL_FROM_NAME'), + }; + + console.log('Mail Config:', mailConfig); + + return { + transport: { + host: mailConfig.host, + port: mailConfig.port, + secure: true, + auth: { + user: mailConfig.user, + pass: mailConfig.pass, + }, + }, + defaults: { + from: `"${mailConfig.fromName}" <${mailConfig.user}>`, }, - }, - defaults: { - from: `"${config.get('MAIL_FROM_NAME')}" <${config.get('MAIL_USER')}>`, - }, - template: { - dir: path.join(__dirname, '../../../templates'), // 템플릿 폴더 경로 - adapter: new HandlebarsAdapter(), - options: { - strict: true, + template: { + dir: path.join(__dirname, '../templates'), // 템플릿 폴더 경로 + adapter: new HandlebarsAdapter(), + options: { + strict: true, + }, }, - }, - }), + }; + }, }), ], exports: [MailerModule], diff --git a/src/common/modules/typeorm.module.ts b/src/common/modules/typeorm.module.ts index 3b056a3..73697af 100644 --- a/src/common/modules/typeorm.module.ts +++ b/src/common/modules/typeorm.module.ts @@ -22,7 +22,7 @@ export class TypeOrmExModule { inject: [getDataSourceToken()], provide: repository, useFactory: (dataSource: DataSource): typeof repository => { - const baseRepository = dataSource.getRepository(entity); + const baseRepository = dataSource.getRepository(entity); return new repository( baseRepository.target, baseRepository.manager, diff --git a/templates/verify-email.hbs b/src/common/templates/verify-email.hbs similarity index 100% rename from templates/verify-email.hbs rename to src/common/templates/verify-email.hbs diff --git a/src/common/transaction/transaction.module.ts b/src/common/transaction/transaction.module.ts index 4c1d9b3..d9cae14 100644 --- a/src/common/transaction/transaction.module.ts +++ b/src/common/transaction/transaction.module.ts @@ -1,11 +1,9 @@ // src/common/transaction/transaction.module.ts import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { TransactionService } from './transaction.service'; @Module({ - imports: [TypeOrmModule.forFeature([])], providers: [TransactionService], exports: [TransactionService], }) diff --git a/src/common/transaction/transaction.service.ts b/src/common/transaction/transaction.service.ts index d2967bb..7d44ef2 100644 --- a/src/common/transaction/transaction.service.ts +++ b/src/common/transaction/transaction.service.ts @@ -1,4 +1,3 @@ -// src/common/transaction/transaction.service.ts import { Injectable } from '@nestjs/common'; import { DataSource, EntityManager } from 'typeorm'; diff --git a/src/resume/entities/resume-daily-log.entity.ts b/src/resume/entities/resume-daily-log.entity.ts index 841a3ff..a5cc64b 100644 --- a/src/resume/entities/resume-daily-log.entity.ts +++ b/src/resume/entities/resume-daily-log.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; import { DailyTag } from '../enums/daily-tag-enum'; @@ -23,8 +24,8 @@ registerEnumType(WorkArea, { name: 'WorkArea' }); @Entity() export class DailyLog { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => ID) @Column() @@ -32,25 +33,25 @@ export class DailyLog { @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) - @JoinColumn({ name: 'userId' }) + @JoinColumn({ name: 'user_id' }) user: User; @Field(() => ID, { nullable: true }) - @Column({ nullable: true }) + @Column({ name: 'project_id', nullable: true }) projectId?: string; @Field(() => Project, { nullable: true }) @ManyToOne(() => Project, { nullable: true, onDelete: 'SET NULL' }) - @JoinColumn({ name: 'projectId' }) + @JoinColumn({ name: 'project_id' }) project?: Project; @Field(() => ID, { nullable: true }) - @Column({ nullable: true }) + @Column({ name: 'career_id', nullable: true }) careerId?: string; @Field(() => Career, { nullable: true }) @ManyToOne(() => Career, { nullable: true, onDelete: 'SET NULL' }) - @JoinColumn({ name: 'careerId' }) + @JoinColumn({ name: 'career_id' }) career?: Career; @Field() diff --git a/src/resume/entities/resume-user-activity.entity.ts b/src/resume/entities/resume-user-activity.entity.ts index 2884a73..7576634 100644 --- a/src/resume/entities/resume-user-activity.entity.ts +++ b/src/resume/entities/resume-user-activity.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; @@ -16,8 +17,8 @@ import { User } from '../../user/entities/user.entity'; @Entity() export class Activity { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) @@ -36,12 +37,15 @@ export class Activity { @Column({ nullable: true }) organization?: string; + @Field(() => Date) @Column({ type: 'date' }) startDate: Date; + @Field(() => Date, { nullable: true }) @Column({ type: 'date', nullable: true }) endDate?: Date; + @Field({ nullable: true }) @Column('text', { nullable: true }) description?: string; diff --git a/src/resume/entities/resume-user-career.entity.ts b/src/resume/entities/resume-user-career.entity.ts index 4114728..5407e7c 100644 --- a/src/resume/entities/resume-user-career.entity.ts +++ b/src/resume/entities/resume-user-career.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; import { IndustryType } from '../enums/industry-type.enum'; @@ -24,8 +25,8 @@ registerEnumType(WorkArea, { name: 'WorkArea' }); @Entity() export class Career { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) diff --git a/src/resume/entities/resume-user-certificate.entity.ts b/src/resume/entities/resume-user-certificate.entity.ts index 2177c23..dfdeb28 100644 --- a/src/resume/entities/resume-user-certificate.entity.ts +++ b/src/resume/entities/resume-user-certificate.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; @@ -16,8 +17,8 @@ import { User } from '../../user/entities/user.entity'; @Entity() export class Certificate { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) diff --git a/src/resume/entities/resume-user-daily-limit.entity.ts b/src/resume/entities/resume-user-daily-limit.entity.ts index 8ba09e4..4c86b11 100644 --- a/src/resume/entities/resume-user-daily-limit.entity.ts +++ b/src/resume/entities/resume-user-daily-limit.entity.ts @@ -1,4 +1,4 @@ -import { Field, registerEnumType } from '@nestjs/graphql'; +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Column, @@ -6,8 +6,10 @@ import { Entity, JoinColumn, ManyToOne, + PrimaryColumn, PrimaryGeneratedColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; import { DailyLogUsageType } from '../enums/daily-log-usage-type-enum'; @@ -16,17 +18,20 @@ registerEnumType(DailyLogUsageType, { name: 'DailyLogUsageType', }); +@ObjectType() @Entity() export class DailyLogUsage { - @PrimaryGeneratedColumn('uuid') - id: string; + @Field(() => ID) + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) - @JoinColumn({ name: 'userId' }) + @JoinColumn({ name: 'user_id' }) user: User; - @Column() + @Field(() => ID) + @Column({ name: 'user_id' }) userId: string; @Field(() => DailyLogUsageType) diff --git a/src/resume/entities/resume-user-download-limit.entity.ts b/src/resume/entities/resume-user-download-limit.entity.ts index 177d94d..1307f72 100644 --- a/src/resume/entities/resume-user-download-limit.entity.ts +++ b/src/resume/entities/resume-user-download-limit.entity.ts @@ -7,8 +7,9 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; @@ -16,8 +17,8 @@ import { User } from '../../user/entities/user.entity'; @Entity() export class ResumeDownload { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) diff --git a/src/resume/entities/resume-user-education.entity.ts b/src/resume/entities/resume-user-education.entity.ts index dec2a5e..a479ece 100644 --- a/src/resume/entities/resume-user-education.entity.ts +++ b/src/resume/entities/resume-user-education.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; @@ -16,8 +17,8 @@ import { User } from '../../user/entities/user.entity'; @Entity() export class Education { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) diff --git a/src/resume/entities/resume-user-profile.entity.ts b/src/resume/entities/resume-user-profile.entity.ts index 06955ce..8f4bfca 100644 --- a/src/resume/entities/resume-user-profile.entity.ts +++ b/src/resume/entities/resume-user-profile.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; @@ -16,8 +17,8 @@ import { User } from '../../user/entities/user.entity'; @Entity() export class UserProfile { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field(() => User) @ManyToOne(() => User, { onDelete: 'SET NULL' }) diff --git a/src/resume/entities/resume-user-project.entity.ts b/src/resume/entities/resume-user-project.entity.ts index 36ccde3..d854eaa 100644 --- a/src/resume/entities/resume-user-project.entity.ts +++ b/src/resume/entities/resume-user-project.entity.ts @@ -6,9 +6,10 @@ import { Entity, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { User } from '../../user/entities/user.entity'; import { ProjectType } from '../enums/project-type.enum'; @@ -20,8 +21,8 @@ registerEnumType(ProjectType, { name: 'ProjectType' }); @Entity() export class Project { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); @Field() @Column() diff --git a/src/resume/inputs/resume-user-activity.input.ts b/src/resume/inputs/resume-user-activity.input.ts index 45f5bb8..6b9bf7b 100644 --- a/src/resume/inputs/resume-user-activity.input.ts +++ b/src/resume/inputs/resume-user-activity.input.ts @@ -4,10 +4,6 @@ import { IsDate, IsOptional, IsString } from 'class-validator'; @InputType() export class CreateActivityInput { - @Field() - @IsString() - userId: string; - @Field() @IsString() title: string; @@ -26,9 +22,10 @@ export class CreateActivityInput { @IsDate() endDate?: Date; - @Field() + @Field({ nullable: true }) + @IsOptional() @IsString() - description: string; + description?: string; } @InputType() diff --git a/src/resume/resolvers/resume-daily-log.resolver.ts b/src/resume/resolvers/resume-daily-log.resolver.ts index b803312..b5ad22d 100644 --- a/src/resume/resolvers/resume-daily-log.resolver.ts +++ b/src/resume/resolvers/resume-daily-log.resolver.ts @@ -1,4 +1,4 @@ -import { BadRequestException, UseGuards } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -11,11 +11,11 @@ import { } from '../inputs/resume-daily-log.input'; import { DailyLogService } from '../services/resume-daily-log.service'; -@Resolver(() => DailyLog) +@Resolver() export class DailyLogResolver { constructor(private readonly dailyLogService: DailyLogService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => DailyLog) createDailyLog( @CurrentUser() user: User, @@ -24,31 +24,28 @@ export class DailyLogResolver { return this.dailyLogService.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => DailyLog) updateDailyLog( // @CurrentUser() user: User, @Args('input') input: UpdateDailyLogInput, ) { - // return this.dailyLogService.update(user, input); // 토큰받는거 처리 return this.dailyLogService.update(input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [DailyLog]) getMyDailyLogs(@CurrentUser() user: User) { return this.dailyLogService.getMany(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => DailyLog) - // getMyDailyLog(@CurrentUser() user: User, @Args('id') id: string) { - // return this.dailyLogService.getOne(user, id); - // } getMyDailyLog(@Args('id') id: string) { return this.dailyLogService.getOne(id); } + @UseAuthGuard() @Query(() => [DailyLog]) getMyDailyLogsByMonth( @CurrentUser() user: User, diff --git a/src/resume/resolvers/resume-user-activity.resolver.ts b/src/resume/resolvers/resume-user-activity.resolver.ts index 8d408d4..f267430 100644 --- a/src/resume/resolvers/resume-user-activity.resolver.ts +++ b/src/resume/resolvers/resume-user-activity.resolver.ts @@ -1,14 +1,9 @@ -import { - BadRequestException, - NotFoundException, - UseGuards, -} from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { UseAuthGuard } from 'src/common/decorators/auth-guard.decorator'; -import { CurrentUser } from 'src/common/decorators/user.decorator'; -import { User } from 'src/user/entities/user.entity'; - +import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; +import { CurrentUser } from '../../common/decorators/user.decorator'; +import { User } from '../../user/entities/user.entity'; import { Activity } from '../entities/resume-user-activity.entity'; import { CreateActivityInput, @@ -16,18 +11,18 @@ import { } from '../inputs/resume-user-activity.input'; import { ActivityService } from '../services/resume-user-activity.service'; -@Resolver(() => Activity) +@Resolver() export class ActivityResolver { constructor(private readonly activityService: ActivityService) {} @Query(() => [Activity]) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async activities(@CurrentUser() user: User): Promise { return this.activityService.findAllByUserId(user.id); } @Query(() => Activity) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async activity(@Args('id') id: string): Promise { const activity = await this.activityService.findOneById(id); if (!activity) throw new NotFoundException('Activity not found'); @@ -35,7 +30,7 @@ export class ActivityResolver { } @Mutation(() => Activity) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async createActivity( @CurrentUser() user: User, @Args('input') input: CreateActivityInput, @@ -44,7 +39,7 @@ export class ActivityResolver { } @Mutation(() => Activity) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async updateActivity( @Args('input') input: UpdateActivityInput, ): Promise { @@ -52,7 +47,7 @@ export class ActivityResolver { } @Mutation(() => Activity) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async deleteActivity(@Args('id') id: string): Promise { return this.activityService.delete(id); } diff --git a/src/resume/resolvers/resume-user-career.resolver.ts b/src/resume/resolvers/resume-user-career.resolver.ts index 0966441..3062fd9 100644 --- a/src/resume/resolvers/resume-user-career.resolver.ts +++ b/src/resume/resolvers/resume-user-career.resolver.ts @@ -1,4 +1,3 @@ -import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -11,11 +10,11 @@ import { } from '../inputs/resume-user-career.input'; import { CareerService } from '../services/resume-user-career.service'; -@Resolver(() => Career) +@Resolver() export class CareerResolver { constructor(private readonly careerService: CareerService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Career) createCareer( @CurrentUser() user: User, @@ -24,25 +23,25 @@ export class CareerResolver { return this.careerService.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Career) updateCareer(@Args('input') input: UpdateCareerInput) { return this.careerService.update(input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Career) deleteCareer(@Args('id') id: string) { return this.careerService.delete(id); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [Career]) getMyCareers(@CurrentUser() user: User) { return this.careerService.getMany(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Career) getMyCareer(@Args('id') id: string) { return this.careerService.getOne(id); diff --git a/src/resume/resolvers/resume-user-certification.resolver.ts b/src/resume/resolvers/resume-user-certification.resolver.ts index 196f8c3..d712eec 100644 --- a/src/resume/resolvers/resume-user-certification.resolver.ts +++ b/src/resume/resolvers/resume-user-certification.resolver.ts @@ -1,4 +1,3 @@ -import { BadRequestException, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -11,11 +10,11 @@ import { } from '../inputs/resume-user-certificate.input'; import { CertificateService } from '../services/resume-user-certification.service'; -@Resolver(() => Certificate) +@Resolver() export class CertificateResolver { constructor(private readonly certificateService: CertificateService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Certificate) createCertificate( @CurrentUser() user: User, @@ -24,25 +23,25 @@ export class CertificateResolver { return this.certificateService.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Certificate) updateCertificate(@Args('input') input: UpdateCertificateInput) { return this.certificateService.update(input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Certificate) deleteCertificate(@Args('id') id: string) { return this.certificateService.delete(id); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [Certificate]) getCertificates(@CurrentUser() user: User) { return this.certificateService.findAllByUserId(user.id); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Certificate) getCertificate(@Args('id') id: string) { return this.certificateService.findOneById(id); diff --git a/src/resume/resolvers/resume-user-daily-limit.resolver.ts b/src/resume/resolvers/resume-user-daily-limit.resolver.ts index 2594cd2..a1b86b2 100644 --- a/src/resume/resolvers/resume-user-daily-limit.resolver.ts +++ b/src/resume/resolvers/resume-user-daily-limit.resolver.ts @@ -1,5 +1,3 @@ -// src/resume/resolvers/daily-log-usage.resolver.ts -import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -10,11 +8,11 @@ import { DailyLogUsageType } from '../enums/daily-log-usage-type-enum'; import { CreateDailyLogUsageInput } from '../inputs/resume-user-daily-limit.input'; import { DailyLogUsageService } from '../services/resume-user-daily-limit.service'; -@Resolver(() => DailyLogUsage) +@Resolver() export class DailyLogUsageResolver { constructor(private readonly service: DailyLogUsageService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => DailyLogUsage) createDailyLogUsage( @CurrentUser() user: User, @@ -23,13 +21,13 @@ export class DailyLogUsageResolver { return this.service.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [DailyLogUsage]) getMyDailyLogUsages(@CurrentUser() user: User) { return this.service.getAllLogs(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Number) getMyTodayUsageCount( @CurrentUser() user: User, @@ -38,7 +36,7 @@ export class DailyLogUsageResolver { return this.service.getTodayCount(user, type); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Number) getMyMonthlyUsageCount( @CurrentUser() user: User, diff --git a/src/resume/resolvers/resume-user-download-limit.resolver.ts b/src/resume/resolvers/resume-user-download-limit.resolver.ts index a9700dd..8081b72 100644 --- a/src/resume/resolvers/resume-user-download-limit.resolver.ts +++ b/src/resume/resolvers/resume-user-download-limit.resolver.ts @@ -1,4 +1,3 @@ -import { UseGuards } from '@nestjs/common'; import { Int, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -7,23 +6,23 @@ import { User } from '../../user/entities/user.entity'; import { ResumeDownload } from '../entities/resume-user-download-limit.entity'; import { ResumeDownloadService } from '../services/resume-user-download-limit.service'; -@Resolver(() => ResumeDownload) +@Resolver() export class ResumeDownloadResolver { constructor(private readonly service: ResumeDownloadService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => ResumeDownload) createResumeDownload(@CurrentUser() user: User) { return this.service.download(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [ResumeDownload]) getMyResumeDownloads(@CurrentUser() user: User) { return this.service.getAll(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Int) getMyResumeDownloadCount(@CurrentUser() user: User) { return this.service.getCount(user); diff --git a/src/resume/resolvers/resume-user-education.resolver.ts b/src/resume/resolvers/resume-user-education.resolver.ts index c9dac32..6c0fe2f 100644 --- a/src/resume/resolvers/resume-user-education.resolver.ts +++ b/src/resume/resolvers/resume-user-education.resolver.ts @@ -1,8 +1,4 @@ -import { - BadRequestException, - NotFoundException, - UseGuards, -} from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -15,18 +11,18 @@ import { } from '../inputs/resume-user-education.input'; import { EducationService } from '../services/resume-user-education.service'; -@Resolver(() => Education) +@Resolver() export class EducationResolver { constructor(private readonly educationService: EducationService) {} @Query(() => [Education]) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async getEducations(@CurrentUser() user: User): Promise { return this.educationService.findAllByUserId(user.id); } @Query(() => Education) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async getEducation(@Args('id') id: string): Promise { const education = await this.educationService.findOneById(id); if (!education) throw new NotFoundException('Education not found'); @@ -34,7 +30,7 @@ export class EducationResolver { } @Mutation(() => Education) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async createEducation( @CurrentUser() user: User, @Args('input') input: CreateEducationInput, @@ -43,7 +39,7 @@ export class EducationResolver { } @Mutation(() => Education) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async updateEducation( @Args('input') input: UpdateEducationInput, ): Promise { @@ -51,7 +47,7 @@ export class EducationResolver { } @Mutation(() => Education) - @UseGuards(UseAuthGuard) + @UseAuthGuard() async deleteEducation(@Args('id') id: string): Promise { return this.educationService.delete(id); } diff --git a/src/resume/resolvers/resume-user-profile.resolver.ts b/src/resume/resolvers/resume-user-profile.resolver.ts index 5fb83fa..530c9c8 100644 --- a/src/resume/resolvers/resume-user-profile.resolver.ts +++ b/src/resume/resolvers/resume-user-profile.resolver.ts @@ -1,4 +1,3 @@ -import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -11,11 +10,11 @@ import { } from '../inputs/resume-user-profile.input'; import { UserProfileService } from '../services/resume-user-profile.service'; -@Resolver(() => UserProfile) +@Resolver() export class UserProfileResolver { constructor(private readonly userProfileService: UserProfileService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => UserProfile) createUserProfile( @CurrentUser() user: User, @@ -24,7 +23,7 @@ export class UserProfileResolver { return this.userProfileService.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => UserProfile) updateUserProfile( @CurrentUser() user: User, @@ -33,7 +32,7 @@ export class UserProfileResolver { return this.userProfileService.update(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => UserProfile, { nullable: true }) getMyUserProfile(@CurrentUser() user: User) { return this.userProfileService.get(user.id); diff --git a/src/resume/resolvers/resume-user-project.resolver.ts b/src/resume/resolvers/resume-user-project.resolver.ts index 81eb0e7..1502f23 100644 --- a/src/resume/resolvers/resume-user-project.resolver.ts +++ b/src/resume/resolvers/resume-user-project.resolver.ts @@ -1,4 +1,3 @@ -import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { UseAuthGuard } from '../../common/decorators/auth-guard.decorator'; @@ -11,11 +10,11 @@ import { } from '../inputs/resume-user-project.input'; import { ProjectService } from '../services/resume-user-project.service'; -@Resolver(() => Project) +@Resolver() export class ProjectResolver { constructor(private readonly projectService: ProjectService) {} - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Project) createProject( @CurrentUser() user: User, @@ -24,25 +23,25 @@ export class ProjectResolver { return this.projectService.create(user, input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Project) updateProject(@Args('input') input: UpdateProjectInput) { return this.projectService.update(input); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Mutation(() => Project) deleteProject(@Args('id') id: string) { return this.projectService.delete(id); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => [Project]) getMyProjects(@CurrentUser() user: User) { return this.projectService.getMany(user); } - @UseGuards(UseAuthGuard) + @UseAuthGuard() @Query(() => Project) getMyProject(@Args('id') id: string) { return this.projectService.getOne(id); diff --git a/src/resume/resume.module.ts b/src/resume/resume.module.ts index f5f4c53..e4cb960 100644 --- a/src/resume/resume.module.ts +++ b/src/resume/resume.module.ts @@ -2,6 +2,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ChatGPTDailyExternalModule } from '../common/external/external.module'; +import { TypeOrmExModule } from '../common/modules/typeorm.module'; +import { TransactionModule } from '../common/transaction/transaction.module'; +import { UtilModule } from '../common/util/util.module'; import { DailyLog } from './entities/resume-daily-log.entity'; import { Activity } from './entities/resume-user-activity.entity'; import { Career } from './entities/resume-user-career.entity'; @@ -41,62 +44,55 @@ import { ProjectService } from './services/resume-user-project.service'; @Module({ imports: [ - TypeOrmModule.forFeature([ - UserProfile, - DailyLog, - Project, - Career, - Activity, - Certificate, - Education, - DailyLogUsage, - ResumeDownload, + TypeOrmExModule.forCustomRepository([ + UserProfileRepository, + DailyLogRepository, + ProjectRepository, + CareerRepository, + ActivityRepository, + CertificateRepository, + EducationRepository, + DailyLogUsageRepository, + ResumeDownloadRepository, ]), ChatGPTDailyExternalModule, + TransactionModule, + UtilModule, ], providers: [ // Profile - UserProfileRepository, UserProfileService, UserProfileResolver, - // Career - CareerRepository, + // // Career CareerService, CareerResolver, - // Project - ProjectRepository, + // // Project ProjectService, ProjectResolver, - // DailyLog - DailyLogRepository, + // // DailyLog DailyLogService, DailyLogResolver, - // Activity - ActivityRepository, + // // Activity ActivityResolver, ActivityService, - // Certification - CertificateRepository, + // // Certification CertificateResolver, CertificateService, - // Education - EducationRepository, + // // Education EducationResolver, EducationService, - // Daily limit - DailyLogUsageRepository, + // // Daily limit DailyLogUsageResolver, DailyLogUsageService, - // Resume Download - ResumeDownloadRepository, + // // Resume Download ResumeDownloadResolver, ResumeDownloadService, ], diff --git a/src/resume/services/resume-daily-log.service.ts b/src/resume/services/resume-daily-log.service.ts index aa07a77..afcb24d 100644 --- a/src/resume/services/resume-daily-log.service.ts +++ b/src/resume/services/resume-daily-log.service.ts @@ -17,7 +17,6 @@ export class DailyLogService { constructor( private readonly logRepo: DailyLogRepository, private readonly chatgptText: ChatGPTDailyTextService, - // private readonly chatgptVoice: ChatGPTDailyVoiceService, ) {} async create(user: User, input: CreateDailyLogInput): Promise { diff --git a/src/resume/services/resume-user-activity.service.ts b/src/resume/services/resume-user-activity.service.ts index dc57744..cd5e1df 100644 --- a/src/resume/services/resume-user-activity.service.ts +++ b/src/resume/services/resume-user-activity.service.ts @@ -18,12 +18,19 @@ export class ActivityService { ) {} async create(user: User, input: CreateActivityInput): Promise { + console.log('CreateActivityInput type:', typeof input); + console.log('CreateActivityInput:', input); + console.log('User type:', typeof user); + console.log('User:', user); + return this.transactionService.runInTransaction(async (manager) => { const activity = await this.activityRepo.createActivity( user.id, input, manager, ); + console.log('Created Activity type:', typeof activity); + console.log('Created Activity:', activity); return activity; }); } diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index fcfb747..5645e9b 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -1,4 +1,10 @@ -import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { + Field, + HideField, + ID, + ObjectType, + registerEnumType, +} from '@nestjs/graphql'; import * as bcrypt from 'bcrypt'; import { IsEmail, IsEnum } from 'class-validator'; @@ -8,9 +14,10 @@ import { Column, CreateDateColumn, Entity, - PrimaryGeneratedColumn, + PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { ulid } from 'ulid'; import { BCRYPT_HASH_ROUNDS } from '../../common/constants/bcrypt-hash-rounds.constant'; import { SocialProvider } from '../enum/social-provider.enum'; @@ -23,23 +30,24 @@ registerEnumType(SocialProvider, { @Entity() export class User { @Field(() => ID) - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryColumn({ type: 'varchar', length: 26 }) + id: string = ulid(); // @Field(() => String) @IsEmail() @Column() - email?: string; + email: string; @Field(() => String) @Column() username: string; - @Column({ nullable: true }) + @HideField() + @Column() password: string; - @Field(() => String) - @Column() + @Field(() => String, { nullable: true }) + @Column({ nullable: true }) nickname?: string; @Field(() => String) @@ -71,6 +79,7 @@ export class User { }) updatedAt: Date; + @HideField() @Field(() => String, { nullable: true }) @Column({ nullable: true }) refreshToken?: string; @@ -92,6 +101,19 @@ export class User { this.provider = SocialProvider.LOCAL; } } + + @BeforeInsert() + generateId() { + this.id = this.id || ulid(); + } + + // @BeforeInsert() + // @BeforeUpdate() + // async hashPassword() { + // if (this.password && !this.password.startsWith('$2b$')) { + // this.password = await bcrypt.hash(this.password, BCRYPT_HASH_ROUNDS); + // } + // } } @ObjectType() diff --git a/src/user/inputs/user.input.ts b/src/user/inputs/user.input.ts index 15b463b..4564a95 100644 --- a/src/user/inputs/user.input.ts +++ b/src/user/inputs/user.input.ts @@ -21,7 +21,6 @@ export class CreateUserInput implements Partial { password?: string; @Field(() => String) - @IsNotEmpty() nickname?: string; @Field(() => String) diff --git a/src/user/user.resolver.ts b/src/user/user.resolver.ts index 96c3052..76bbc25 100644 --- a/src/user/user.resolver.ts +++ b/src/user/user.resolver.ts @@ -38,13 +38,13 @@ export class UserResolver { } @Mutation(() => User) - @UseAuthGuard('admin') + @UseAuthGuard() createUser(@Args('input') input: CreateUserInput) { return this.userService.create(input); } @Mutation(() => GraphQLJSON) - @UseAuthGuard('admin') + @UseAuthGuard() updateUser(@Args('id') id: string, @Args('input') input: UpdateUserInput) { return this.userService.update(id, input); }