Skip to content

Commit f0f1cfb

Browse files
author
Kerwin
committed
feat: support password reset
1 parent 77c8a32 commit f0f1cfb

File tree

20 files changed

+360
-44
lines changed

20 files changed

+360
-44
lines changed

service/src/index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import {
2727
updateChat,
2828
updateConfig,
2929
updateUserInfo,
30+
updateUserPassword,
3031
verifyUser,
3132
} from './storage/mongo'
3233
import { limiter } from './middleware/limiter'
3334
import { isEmail, isNotEmptyString } from './utils/is'
34-
import { sendNoticeMail, sendTestMail, sendVerifyMail, sendVerifyMailAdmin } from './utils/mail'
35-
import { checkUserVerify, checkUserVerifyAdmin, getUserVerifyUrl, getUserVerifyUrlAdmin, md5 } from './utils/security'
35+
import { sendNoticeMail, sendResetPasswordMail, sendTestMail, sendVerifyMail, sendVerifyMailAdmin } from './utils/mail'
36+
import { checkUserResetPassword, checkUserVerify, checkUserVerifyAdmin, getUserResetPasswordUrl, getUserVerifyUrl, getUserVerifyUrlAdmin, md5 } from './utils/security'
3637
import { rootAuth } from './middleware/rootAuth'
3738

3839
dotenv.config()
@@ -468,6 +469,43 @@ router.post('/user-login', async (req, res) => {
468469
}
469470
})
470471

472+
router.post('/user-send-reset-mail', async (req, res) => {
473+
try {
474+
const { username } = req.body as { username: string }
475+
if (!username || !isEmail(username))
476+
throw new Error('请输入格式正确的邮箱 | Please enter a correctly formatted email address.')
477+
478+
const user = await getUser(username)
479+
if (user == null || user.status !== Status.Normal)
480+
throw new Error('账户状态异常 | Account status abnormal.')
481+
await sendResetPasswordMail(username, await getUserResetPasswordUrl(username))
482+
res.send({ status: 'Success', message: '重置邮件已发送 | Reset email has been sent', data: null })
483+
}
484+
catch (error) {
485+
res.send({ status: 'Fail', message: error.message, data: null })
486+
}
487+
})
488+
489+
router.post('/user-reset-password', async (req, res) => {
490+
try {
491+
const { username, password, sign } = req.body as { username: string; password: string; sign: string }
492+
if (!username || !password || !isEmail(username))
493+
throw new Error('用户名或密码为空 | Username or password is empty')
494+
if (!sign || !checkUserResetPassword(sign, username))
495+
throw new Error('链接失效, 请重新发送 | The link is invalid, please resend.')
496+
const user = await getUser(username)
497+
if (user == null || user.status !== Status.Normal)
498+
throw new Error('账户状态异常 | Account status abnormal.')
499+
500+
updateUserPassword(user._id.toString(), md5(password))
501+
502+
res.send({ status: 'Success', message: '密码重置成功 | Password reset successful', data: null })
503+
}
504+
catch (error) {
505+
res.send({ status: 'Fail', message: error.message, data: null })
506+
}
507+
})
508+
471509
router.post('/user-info', auth, async (req, res) => {
472510
try {
473511
const { name, avatar, description } = req.body as UserInfo

service/src/storage/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ export class UserInfo {
2020
verifyTime?: string
2121
avatar?: string
2222
description?: string
23+
updateTime?: string
2324
constructor(email: string, password: string) {
2425
this.name = email
2526
this.email = email
2627
this.password = password
2728
this.status = Status.PreVerify
2829
this.createTime = new Date().toLocaleString()
2930
this.verifyTime = null
31+
this.updateTime = new Date().toLocaleString()
3032
}
3133
}
3234

service/src/storage/mongo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ export async function updateUserInfo(userId: string, user: UserInfo) {
160160
return result
161161
}
162162

163+
export async function updateUserPassword(userId: string, password: string) {
164+
const result = userCol.updateOne({ _id: new ObjectId(userId) }
165+
, { $set: { password, updateTime: new Date().toLocaleString() } })
166+
return result
167+
}
168+
163169
export async function getUser(email: string): Promise<UserInfo> {
164170
email = email.toLowerCase()
165171
return await userCol.findOne({ email }) as UserInfo

service/src/utils/mail.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export async function sendVerifyMailAdmin(toMail: string, verifyName: string, ve
2727
sendMail(toMail, `${config.siteConfig.siteTitle} 账号申请`, mailHtml, config.mailConfig)
2828
}
2929

30+
export async function sendResetPasswordMail(toMail: string, verifyUrl: string) {
31+
const config = (await getCacheConfig())
32+
const templatesPath = path.join(__dirname, 'templates')
33+
const mailTemplatePath = path.join(templatesPath, 'mail.resetpassword.template.html')
34+
let mailHtml = fs.readFileSync(mailTemplatePath, 'utf8')
35+
mailHtml = mailHtml.replace(/\${VERIFY_URL}/g, verifyUrl)
36+
mailHtml = mailHtml.replace(/\${SITE_TITLE}/g, config.siteConfig.siteTitle)
37+
sendMail(toMail, `${config.siteConfig.siteTitle} 密码重置`, mailHtml, config.mailConfig)
38+
}
39+
3040
export async function sendNoticeMail(toMail: string) {
3141
const config = (await getCacheConfig())
3242

service/src/utils/security.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,30 @@ export async function getUserVerifyUrl(username: string) {
1919
}
2020

2121
function getUserVerify(username: string) {
22+
return getVerify(username, '')
23+
}
24+
function getVerify(username: string, key: string) {
2225
const expired = new Date().getTime() + (12 * 60 * 60 * 1000)
23-
const sign = `${username}-${expired}`
26+
const sign = `${username}${key}-${expired}`
2427
return `${sign}-${md5(sign)}`
2528
}
2629

27-
export function checkUserVerify(verify: string) {
30+
function checkVerify(verify: string) {
2831
const vs = verify.split('-')
2932
const sign = vs[vs.length - 1]
3033
const expired = vs[vs.length - 2]
3134
vs.splice(vs.length - 2, 2)
32-
const username = vs.join('-')
35+
const prefix = vs.join('-')
3336
// 简单点没校验有效期
34-
if (sign === md5(`${username}-${expired}`))
35-
return username
37+
if (sign === md5(`${prefix}-${expired}`))
38+
return prefix.split('|')[0]
3639
throw new Error('Verify failed')
3740
}
3841

42+
export function checkUserVerify(verify: string) {
43+
return checkVerify(verify)
44+
}
45+
3946
// 可以换 aes 等方式
4047
export async function getUserVerifyUrlAdmin(username: string) {
4148
const sign = getUserVerifyAdmin(username)
@@ -44,19 +51,26 @@ export async function getUserVerifyUrlAdmin(username: string) {
4451
}
4552

4653
function getUserVerifyAdmin(username: string) {
47-
const expired = new Date().getTime() + (12 * 60 * 60 * 1000)
48-
const sign = `${username}|${process.env.ROOT_USER}-${expired}`
49-
return `${sign}-${md5(sign)}`
54+
return getVerify(username, `|${process.env.ROOT_USER}`)
5055
}
5156

5257
export function checkUserVerifyAdmin(verify: string) {
53-
const vs = verify.split('-')
54-
const sign = vs[vs.length - 1]
55-
const expired = vs[vs.length - 2]
56-
vs.splice(vs.length - 2, 2)
57-
const username = vs.join('-')
58-
// 简单点没校验有效期
59-
if (sign === md5(`${username}-${expired}`))
60-
return username.split('|')[0]
58+
return checkVerify(verify)
59+
}
60+
61+
export async function getUserResetPasswordUrl(username: string) {
62+
const sign = getUserResetPassword(username)
63+
const config = await getCacheConfig()
64+
return `${config.siteConfig.siteDomain}/#/chat/?verifyresetpassword=${sign}`
65+
}
66+
67+
function getUserResetPassword(username: string) {
68+
return getVerify(username, '|rp')
69+
}
70+
71+
export function checkUserResetPassword(verify: string, username: string) {
72+
const name = checkVerify(verify)
73+
if (name === username)
74+
return name
6175
throw new Error('Verify failed')
6276
}

service/src/utils/templates/mail.admin.template.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@
6767
margin-bottom: 16px;
6868
">
6969
<hr>
70-
<span class="text_3" style="
71-
<div style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
70+
<span class="text_3" style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
7271
<h1 style="color: #0088cc;">
7372
账号申请邮箱:${TO_MAIL},账号开通链接为(12小时内有效):
7473
</span>

service/src/utils/templates/mail.notice.template.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@
6767
margin-bottom: 16px;
6868
">
6969
<hr>
70-
<span class="text_3" style="
71-
<div style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
70+
<span class="text_3" style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
7271
<h1 style="color: #0088cc;">
7372
感谢您使用
7473
<a target="_blank" style="text-decoration: none; color: #0088cc;">${SITE_TITLE}</a>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<html>
2+
3+
<head> </head>
4+
5+
<body>
6+
<div class="page flex-col">
7+
<div class="box_3 flex-col" style="
8+
display: flex;
9+
position: relative;
10+
width: 100%;
11+
height: 206px;
12+
background: #ef859d2e;
13+
top: 0;
14+
left: 0;
15+
justify-content: center;
16+
">
17+
<div class="section_1 flex-col" style="
18+
background-image: url(&quot;https://ghproxy.com/https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg&quot;);
19+
position: absolute;
20+
width: 152px;
21+
height: 152px;
22+
display: flex;
23+
top: 130px;
24+
background-size: cover;
25+
border-radius: 50%;
26+
margin: 10px;
27+
"></div>
28+
</div>
29+
<div class="box_4 flex-col" style="
30+
margin-top: 92px;
31+
display: flex;
32+
flex-direction: column;
33+
align-items: center;
34+
">
35+
<div class="text-group_5 flex-col justify-between" style="
36+
display: flex;
37+
flex-direction: column;
38+
align-items: center;
39+
margin: 0 20px;
40+
">
41+
<span class="text_1" style="
42+
font-size: 26px;
43+
font-family: PingFang-SC-Bold, PingFang-SC;
44+
font-weight: bold;
45+
color: #000000;
46+
line-height: 37px;
47+
text-align: center;
48+
">
49+
<target="_blank" style="text-decoration: none; color: #0088cc;">${SITE_TITLE}</a> 重置密码
50+
</span>
51+
52+
<div class="box_2 flex-row" style="
53+
margin: 0 20px;
54+
min-height: 128px;
55+
background: #F7F7F7;
56+
border-radius: 12px;
57+
margin-top: 34px;
58+
display: flex;
59+
flex-direction: column;
60+
align-items: flex-start;
61+
padding: 32px 16px;
62+
width: calc(100% - 40px);
63+
">
64+
65+
<div class="text-wrapper_4 flex-col justify-between" style="
66+
display: flex;
67+
flex-direction: column;
68+
margin-left: 30px;
69+
margin-bottom: 16px;
70+
">
71+
<hr>
72+
<span class="text_3" style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
73+
<h1 style="color: #0088cc;">
74+
感谢您使用
75+
<a target="_blank" style="text-decoration: none; color: #0088cc;">${SITE_TITLE}</a>
76+
您的重置密码链接为(12小时内有效):
77+
</span>
78+
</div>
79+
<hr style="
80+
display: flex;
81+
position: relative;
82+
border: 1px dashed #ef859d2e;
83+
box-sizing: content-box;
84+
height: 0px;
85+
overflow: visible;
86+
width: 100%;
87+
">
88+
<div class="text-wrapper_4 flex-col justify-between" style="
89+
display: flex;
90+
flex-direction: column;
91+
margin-left: 30px;
92+
">
93+
<hr>
94+
</h1>
95+
<p style="margin-top: 20px;">
96+
请点击以下按钮进行重置密码:
97+
<span class="text_4" style="
98+
margin-top: 6px;
99+
margin-right: 22px;
100+
font-size: 16px;
101+
font-family: PingFangSC-Regular, PingFang SC;
102+
font-weight: 400;
103+
color: #000000;
104+
line-height: 22px;
105+
"></span>
106+
</div>
107+
108+
<a target="_blank" class="text-wrapper_2 flex-col" style="
109+
min-width: 106px;
110+
height: 38px;
111+
background: #ef859d38;
112+
border-radius: 32px;
113+
display: flex;
114+
align-items: center;
115+
justify-content: center;
116+
text-decoration: none;
117+
margin: auto;
118+
margin-top: 32px;
119+
" href="${VERIFY_URL}">
120+
<span class="text_5" style="
121+
color: #DB214B;
122+
">重置密码</span>
123+
</a>
124+
</div>
125+
<div class="text-group_6 flex-col justify-between" style="
126+
display: flex;
127+
flex-direction: column;
128+
align-items: center;
129+
margin-top: 34px;
130+
">
131+
<span class="text_6" style="
132+
height: 17px;
133+
font-size: 12px;
134+
font-family: PingFangSC-Regular, PingFang SC;
135+
font-weight: 400;
136+
color: #00000045;
137+
line-height: 17px;
138+
">此邮件由服务器自动发出,直接回复无效。</span>
139+
</div>
140+
</div>
141+
</div>
142+
</body>
143+
144+
</html>

service/src/utils/templates/mail.template.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@
6767
margin-bottom: 16px;
6868
">
6969
<hr>
70-
<span class="text_3" style="
71-
<div style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
70+
<span class="text_3" style="font-family: Arial, sans-serif; font-size: 16px; color: #333;">
7271
<h1 style="color: #0088cc;">
7372
感谢您使用
7473
<a target="_blank" style="text-decoration: none; color: #0088cc;">${SITE_TITLE}</a>

src/api/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ export function fetchLogin<T = any>(username: string, password: string) {
8686
})
8787
}
8888

89+
export function fetchSendResetMail<T = any>(username: string) {
90+
return post<T>({
91+
url: '/user-send-reset-mail',
92+
data: { username },
93+
})
94+
}
95+
96+
export function fetchResetPassword<T = any>(username: string, password: string, sign: string) {
97+
return post<T>({
98+
url: '/user-reset-password',
99+
data: { username, password, sign },
100+
})
101+
}
102+
89103
export function fetchRegister<T = any>(username: string, password: string) {
90104
return post<T>({
91105
url: '/user-register',

0 commit comments

Comments
 (0)