修改用户名

This commit is contained in:
xmdhs 2023-10-06 00:51:11 +08:00
parent c7f07491ad
commit eaf9500907
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
13 changed files with 254 additions and 36 deletions

View File

@ -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 <Navigate to="/login" />
}
if (!loading && data && needAdmin && !data.is_admin) {
navigate("/login")
return <></>
return <Navigate to="/login" />
}
return <> {children}</>
}

View File

@ -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<unknown>(r)
}
export async function getConfig() {
const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/config")
return await apiGet<ApiConfig>(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<unknown>(r)
}

View File

@ -12,14 +12,12 @@ export interface Api<T> {
data: T
}
export type ApiErr = Api<unknown>
interface captcha {
type: string
siteKey: string
}
export type ApiCaptcha = Api<captcha>
export interface ApiUser {
uid: string
@ -39,4 +37,9 @@ export interface YggProfile {
name: string
value: string
}[]
}
export interface ApiConfig {
captcha: captcha
AllowChangeName: boolean
}

View File

@ -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<refType, prop>(({ onSuccess }, ref) => {
const Turnstileref = useRef<TurnstileInstance>(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<ApiCaptcha>), {
const { data, error, loading } = useRequest(getConfig, {
cacheKey: "/api/v1/config",
staleTime: 600000,
loadingDelay: 200
})
@ -30,6 +32,12 @@ const CaptchaWidget = forwardRef<refType, prop>(({ onSuccess }, ref) => {
}
}
})
useEffect(() => {
if (data?.captcha?.type != "turnstile") {
onSuccess("ok")
return
}
}, [data?.captcha?.type, onSuccess])
if (error) {
@ -39,18 +47,10 @@ const CaptchaWidget = forwardRef<refType, prop>(({ onSuccess }, ref) => {
if (loading) {
return <Skeleton variant="rectangular" width={300} height={65} />
}
if (data?.code != 0) {
console.warn(error)
return <Alert severity="warning">{String(data?.msg)}</Alert>
}
if (data.data.type != "turnstile") {
onSuccess("ok")
return <></>
}
return (
<>
<Turnstile siteKey={data?.data.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={Turnstileref} scriptOptions={{ async: true }} />
<Turnstile siteKey={data?.captcha?.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={Turnstileref} scriptOptions={{ async: true }} />
<Button onClick={() => setKey(key + 1)}></Button>
</>
)

View File

@ -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))
};

View File

@ -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 (<>
<Box sx={{
display: "grid", gap: "1em",
gridTemplateColumns: {
lg: "1fr 1fr"
}
}}>
<ChangePasswd />
{data?.AllowChangeName && <ChangeName />}
</Box>
</>)
}
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 && <Loading />}
</>)
}
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 (<>
<Card sx={{ height: "min-content" }}>
<CardHeader title="更改用户名" />
<CardContent>
<TextField
margin='dense'
fullWidth
label="新用户名"
type='text'
required
error={err != ""}
helperText={err}
onChange={v => setName(v.target.value)}
autoComplete="username"
/>
<Button sx={{ marginTop: "1em" }} onClick={handelClick} variant='contained'></Button>
</CardContent>
</Card>
<Dialog
open={open}
onClose={handleClose}
>
<DialogTitle>
</DialogTitle>
<DialogContent>
<DialogContentText>
{`用户名改为`} {<code> {name} </code>} {``}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}></Button>
<Button onClick={handleSubmit} autoFocus>
</Button>
</DialogActions>
</Dialog>
{load && <Loading />}
</>)
}

View File

@ -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,
}

View File

@ -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,
})
}
}

View File

@ -11,4 +11,5 @@ const (
ErrRegLimit
ErrAuth
ErrPassWord
ErrExitsName
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}