1. 이메일 모듈 만들어서 유저 인증하기
Mailgun 서비스 이용하기
fake number을 사용해도 된다.
Sending menu에서 API를 선택하면 아래 node.js mailgun이 있지만 수동으로 만들기 위해서 cURL을 사용한다.
카드등록 시 5,000 번 메일 이용이 가능하지만 등록을 안 했을 시는 셀프 인증을 통해서 진행한다. 셀프 인증은 오른쪽 상단 Authorized Recipients 아래 메일을 입력하고 해당 메일에서 I Agree를 클릭해서 진행하면 된다.
수동으로 만들고 싶지 않다면 nestjs에서 제공하는 nodemailer를 사용한다. 그게 아니라면 API를 이용한 Email module을 만들어보자.
nest g mo mail로 모듈을 생성한다.
모듈 생성 후 mail module에 CONFIG_OPTIONS를 입력한다. 이는 common directory 아래 common.constatnts.js에서 import한다.
import { DynamicModule, Module } from '@nestjs/common';
import { CONFIG_OPTIONS } from 'src/common/common.constants';
import { MailModuleOptions } from './mail.interfaces';
@Module({})
export class MailModule {
static forRoot(options: MailModuleOptions): DynamicModule {
return {
module: MailModule,
providers: [
{
provide: CONFIG_OPTIONS,
useValue: options,
},
],
exports: [],
};
}
}
mail.interfaces.ts 아래 필요한 항목을 작성한다.
export interface MailModuleOptions {
apiKey: string;
domain: string;
fromEmail:string;
}
.env.dev에 필요한 항목을 넣고 app.module.ts 아래 MailModule.forRoot를 작성하고 스키마 유효성을 joi로 검사한다.
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import * as Joi from 'joi';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { CommonModule } from './common/common.module';
import { User } from './users/entities/user.entity';
import { JwtModule } from './jwt/jwt.module';
import { JwtMiddleware } from './jwt/jwt.middleware';
import { AuthModule } from './auth/auth.module';
import { Verification } from './users/entities/verification.entity';
import { MailModule } from './mail/mail.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.test',
ignoreEnvFile: process.env.NODE_ENV === 'prod',
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('dev', 'prod')
.required(),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.string().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
PRIVATE_KEY: Joi.string().required(),
MAILGUN_API_KEY: Joi.string().required(),
MAILGUN_DOMAIN_NAME: Joi.string().required(),
MAILGUN_FROM_EMAIL: Joi.string().required(),
}),
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
synchronize: process.env.NODE_ENV !== 'prod',
logging: process.env.NODE_ENV !== 'prod',
entities: [User, Verification],
}),
GraphQLModule.forRoot({
autoSchemaFile: true,
context: ({ req }) => ({ user: req['user'] }),
}),
JwtModule.forRoot({
privateKey: process.env.PRIVATE_KEY,
}),
MailModule.forRoot({
apiKey: process.env.MAILGUN_API_KEY,
domain: process.env.MAILGUN_DOMAIN_NAME,
fromEmail: process.env.MAILGUN_FROM_EMAIL,
}),
UsersModule,
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(JwtMiddleware)
.forRoutes({ path: '/graphql', method: RequestMethod.POST });
}
}
jwt.service.ts와 비슷한 mail.service.ts에 만들어주고 mail module에 MailService를 provider 하고 export 한다.
import { DynamicModule, Module } from '@nestjs/common';
import { CONFIG_OPTIONS } from 'src/common/common.constants';
import { MailModuleOptions } from './mail.interfaces';
import { MailService } from './mail.service';
@Module({})
export class MailModule {
static forRoot(options: MailModuleOptions): DynamicModule {
return {
module: MailModule,
providers: [
{
provide: CONFIG_OPTIONS,
useValue: options,
},
MailService,
],
exports: [MailService],
};
}
}
API를 사용하기 위해 curl을 사용한다. node.js에는 fetch가 없어서 node i request를 사용한다. 그런데 Deprecated 되어서 got를 사용한다.
npm i got를 설치한다.
header에 base64형식으로 인코딩 한다.
// Mailgun cURL
curl -s --user 'api:YOUR_API_KEY' \
https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages \
-F from='Excited User <mailgun@YOUR_DOMAIN_NAME>' \
-F to=YOU@YOUR_DOMAIN_NAME \
-F to=bar@example.com \
-F subject='Hello' \
-F text='Testing some Mailgun awesomeness!'
여기서 -F는 form을 의미하고 이를 위해 form data npm을 설치한다.
npm i form-data
form_data_1.default is not a constructor 에러는 import * as FormData from 'form-data'; 를 통해서 해결한다.
import got from 'got';
import * as FormData from 'form-data';
import { Inject, Injectable } from '@nestjs/common';
import { CONFIG_OPTIONS } from 'src/common/common.constants';
import { MailModuleOptions } from './mail.interfaces';
@Injectable()
export class MailService {
constructor(
@Inject(CONFIG_OPTIONS) private readonly options: MailModuleOptions,
) {
this.sendEmail('testing', 'test');
}
private async sendEmail(subject: string, content: string) {
const form = new FormData();
form.append('from', `Excited User <mailgun@${this.options.domain}>`);
form.append('to', `seunghwansuh@kakao.com`);
form.append('subject', subject);
form.append('text', content);
const response = await got(
`https://api.mailgun.net/v3/${this.options.domain}/messages`,
{
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(
`api:${this.options.apiKey}`
).toString('base64')}`,
},
body: form,
},
);
console.log(response.body);
}
}
나중에 따로 import 하지 않게 Global로 사용할 수 있게 한다.
최종 mail.service.ts
import got from 'got';
import * as FormData from 'form-data';
import { Inject, Injectable } from '@nestjs/common';
import { CONFIG_OPTIONS } from 'src/common/common.constants';
import { EmailVar, MailModuleOptions } from './mail.interfaces';
@Injectable()
export class MailService {
constructor(
@Inject(CONFIG_OPTIONS) private readonly options: MailModuleOptions,
) {}
private async sendEmail(
subject: string,
template: string,
emailVars: EmailVar[],
) {
const form = new FormData();
form.append(
'from',
`Daniel from Nuber Eats <mailgun@${this.options.domain}>`,
);
form.append('to', `seunghwansuh@kakao.com`);
form.append('subject', subject);
form.append('template', template);
emailVars.forEach(eVar => form.append(`v:${eVar.key}`, eVar.value));
try {
await got(`https://api.mailgun.net/v3/${this.options.domain}/messages`, {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(
`api:${this.options.apiKey}`,
).toString('base64')}`,
},
body: form,
});
} catch (error) {
console.log(error);
}
}
sendVerificationEmail(email: string, code: string) {
this.sendEmail('Verify Your Email', 'verify-email', [
{ key: 'code', value: code },
{ key: 'username', value: email },
]);
}
}
mail.module.ts
import { DynamicModule, Global, Module } from '@nestjs/common';
import { CONFIG_OPTIONS } from 'src/common/common.constants';
import { MailModuleOptions } from './mail.interfaces';
import { MailService } from './mail.service';
@Module({})
@Global()
export class MailModule {
static forRoot(options: MailModuleOptions): DynamicModule {
return {
module: MailModule,
providers: [
{
provide: CONFIG_OPTIONS,
useValue: options,
},
MailService,
],
exports: [MailService],
};
}
}
user.service.ts에 create와 edit에 추가해준다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { JwtService } from 'src/jwt/jwt.service';
import { EditProfileInput, EditProfileOutput } from './dtos/edit-profile.dto';
import { Verification } from './entities/verification.entity';
import { VerifyEmailOutput } from './dtos/verify-email.dto';
import { UserProfileOutput } from './dtos/user-profile.dto';
import { MailService } from 'src/mail/mail.service';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly users: Repository<User>,
@InjectRepository(Verification)
private readonly verifications: Repository<Verification>,
private readonly jwtService: JwtService,
private readonly mailService: MailService,
) {}
async createAccount({
email,
password,
role,
}: CreateAccountInput): Promise<CreateAccountOutput> {
try {
const exists = await this.users.findOne({ email });
if (exists) {
return { ok: false, error: 'There is a user with that email already' };
}
const user = await this.users.save(
this.users.create({ email, password, role }),
);
const verification = await this.verifications.save(
this.verifications.create({
user,
}),
);
this.mailService.sendVerificationEmail(user.email, verification.code);
return { ok: true };
} catch (e) {
return { ok: false, error: "Couldn't create account" };
}
}
async editProfile(
userId: number,
{ email, password }: EditProfileInput,
): Promise<EditProfileOutput> {
try {
const user = await this.users.findOne(userId);
if (email) {
user.email = email;
user.verified = false;
const verification = await this.verifications.save(
this.verifications.create({ user }),
);
this.mailService.sendVerificationEmail(user.email, verification.code);
}
if (password) {
user.password = password;
}
await this.users.save(user);
return {
ok: true,
};
} catch (error) {
return { ok: false, error: 'Could not update profile.' };
}
}
'Uber Eats' 카테고리의 다른 글
Uber Eats # 20 Metadata로 사용자 분기 처리하기 (0) | 2021.03.29 |
---|---|
Uber Eats # 18 User Profile (0) | 2021.03.13 |
Uber Eats # 17 AuthGuard (0) | 2021.03.13 |
Uber Eats # 16 User Authentication (0) | 2021.03.11 |
Uber Eats # 15 User Resolver and Service, InputType과 ObjectType 비교 (0) | 2021.03.09 |