删除 yggdrasil 中的 皮肤上传接口

因为几乎没有几个启动器实现了皮肤上传,所以删除了也几乎没有影响
This commit is contained in:
xmdhs 2023-10-11 16:36:11 +08:00
parent 6cba9ada31
commit cad8e6e317
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
16 changed files with 190 additions and 251 deletions

View File

@ -42,6 +42,7 @@ texturePath: "skin"
# 材质静态文件提供基础地址
# 如果静态文件位于 oss 上,比如 https://s3.amazonaws.com/example/1.png
# 则填写 https://s3.amazonaws.com/example
# 若不需要可不填写
textureBaseUrl: ""
# 用于在支持的启动器中展示本站的注册地址

View File

@ -1,53 +0,0 @@
# 为 true 则 uuid 生成方式于离线模式相同,若从离线模式切换不会丢失数据。
# 已有用户数据的情况下勿更改此项
offlineUUID: true
port: "127.0.0.1:8080"
Log:
Level: "debug"
# json 格式输出
Json: false
sql:
dsn: "123"
# 输出每条执行的 sql 语句
debug: false
cache:
# 默认使用内存缓存,若需要集群部署,请更换 redis
type: ""
# 内存缓存使用大小,单位 b
ram: 10000000
# 位于反向代理后启用,用于记录真实 ip
raelIP: false
# ip 段最大注册用户ipv4 为 /24 ipv6 为 /48
maxIpUser: 10
# 运行后勿修改,若为集群需设置为一致
rsaPriKey: ""
# 材质文件保存路径,如果需要对象存储可以把对象储存挂载到本地目录上
texturePath: "skin"
# 材质静态文件提供基础地址
# 如果静态文件位于 oss 上,比如 https://s3.amazonaws.com/example/1.png
# 则填写 https://s3.amazonaws.com/example
textureBaseUrl: ""
# 用于在支持的启动器中展示本站的注册地址
# 填写类似 https://example.com
webBaseUrl: ""
# 皮肤站名字,用于在多个地方展示
serverName: ""
captcha:
# 验证码类型,目前只支持 cloudflare turnstile
# 填写 turnstile
type: ""
siteKey: ""
secret: ""

View File

@ -1,4 +1,4 @@
import type { tokenData, ApiUser, ApiServerInfo, YggProfile, ApiConfig, List, UserInfo, EditUser } from '@/apis/model'
import type { tokenData, ApiUser, YggProfile, ApiConfig, List, UserInfo, EditUser } from '@/apis/model'
import { apiGet } from '@/apis/utils'
import root from '@/utils/root'
@ -37,12 +37,6 @@ export async function userInfo(token: string) {
return await apiGet<ApiUser>(v)
}
export async function serverInfo() {
const v = await fetch(root() + "/api/yggdrasil")
return await v.json() as ApiServerInfo
}
export async function yggProfile(uuid: string) {
if (uuid == "") return
const v = await fetch(root() + "/api/yggdrasil/sessionserver/session/minecraft/profile/" + uuid)
@ -53,21 +47,19 @@ export async function yggProfile(uuid: string) {
return data as YggProfile
}
export async function upTextures(uuid: string, token: string, textureType: 'skin' | 'cape', model: 'slim' | '', file: File) {
export async function upTextures(token: string, textureType: 'skin' | 'cape', model: 'slim' | '', file: File) {
const f = new FormData()
f.set("file", file)
f.set("model", model)
const r = await fetch(root() + "/api/yggdrasil/api/user/profile/" + uuid + "/" + textureType, {
const r = await fetch(root() + "/api/v1/user/skin/" + textureType, {
method: "PUT",
body: f,
headers: {
"Authorization": "Bearer " + token
}
})
if (r.status != 204) {
throw new Error("上传失败 " + String(r.status))
}
return await apiGet<unknown>(r)
}
export async function changePasswd(old: string, newpa: string, token: string) {

View File

@ -28,12 +28,6 @@ export interface ApiUser {
is_admin: boolean
}
export interface ApiServerInfo {
meta: {
serverName: string
}
}
export interface YggProfile {
name: string
properties: {
@ -45,6 +39,7 @@ export interface YggProfile {
export interface ApiConfig {
captcha: captcha
AllowChangeName: boolean
serverName: string
}
export interface UserInfo {

View File

@ -1,16 +1,16 @@
import { serverInfo } from '@/apis/apis'
import { getConfig } from '@/apis/apis'
import { useTitle as auseTitle, useRequest } from 'ahooks'
import { useEffect } from 'react'
export default function useTitle(title: string) {
const { data, error } = useRequest(serverInfo, {
cacheKey: "/api/yggdrasil",
const { data, error } = useRequest(getConfig, {
cacheKey: "/api/v1/config",
staleTime: 60000,
})
useEffect(() => {
error && console.warn(error)
}, [error])
auseTitle(title + " - " + data?.meta.serverName ?? "", {
auseTitle(title + " - " + data?.serverName ?? "", {
restoreOnUnmount: true
})
}

View File

@ -23,7 +23,7 @@ import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import Button from '@mui/material/Button';
import { useNavigate } from "react-router-dom";
import { useRequest, useMemoizedFn } from 'ahooks';
import { serverInfo, userInfo } from '@/apis/apis'
import { getConfig, userInfo } from '@/apis/apis'
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import { memo } from 'react';
@ -94,8 +94,8 @@ const MyToolbar = memo(function MyToolbar() {
const setErr = useSetAtom(LayoutAlertErr)
const setOpen = useSetAtom(DrawerOpen)
const server = useRequest(serverInfo, {
cacheKey: "/api/yggdrasil",
const server = useRequest(getConfig, {
cacheKey: "/api/v1/config",
staleTime: 60000,
onError: e => {
console.warn(e)
@ -131,7 +131,7 @@ const MyToolbar = memo(function MyToolbar() {
}
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Link to="/" style={{ color: "unset", textDecoration: "unset" }}>
{server.data?.meta.serverName ?? "皮肤站"}
{server.data?.serverName ?? "皮肤站"}
</Link>
</Typography>
{nowUser.name != "" && (

View File

@ -14,7 +14,7 @@ import Box from "@mui/material/Box";
import ReactSkinview3d from '@/components/Skinview3d'
import { useUnmount } from "ahooks";
import { useAtomValue, useSetAtom } from "jotai";
import { LayoutAlertErr, token, user } from "@/store/store";
import { LayoutAlertErr, token } from "@/store/store";
import { upTextures } from "@/apis/apis";
import Loading from "@/components/Loading";
import Snackbar from "@mui/material/Snackbar";
@ -25,7 +25,6 @@ const Textures = function Textures() {
const [file, setFile] = useState<File | null>(null)
const setErr = useSetAtom(LayoutAlertErr)
const [loading, setLoading] = useState(false)
const userinfo = useAtomValue(user)
const nowToken = useAtomValue(token)
const [ok, setOk] = useState(false)
const [skinInfo, setSkinInfo] = useState({
@ -73,8 +72,8 @@ const Textures = function Textures() {
setLoading(true)
const textureType = redioValue == "cape" ? "cape" : "skin"
const model = redioValue == "slim" ? "slim" : ""
upTextures(userinfo.uuid, nowToken, textureType, model, file).catch(e => [setErr(String(e)), console.warn(e)]).
finally(() => setLoading(false)).then(() => setOk(true))
upTextures(nowToken, textureType, model, file).then(() => setOk(true)).catch(e => [setErr(String(e)), console.warn(e)]).
finally(() => setLoading(false))
}

View File

@ -1,9 +1,14 @@
package handle
import (
"bytes"
"fmt"
"image/png"
"io"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/xmdhs/authlib-skin/model"
"github.com/xmdhs/authlib-skin/utils"
)
@ -123,3 +128,64 @@ func (h *Handel) ChangeName() http.HandlerFunc {
})
}
}
func (h *Handel) PutTexture() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
t := ctx.Value(tokenKey).(*model.TokenClaims)
models := r.FormValue("model")
textureType := chi.URLParamFromCtx(ctx, "textureType")
if textureType != "skin" && textureType != "cape" {
h.logger.DebugContext(ctx, "上传类型错误")
h.handleError(ctx, w, "上传类型错误", model.ErrInput, 400, slog.LevelDebug)
}
skin, err := func() ([]byte, error) {
f, _, err := r.FormFile("file")
if err != nil {
return nil, err
}
b, err := io.ReadAll(io.LimitReader(f, 50*1000))
if err != nil {
return nil, err
}
pc, err := png.DecodeConfig(bytes.NewReader(b))
if err != nil {
return nil, err
}
if pc.Height > 200 || pc.Width > 200 {
return nil, fmt.Errorf("材质大小超过限制")
}
p, err := png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
bw := bytes.NewBuffer(nil)
err = png.Encode(bw, p)
return bw.Bytes(), err
}()
if err != nil {
h.handleError(ctx, w, err.Error(), model.ErrInput, 400, slog.LevelDebug)
return
}
switch models {
case "slim":
case "":
default:
h.logger.DebugContext(ctx, "错误的皮肤的材质模型")
h.handleError(ctx, w, "错误的皮肤的材质模型", model.ErrInput, 400, slog.LevelDebug)
return
}
err = h.webService.PutTexture(ctx, t, skin, models, textureType)
if err != nil {
h.handleErrorService(ctx, w, err)
return
}
encodeJson(w, model.API[any]{
Code: 0,
})
}
}

View File

@ -1,12 +1,8 @@
package yggdrasil
import (
"bytes"
"context"
"errors"
"fmt"
"image/png"
"io"
"log/slog"
"net/http"
"strings"
@ -46,74 +42,6 @@ func (y *Yggdrasil) validTextureType(ctx context.Context, w http.ResponseWriter,
return true
}
func (y *Yggdrasil) PutTexture() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uuid, textureType, ok := getUUIDbyParams(ctx, y.logger, w)
if !ok {
return
}
t := ctx.Value(tokenKey).(*model.TokenClaims)
if uuid != t.Subject {
y.logger.DebugContext(ctx, "uuid 不相同")
w.WriteHeader(401)
return
}
model := r.FormValue("model")
skin, err := func() ([]byte, error) {
f, _, err := r.FormFile("file")
if err != nil {
return nil, err
}
b, err := io.ReadAll(io.LimitReader(f, 50*1000))
if err != nil {
return nil, err
}
pc, err := png.DecodeConfig(bytes.NewReader(b))
if err != nil {
return nil, err
}
if pc.Height > 200 || pc.Width > 200 {
return nil, fmt.Errorf("材质大小超过限制")
}
p, err := png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
bw := bytes.NewBuffer(nil)
err = png.Encode(bw, p)
return bw.Bytes(), err
}()
if err != nil {
y.logger.DebugContext(ctx, err.Error())
handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: err.Error()}, 400)
return
}
if !y.validTextureType(ctx, w, textureType) {
return
}
switch model {
case "slim":
case "":
default:
y.logger.DebugContext(ctx, "错误的皮肤的材质模型")
handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: "错误的皮肤的材质模型"}, 400)
return
}
err = y.yggdrasilService.PutTexture(ctx, t, skin, model, textureType)
if err != nil {
y.handleYgError(ctx, w, err)
return
}
w.WriteHeader(204)
}
}
func getUUIDbyParams(ctx context.Context, l *slog.Logger, w http.ResponseWriter) (string, string, bool) {
uuid := chi.URLParamFromCtx(ctx, "uuid")
textureType := chi.URLParamFromCtx(ctx, "textureType")

View File

@ -31,8 +31,9 @@ type TokenClaims struct {
}
type Captcha struct {
Type string `json:"type"`
SiteKey string `json:"siteKey"`
Type string `json:"type"`
SiteKey string `json:"siteKey"`
ServerName string `json:"serverName"`
}
type UserInfo struct {

View File

@ -46,7 +46,6 @@ func newYggdrasil(handelY *yggdrasil.Yggdrasil) http.Handler {
r.Post("/authserver/invalidate", handelY.Invalidate())
r.Post("/authserver/refresh", handelY.Refresh())
r.Put("/api/user/profile/{uuid}/{textureType}", handelY.PutTexture())
r.Delete("/api/user/profile/{uuid}/{textureType}", handelY.DelTexture())
r.Post("/sessionserver/session/minecraft/join", handelY.SessionJoin())
@ -78,7 +77,7 @@ func newSkinApi(handel *handle.Handel) http.Handler {
r.Get("/user", handel.UserInfo())
r.Post("/user/password", handel.ChangePasswd())
r.Post("/user/name", handel.ChangeName())
r.Put("/user/skin/{textureType}", handel.PutTexture())
})
r.Group(func(r chi.Router) {

View File

@ -117,7 +117,7 @@ func (w *WebService) EditUser(ctx context.Context, u model.EditUser, uid int) er
uuid = userProfile.UUID
tl := []string{"skin", "cape"}
for _, v := range tl {
err := utilsService.DelTexture(ctx, userProfile.ID, v, w.client, w.config)
err := utilsService.DelTexture(ctx, userProfile.ID, v, w.client, w.config.TexturePath)
if err != nil {
return err
}

View File

@ -15,8 +15,9 @@ import (
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,
Type: w.config.Captcha.Type,
SiteKey: w.config.Captcha.SiteKey,
ServerName: w.config.ServerName,
},
AllowChangeName: !w.config.OfflineUUID,
}

96
service/texture.go Normal file
View File

@ -0,0 +1,96 @@
package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"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/model"
utilsService "github.com/xmdhs/authlib-skin/service/utils"
"github.com/xmdhs/authlib-skin/utils"
)
func (w *WebService) PutTexture(ctx context.Context, t *model.TokenClaims, texturebyte []byte, model string, textureType string) error {
up, err := w.client.UserProfile.Query().Where(userprofile.HasUserWith(user.ID(t.UID))).First(ctx)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = utilsService.DelTexture(ctx, up.ID, textureType, w.client, w.config.TexturePath)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
hashstr := getHash(texturebyte)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
u, err := w.client.User.Query().Where(user.HasProfileWith(userprofile.ID(up.ID))).Only(ctx)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
t, err := tx.Texture.Query().Where(texture.TextureHash(hashstr)).Only(ctx)
if err != nil {
var ne *ent.NotFoundError
if !errors.As(err, &ne) {
return err
}
}
if t == nil {
t, err = tx.Texture.Create().SetCreatedUser(u).SetTextureHash(hashstr).Save(ctx)
if err != nil {
return err
}
}
err = tx.UserTexture.Create().SetTexture(t).SetType(textureType).SetUserProfile(up).SetVariant(model).Exec(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = createTextureFile(w.config.TexturePath, texturebyte, hashstr)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = w.cache.Del([]byte("Profile" + t.Subject))
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
return nil
}
func getHash(b []byte) string {
hashed := sha256.Sum256(b)
return hex.EncodeToString(hashed[:])
}
func createTextureFile(path string, b []byte, hashstr string) error {
p := filepath.Join(path, hashstr[:2], hashstr[2:4], hashstr)
err := os.MkdirAll(filepath.Dir(p), 0755)
if err != nil {
return fmt.Errorf("createTextureFile: %w", err)
}
f, err := os.Stat(p)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("createTextureFile: %w", err)
}
if f == nil {
err := os.WriteFile(p, b, 0644)
if err != nil {
return fmt.Errorf("createTextureFile: %w", err)
}
}
return nil
}

View File

@ -8,13 +8,12 @@ import (
"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 {
func DelTexture(ctx context.Context, userProfileID int, textureType string, client *ent.Client, texturePath string) error {
// 查找此用户该类型下是否已经存在皮肤
tl, err := client.UserTexture.Query().Where(usertexture.And(
usertexture.UserProfileID(userProfileID),
@ -42,7 +41,7 @@ func DelTexture(ctx context.Context, userProfileID int, textureType string, clie
}
return fmt.Errorf("DelTexture: %w", err)
}
path := filepath.Join(config.TexturePath, t.TextureHash[:2], t.TextureHash[2:4], t.TextureHash)
path := filepath.Join(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)

View File

@ -2,20 +2,13 @@ package yggdrasil
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"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/model"
utilsService "github.com/xmdhs/authlib-skin/service/utils"
"github.com/xmdhs/authlib-skin/utils"
)
var (
@ -23,7 +16,7 @@ var (
)
func (y *Yggdrasil) delTexture(ctx context.Context, userProfileID int, textureType string) error {
return utilsService.DelTexture(ctx, userProfileID, textureType, y.client, y.config)
return utilsService.DelTexture(ctx, userProfileID, textureType, y.client, y.config.TexturePath)
}
func (y *Yggdrasil) DelTexture(ctx context.Context, t *model.TokenClaims, textureType string) error {
@ -41,81 +34,3 @@ func (y *Yggdrasil) DelTexture(ctx context.Context, t *model.TokenClaims, textur
}
return nil
}
func (y *Yggdrasil) PutTexture(ctx context.Context, t *model.TokenClaims, texturebyte []byte, model string, textureType string) error {
up, err := y.client.UserProfile.Query().Where(userprofile.HasUserWith(user.ID(t.UID))).First(ctx)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = y.delTexture(ctx, up.ID, textureType)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
hashstr := getHash(texturebyte)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
u, err := y.client.User.Query().Where(user.HasProfileWith(userprofile.ID(up.ID))).Only(ctx)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = utils.WithTx(ctx, y.client, func(tx *ent.Tx) error {
t, err := tx.Texture.Query().Where(texture.TextureHash(hashstr)).Only(ctx)
if err != nil {
var ne *ent.NotFoundError
if !errors.As(err, &ne) {
return err
}
}
if t == nil {
t, err = tx.Texture.Create().SetCreatedUser(u).SetTextureHash(hashstr).Save(ctx)
if err != nil {
return err
}
}
err = tx.UserTexture.Create().SetTexture(t).SetType(textureType).SetUserProfile(up).SetVariant(model).Exec(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = createTextureFile(y.config.TexturePath, texturebyte, hashstr)
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
err = y.cache.Del([]byte("Profile" + t.Subject))
if err != nil {
return fmt.Errorf("PutTexture: %w", err)
}
return nil
}
func getHash(b []byte) string {
hashed := sha256.Sum256(b)
return hex.EncodeToString(hashed[:])
}
func createTextureFile(path string, b []byte, hashstr string) error {
p := filepath.Join(path, hashstr[:2], hashstr[2:4], hashstr)
err := os.MkdirAll(filepath.Dir(p), 0755)
if err != nil {
return fmt.Errorf("createTextureFile: %w", err)
}
f, err := os.Stat(p)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("createTextureFile: %w", err)
}
if f == nil {
err := os.WriteFile(p, b, 0644)
if err != nil {
return fmt.Errorf("createTextureFile: %w", err)
}
}
return nil
}