diff --git a/frontend/src/apis/apis.ts b/frontend/src/apis/apis.ts index 88259bb..669140c 100644 --- a/frontend/src/apis/apis.ts +++ b/frontend/src/apis/apis.ts @@ -1,4 +1,4 @@ -import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig, List, UserInfo } from '@/apis/model' +import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig, List, UserInfo, EditUser } from '@/apis/model' import { apiGet } from '@/apis/utils' export async function login(username: string, password: string) { @@ -116,4 +116,15 @@ export async function ListUser(page: number, token: string, email: string, name: } }) return await apiGet>(r) +} + +export async function editUser(u: EditUser, token: string, uid: string) { + const r = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/admin/user/" + uid,{ + method: "POST", + headers: { + "Authorization": "Bearer " + token + }, + body: JSON.stringify(u) + }) + 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 18b8dca..da71398 100644 --- a/frontend/src/apis/model.ts +++ b/frontend/src/apis/model.ts @@ -56,4 +56,11 @@ export interface UserInfo { email: string reg_ip: string name: string +} + +export interface EditUser { + email: string + name: string + password: string + is_admin: boolean } \ No newline at end of file diff --git a/frontend/src/views/admin/UserAdmin.tsx b/frontend/src/views/admin/UserAdmin.tsx index 969ce45..de7ad47 100644 --- a/frontend/src/views/admin/UserAdmin.tsx +++ b/frontend/src/views/admin/UserAdmin.tsx @@ -6,7 +6,7 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import useTitle from '@/hooks/useTitle'; -import { useMemoizedFn, useRequest } from 'ahooks'; +import { useRequest } from 'ahooks'; import { ListUser } from '@/apis/apis'; import { useEffect, useState } from 'react'; import { useAtomValue } from 'jotai'; @@ -113,15 +113,19 @@ interface MyDialogProp { } function MyDialog({ open, row, setOpen }: MyDialogProp) { - const handleClose = useMemoizedFn(() => { + const handleClose = () => { setOpen(false) - }) + } const [nrow, setNrow] = useState(row) useEffect(() => { setNrow(row) }, [row]) + const handleOpen = () => { + + } + return ( @@ -143,7 +147,7 @@ function MyDialog({ open, row, setOpen }: MyDialogProp) { - + ) diff --git a/handle/admin.go b/handle/admin.go index a1c5b1b..3988cb7 100644 --- a/handle/admin.go +++ b/handle/admin.go @@ -7,9 +7,12 @@ import ( "net/http" "strconv" + "github.com/go-chi/chi/v5" "github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/service" "github.com/xmdhs/authlib-skin/service/utils" + + U "github.com/xmdhs/authlib-skin/utils" ) type tokenValue string @@ -81,3 +84,33 @@ func (h *Handel) ListUser() http.HandlerFunc { encodeJson(w, model.API[model.List[model.UserList]]{Data: model.List[model.UserList]{List: ul, Total: uc}}) } } + +func (h *Handel) EditUser() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + uid := chi.URLParamFromCtx(ctx, "uid") + if uid == "" { + h.handleError(ctx, w, "uid 为空", model.ErrInput, 400, slog.LevelDebug) + return + } + uidi, err := strconv.Atoi(uid) + if err != nil { + h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug) + return + } + + a, err := U.DeCodeBody[model.EditUser](r.Body, h.validate) + if err != nil { + h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug) + return + } + err = h.webService.EditUser(ctx, a, uidi) + if err != nil { + h.handleError(ctx, w, err.Error(), model.ErrService, 500, slog.LevelWarn) + return + } + encodeJson[any](w, model.API[any]{ + Code: 0, + }) + } +} diff --git a/model/model.go b/model/model.go index 5758365..ced49c0 100644 --- a/model/model.go +++ b/model/model.go @@ -61,3 +61,12 @@ type Config struct { Captcha Captcha `json:"captcha"` AllowChangeName bool } + +type EditUser struct { + Email string `json:"email" validate:"required,email"` + Name string `json:"name" validate:"required,min=3,max=16"` + Password string `json:"password"` + IsAdmin bool `json:"is_admin"` + IsDisable bool `json:"is_disable"` + DelTextures bool `json:"del_textures"` +} diff --git a/server/route/route.go b/server/route/route.go index a79f6c6..66156d1 100644 --- a/server/route/route.go +++ b/server/route/route.go @@ -81,6 +81,7 @@ func newSkinApi(handel *handle.Handel) http.Handler { r.Use(handel.NeedAuth) r.Use(handel.NeedAdmin) r.Get("/admin/users", handel.ListUser()) + r.Post("/admin/user/{uid}", handel.EditUser()) }) return r diff --git a/service/admin.go b/service/admin.go index 55eebfe..e8bfc76 100644 --- a/service/admin.go +++ b/service/admin.go @@ -5,12 +5,15 @@ import ( "errors" "fmt" + "github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent/predicate" "github.com/xmdhs/authlib-skin/db/ent/user" "github.com/xmdhs/authlib-skin/db/ent/userprofile" + "github.com/xmdhs/authlib-skin/db/ent/usertoken" "github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model/yggdrasil" utilsService "github.com/xmdhs/authlib-skin/service/utils" + "github.com/xmdhs/authlib-skin/utils" ) var ErrNotAdmin = errors.New("无权限") @@ -72,3 +75,56 @@ func (w *WebService) ListUser(ctx context.Context, page int, email, name string) } return ul, uc, nil } + +func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) error { + err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error { + up := tx.User.UpdateOneID(uid).SetEmail(u.Email) + if u.Password != "" { + p, s := utils.Argon2ID(u.Password) + up = up.SetPassword(p).SetSalt(s) + err := tx.UserToken.Update().Where(usertoken.HasUserWith(user.ID(uid))).AddTokenID(1).Exec(ctx) + if err != nil { + return err + } + } + err := tx.UserProfile.Update().Where(userprofile.HasUserWith(user.ID(uid))).SetName(u.Name).Exec(ctx) + if err != nil { + return err + } + + if u.DelTextures { + up, err := tx.UserProfile.Query().Where(userprofile.ID(uid)).First(ctx) + if err != nil { + return err + } + tl := []string{"skin", "cape"} + for _, v := range tl { + err := utilsService.DelTexture(ctx, up.ID, v, w.client, w.config) + if err != nil { + return err + } + } + } + state := 0 + if u.IsAdmin { + state = utilsService.SetAdmin(state) + } + if u.IsDisable { + state = utilsService.SetDisable(state) + } + + up = up.SetState(state) + + err = up.Exec(ctx) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return fmt.Errorf("EditUser: %w", err) + } + return nil +} diff --git a/service/utils/auth.go b/service/utils/auth.go index 1f9e291..a4971c6 100644 --- a/service/utils/auth.go +++ b/service/utils/auth.go @@ -87,3 +87,11 @@ func IsAdmin(state int) bool { func IsDisable(state int) bool { return state&2 == 2 } + +func SetAdmin(state int) int { + return state | 1 +} + +func SetDisable(state int) int { + return state | 2 +} diff --git a/service/utils/texture.go b/service/utils/texture.go new file mode 100644 index 0000000..8b2dda0 --- /dev/null +++ b/service/utils/texture.go @@ -0,0 +1,69 @@ +package utils + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/samber/lo" + "github.com/xmdhs/authlib-skin/config" + "github.com/xmdhs/authlib-skin/db/ent" + "github.com/xmdhs/authlib-skin/db/ent/texture" + "github.com/xmdhs/authlib-skin/db/ent/usertexture" +) + +func DelTexture(ctx context.Context, userProfileID int, textureType string, client *ent.Client, config config.Config) error { + // 查找此用户该类型下是否已经存在皮肤 + tl, err := client.UserTexture.Query().Where(usertexture.And( + usertexture.UserProfileID(userProfileID), + usertexture.Type(textureType), + )).All(ctx) + if err != nil { + return fmt.Errorf("DelTexture: %w", err) + } + if len(tl) == 0 { + return nil + } + // 若存在,查找是否被引用 + for _, v := range tl { + c, err := client.UserTexture.Query().Where(usertexture.TextureID(v.TextureID)).Count(ctx) + if err != nil { + return fmt.Errorf("DelTexture: %w", err) + } + if c == 1 { + // 若没有其他用户使用该皮肤,删除文件和记录 + t, err := client.Texture.Query().Where(texture.ID(v.TextureID)).Only(ctx) + if err != nil { + var nf *ent.NotFoundError + if errors.As(err, &nf) { + continue + } + return fmt.Errorf("DelTexture: %w", err) + } + path := filepath.Join(config.TexturePath, t.TextureHash[:2], t.TextureHash[2:4], t.TextureHash) + err = os.Remove(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("DelTexture: %w", err) + } + // Texture 表中删除记录 + err = client.Texture.DeleteOneID(v.TextureID).Exec(ctx) + if err != nil { + return fmt.Errorf("DelTexture: %w", err) + } + } + } + ids := lo.Map[*ent.UserTexture, int](tl, func(item *ent.UserTexture, index int) int { + return item.ID + }) + // 中间表删除记录 + // UserProfile 上没有于此相关的字段,所以无需操作 + _, err = client.UserTexture.Delete().Where(usertexture.IDIn(ids...)).Exec(ctx) + // 小概率皮肤上传后,高并发时被此处清理。问题不大重新上传一遍就行。 + // 条件为使用一个独一无二的皮肤的用户,更换皮肤时,另一个用户同时更换自己的皮肤到这个独一无二的皮肤上。 + if err != nil { + return fmt.Errorf("DelTexture: %w", err) + } + return nil +} diff --git a/service/yggdrasil/texture.go b/service/yggdrasil/texture.go index ef5f4e7..38bae00 100644 --- a/service/yggdrasil/texture.go +++ b/service/yggdrasil/texture.go @@ -9,13 +9,12 @@ import ( "os" "path/filepath" - "github.com/samber/lo" "github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent/texture" "github.com/xmdhs/authlib-skin/db/ent/user" "github.com/xmdhs/authlib-skin/db/ent/userprofile" - "github.com/xmdhs/authlib-skin/db/ent/usertexture" "github.com/xmdhs/authlib-skin/model" + utilsService "github.com/xmdhs/authlib-skin/service/utils" "github.com/xmdhs/authlib-skin/utils" ) @@ -24,57 +23,7 @@ var ( ) func (y *Yggdrasil) delTexture(ctx context.Context, userProfileID int, textureType string) error { - // 查找此用户该类型下是否已经存在皮肤 - tl, err := y.client.UserTexture.Query().Where(usertexture.And( - usertexture.UserProfileID(userProfileID), - usertexture.Type(textureType), - )).All(ctx) - if err != nil { - return fmt.Errorf("delTexture: %w", err) - } - if len(tl) == 0 { - return nil - } - // 若存在,查找是否被引用 - for _, v := range tl { - c, err := y.client.UserTexture.Query().Where(usertexture.TextureID(v.TextureID)).Count(ctx) - if err != nil { - return fmt.Errorf("delTexture: %w", err) - } - if c == 1 { - // 若没有其他用户使用该皮肤,删除文件和记录 - t, err := y.client.Texture.Query().Where(texture.ID(v.TextureID)).Only(ctx) - if err != nil { - var nf *ent.NotFoundError - if errors.As(err, &nf) { - continue - } - return fmt.Errorf("delTexture: %w", err) - } - path := filepath.Join(y.config.TexturePath, t.TextureHash[:2], t.TextureHash[2:4], t.TextureHash) - err = os.Remove(path) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("delTexture: %w", err) - } - // Texture 表中删除记录 - err = y.client.Texture.DeleteOneID(v.TextureID).Exec(ctx) - if err != nil { - return fmt.Errorf("delTexture: %w", err) - } - } - } - ids := lo.Map[*ent.UserTexture, int](tl, func(item *ent.UserTexture, index int) int { - return item.ID - }) - // 中间表删除记录 - // UserProfile 上没有于此相关的字段,所以无需操作 - _, err = y.client.UserTexture.Delete().Where(usertexture.IDIn(ids...)).Exec(ctx) - // 小概率皮肤上传后,高并发时被此处清理。问题不大重新上传一遍就行。 - // 条件为使用一个独一无二的皮肤的用户,更换皮肤时,另一个用户同时更换自己的皮肤到这个独一无二的皮肤上。 - if err != nil { - return fmt.Errorf("delTexture: %w", err) - } - return nil + return utilsService.DelTexture(ctx, userProfileID, textureType, y.client, y.config) } func (y *Yggdrasil) DelTexture(ctx context.Context, uuid string, t *model.TokenClaims, textureType string) error {