Skip to content

Commit

Permalink
feat: 修改验证账号逻辑,需要管理员开通,修复一些bug
Browse files Browse the repository at this point in the history
  • Loading branch information
“huangzhenting” committed Mar 25, 2023
1 parent f54b7c7 commit 8de040c
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 12 deletions.
15 changes: 15 additions & 0 deletions docker-compose/docker-compose-mongodb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3'

services:
mongo:
image: mongo
container_name: mongodb
restart: always
ports:
- '27017:27017'
volumes:
- ./mongodb:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: chatgpt
MONGO_INITDB_ROOT_PASSWORD: password
MONGO_INITDB_DATABASE: chatgpt
56 changes: 52 additions & 4 deletions service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,31 @@ import { auth } from './middleware/auth'
import { clearConfigCache, getCacheConfig, getOriginConfig } from './storage/config'
import type { ChatOptions, Config, MailConfig, SiteConfig, UserInfo } from './storage/model'
import { Status } from './storage/model'
import { clearChat, createChatRoom, createUser, deleteAllChatRooms, deleteChat, deleteChatRoom, existsChatRoom, getChat, getChatRooms, getChats, getUser, getUserById, insertChat, renameChatRoom, updateChat, updateConfig, updateUserInfo, verifyUser } from './storage/mongo'
import {
clearChat,
createChatRoom,
createUser,
deleteAllChatRooms,
deleteChat,
deleteChatRoom,
existsChatRoom,
getChat,
getChatRooms,
getChats,
getUser,
getUserById,
insertChat,
renameChatRoom,
updateChat,
updateConfig,
updateUserInfo,
verifyUser,
verifyUserAdmin,
} from './storage/mongo'
import { limiter } from './middleware/limiter'
import { isNotEmptyString } from './utils/is'
import { sendTestMail, sendVerifyMail } from './utils/mail'
import { checkUserVerify, getUserVerifyUrl, md5 } from './utils/security'
import { sendNoticeMail, sendTestMail, sendVerifyMail, sendVerifyMailAdmin } from './utils/mail'
import { checkUserVerify, checkUserVerifyAdmin, getUserVerifyUrl, getUserVerifyUrlAdmin, md5 } from './utils/security'
import { rootAuth } from './middleware/rootAuth'

const app = express()
Expand Down Expand Up @@ -321,6 +341,8 @@ router.post('/user-login', async (req, res) => {
|| user.password !== md5(password)) {
if (user != null && user.status === Status.PreVerify)
throw new Error('请去邮箱中验证 | Please verify in the mailbox')
if (user != null && user.status === Status.AdminVerify)
throw new Error('请等待管理员开通 | Please wait for the admin to activate')
throw new Error('用户不存在或密码错误 | User does not exist or incorrect password.')
}
const config = await getCacheConfig()
Expand Down Expand Up @@ -360,8 +382,34 @@ router.post('/verify', async (req, res) => {
if (!token)
throw new Error('Secret key is empty')
const username = await checkUserVerify(token)
const user = await getUser(username)
if (user != null && user.status === Status.Normal) {
res.send({ status: 'Fail', message: '邮箱已存在 | The email exists', data: null })
return
}
await verifyUser(username)
res.send({ status: 'Success', message: '验证成功 | Verify successfully', data: null })
await sendVerifyMailAdmin(username, await getUserVerifyUrlAdmin(username))
res.send({ status: 'Success', message: '验证成功, 请等待管理员开通 | Verify successfully, Please wait for the admin to activate', data: null })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
}
})

router.post('/verifyadmin', async (req, res) => {
try {
const { token } = req.body as { token: string }
if (!token)
throw new Error('Secret key is empty')
const username = await checkUserVerifyAdmin(token)
const user = await getUser(username)
if (user != null && user.status === Status.Normal) {
res.send({ status: 'Fail', message: '邮箱已存在 | The email exists', data: null })
return
}
await verifyUserAdmin(username)
await sendNoticeMail(username)
res.send({ status: 'Success', message: '开通成功 | Activate successfully', data: null })
}
catch (error) {
res.send({ status: 'Fail', message: error.message, data: null })
Expand Down
8 changes: 7 additions & 1 deletion service/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import jwt from 'jsonwebtoken'
import { getCacheConfig } from '../storage/config'
import { getUserById } from '../storage/mongo'
import { Status } from '../storage/model'

const auth = async (req, res, next) => {
const config = await getCacheConfig()
Expand All @@ -8,7 +10,11 @@ const auth = async (req, res, next) => {
const token = req.header('Authorization').replace('Bearer ', '')
const info = jwt.verify(token, config.siteConfig.loginSalt.trim())
req.headers.userId = info.userId
next()
const user = await getUserById(info.userId)
if (user == null || user.status !== Status.Normal)
throw new Error('用户不存在 | User does not exist.')
else
next()
}
catch (error) {
res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null })
Expand Down
1 change: 1 addition & 0 deletions service/src/storage/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Status {
InversionDeleted = 2,
ResponseDeleted = 3,
PreVerify = 4,
AdminVerify = 5,
}

export class UserInfo {
Expand Down
7 changes: 7 additions & 0 deletions service/src/storage/mongo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as dotenv from 'dotenv'
import { MongoClient, ObjectId } from 'mongodb'
import { ChatInfo, ChatRoom, Status, UserInfo } from './model'
import type { ChatOptions, Config } from './model'

dotenv.config()
const url = process.env.MONGODB_URL
const client = new MongoClient(url)
const chatCol = client.db('chatgpt').collection('chat')
Expand Down Expand Up @@ -149,6 +151,11 @@ export async function getUserById(userId: string): Promise<UserInfo> {
}

export async function verifyUser(email: string) {
email = email.toLowerCase()
return await userCol.updateOne({ email }, { $set: { status: Status.AdminVerify, verifyTime: new Date().toLocaleString() } })
}

export async function verifyUserAdmin(email: string) {
email = email.toLowerCase()
return await userCol.updateOne({ email }, { $set: { status: Status.Normal, verifyTime: new Date().toLocaleString() } })
}
Expand Down
23 changes: 23 additions & 0 deletions service/src/utils/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ export async function sendVerifyMail(toMail: string, verifyUrl: string) {
sendMail(toMail, `${config.siteConfig.siteTitle} 账号验证`, mailHtml, config.mailConfig)
}

export async function sendVerifyMailAdmin(toMail: string, verifyUrl: string) {
const config = (await getCacheConfig())

const templatesPath = path.join(__dirname, 'templates')
const mailTemplatePath = path.join(templatesPath, 'mail.admin.template.html')
let mailHtml = fs.readFileSync(mailTemplatePath, 'utf8')
mailHtml = mailHtml.replace(/\${TO_MAIL}/g, toMail)
mailHtml = mailHtml.replace(/\${VERIFY_URL}/g, verifyUrl)
mailHtml = mailHtml.replace(/\${SITE_TITLE}/g, config.siteConfig.siteTitle)
sendMail(config.mailConfig.smtpUserName, `${config.siteConfig.siteTitle} 账号申请`, mailHtml, config.mailConfig)
}

export async function sendNoticeMail(toMail: string) {
const config = (await getCacheConfig())

const templatesPath = path.join(__dirname, 'templates')
const mailTemplatePath = path.join(templatesPath, 'mail.notice.template.html')
let mailHtml = fs.readFileSync(mailTemplatePath, 'utf8')
mailHtml = mailHtml.replace(/\${SITE_DOMAIN}/g, config.siteConfig.siteDomain)
mailHtml = mailHtml.replace(/\${SITE_TITLE}/g, config.siteConfig.siteTitle)
sendMail(toMail, `${config.siteConfig.siteTitle} 账号开通`, mailHtml, config.mailConfig)
}

export async function sendTestMail(toMail: string, config: MailConfig) {
return sendMail(toMail, '测试邮件|Test mail', '这是一封测试邮件|This is test mail', config)
}
Expand Down
25 changes: 25 additions & 0 deletions service/src/utils/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,28 @@ export function checkUserVerify(verify: string) {
return username
throw new Error('Verify failed')
}

// 可以换 aes 等方式
export async function getUserVerifyUrlAdmin(username: string) {
const sign = getUserVerifyAdmin(username)
const config = await getCacheConfig()
return `${config.siteConfig.siteDomain}/#/chat/?verifytokenadmin=${sign}`
}

function getUserVerifyAdmin(username: string) {
const expired = new Date().getTime() + (12 * 60 * 60 * 1000)
const sign = `${username}|${process.env.ROOT_USER}-${expired}`
return `${sign}-${md5(sign)}`
}

export function checkUserVerifyAdmin(verify: string) {
const vs = verify.split('-')
const sign = vs[vs.length - 1]
const expired = vs[vs.length - 2]
vs.splice(vs.length - 2, 2)
const username = vs.join('-')
// 简单点没校验有效期
if (sign === md5(`${username}-${expired}`))
return username.split('|')[0]
throw new Error('Verify failed')
}
140 changes: 140 additions & 0 deletions service/src/utils/templates/mail.admin.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<html>
<head> </head>
<body>
<div class="page flex-col">
<div class="box_3 flex-col" style="
display: flex;
position: relative;
width: 100%;
height: 206px;
background: #ef859d2e;
top: 0;
left: 0;
justify-content: center;
">
<div class="section_1 flex-col" style="
background-image: url(&quot;https://ghproxy.com/https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg&quot;);
position: absolute;
width: 152px;
height: 152px;
display: flex;
top: 130px;
background-size: cover;
border-radius: 50%;
margin: 10px;
"></div>
</div>
<div class="box_4 flex-col" style="
margin-top: 92px;
display: flex;
flex-direction: column;
align-items: center;
">
<div class="text-group_5 flex-col justify-between" style="
display: flex;
flex-direction: column;
align-items: center;
margin: 0 20px;
">
<span class="text_1" style="
font-size: 26px;
font-family: PingFang-SC-Bold, PingFang-SC;
font-weight: bold;
color: #000000;
line-height: 37px;
text-align: center;
">
<target="_blank" style="text-decoration: none; color: #0088cc;">${SITE_TITLE}</a> 账号申请
</span>

<div class="box_2 flex-row" style="
margin: 0 20px;
min-height: 128px;
background: #F7F7F7;
border-radius: 12px;
margin-top: 34px;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 32px 16px;
width: calc(100% - 40px);
">

<div class="text-wrapper_4 flex-col justify-between" style="
display: flex;
flex-direction: column;
margin-left: 30px;
margin-bottom: 16px;
">
<hr>
<span class="text_3" style="
<div style=" font-family: Arial, sans-serif; font-size: 16px; color: #333;">
<h1 style="color: #0088cc;">
账号申请邮箱:${TO_MAIL},账号开通链接为(12小时内有效):
</span>
</div>
<hr style="
display: flex;
position: relative;
border: 1px dashed #ef859d2e;
box-sizing: content-box;
height: 0px;
overflow: visible;
width: 100%;
">
<div class="text-wrapper_4 flex-col justify-between" style="
display: flex;
flex-direction: column;
margin-left: 30px;
">
<hr>
</h1>
<p style="margin-top: 20px;">
请点击以下按钮进行开通:
<span class="text_4" style="
margin-top: 6px;
margin-right: 22px;
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #000000;
line-height: 22px;
"></span>
</div>

<a class="text-wrapper_2 flex-col" style="
min-width: 106px;
height: 38px;
background: #ef859d38;
border-radius: 32px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
margin: auto;
margin-top: 32px;
" href="${VERIFY_URL}">
<span class="text_5" style="
color: #DB214B;
">点我开通</span>
</a>
</div>
<div class="text-group_6 flex-col justify-between" style="
display: flex;
flex-direction: column;
align-items: center;
margin-top: 34px;
">
<span class="text_6" style="
height: 17px;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #00000045;
line-height: 17px;
">此邮件由服务器自动发出,直接回复无效。</span>
</div>
</div>
</div>
</body>
</html>
Loading

0 comments on commit 8de040c

Please sign in to comment.