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"
+ />
+
+
+
+
+ {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
+}