前端发送邮箱部分
This commit is contained in:
parent
bdb986c468
commit
f121dca589
@ -9,6 +9,7 @@ import Layout from '@/views/Layout'
|
|||||||
import UserAdmin from "@/views/admin/UserAdmin";
|
import UserAdmin from "@/views/admin/UserAdmin";
|
||||||
import NeedLogin from "@/components/NeedLogin";
|
import NeedLogin from "@/components/NeedLogin";
|
||||||
import Index from "@/views/Index";
|
import Index from "@/views/Index";
|
||||||
|
import SignUpEmail from "@/views/SignUpEmail";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: "*", Component: Root },
|
{ path: "*", Component: Root },
|
||||||
@ -23,6 +24,7 @@ function Root() {
|
|||||||
<Route path="/*" element={<p>404</p>} />
|
<Route path="/*" element={<p>404</p>} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/register_email" element={<SignUpEmail />} />
|
||||||
|
|
||||||
<Route element={<NeedLogin><Outlet /></NeedLogin>}>
|
<Route element={<NeedLogin><Outlet /></NeedLogin>}>
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
@ -118,3 +118,14 @@ export async function editUser(u: EditUser, token: string, uid: string) {
|
|||||||
})
|
})
|
||||||
return await apiGet<unknown>(r)
|
return await apiGet<unknown>(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendRegEmail(email: string, captchaToken: string) {
|
||||||
|
const r = await fetch(root() + "/api/v1/user/reg_email", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"email": email,
|
||||||
|
"captchaToken": captchaToken
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return await apiGet<unknown>(r)
|
||||||
|
}
|
@ -40,6 +40,8 @@ export interface ApiConfig {
|
|||||||
captcha: captcha
|
captcha: captcha
|
||||||
AllowChangeName: boolean
|
AllowChangeName: boolean
|
||||||
serverName: string
|
serverName: string
|
||||||
|
NeedEmail: boolean
|
||||||
|
AllowDomain: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
|
@ -64,6 +64,9 @@ export default function SignIn() {
|
|||||||
console.warn(v)
|
console.warn(v)
|
||||||
if (v instanceof ApiErr) {
|
if (v instanceof ApiErr) {
|
||||||
switch (v.code) {
|
switch (v.code) {
|
||||||
|
case 10:
|
||||||
|
setErr("验证码错误")
|
||||||
|
return
|
||||||
case 6:
|
case 6:
|
||||||
setErr("密码或用户名错误")
|
setErr("密码或用户名错误")
|
||||||
return
|
return
|
||||||
|
@ -69,6 +69,9 @@ export default function SignUp() {
|
|||||||
console.warn(v)
|
console.warn(v)
|
||||||
if (v instanceof ApiErr) {
|
if (v instanceof ApiErr) {
|
||||||
switch (v.code) {
|
switch (v.code) {
|
||||||
|
case 10:
|
||||||
|
setRegErr("验证码错误")
|
||||||
|
return
|
||||||
case 3:
|
case 3:
|
||||||
setRegErr("邮箱已存在")
|
setRegErr("邮箱已存在")
|
||||||
return
|
return
|
||||||
|
176
frontend/src/views/SignUpEmail.tsx
Normal file
176
frontend/src/views/SignUpEmail.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { useRequest, useTitle } from 'ahooks';
|
||||||
|
import { getConfig, sendRegEmail } from '@/apis/apis';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import CaptchaWidget from '@/components/CaptchaWidget';
|
||||||
|
import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget'
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ApiErr } from '@/apis/error';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
|
||||||
|
export default function SignUpEmail() {
|
||||||
|
const [err, setErr] = useState("");
|
||||||
|
const [domain, setDomain] = useState("");
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const captchaRef = useRef<CaptchaWidgetRef>(null)
|
||||||
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
useTitle("注册")
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [helperText, setHelperText] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const server = useRequest(getConfig, {
|
||||||
|
cacheKey: "/api/v1/config",
|
||||||
|
staleTime: 60000,
|
||||||
|
onError: e => {
|
||||||
|
console.warn(e)
|
||||||
|
setErr(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (server.data?.AllowDomain.length != 0) {
|
||||||
|
setDomain(server.data?.AllowDomain[0] ?? "")
|
||||||
|
}
|
||||||
|
}, [server.data?.AllowDomain])
|
||||||
|
|
||||||
|
const emailonChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
setEmail(e.target.value)
|
||||||
|
if (e.target.value == "") {
|
||||||
|
setHelperText("邮箱不得为空")
|
||||||
|
}
|
||||||
|
setHelperText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (email == "") {
|
||||||
|
setHelperText("邮箱不得为空")
|
||||||
|
}
|
||||||
|
const sendEmail = (() => {
|
||||||
|
if (domain != "") {
|
||||||
|
return `${email}@${domain}`
|
||||||
|
}
|
||||||
|
return email
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(sendEmail)){
|
||||||
|
setHelperText("邮箱格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.data?.captcha.type != "" && captchaToken == "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
sendRegEmail(sendEmail, captchaToken).then(() => setOpen(true)).catch(e => {
|
||||||
|
captchaRef.current?.reload()
|
||||||
|
console.warn(e)
|
||||||
|
if (e instanceof ApiErr) {
|
||||||
|
switch (e.code) {
|
||||||
|
case 10:
|
||||||
|
setErr("验证码错误")
|
||||||
|
return
|
||||||
|
case 11:
|
||||||
|
setErr("暂时无法对此邮箱发送邮件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setErr(String(e))
|
||||||
|
}).finally(() => setLoading(false))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main" maxWidth="xs">
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||||
|
<LockOutlinedIcon />
|
||||||
|
</Avatar>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
输入邮箱
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" noValidate onSubmit={onSubmit} sx={{ mt: 3 }}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sx={{ display: 'grid', columnGap: '3px', gridTemplateColumns: "1fr auto" }}>
|
||||||
|
<TextField fullWidth
|
||||||
|
required
|
||||||
|
name="email"
|
||||||
|
label="邮箱"
|
||||||
|
value={email}
|
||||||
|
helperText={helperText}
|
||||||
|
error={helperText != ""}
|
||||||
|
onChange={emailonChange}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
server.data?.AllowDomain.length != 0 &&
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel>域名</InputLabel>
|
||||||
|
<Select label="域名" value={domain} onChange={v => setDomain(v.target.value)}>
|
||||||
|
{server.data?.AllowDomain.map(v => <MenuItem value={v}>@{v}</MenuItem>)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<CaptchaWidget ref={captchaRef} onSuccess={setCaptchaToken} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
>
|
||||||
|
继续
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={err !== ""}>
|
||||||
|
<Alert onClose={() => setErr("")} severity="error">{err}</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
<Dialog open={open}>
|
||||||
|
<DialogTitle>邮件已发送</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>请到收件箱(或垃圾箱)点击验证链接以继续完成注册。</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>返回首页</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
{loading && <Loading />}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -63,6 +63,8 @@ type Config struct {
|
|||||||
Captcha Captcha `json:"captcha"`
|
Captcha Captcha `json:"captcha"`
|
||||||
AllowChangeName bool
|
AllowChangeName bool
|
||||||
ServerName string `json:"serverName"`
|
ServerName string `json:"serverName"`
|
||||||
|
NeedEmail bool
|
||||||
|
AllowDomain []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditUser struct {
|
type EditUser struct {
|
||||||
|
@ -81,6 +81,7 @@ func newSkinApi(handel *handle.Handel, userHandel *handle.UserHandel, adminHande
|
|||||||
r.Post("/user/reg", userHandel.Reg())
|
r.Post("/user/reg", userHandel.Reg())
|
||||||
r.Post("/user/login", userHandel.Login())
|
r.Post("/user/login", userHandel.Login())
|
||||||
r.Get("/config", handel.GetConfig())
|
r.Get("/config", handel.GetConfig())
|
||||||
|
r.Post("/user/reg_email", userHandel.SendRegEmail())
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(adminHandel.NeedAuth)
|
r.Use(adminHandel.NeedAuth)
|
||||||
|
@ -63,9 +63,15 @@ func InitializeRoute(ctx context.Context, c config.Config) (*http.Server, func()
|
|||||||
handleError := handelerror.NewHandleError(logger)
|
handleError := handelerror.NewHandleError(logger)
|
||||||
httpClient := ProvideHttpClient()
|
httpClient := ProvideHttpClient()
|
||||||
captchaService := captcha.NewCaptchaService(c, httpClient)
|
captchaService := captcha.NewCaptchaService(c, httpClient)
|
||||||
userSerice := service.NewUserSerice(c, client, captchaService, authService, cache)
|
emailService, err := email.NewEmail(privateKey, c, cache)
|
||||||
|
if err != nil {
|
||||||
|
cleanup2()
|
||||||
|
cleanup()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
userService := service.NewUserSerice(c, client, captchaService, authService, cache, emailService)
|
||||||
textureService := service.NewTextureService(client, c, cache)
|
textureService := service.NewTextureService(client, c, cache)
|
||||||
userHandel := handle.NewUserHandel(handleError, validate, userSerice, logger, textureService)
|
userHandel := handle.NewUserHandel(handleError, validate, userService, logger, textureService)
|
||||||
adminService := service.NewAdminService(authService, client, c, cache)
|
adminService := service.NewAdminService(authService, client, c, cache)
|
||||||
adminHandel := handle.NewAdminHandel(handleError, adminService, validate)
|
adminHandel := handle.NewAdminHandel(handleError, adminService, validate)
|
||||||
httpHandler := route.NewRoute(yggdrasil3, handel, c, handler, userHandel, adminHandel)
|
httpHandler := route.NewRoute(yggdrasil3, handel, c, handler, userHandel, adminHandel)
|
||||||
|
@ -52,16 +52,23 @@ func NewEmail(pri *rsa.PrivateKey, c config.Config, cache cache.Cache) (*EmailSe
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EmailService) getRandEmailUser() EmailConfig {
|
func (e EmailService) getRandEmailUser() (EmailConfig, error) {
|
||||||
|
if len(e.emailConfig) == 0 {
|
||||||
|
return EmailConfig{}, fmt.Errorf("没有可用的邮箱账号")
|
||||||
|
}
|
||||||
|
|
||||||
i := rand.Intn(len(e.emailConfig))
|
i := rand.Intn(len(e.emailConfig))
|
||||||
return e.emailConfig[i]
|
return e.emailConfig[i], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EmailService) SendEmail(ctx context.Context, to string, subject, body string) error {
|
func (e EmailService) SendEmail(ctx context.Context, to string, subject, body string) error {
|
||||||
u := e.getRandEmailUser()
|
u, err := e.getRandEmailUser()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SendRegVerify: %w", err)
|
||||||
|
}
|
||||||
m := mail.NewMsg()
|
m := mail.NewMsg()
|
||||||
|
|
||||||
err := m.From(u.Name)
|
err = m.From(u.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("SendRegVerify: %w", err)
|
return fmt.Errorf("SendRegVerify: %w", err)
|
||||||
}
|
}
|
||||||
@ -114,7 +121,7 @@ func (e EmailService) SendVerifyUrl(ctx context.Context, email string, interval
|
|||||||
u := url.URL{
|
u := url.URL{
|
||||||
Host: host,
|
Host: host,
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Path: "/test?" + code,
|
Path: "/register?code=" + url.QueryEscape(code),
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.config.WebBaseUrl != "" {
|
if e.config.WebBaseUrl != "" {
|
||||||
|
@ -258,7 +258,7 @@ func (w *UserService) SendRegEmail(ctx context.Context, email, CaptchaToken, hos
|
|||||||
|
|
||||||
err = w.emailService.SendVerifyUrl(ctx, email, 60, host)
|
err = w.emailService.SendVerifyUrl(ctx, email, 60, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("SendRegEmail: %w", ErrNotAllowDomain)
|
return fmt.Errorf("SendRegEmail: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -35,5 +35,7 @@ func (w *WebService) GetConfig(ctx context.Context) model.Config {
|
|||||||
},
|
},
|
||||||
ServerName: w.config.ServerName,
|
ServerName: w.config.ServerName,
|
||||||
AllowChangeName: !w.config.OfflineUUID,
|
AllowChangeName: !w.config.OfflineUUID,
|
||||||
|
NeedEmail: w.config.Email.Enable,
|
||||||
|
AllowDomain: w.config.Email.AllowDomain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user