Skip to content
This repository was archived by the owner on Mar 3, 2020. It is now read-only.

Commit b7b3cd8

Browse files
committed
Implement email configuration
1 parent 32aa32f commit b7b3cd8

File tree

10 files changed

+343
-39
lines changed

10 files changed

+343
-39
lines changed

velog-backend/src/lib/common.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { Middleware } from 'koa';
55
import removeMd from 'remove-markdown';
66
import crypto from 'crypto';
77
import axios from 'axios';
8+
import sendMail from 'lib/sendMail';
9+
import EmailCert from 'database/models/EmailCert';
810
import { generate } from './token';
911

1012
export const primaryUUID = {
@@ -138,3 +140,29 @@ export function getHost() {
138140
? 'https://localhost:3000/'
139141
: 'https://velog.io/';
140142
}
143+
144+
export async function sendCertEmail(userId: string, email: string) {
145+
const emailCert = await EmailCert.build({
146+
fk_user_id: userId,
147+
}).save();
148+
await sendMail({
149+
to: email,
150+
subject: 'Velog 이메일 인증',
151+
from: 'Velog <[email protected]>',
152+
body: `<a href="https://velog.io"><img src="https://images.velog.io/email-logo.png" style="display: block; width: 128px; margin: 0 auto;"/></a>
153+
<div style="max-width: 100%; width: 400px; margin: 0 auto; padding: 1rem; text-align: justify; background: #f8f9fa; border: 1px solid #dee2e6; box-sizing: border-box; border-radius: 4px; color: #868e96; margin-top: 0.5rem; box-sizing: border-box; text-align: center;">
154+
안녕하세요! velog 이메일 인증을 진행하시려면 <br/>다음 버튼을 눌러주세요.
155+
</div>
156+
157+
<a href="https://velog.io/certify?code=${
158+
emailCert.code
159+
}" style="text-decoration: none; width: 400px; text-align:center; display:block; margin: 0 auto; margin-top: 1rem; background: #845ef7; padding-top: 1rem; color: white; font-size: 1.25rem; padding-bottom: 1rem; font-weight: 600; border-radius: 4px;">이메일 인증</a>
160+
161+
<div style="text-align: center; margin-top: 1rem; color: #868e96; font-size: 0.85rem;"><div>혹은, 다음 링크를 열어주세요:<br/> <a style="color: #b197fc;" href="https://velog.io/certify?code=${
162+
emailCert.code
163+
}">https://velog.io/certify?code=${
164+
emailCert.code
165+
}</a></div><br/><div>이 링크는 24시간동안 유효합니다. </div></div>`,
166+
});
167+
return emailCert;
168+
}

velog-backend/src/router/auth/auth.ctrl.js

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import EmailAuth from 'database/models/EmailAuth';
99
import SocialAccount from 'database/models/SocialAccount';
1010
import EmailCert from 'database/models/EmailCert';
1111
import { generate, decode } from 'lib/token';
12+
import { sendCertEmail } from 'lib/common';
1213
import getSocialProfile, { type Profile } from 'lib/getSocialProfile';
13-
1414
import type { UserModel } from 'database/models/User';
1515
import type { UserProfileModel } from 'database/models/UserProfile';
1616
import type { EmailAuthModel } from 'database/models/EmailAuth';
@@ -20,31 +20,6 @@ import UserMeta from '../../database/models/UserMeta';
2020

2121
const s3 = new AWS.S3({ region: 'ap-northeast-2', signatureVersion: 'v4' });
2222

23-
async function sendCertEmail(userId, email) {
24-
const emailCert = await EmailCert.build({
25-
fk_user_id: userId,
26-
}).save();
27-
await sendMail({
28-
to: email,
29-
subject: 'Velog 이메일 인증',
30-
from: 'Velog <[email protected]>',
31-
body: `<a href="https://velog.io"><img src="https://images.velog.io/email-logo.png" style="display: block; width: 128px; margin: 0 auto;"/></a>
32-
<div style="max-width: 100%; width: 400px; margin: 0 auto; padding: 1rem; text-align: justify; background: #f8f9fa; border: 1px solid #dee2e6; box-sizing: border-box; border-radius: 4px; color: #868e96; margin-top: 0.5rem; box-sizing: border-box; text-align: center;">
33-
안녕하세요! velog 이메일 인증을 진행하시려면 <br/>다음 버튼을 눌러주세요.
34-
</div>
35-
36-
<a href="https://velog.io/certify?code=${
37-
emailCert.code
38-
}" style="text-decoration: none; width: 400px; text-align:center; display:block; margin: 0 auto; margin-top: 1rem; background: #845ef7; padding-top: 1rem; color: white; font-size: 1.25rem; padding-bottom: 1rem; font-weight: 600; border-radius: 4px;">이메일 인증</a>
39-
40-
<div style="text-align: center; margin-top: 1rem; color: #868e96; font-size: 0.85rem;"><div>혹은, 다음 링크를 열어주세요:<br/> <a style="color: #b197fc;" href="https://velog.io/certify?code=${
41-
emailCert.code
42-
}">https://velog.io/certify?code=${
43-
emailCert.code
44-
}</a></div><br/><div>이 링크는 24시간동안 유효합니다. </div></div>`,
45-
});
46-
return emailCert;
47-
}
4823

4924
export const sendAuthEmail = async (ctx: Context): Promise<*> => {
5025
type BodySchema = {

velog-backend/src/router/common/common.ctrl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Post from 'database/models/Post';
55
import UserMeta from 'database/models/UserMeta';
66
import { generate, decode } from 'lib/token';
77
import { generateUnsubscribeToken, getHost } from 'lib/common';
8+
89
import {
910
getTagsList,
1011
getPostsCountByTagId,

velog-backend/src/router/me/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ me.patch('/profile', meCtrl.updateProfile);
1111
me.get('/unregister-token', meCtrl.generateUnregisterToken);
1212
me.post('/unregister', meCtrl.unregister);
1313
me.get('/email-info', meCtrl.getEmailInfo);
14+
me.patch('/email', meCtrl.changeEmail);
15+
me.post('/resend-certmail', meCtrl.resendCertmail);
1416

1517
export default me;

velog-backend/src/router/me/me.ctrl.js

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// @flow
22
import type { Context } from 'koa';
33
import UserProfile from 'database/models/UserProfile';
4-
import { validateSchema, checkEmpty } from 'lib/common';
4+
import { validateSchema, checkEmpty, sendCertEmail } from 'lib/common';
55
import { generate, decode } from 'lib/token';
66

77
import Joi from 'joi';
88
import User from '../../database/models/User';
99
import UserMeta from '../../database/models/UserMeta';
10+
import EmailCert from '../../database/models/EmailCert';
1011

1112
export const updateProfile = async (ctx: Context): Promise<*> => {
1213
const { user } = ctx;
@@ -125,7 +126,7 @@ export const getEmailInfo = async (ctx: Context) => {
125126
const { email_notification, email_promotion } = userMeta;
126127
const { email, is_certified } = user;
127128
ctx.body = {
128-
email: null,
129+
email,
129130
is_certified,
130131
permissions: {
131132
email_notification,
@@ -136,3 +137,79 @@ export const getEmailInfo = async (ctx: Context) => {
136137
ctx.throw(500, e);
137138
}
138139
};
140+
141+
export const changeEmail = async (ctx: Context) => {
142+
// validate email
143+
const schema = Joi.object().keys({
144+
email: Joi.string()
145+
.email()
146+
.required(),
147+
});
148+
const result = Joi.validate(ctx.request.body, schema);
149+
if (result.error) {
150+
ctx.body = {
151+
name: 'WRONG_EMAIL',
152+
};
153+
ctx.status = 400;
154+
return;
155+
}
156+
157+
// parse email from reqbody
158+
const { email } = ((ctx.request.body: any): { email: string });
159+
160+
try {
161+
// get user
162+
const user = await User.findById(ctx.user.id);
163+
// do duplication check
164+
const exists = await User.findOne({
165+
where: {
166+
email,
167+
},
168+
});
169+
if (exists) {
170+
ctx.status = 409;
171+
ctx.body = {
172+
name: 'EMAIL_EXISTS',
173+
};
174+
return;
175+
}
176+
177+
// change email
178+
user.email = email;
179+
user.is_certified = false;
180+
await user.save();
181+
// list all email certs and disable status
182+
await EmailCert.update(
183+
{
184+
status: false,
185+
},
186+
{
187+
where: {
188+
fk_user_id: ctx.user.id,
189+
status: true,
190+
},
191+
},
192+
);
193+
sendCertEmail(ctx.user.id, email);
194+
ctx.status = 204;
195+
} catch (e) {
196+
ctx.throw(500, e);
197+
}
198+
};
199+
200+
export const resendCertmail = async (ctx: Context) => {
201+
try {
202+
const user = await User.findById(ctx.user.id);
203+
if (user.is_certified) {
204+
ctx.status = 409;
205+
ctx.body = {
206+
name: 'ALREADY_CERTIFIED',
207+
};
208+
return;
209+
}
210+
sendCertEmail(ctx.user.id, user.email);
211+
ctx.status = 204;
212+
} catch (e) {
213+
ctx.throw(500, e);
214+
}
215+
};

velog-frontend/src/components/settings/SettingEmail/SettingEmail.js

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,111 @@
11
// @flow
2-
3-
import React, { Component } from 'react';
2+
import React, { Component, Fragment } from 'react';
43
import type { EmailInfoData } from 'store/modules/settings';
54
import WarningIcon from 'react-icons/lib/md/warning';
65
import cx from 'classnames';
76
import './SettingEmail.scss';
87

98
type Props = {
109
emailInfo: EmailInfoData,
10+
onChangeEmail: (email: string) => Promise<any>,
11+
onResendCertmail: () => Promise<any>,
12+
};
13+
14+
type State = {
15+
edit: boolean,
16+
email: string,
17+
status: 'idle' | 'success' | 'error',
1118
};
1219

13-
class SettingEmail extends Component<Props> {
20+
class SettingEmail extends Component<Props, State> {
21+
state = {
22+
edit: false,
23+
email: '',
24+
status: 'idle',
25+
};
26+
27+
onStartEditing = () => {
28+
this.setState({
29+
edit: true,
30+
});
31+
};
32+
33+
onChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
34+
this.setState({
35+
email: e.target.value,
36+
});
37+
};
38+
39+
onConfirm = async () => {
40+
const { email } = this.state;
41+
try {
42+
await this.props.onChangeEmail(email);
43+
this.setState({
44+
status: 'success',
45+
});
46+
} catch (e) {
47+
this.setState({
48+
status: 'error',
49+
});
50+
// handle error
51+
console.log(e);
52+
}
53+
this.setState({
54+
edit: false,
55+
});
56+
};
57+
58+
onCancel = () => {
59+
this.setState({
60+
edit: false,
61+
email: '',
62+
});
63+
};
64+
1465
render() {
1566
const { emailInfo } = this.props;
67+
const { edit, email } = this.state;
1668
return (
1769
<div className="SettingEmail">
18-
<div className="current-email">
19-
<div className={cx('email', { empty: !emailInfo.email })}>
20-
{!emailInfo.email && <WarningIcon />}
21-
{emailInfo.email || '이메일이 존재하지 않습니다.'}
70+
{edit ? (
71+
<div className="edit-email">
72+
<div className="email-form">
73+
<input value={email} onChange={this.onChange} />
74+
<button className="confirm" onClick={this.onConfirm}>
75+
변경
76+
</button>
77+
<button className="cancel" onClick={this.onCancel}>
78+
취소
79+
</button>
80+
</div>
81+
<div className="caution">
82+
이메일 변경을 하시면 이전 이메일로 다시 로그인 할 수 없습니다.
83+
</div>
2284
</div>
23-
</div>
85+
) : (
86+
<Fragment>
87+
{!emailInfo.is_certified &&
88+
emailInfo.email && (
89+
<div className="need-certify">
90+
<span>
91+
{this.state.status === 'success'
92+
? '인증 메일이 발송되었습니다. 메일을 확인해주세요.'
93+
: '인증되지 않은 이메일입니다.'}
94+
</span>
95+
<button onClick={this.props.onResendCertmail}>인증 메일 재발송</button>
96+
</div>
97+
)}
98+
<div className="current-email">
99+
<div className={cx('email', { empty: !emailInfo.email })}>
100+
{!emailInfo.email && <WarningIcon />}
101+
{emailInfo.email || '이메일이 존재하지 않습니다.'}
102+
<button className="email-action" onClick={this.onStartEditing}>
103+
{emailInfo.email ? '변경' : '등록'}
104+
</button>
105+
</div>
106+
</div>
107+
</Fragment>
108+
)}
24109
</div>
25110
);
26111
}

0 commit comments

Comments
 (0)