diff --git a/.env b/.env index 1f489e6..3854287 100644 --- a/.env +++ b/.env @@ -19,7 +19,7 @@ GMAIL_USER=bkd.scheduler@gmail.com GMAIL_PASS= # 앱 비밀번호 또는 OAuth2 토큰 GMAIL_CLIENT_ID=688417162908-iqvnj4ceb8t1dkbjr70dtcafo27m8kqe.apps.googleusercontent.com GMAIL_CLIENT_SECRET=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 -GMAIL_REFRESH_TOKEN=1//04_pSivNoGpPUCgYIARAAGAQSNwF-L9IrO0Kx6jSzq_eQNjdl65f0O2iqKSNpFeZ3gtIGMhOk0oiZsnKrPfWs8jvuEic1NhUoZ0g +GMAIL_REFRESH_TOKEN=1//04P8ekVQmkdtnCgYIARAAGAQSNwF-L9IrqPOyH8oYB-mdjUqw9jGHienVLBTWFdiZgpRnPgFmYnAdbjnstd9RkRVeJErB0NRAwg4 # SMTP 추가 옵션 SMTP_AUTH=true diff --git a/package.json b/package.json index 8bda7f1..95e462b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "dotenv": "^17.2.3", "drizzle-kit": "^0.31.7", "drizzle-orm": "^0.44.7", + "googleapis": "^166.0.0", "ioredis": "^5.8.2", + "nodemailer": "^7.0.10", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" @@ -43,6 +45,7 @@ "@types/ioredis": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/nodemailer": "^7.0.4", "@types/pg": "^8.15.6", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", diff --git a/src/app.module.ts b/src/app.module.ts index 5e2e0e7..d2055ac 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,9 +3,11 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { DbModule } from './db/db.module'; import { RedisModule } from './redis/redis.module'; +import { AccountModule } from './modules/account/account.module'; +import { MailerModule } from './util/mailer/mailer.module'; @Module({ - imports: [DbModule, RedisModule], + imports: [DbModule, RedisModule, MailerModule, AccountModule], controllers: [AppController], providers: [AppService], }) diff --git a/src/main.ts b/src/main.ts index f76bc8d..9cc8cdd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import dotenv from 'dotenv'; + +dotenv.config(); async function bootstrap() { const app = await NestFactory.create(AppModule); diff --git a/src/modules/account/account.controller.ts b/src/modules/account/account.controller.ts index 90e654e..219df5e 100644 --- a/src/modules/account/account.controller.ts +++ b/src/modules/account/account.controller.ts @@ -1,14 +1,27 @@ -import { Controller, Get, Post, Query } from "@nestjs/common"; +import { Body, Controller, Get, Post, Query } from "@nestjs/common"; import { CheckDuplicationRequestDto } from "./dto/checkDuplication/check-duplication-request.dto"; import { CheckDuplicationResponseDto } from "./dto/checkDuplication/check-duplication-response.dto"; import { AccountService } from "./account.service"; +import { SendVerificationCodeRequestDto } from "./dto/checkDuplication/send-verification-code-request.dto"; +import { SendVerificationCodeResponseDto } from "./dto/checkDuplication/send-verification-code-response.dto"; @Controller('account') export class AccountController { constructor(private readonly accountService: AccountService) {} + @Get('/') + async test() { + return "Test" + } + @Get('check-duplication') async checkDuplication(@Query() query: CheckDuplicationRequestDto): Promise { - return this.accountService.checkDuplication(query); + return await this.accountService.checkDuplication(query); + } + + @Post('send-verification-code') + async sendVerificationCode(@Body() body: SendVerificationCodeRequestDto): Promise { + const result = await this.accountService.sendVerificationCode(body); + return result; } } \ No newline at end of file diff --git a/src/modules/account/account.module.ts b/src/modules/account/account.module.ts index eefd14a..62e3171 100644 --- a/src/modules/account/account.module.ts +++ b/src/modules/account/account.module.ts @@ -5,6 +5,7 @@ import { AccountService } from "./account.service"; @Module({ controllers: [AccountController], - providers: [AccountService, AccountRepo] + providers: [AccountService, AccountRepo], + exports: [AccountService, AccountRepo] }) export class AccountModule {} \ No newline at end of file diff --git a/src/modules/account/account.service.ts b/src/modules/account/account.service.ts index f3676c3..0522b83 100644 --- a/src/modules/account/account.service.ts +++ b/src/modules/account/account.service.ts @@ -2,14 +2,33 @@ import { Injectable } from "@nestjs/common"; import { AccountRepo } from "./account.repo"; import { CheckDuplicationRequestDto } from "./dto/checkDuplication/check-duplication-request.dto"; import { CheckDuplicationResponseDto } from "./dto/checkDuplication/check-duplication-response.dto"; +import { SendVerificationCodeRequestDto } from "./dto/checkDuplication/send-verification-code-request.dto"; +import { MailerService } from "src/util/mailer/mailer.service"; +import { Generator } from "src/util/generator"; +import { SendVerificationCodeResponseDto } from "./dto/checkDuplication/send-verification-code-response.dto"; @Injectable() export class AccountService { - constructor(private readonly accountRepo: AccountRepo) {} + constructor( + private readonly accountRepo: AccountRepo + , private readonly mailerService: MailerService + ) {} async checkDuplication(data: CheckDuplicationRequestDto): Promise { const count = await this.accountRepo.checkDuplication(data.type, data.value); return { isDuplicated: count > 0 }; } + + async sendVerificationCode(data: SendVerificationCodeRequestDto): Promise { + const code = Generator.getVerificationCode(); + const html = `

Your verification code is: ${code}

`; + const result = await this.mailerService.sendMail(data.email, " 이메일 인증 코드", html); + + if (result.rejected.length > 0) { + return { success: false, error: result.response } + } else { + return { success: true, message: "이메일 발송 완료" }; + } + } } \ No newline at end of file diff --git a/src/modules/account/dto/checkDuplication/send-verification-code-request.dto.ts b/src/modules/account/dto/checkDuplication/send-verification-code-request.dto.ts new file mode 100644 index 0000000..d924521 --- /dev/null +++ b/src/modules/account/dto/checkDuplication/send-verification-code-request.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from "@nestjs/class-validator"; + +export class SendVerificationCodeRequestDto { + @IsEmail() + email: string; +} \ No newline at end of file diff --git a/src/modules/account/dto/checkDuplication/send-verification-code-response.dto.ts b/src/modules/account/dto/checkDuplication/send-verification-code-response.dto.ts new file mode 100644 index 0000000..04f8725 --- /dev/null +++ b/src/modules/account/dto/checkDuplication/send-verification-code-response.dto.ts @@ -0,0 +1,5 @@ +export class SendVerificationCodeResponseDto { + success: boolean; + message?: string; + error?: string; +} \ No newline at end of file diff --git a/src/util/converter.ts b/src/util/converter.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/util/generator.ts b/src/util/generator.ts new file mode 100644 index 0000000..1547cfa --- /dev/null +++ b/src/util/generator.ts @@ -0,0 +1,5 @@ +export class Generator { + static getVerificationCode() { + return Math.random().toString().slice(2, 8); + } +} \ No newline at end of file diff --git a/src/util/mailer/mailer.module.ts b/src/util/mailer/mailer.module.ts new file mode 100644 index 0000000..2f52c92 --- /dev/null +++ b/src/util/mailer/mailer.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { MailerService } from './mailer.service'; + +@Global() +@Module({ + providers: [MailerService], + exports: [MailerService] +}) +export class MailerModule {} \ No newline at end of file diff --git a/src/util/mailer/mailer.service.ts b/src/util/mailer/mailer.service.ts new file mode 100644 index 0000000..4a3d1e7 --- /dev/null +++ b/src/util/mailer/mailer.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import nodemailer from 'nodemailer'; +import { google } from 'googleapis'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +@Injectable() +export class MailerService { + private oauth2Client = new google.auth.OAuth2( + process.env.GMAIL_CLIENT_ID, + process.env.GMAIL_CLIENT_SECRET, + 'https://developers.google.com/oauthplayground' + ); + + constructor() { + this.oauth2Client.setCredentials({ + refresh_token: process.env.GMAIL_REFRESH_TOKEN + }); + } + + async sendMail(to: string, subject: string, html: string) { + const accessToken = await this.oauth2Client.getAccessToken(); + console.log(accessToken); + const options: SMTPTransport.Options = { + host: "smtp.gmail.com", + port: 465, + secure: true, + auth: { + type: "OAuth2", + user: process.env.GMAIL_USER, + clientId: process.env.GMAIL_CLIENT_ID, + clientSecret: process.env.GMAIL_CLIENT_SECRET, + refreshToken: process.env.GMAIL_REFRESH_TOKEN, + accessToken: accessToken?.token || '' + } + } + + const transporter = nodemailer.createTransport(options); + + return transporter.sendMail({ + from: `Scheduler ${process.env.GMAIL_USER}>`, + to, + subject, + html + }) + } +} \ No newline at end of file