From eaf95009077d5ba78d89ed528347ff45968188ac Mon Sep 17 00:00:00 2001 From: xmdhs Date: Fri, 6 Oct 2023 00:51:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/Route.tsx | 8 +- frontend/src/apis/apis.ts | 20 +++- frontend/src/apis/model.ts | 7 +- frontend/src/components/CaptchaWidget.tsx | 24 ++--- frontend/src/views/Register.tsx | 19 +++- frontend/src/views/profile/Security.tsx | 111 +++++++++++++++++++++- handle/{captcha.go => config.go} | 6 +- handle/user.go | 31 ++++++ model/const.go | 1 + model/model.go | 9 ++ server/route/route.go | 4 +- service/captcha.go | 11 ++- service/user.go | 39 +++++++- 13 files changed, 254 insertions(+), 36 deletions(-) rename handle/{captcha.go => config.go} (71%) diff --git a/frontend/src/Route.tsx b/frontend/src/Route.tsx index 1c6772b..3639f45 100644 --- a/frontend/src/Route.tsx +++ b/frontend/src/Route.tsx @@ -1,4 +1,4 @@ -import { Routes, Route, createBrowserRouter, RouterProvider, useNavigate, Outlet } from "react-router-dom"; +import { Routes, Route, createBrowserRouter, RouterProvider, useNavigate, Outlet, Navigate } from "react-router-dom"; import { ScrollRestoration } from "react-router-dom"; import Login from '@/views/Login' import Register from '@/views/Register' @@ -69,12 +69,10 @@ function NeedLogin({ children, needAdmin = false }: { children: JSX.Element, nee }, }) if (t == "") { - navigate("/login") - return <> + return } if (!loading && data && needAdmin && !data.is_admin) { - navigate("/login") - return <> + return } return <> {children} } \ No newline at end of file diff --git a/frontend/src/apis/apis.ts b/frontend/src/apis/apis.ts index 1345001..4bd5c55 100644 --- a/frontend/src/apis/apis.ts +++ b/frontend/src/apis/apis.ts @@ -1,4 +1,4 @@ -import type { tokenData, ApiUser, ApiServerInfo, YggProfile } from '@/apis/model' +import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig } from '@/apis/model' import { apiGet } from '@/apis/utils' export async function login(username: string, password: string) { @@ -84,4 +84,22 @@ export async function changePasswd(old: string, newpa: string, token: string) { } }) return await apiGet(r) +} + +export async function getConfig() { + const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/config") + return await apiGet(r) +} + +export async function changeName(name: string, token: string) { + const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/name", { + method: "POST", + body: JSON.stringify({ + "name": name, + }), + headers: { + "Authorization": "Bearer " + token + } + }) + return await apiGet(r) } \ No newline at end of file diff --git a/frontend/src/apis/model.ts b/frontend/src/apis/model.ts index b6e3e38..998fc5d 100644 --- a/frontend/src/apis/model.ts +++ b/frontend/src/apis/model.ts @@ -12,14 +12,12 @@ export interface Api { data: T } -export type ApiErr = Api interface captcha { type: string siteKey: string } -export type ApiCaptcha = Api export interface ApiUser { uid: string @@ -39,4 +37,9 @@ export interface YggProfile { name: string value: string }[] +} + +export interface ApiConfig { + captcha: captcha + AllowChangeName: boolean } \ No newline at end of file diff --git a/frontend/src/components/CaptchaWidget.tsx b/frontend/src/components/CaptchaWidget.tsx index 65b60d1..5941fce 100644 --- a/frontend/src/components/CaptchaWidget.tsx +++ b/frontend/src/components/CaptchaWidget.tsx @@ -1,11 +1,11 @@ import { Turnstile } from '@marsidev/react-turnstile' import Button from '@mui/material/Button' -import { useRef, useState, memo, forwardRef, useImperativeHandle } from 'react' +import { useRef, useState, memo, forwardRef, useImperativeHandle, useEffect } from 'react' import type { TurnstileInstance } from '@marsidev/react-turnstile' -import { ApiCaptcha } from '@/apis/model'; import Alert from '@mui/material/Alert'; import Skeleton from '@mui/material/Skeleton'; import { useRequest } from 'ahooks'; +import { getConfig } from '@/apis/apis'; interface prop { onSuccess: ((token: string) => void) @@ -19,7 +19,9 @@ export type refType = { const CaptchaWidget = forwardRef(({ onSuccess }, ref) => { const Turnstileref = useRef(null) const [key, setKey] = useState(1) - const { data, error, loading } = useRequest(() => fetch(import.meta.env.VITE_APIADDR + '/api/v1/captcha').then(v => v.json() as Promise), { + const { data, error, loading } = useRequest(getConfig, { + cacheKey: "/api/v1/config", + staleTime: 600000, loadingDelay: 200 }) @@ -30,6 +32,12 @@ const CaptchaWidget = forwardRef(({ onSuccess }, ref) => { } } }) + useEffect(() => { + if (data?.captcha?.type != "turnstile") { + onSuccess("ok") + return + } + }, [data?.captcha?.type, onSuccess]) if (error) { @@ -39,18 +47,10 @@ const CaptchaWidget = forwardRef(({ onSuccess }, ref) => { if (loading) { return } - if (data?.code != 0) { - console.warn(error) - return {String(data?.msg)} - } - if (data.data.type != "turnstile") { - onSuccess("ok") - return <> - } return ( <> - + ) diff --git a/frontend/src/views/Register.tsx b/frontend/src/views/Register.tsx index c440331..2839c77 100644 --- a/frontend/src/views/Register.tsx +++ b/frontend/src/views/Register.tsx @@ -19,6 +19,7 @@ import { useNavigate } from "react-router-dom"; import CaptchaWidget from '@/components/CaptchaWidget'; import type { refType as CaptchaWidgetRef } from '@/components/CaptchaWidget' import useTitle from '@/hooks/useTitle'; +import { ApiErr } from '@/apis/error'; export default function SignUp() { const [regErr, setRegErr] = useState(""); @@ -51,7 +52,23 @@ export default function SignUp() { setLoading(true) register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken). then(() => navigate("/login")). - catch(v => [setRegErr(String(v)), console.warn(v), captchaRef.current?.reload()]). + catch(v => { + captchaRef.current?.reload() + console.warn(v) + + if (v instanceof ApiErr) { + switch (v.code) { + case 3: + setRegErr("邮箱已存在") + break + case 7: + setRegErr("用户名已存在") + break + } + return + } + setRegErr(String(v)) + }). finally(() => setLoading(false)) }; diff --git a/frontend/src/views/profile/Security.tsx b/frontend/src/views/profile/Security.tsx index 40dcfe5..3c4eed3 100644 --- a/frontend/src/views/profile/Security.tsx +++ b/frontend/src/views/profile/Security.tsx @@ -5,15 +5,47 @@ import CardHeader from "@mui/material/CardHeader"; import TextField from "@mui/material/TextField"; import { useEffect, useState } from "react"; import { produce } from 'immer' -import { changePasswd } from "@/apis/apis"; -import { useAtom, useSetAtom } from "jotai"; +import { changeName, changePasswd, getConfig } from "@/apis/apis"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { LayoutAlertErr, token, user } from "@/store/store"; import Loading from "@/components/Loading"; import { ApiErr } from "@/apis/error"; import { useNavigate } from "react-router-dom"; import useTitle from "@/hooks/useTitle"; +import Box from "@mui/material/Box"; +import { useRequest } from "ahooks"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogActions from "@mui/material/DialogActions"; export default function Security() { + useTitle("安全设置") + const setLayoutErr = useSetAtom(LayoutAlertErr) + + const { data } = useRequest(getConfig, { + cacheKey: "/api/v1/config", + staleTime: 600000, + onError: e => { + setLayoutErr(String(e)) + } + }) + + return (<> + + + {data?.AllowChangeName && } + + ) +} + +function ChangePasswd() { const [pass, setPass] = useState({ old: "", pass1: "", @@ -26,7 +58,6 @@ export default function Security() { const setLayoutErr = useSetAtom(LayoutAlertErr) const setUser = useSetAtom(user) const navigate = useNavigate(); - useTitle("安全设置") useEffect(() => { if (pass.pass1 != pass.pass2 && pass.pass2 != "") { @@ -91,4 +122,78 @@ export default function Security() { {load && } ) +} + +function ChangeName() { + const [err, setErr] = useState("") + const [name, setName] = useState("") + const [open, setOpen] = useState(false) + const [load, setLoad] = useState(false) + const nowToken = useAtomValue(token) + const setUser = useSetAtom(user) + + const handelClick = () => { + if (name == "") return + setOpen(true) + } + + const handleClose = () => { + setOpen(false) + } + + const handleSubmit = () => { + if (load) return + setLoad(true) + changeName(name, nowToken).then(() => { + setName("") + setUser(v => { return { name: name, uuid: v.uuid } }) + }).catch(e => { + if (e instanceof ApiErr && e.code == 7) { + setErr("用户名已存在") + return + } + setErr(String(e)) + console.warn(e) + }).finally(() => [setLoad(false), setOpen(false)]) + } + + return (<> + + + + setName(v.target.value)} + autoComplete="username" + /> + + + + + + 确认修改后的用户名 + + + + {`用户名改为`} { {name} } {`?`} + + + + + + + + {load && } + ) } \ No newline at end of file diff --git a/handle/captcha.go b/handle/config.go similarity index 71% rename from handle/captcha.go rename to handle/config.go index 7e7c1ae..62d8c9e 100644 --- a/handle/captcha.go +++ b/handle/config.go @@ -8,11 +8,11 @@ import ( "github.com/xmdhs/authlib-skin/model" ) -func (h *Handel) GetCaptcha() httprouter.Handle { +func (h *Handel) GetConfig() httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { ctx := r.Context() - c := h.webService.GetCaptcha(ctx) - m := model.API[model.Captcha]{ + c := h.webService.GetConfig(ctx) + m := model.API[model.Config]{ Code: 0, Data: c, } diff --git a/handle/user.go b/handle/user.go index b832fe4..971ad7e 100644 --- a/handle/user.go +++ b/handle/user.go @@ -38,6 +38,10 @@ func (h *Handel) Reg() httprouter.Handle { h.handleError(ctx, w, err.Error(), model.ErrExistUser, 400, slog.LevelDebug) return } + if errors.Is(err, service.ErrExitsName) { + h.handleError(ctx, w, err.Error(), model.ErrExitsName, 400, slog.LevelDebug) + return + } if errors.Is(err, service.ErrRegLimit) { h.handleError(ctx, w, err.Error(), model.ErrRegLimit, 400, slog.LevelDebug) return @@ -103,3 +107,30 @@ func (h *Handel) ChangePasswd() httprouter.Handle { } } + +func (h *Handel) ChangeName() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + ctx := r.Context() + token := h.getTokenbyAuthorization(ctx, w, r) + if token == "" { + return + } + c, err := utils.DeCodeBody[model.ChangeName](r.Body, h.validate) + if err != nil { + h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug) + return + } + err = h.webService.ChangeName(ctx, c.Name, token) + if err != nil { + if errors.Is(err, service.ErrExitsName) { + h.handleError(ctx, w, err.Error(), model.ErrExitsName, 400, slog.LevelDebug) + return + } + h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn) + return + } + encodeJson(w, model.API[any]{ + Code: 0, + }) + } +} diff --git a/model/const.go b/model/const.go index 98d8061..893d3e3 100644 --- a/model/const.go +++ b/model/const.go @@ -11,4 +11,5 @@ const ( ErrRegLimit ErrAuth ErrPassWord + ErrExitsName ) diff --git a/model/model.go b/model/model.go index 596dfc3..461b27a 100644 --- a/model/model.go +++ b/model/model.go @@ -51,3 +51,12 @@ type UserList struct { Email string `json:"email"` RegIp string `json:"reg_ip"` } + +type ChangeName struct { + Name string `json:"name" validate:"required,min=3,max=16"` +} + +type Config struct { + Captcha Captcha `json:"captcha"` + AllowChangeName bool +} diff --git a/server/route/route.go b/server/route/route.go index f0ac03e..ad500e6 100644 --- a/server/route/route.go +++ b/server/route/route.go @@ -53,9 +53,11 @@ func newYggdrasil(r *httprouter.Router, handelY yggdrasil.Yggdrasil) error { func newSkinApi(r *httprouter.Router, handel *handle.Handel) error { r.PUT("/api/v1/user/reg", handel.Reg()) - r.GET("/api/v1/captcha", handel.GetCaptcha()) + r.GET("/api/v1/config", handel.GetConfig()) r.GET("/api/v1/user", handel.UserInfo()) r.POST("/api/v1/user/password", handel.ChangePasswd()) + r.POST("/api/v1/user/name", handel.ChangeName()) + r.GET("/api/v1/admin/users", handel.NeedAdmin(handel.ListUser())) return nil } diff --git a/service/captcha.go b/service/captcha.go index d8f0cc9..75ed19d 100644 --- a/service/captcha.go +++ b/service/captcha.go @@ -11,10 +11,13 @@ import ( "github.com/xmdhs/authlib-skin/model" ) -func (w *WebService) GetCaptcha(ctx context.Context) model.Captcha { - return model.Captcha{ - Type: w.config.Captcha.Type, - SiteKey: w.config.Captcha.SiteKey, +func (w *WebService) GetConfig(ctx context.Context) model.Config { + return model.Config{ + Captcha: model.Captcha{ + Type: w.config.Captcha.Type, + SiteKey: w.config.Captcha.SiteKey, + }, + AllowChangeName: !w.config.OfflineUUID, } } diff --git a/service/user.go b/service/user.go index 445839f..7058d5b 100644 --- a/service/user.go +++ b/service/user.go @@ -19,10 +19,11 @@ import ( ) var ( - ErrExistUser = errors.New("邮箱已存在") - ErrExitsName = errors.New("用户名已存在") - ErrRegLimit = errors.New("超过注册 ip 限制") - ErrPassWord = errors.New("错误的密码") + ErrExistUser = errors.New("邮箱已存在") + ErrExitsName = errors.New("用户名已存在") + ErrRegLimit = errors.New("超过注册 ip 限制") + ErrPassWord = errors.New("错误的密码") + ErrChangeName = errors.New("离线模式 uuid 不允许修改用户名") ) func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string) error { @@ -138,3 +139,33 @@ func (w *WebService) ChangePasswd(ctx context.Context, p model.ChangePasswd, tok } return nil } + +func (w *WebService) changeName(ctx context.Context, newName string, uid int) error { + if w.config.OfflineUUID { + return fmt.Errorf("changeName: %w", ErrChangeName) + } + c, err := w.client.UserProfile.Query().Where(userprofile.Name(newName)).Count(ctx) + if err != nil { + return fmt.Errorf("changeName: %w", err) + } + if c != 0 { + return fmt.Errorf("changeName: %w", ErrExitsName) + } + err = w.client.UserProfile.Update().Where(userprofile.HasUserWith(user.ID(uid))).SetName(newName).Exec(ctx) + if err != nil { + return fmt.Errorf("changeName: %w", err) + } + return err +} + +func (w *WebService) ChangeName(ctx context.Context, newName string, token string) error { + t, err := utilsService.Auth(ctx, yggdrasil.ValidateToken{AccessToken: token}, w.client, w.cache, &w.prikey.PublicKey, false) + if err != nil { + return fmt.Errorf("ChangeName: %w", err) + } + err = w.changeName(ctx, newName, t.UID) + if err != nil { + return fmt.Errorf("ChangeName: %w", err) + } + return nil +}