Skip to content

Commit

Permalink
注册功能完成
Browse files Browse the repository at this point in the history
  • Loading branch information
LeafYeeXYZ committed Aug 2, 2024
1 parent a460156 commit a935823
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 14 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ MailBox is a web application that allows you to send and receive emails serverle
## TODO
- [x] 身份验证和用户数据存储 (MongoDB)
- [x] 接收邮件功能 (Cloudflare Mail Workers -> Next.js -> MongoDB)
- [x] 注册功能 (服务端注册条件控制)
- [ ] 单条邮件阅读组件
- [ ] 夜间模式
- [ ] 个人资料页面
- [ ] 发送邮件功能 (Resend)
- [ ] 已发送邮件页面
- [ ] 支持 Markdown 写邮件 (Marked)
- [ ] 注册功能 (服务端注册条件控制)
- [ ] 找回密码功能 (向备用邮箱发送验证码)
- [ ] AI 总结邮件内容生成邮件摘要 (Cloudflare Workers AI)

Expand All @@ -29,6 +31,9 @@ Deploy this `Next.js` project to `Vercel` with the following environment variabl
| `RESEND_API_KEY` | API key of Resend | | Yes |
| `MONGODB_URI` | URI of MongoDB Atlas | | Yes |
| `PEER_AUTH_KEY` | For authenticating between Cloudflare Workers and Next.js | | Yes |
| `NEXT_PUBLIC_MAIL_SERVER` | The domain of your mail server, e.g. `mail.example.com` | | Yes |
| `REGISTRY_KEY` | If set, only users with this key can register | | |
| `NEXT_PUBLIC_REGISTRY_SET` | If `REGISTRY_KEY` is set, this should be set to `true` | | |

### 4 Config Workers Environment Variables
Create `/workers/wrangler.toml` and add the following content.
Expand Down
2 changes: 1 addition & 1 deletion app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export default function AuthLayout({ children }: Readonly<{ children: React.Reac

return (
<div
className='flex items-center justify-center h-dvh w-dvw bg-gray-50 dark:bg-gray-950'
className='flex items-center justify-center h-dvh w-dvw bg-gray-50'
>
<div
className='w-[95%] h-[95%] max-w-[45rem] max-h-[30rem] flex flex-row gap-4 p-4 bg-white rounded-lg shadow-lg'
Expand Down
42 changes: 42 additions & 0 deletions app/(auth)/registry/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use server'

import { MongoClient } from 'mongodb'
import sha256 from 'crypto-js/sha256'

// 连接 MongoDB
const client = new MongoClient(process.env.MONGODB_URI!)
const db = client.db('mailbox')
const user = db.collection('user')

export async function registry(
username: string,
email: string,
password: string,
authCode: string = ''
): Promise<string> {
const fullEmail = `${email}@${process.env.NEXT_PUBLIC_MAIL_SERVER}`
try {
// 验证 authCode
if (process.env.REGISTRY_KEY && authCode !== process.env.REGISTRY_KEY) {
return '注册码错误'
}
// 查询是否已存在
const result = await user.findOne({ email: fullEmail })
if (result) {
return '邮箱已注册'
}
// 插入数据
await user.insertOne({
username,
email: fullEmail,
password: sha256(password).toString(),
role: 'user',
active: true,
createTime: Date.now(),
updateTime: Date.now()
})
return '200'
} catch (err) {
throw new Error(err instanceof Error ? err.message : '未知错误')
}
}
151 changes: 146 additions & 5 deletions app/(auth)/registry/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,155 @@
'use client'

import { Button } from 'antd'
import { Button, Input, Form, Flex, message } from 'antd'
import { UserOutlined, KeyOutlined, MailOutlined, CodeOutlined } from '@ant-design/icons'
import { useRouter } from 'next/navigation'
import { useState, useOptimistic } from 'react'
import { registry } from './action'

type FieldType = {
username: string
email: string // without '@' and domain
password: string // before encryption
confirm: string // should be the same as password
authCode?: string // registration code
}

export default function Login() {

export default function Register() {
const router = useRouter()
const [messageAPI, contextHolder] = message.useMessage()
const [disableForm] = useState(false)
const [disableState, setDisableState] = useOptimistic(
disableForm,
(_, value: boolean) => value
)

const handleSubmit = async (values: FieldType) => {
setDisableState(true)
messageAPI.open({
type: 'loading',
content: '正在注册...',
duration: 0,
key: 'registering'
})
await registry(values.username, values.email, values.password, values.authCode ?? '')
.then(res => {
if (res === '200') {
messageAPI.destroy()
messageAPI.open({
type: 'success',
content: '注册成功 (2秒后自动跳转至登录页面)',
duration: 2,
key: 'success'
})
setTimeout(() => {
router.push('/login')
}, 2000)
} else {
messageAPI.destroy()
messageAPI.open({
type: 'error',
content: `注册失败: ${res}`,
duration: 3,
key: 'error'
})
}
}).catch(() => {
messageAPI.destroy()
messageAPI.open({
type: 'error',
content: '注册失败, 未知错误',
duration: 3,
key: 'error'
})
})
}

return (
<div className='flex flex-col items-center justify-center h-full w-full'>
<div className="text-center text-lg font-bold my-4">开发中...</div>
<Button type='primary' onClick={() => router.push('/login')}>返回登录</Button>
<div className='relative w-full h-full flex flex-col items-center justify-center'>
{contextHolder}
<Form<FieldType>
name='registry'
className='w-11/12'
onFinish={handleSubmit}
disabled={disableState}
>
<Form.Item>
<p className='mb-2 text-2xl font-bold text-center'>注册</p>
</Form.Item>

<Form.Item
name='username'
rules={[{ required: true, message: '请输入用户名' },
() => ({
validator(_, value) {
if (!value || value.length <= 20 && !/\s/.test(value)) {
return Promise.resolve()
}
return Promise.reject(new Error('用户名最长20个字符且不能有空格'))
},
})
]}
>
<Input prefix={<UserOutlined />} placeholder='用户名 (支持中文)' />
</Form.Item>

<Form.Item
name='email'
rules={[{ required: true, message: '请输入邮箱地址', },
() => ({
validator(_, value) {
if (!value || /^[a-z0-9]{1,20}$/.test(value)) {
return Promise.resolve()
}
return Promise.reject(new Error('请输入20个字符以内的小写字母或数字'))
},
})
]}
>
<Input addonAfter={`@${process.env.NEXT_PUBLIC_MAIL_SERVER}`} prefix={<MailOutlined />} placeholder='邮箱地址' type='text' />
</Form.Item>

<Form.Item
name='password'
rules={[{ required: true, message: '请输入密码' }]}
>
<Input prefix={<KeyOutlined />} type="password" placeholder='密码' />
</Form.Item>

<Form.Item
name='confirm'
dependencies={['password']}
rules={[{ required: true, message: '请再次输入密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve()
}
return Promise.reject(new Error('两次输入的密码不一致'))
},
})]}
>
<Input prefix={<KeyOutlined />} type="password" placeholder='确认密码' />
</Form.Item>

{
process.env.NEXT_PUBLIC_REGISTRY_SET === 'true' &&
<Form.Item
name='authCode'
rules={[{ required: true, message: '请输入注册码' }]}
>
<Input prefix={<CodeOutlined />} placeholder='注册码' />
</Form.Item>
}

<Form.Item>
<Flex justify="space-between" align="center" className='mt-2'>
<Button type='default' htmlType='button' className='w-[48%]' onClick={() => router.push('/login')}>返回登录</Button>
<Button type='primary' htmlType='submit' className='w-[48%]'>立即注册</Button>
</Flex>
</Form.Item>
</Form>
</div>
)
}
2 changes: 1 addition & 1 deletion app/(auth)/reset/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Reset() {
const router = useRouter()
return (
<div className='flex flex-col items-center justify-center h-full w-full'>
<div className="text-center text-lg font-bold my-4">开发中...</div>
<div className="text-center text-lg font-bold my-4">本功能开发中...</div>
<Button type='primary' onClick={() => router.push('/login')}>返回登录</Button>
</div>
)
Expand Down
8 changes: 4 additions & 4 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { Nav } from './components/Nav'
export default function DashboardLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<div
className='flex flex-col sm:flex-row align-center justify-center h-dvh w-dvw bg-gray-50 dark:bg-gray-950'
className='flex flex-col sm:flex-row align-center justify-center h-dvh w-dvw bg-gray-50'
>
<div
className='w-full sm:w-40 sm:h-full pl-2 pr-2 pt-2 sm:pt-6 sm:pb-6 sm:pl-4 sm:pr-0 bg-gray-50 dark:bg-gray-950'
className='w-full sm:w-40 sm:h-full pl-2 pr-2 pt-2 sm:pt-6 sm:pb-6 sm:pl-4 sm:pr-0 bg-gray-50'
>
<Nav />
</div>
<div
className='w-full h-[calc(100%-3rem)] sm:w-[calc(100%-10rem)] sm:h-full py-2 sm:py-4 pl-2 pr-2 sm:pl-4 sm:pr-5 bg-gray-50 dark:bg-gray-950'
className='w-full h-[calc(100%-3rem)] sm:w-[calc(100%-10rem)] sm:h-full py-2 sm:py-4 pl-2 pr-2 sm:pl-4 sm:pr-5 bg-gray-50'
>
<div
className='w-full h-full bg-white dark:bg-gray-950 rounded-2xl border shadow-sm p-4'
className='w-full h-full bg-white rounded-2xl border shadow-sm p-4'
>
{children}
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/COLL_TYPE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ export type UserData = {
/**
* 密码, 经过 sha256 加密
* 登录时客户端先 sha256 加密再与数据库中的密码比对, 登录成功后存储加密后的密码
* 注册时密码直接传递到服务器, 服务器再 md5 加密后存储
* 注册时密码直接传递到服务器(Server Action), 服务器再 sha256 加密后存储
*/
password: string
/** 角色 */
role: 'user' | 'admin'
role: 'user' | 'admin' | 'guest'
/** 是否激活 */
active: boolean
/** 创建时间 */
Expand Down

0 comments on commit a935823

Please sign in to comment.