找回密码
This commit is contained in:
parent
8bdff06dca
commit
50e4be3ef6
@ -9,7 +9,9 @@ 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";
|
import SendEmail from "@/views/SendEmail";
|
||||||
|
import { sendForgotEmail, sendRegEmail } from "@/apis/apis";
|
||||||
|
import Forgot from "@/views/Forgot";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: "*", Component: Root },
|
{ path: "*", Component: Root },
|
||||||
@ -24,7 +26,9 @@ 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 path="/register_email" element={<SendEmail title="注册" sendService={sendRegEmail} />} />
|
||||||
|
<Route path="/forgot_email" element={<SendEmail title="找回密码" anyEmail sendService={sendForgotEmail} />} />
|
||||||
|
<Route path="/forgot" element={<Forgot />} />
|
||||||
|
|
||||||
<Route element={<NeedLogin><Outlet /></NeedLogin>}>
|
<Route element={<NeedLogin><Outlet /></NeedLogin>}>
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
@ -130,3 +130,27 @@ export async function sendRegEmail(email: string, captchaToken: string) {
|
|||||||
})
|
})
|
||||||
return await apiGet<unknown>(r)
|
return await apiGet<unknown>(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendForgotEmail(email: string, captchaToken: string) {
|
||||||
|
const r = await fetch(root() + "/api/v1/user/forgot_email", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"email": email,
|
||||||
|
"captchaToken": captchaToken
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return await apiGet<unknown>(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function forgotPassWord(email: string, emailJwt: string, password: string) {
|
||||||
|
const r = await fetch(root() + "/api/v1/user/forgot", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"email": email,
|
||||||
|
"emailJwt": emailJwt,
|
||||||
|
"passWord": password,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return await apiGet<unknown>(r)
|
||||||
|
}
|
120
frontend/src/views/Forgot.tsx
Normal file
120
frontend/src/views/Forgot.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
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 TextField from '@mui/material/TextField';
|
||||||
|
import { useTitle } from 'ahooks';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import { forgotPassWord } from '@/apis/apis';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function Forgot() {
|
||||||
|
const [err, setErr] = useState("")
|
||||||
|
useTitle("找回密码")
|
||||||
|
const [passerr, setPasserr] = useState("")
|
||||||
|
const [pass, setPass] = useState({
|
||||||
|
pass1: "",
|
||||||
|
pass2: "",
|
||||||
|
})
|
||||||
|
const [load, setLoad] = useState(false)
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [code, setCode] = useState("")
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pass.pass1 != pass.pass2 && pass.pass2 != "") {
|
||||||
|
setPasserr("密码不相等")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPasserr("")
|
||||||
|
}, [pass.pass1, pass.pass2])
|
||||||
|
|
||||||
|
const u = new URL(location.href)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEmail(u.searchParams.get("email") ?? "")
|
||||||
|
setCode(u.searchParams.get("code") ?? "")
|
||||||
|
}, [u.searchParams])
|
||||||
|
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoad(true)
|
||||||
|
forgotPassWord(email, code, pass.pass1).then(() => {
|
||||||
|
navigate("/")
|
||||||
|
}).catch(e => {
|
||||||
|
setErr(String(e))
|
||||||
|
}).finally(() => { setLoad(false) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<TextField
|
||||||
|
margin='dense'
|
||||||
|
fullWidth
|
||||||
|
label="新密码"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
onChange={p => setPass(produce(v => { v.pass1 = p.target.value }))}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
margin='dense'
|
||||||
|
fullWidth
|
||||||
|
label="确认新密码"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
error={passerr != ""}
|
||||||
|
helperText={passerr}
|
||||||
|
onChange={p => setPass(produce(v => { v.pass2 = p.target.value }))}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{load && <Loading />}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
@ -13,7 +13,7 @@ import Snackbar from '@mui/material/Snackbar';
|
|||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { token, user } from '@/store/store'
|
import { token, user } from '@/store/store'
|
||||||
import { login } from '@/apis/apis'
|
import { getConfig, login } from '@/apis/apis'
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import Loading from '@/components/Loading'
|
import Loading from '@/components/Loading'
|
||||||
import CheckInput, { refType } from '@/components/CheckInput'
|
import CheckInput, { refType } from '@/components/CheckInput'
|
||||||
@ -21,6 +21,7 @@ import useTitle from '@/hooks/useTitle';
|
|||||||
import CaptchaWidget from '@/components/CaptchaWidget';
|
import CaptchaWidget from '@/components/CaptchaWidget';
|
||||||
import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget'
|
import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget'
|
||||||
import { ApiErr } from '@/apis/error';
|
import { ApiErr } from '@/apis/error';
|
||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +36,16 @@ export default function SignIn() {
|
|||||||
const captchaRef = React.useRef<CaptchaWidgetRef>(null)
|
const captchaRef = React.useRef<CaptchaWidgetRef>(null)
|
||||||
const [captchaToken, setCaptchaToken] = useState("");
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
|
||||||
|
const server = useRequest(getConfig, {
|
||||||
|
cacheKey: "/api/v1/config",
|
||||||
|
staleTime: 60000,
|
||||||
|
onError: e => {
|
||||||
|
console.warn(e)
|
||||||
|
setErr(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@ -137,9 +148,9 @@ export default function SignIn() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
{/* <Link href="#" variant="body2">
|
{server.data?.NeedEmail && <Link component={RouterLink} to="/forgot_email" variant="body2">
|
||||||
忘记密码?
|
忘记密码?
|
||||||
</Link> */}
|
</Link>}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Link component={RouterLink} to="/register" variant="body2">
|
<Link component={RouterLink} to="/register" variant="body2">
|
||||||
|
@ -12,7 +12,7 @@ import Select from '@mui/material/Select';
|
|||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import { useRequest, useTitle } from 'ahooks';
|
import { useRequest, useTitle } from 'ahooks';
|
||||||
import { getConfig, sendRegEmail } from '@/apis/apis';
|
import { getConfig } from '@/apis/apis';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import Snackbar from '@mui/material/Snackbar';
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
@ -26,14 +26,14 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { ApiErr } from '@/apis/error';
|
import { ApiErr } from '@/apis/error';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
|
|
||||||
export default function SignUpEmail() {
|
export default function SendEmail({ title, anyEmail = false, sendService }: { title: string, anyEmail?: boolean, sendService: (email: string, captchaToken: string) => Promise<unknown> }) {
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [domain, setDomain] = useState("");
|
const [domain, setDomain] = useState("");
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const captchaRef = useRef<CaptchaWidgetRef>(null)
|
const captchaRef = useRef<CaptchaWidgetRef>(null)
|
||||||
const [captchaToken, setCaptchaToken] = useState("");
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
useTitle("注册")
|
useTitle(title)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [helperText, setHelperText] = useState("")
|
const [helperText, setHelperText] = useState("")
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -73,7 +73,7 @@ export default function SignUpEmail() {
|
|||||||
return email
|
return email
|
||||||
})()
|
})()
|
||||||
|
|
||||||
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(sendEmail)){
|
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(sendEmail)) {
|
||||||
setHelperText("邮箱格式错误")
|
setHelperText("邮箱格式错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@ export default function SignUpEmail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
sendRegEmail(sendEmail, captchaToken).then(() => setOpen(true)).catch(e => {
|
sendService(sendEmail, captchaToken).then(() => setOpen(true)).catch(e => {
|
||||||
captchaRef.current?.reload()
|
captchaRef.current?.reload()
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
if (e instanceof ApiErr) {
|
if (e instanceof ApiErr) {
|
||||||
@ -120,7 +120,7 @@ export default function SignUpEmail() {
|
|||||||
<LockOutlinedIcon />
|
<LockOutlinedIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
输入邮箱
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="form" noValidate onSubmit={onSubmit} sx={{ mt: 3 }}>
|
<Box component="form" noValidate onSubmit={onSubmit} sx={{ mt: 3 }}>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@ -135,7 +135,7 @@ export default function SignUpEmail() {
|
|||||||
onChange={emailonChange}
|
onChange={emailonChange}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
server.data?.AllowDomain.length != 0 &&
|
server.data?.AllowDomain.length != 0 && !anyEmail &&
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InputLabel>域名</InputLabel>
|
<InputLabel>域名</InputLabel>
|
||||||
<Select label="域名" value={domain} onChange={v => setDomain(v.target.value)}>
|
<Select label="域名" value={domain} onChange={v => setDomain(v.target.value)}>
|
||||||
@ -164,7 +164,7 @@ export default function SignUpEmail() {
|
|||||||
<Dialog open={open}>
|
<Dialog open={open}>
|
||||||
<DialogTitle>邮件已发送</DialogTitle>
|
<DialogTitle>邮件已发送</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography>请到收件箱(或垃圾箱)点击验证链接以继续完成注册。</Typography>
|
<Typography>请到收件箱(或垃圾箱)点击验证链接以继续。</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleClose}>返回首页</Button>
|
<Button onClick={handleClose}>返回首页</Button>
|
@ -42,6 +42,7 @@ var errorHandlers = []errorHandler{
|
|||||||
{auth.ErrTokenInvalid, model.ErrAuth, 401, slog.LevelDebug},
|
{auth.ErrTokenInvalid, model.ErrAuth, 401, slog.LevelDebug},
|
||||||
{email.ErrTokenInvalid, model.ErrAuth, 401, slog.LevelDebug},
|
{email.ErrTokenInvalid, model.ErrAuth, 401, slog.LevelDebug},
|
||||||
{email.ErrSendLimit, model.ErrEmailSend, 403, slog.LevelDebug},
|
{email.ErrSendLimit, model.ErrEmailSend, 403, slog.LevelDebug},
|
||||||
|
{service.ErrUsername, model.ErrPassWord, 401, slog.LevelInfo},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HandleError) Service(ctx context.Context, w http.ResponseWriter, err error) {
|
func (h *HandleError) Service(ctx context.Context, w http.ResponseWriter, err error) {
|
||||||
|
@ -2,6 +2,7 @@ package handle
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
@ -10,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/xmdhs/authlib-skin/config"
|
||||||
"github.com/xmdhs/authlib-skin/handle/handelerror"
|
"github.com/xmdhs/authlib-skin/handle/handelerror"
|
||||||
"github.com/xmdhs/authlib-skin/model"
|
"github.com/xmdhs/authlib-skin/model"
|
||||||
"github.com/xmdhs/authlib-skin/service"
|
"github.com/xmdhs/authlib-skin/service"
|
||||||
@ -22,16 +24,18 @@ type UserHandel struct {
|
|||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
textureService *service.TextureService
|
textureService *service.TextureService
|
||||||
|
config config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserHandel(handleError *handelerror.HandleError, validate *validator.Validate,
|
func NewUserHandel(handleError *handelerror.HandleError, validate *validator.Validate,
|
||||||
userService *service.UserService, logger *slog.Logger, textureService *service.TextureService) *UserHandel {
|
userService *service.UserService, logger *slog.Logger, textureService *service.TextureService, config config.Config) *UserHandel {
|
||||||
return &UserHandel{
|
return &UserHandel{
|
||||||
handleError: handleError,
|
handleError: handleError,
|
||||||
validate: validate,
|
validate: validate,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
textureService: textureService,
|
textureService: textureService,
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +124,7 @@ func (h *UserHandel) ChangePasswd() http.HandlerFunc {
|
|||||||
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.userService.ChangePasswd(ctx, c, t)
|
err = h.userService.ChangePasswd(ctx, c, t.UID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.handleError.Service(ctx, w, err)
|
h.handleError.Service(ctx, w, err)
|
||||||
return
|
return
|
||||||
@ -213,21 +217,77 @@ func (h *UserHandel) PutTexture() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *UserHandel) NeedEnableEmail(handle http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
if !h.config.Email.Enable {
|
||||||
|
h.handleError.Error(ctx, w, "未开启邮件功能", model.ErrUnknown, 403, slog.LevelInfo)
|
||||||
|
}
|
||||||
|
handle.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *UserHandel) SendRegEmail() http.HandlerFunc {
|
func (h *UserHandel) SendRegEmail() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
c, err := utils.DeCodeBody[model.SendRegEmail](r.Body, h.validate)
|
c, ip, shouldReturn := h.sendMailParameter(ctx, r, w)
|
||||||
if err != nil {
|
if shouldReturn {
|
||||||
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ip, err := utils.GetIP(r)
|
|
||||||
if err != nil {
|
|
||||||
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.userService.SendRegEmail(ctx, c.Email, c.CaptchaToken, r.Host, ip)
|
err := h.userService.SendRegEmail(ctx, c.Email, c.CaptchaToken, r.Host, ip)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError.Service(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encodeJson(w, model.API[any]{
|
||||||
|
Code: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandel) SendForgotEmail() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
c, ip, shouldReturn := h.sendMailParameter(ctx, r, w)
|
||||||
|
if shouldReturn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.userService.SendChangePasswordEmail(ctx, c.Email, c.CaptchaToken, r.Host, ip)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError.Service(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encodeJson(w, model.API[any]{
|
||||||
|
Code: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandel) sendMailParameter(ctx context.Context, r *http.Request, w http.ResponseWriter) (model.SendRegEmail, string, bool) {
|
||||||
|
c, err := utils.DeCodeBody[model.SendRegEmail](r.Body, h.validate)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
||||||
|
return model.SendRegEmail{}, "", true
|
||||||
|
}
|
||||||
|
ip, err := utils.GetIP(r)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
||||||
|
return model.SendRegEmail{}, "", true
|
||||||
|
}
|
||||||
|
return c, ip, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandel) ForgotPassword() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
c, err := utils.DeCodeBody[model.ForgotPassword](r.Body, h.validate)
|
||||||
|
if err != nil {
|
||||||
|
h.handleError.Error(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = h.userService.ForgotPassword(ctx, c.Email, c.PassWord, c.EmailJwt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.handleError.Service(ctx, w, err)
|
h.handleError.Service(ctx, w, err)
|
||||||
return
|
return
|
||||||
|
@ -89,6 +89,12 @@ type LoginRep struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SendRegEmail struct {
|
type SendRegEmail struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
CaptchaToken string `json:"captchaToken"`
|
CaptchaToken string `json:"captchaToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ForgotPassword struct {
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
EmailJwt string `json:"emailJwt" validate:"required"`
|
||||||
|
PassWord string `json:"passWord" validate:"required"`
|
||||||
|
}
|
||||||
|
@ -81,7 +81,13 @@ 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.Group(func(r chi.Router) {
|
||||||
|
r.Use(userHandel.NeedEnableEmail)
|
||||||
r.Post("/user/reg_email", userHandel.SendRegEmail())
|
r.Post("/user/reg_email", userHandel.SendRegEmail())
|
||||||
|
r.Post("/user/forgot_email", userHandel.SendForgotEmail())
|
||||||
|
r.Post("/user/forgot", userHandel.ForgotPassword())
|
||||||
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(adminHandel.NeedAuth)
|
r.Use(adminHandel.NeedAuth)
|
||||||
|
@ -71,7 +71,7 @@ func InitializeRoute(ctx context.Context, c config.Config) (*http.Server, func()
|
|||||||
}
|
}
|
||||||
userService := service.NewUserSerice(c, client, captchaService, authService, cache, emailService)
|
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, userService, logger, textureService)
|
userHandel := handle.NewUserHandel(handleError, validate, userService, logger, textureService, c)
|
||||||
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)
|
||||||
|
@ -99,7 +99,7 @@ func (e EmailService) SendEmail(ctx context.Context, to string, subject, body st
|
|||||||
|
|
||||||
var emailTemplate = lo.Must(template.New("email").Parse(`<p>{{ .msg }}</p><a href="{{.url}}">{{ .url }}</a>`))
|
var emailTemplate = lo.Must(template.New("email").Parse(`<p>{{ .msg }}</p><a href="{{.url}}">{{ .url }}</a>`))
|
||||||
|
|
||||||
func (e EmailService) SendVerifyUrl(ctx context.Context, email string, interval int, host string) error {
|
func (e EmailService) SendVerifyUrl(ctx context.Context, email string, interval int, host string, subject, msg, path string) error {
|
||||||
sendKey := []byte("SendEmail" + email)
|
sendKey := []byte("SendEmail" + email)
|
||||||
sendB, err := e.cache.Get(sendKey)
|
sendB, err := e.cache.Get(sendKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -125,7 +125,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: "/register",
|
Path: path,
|
||||||
}
|
}
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ func (e EmailService) SendVerifyUrl(ctx context.Context, email string, interval
|
|||||||
|
|
||||||
body := bytes.NewBuffer(nil)
|
body := bytes.NewBuffer(nil)
|
||||||
err = emailTemplate.Execute(body, map[string]any{
|
err = emailTemplate.Execute(body, map[string]any{
|
||||||
"msg": "点击下方链接验证你的邮箱,1 天内有效",
|
"msg": msg,
|
||||||
"url": u.String(),
|
"url": u.String(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -167,18 +167,21 @@ func (e EmailService) VerifyJwt(email, jwtStr string) error {
|
|||||||
return fmt.Errorf("VerifyJwt: %w", err)
|
return fmt.Errorf("VerifyJwt: %w", err)
|
||||||
}
|
}
|
||||||
sub, _ := token.Claims.GetSubject()
|
sub, _ := token.Claims.GetSubject()
|
||||||
if !token.Valid || sub != email {
|
iss, _ := token.Claims.GetIssuer()
|
||||||
|
if !token.Valid || sub != email || iss != issuer {
|
||||||
return fmt.Errorf("VerifyJwt: %w", ErrTokenInvalid)
|
return fmt.Errorf("VerifyJwt: %w", ErrTokenInvalid)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const issuer = "authlib-skin email verification"
|
||||||
|
|
||||||
func newJwtToken(jwtKey *rsa.PrivateKey, email string) (string, error) {
|
func newJwtToken(jwtKey *rsa.PrivateKey, email string) (string, error) {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * 24 * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * 24 * time.Hour)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Subject: email,
|
Subject: email,
|
||||||
Issuer: "authlib-skin email verification",
|
Issuer: issuer,
|
||||||
})
|
})
|
||||||
jwts, err := token.SignedString(jwtKey)
|
jwts, err := token.SignedString(jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -27,6 +27,7 @@ var (
|
|||||||
ErrRegLimit = errors.New("超过注册 ip 限制")
|
ErrRegLimit = errors.New("超过注册 ip 限制")
|
||||||
ErrPassWord = errors.New("错误的密码或用户名")
|
ErrPassWord = errors.New("错误的密码或用户名")
|
||||||
ErrChangeName = errors.New("离线模式 uuid 不允许修改用户名")
|
ErrChangeName = errors.New("离线模式 uuid 不允许修改用户名")
|
||||||
|
ErrUsername = errors.New("邮箱不存在")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
@ -181,15 +182,17 @@ func (w *UserService) Info(ctx context.Context, t *model.TokenClaims) (model.Use
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *UserService) ChangePasswd(ctx context.Context, p model.ChangePasswd, t *model.TokenClaims) error {
|
func (w *UserService) ChangePasswd(ctx context.Context, p model.ChangePasswd, uid int, validOldPass bool) error {
|
||||||
u, err := w.client.User.Query().Where(user.IDEQ(t.UID)).WithToken().First(ctx)
|
u, err := w.client.User.Query().Where(user.IDEQ(uid)).WithToken().First(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ChangePasswd: %w", err)
|
return fmt.Errorf("ChangePasswd: %w", err)
|
||||||
}
|
}
|
||||||
err = validatePass(ctx, u, p.Old)
|
if validOldPass {
|
||||||
|
err := validatePass(ctx, u, p.Old)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ChangePasswd: %w", err)
|
return fmt.Errorf("ChangePasswd: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pass, salt := utils.Argon2ID(p.New)
|
pass, salt := utils.Argon2ID(p.New)
|
||||||
if u.Edges.Token != nil {
|
if u.Edges.Token != nil {
|
||||||
err := w.client.UserToken.UpdateOne(u.Edges.Token).AddTokenID(1).Exec(ctx)
|
err := w.client.UserToken.UpdateOne(u.Edges.Token).AddTokenID(1).Exec(ctx)
|
||||||
@ -197,7 +200,7 @@ func (w *UserService) ChangePasswd(ctx context.Context, p model.ChangePasswd, t
|
|||||||
return fmt.Errorf("ChangePasswd: %w", err)
|
return fmt.Errorf("ChangePasswd: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = w.cache.Del([]byte("auth" + strconv.Itoa(t.UID)))
|
err = w.cache.Del([]byte("auth" + strconv.Itoa(uid)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ChangePasswd: %w", err)
|
return fmt.Errorf("ChangePasswd: %w", err)
|
||||||
}
|
}
|
||||||
@ -256,9 +259,45 @@ func (w *UserService) SendRegEmail(ctx context.Context, email, CaptchaToken, hos
|
|||||||
return fmt.Errorf("SendRegEmail: %w", err)
|
return fmt.Errorf("SendRegEmail: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = w.emailService.SendVerifyUrl(ctx, email, 60, host)
|
err = w.emailService.SendVerifyUrl(ctx, email, 60, host, "验证你的邮箱以完成注册", "点击下方链接完成注册,1 天内有效", "/register")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("SendRegEmail: %w", err)
|
return fmt.Errorf("SendRegEmail: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *UserService) SendChangePasswordEmail(ctx context.Context, email, CaptchaToken, host, ip string) error {
|
||||||
|
err := w.captchaService.VerifyCaptcha(ctx, CaptchaToken, ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SendChangePasswordEmail: %w", err)
|
||||||
|
}
|
||||||
|
c, err := w.client.User.Query().Where(user.Email(email)).Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SendChangePasswordEmail: %w", err)
|
||||||
|
}
|
||||||
|
if c == 0 {
|
||||||
|
return fmt.Errorf("SendChangePasswordEmail: %w", ErrUsername)
|
||||||
|
}
|
||||||
|
err = w.emailService.SendVerifyUrl(ctx, email, 60, host, "找回密码邮箱验证", "点击下方链接更改你的密码,1 天内有效", "/forgot")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SendChangePasswordEmail: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *UserService) ForgotPassword(ctx context.Context, email, passWord, emailJwt string) error {
|
||||||
|
err := w.emailService.VerifyJwt(email, emailJwt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ForgotPassword: %w", err)
|
||||||
|
}
|
||||||
|
u, err := w.client.User.Query().Where(user.Email(email)).First(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ForgotPassword: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.ChangePasswd(ctx, model.ChangePasswd{New: passWord}, u.ID, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ForgotPassword: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user