refactor: deduplicate sha1Hex/writeJSON/writeError into pkg/utils

- admin.go: replace local sha1Hex, sha256Hex, writeJSON, writeError with pkg/utils equivalents
- auth.go: replace local writeJSON with utils.WriteJSON; rewrite writeError as thin wrapper
- cas.go: remove local sha1Hex and unused writeJSON; use utils.SHA1Bytes
- pkg/utils.go: add WriteJSON, WriteError; reorder imports
This commit is contained in:
2026-05-29 23:53:33 +03:00
parent d418ae2b54
commit e1cc999ea8
5 changed files with 131 additions and 166 deletions

View File

@@ -2,8 +2,6 @@
package api
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"image/png"
@@ -16,6 +14,7 @@ import (
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/pkg/utils"
)
// Max skin file size: 1 MB (Minecraft skins are ~2-10 KB).
@@ -102,16 +101,16 @@ type serverInfo struct {
}
type launcherLatestResponse struct {
Version string `json:"version"`
Downloads map[string]string `json:"downloads"` // os+arch -> url
SHA256 string `json:"sha256"`
IsMandatory bool `json:"is_mandatory"`
Version string `json:"version"`
Downloads map[string]string `json:"downloads"` // os+arch -> url
SHA256 string `json:"sha256"`
IsMandatory bool `json:"is_mandatory"`
}
type profileResponse struct {
UUID string `json:"uuid"`
Username string `json:"username"`
Textures *textureInfo `json:"textures,omitempty"`
UUID string `json:"uuid"`
Username string `json:"username"`
Textures *textureInfo `json:"textures,omitempty"`
}
type textureInfo struct {
@@ -142,24 +141,24 @@ type errorResponse struct {
func (h *Handler) register(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "Cannot read body")
utils.WriteError(w, http.StatusBadRequest, "Cannot read body")
return
}
var req registerRequest
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
utils.WriteError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if req.Username == "" || req.Email == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "Username, email and password are required")
utils.WriteError(w, http.StatusBadRequest, "Username, email and password are required")
return
}
// Basic email validation.
if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") {
writeError(w, http.StatusBadRequest, "Invalid email address")
utils.WriteError(w, http.StatusBadRequest, "Invalid email address")
return
}
@@ -170,18 +169,18 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) {
req.Username, req.Email,
).Scan(&exists)
if err != nil {
writeError(w, http.StatusInternalServerError, "Database error")
utils.WriteError(w, http.StatusInternalServerError, "Database error")
return
}
if exists > 0 {
writeError(w, http.StatusConflict, "Username or email already taken")
utils.WriteError(w, http.StatusConflict, "Username or email already taken")
return
}
uuid := auth.GenerateUUID()
passwordHash, err := auth.HashPassword(req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to hash password")
utils.WriteError(w, http.StatusInternalServerError, "Failed to hash password")
return
}
@@ -191,28 +190,28 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) {
req.Username, req.Email, passwordHash, uuid,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to create user")
utils.WriteError(w, http.StatusInternalServerError, "Failed to create user")
return
}
writeJSON(w, http.StatusCreated, registerResponse{UUID: uuid})
utils.WriteJSON(w, http.StatusCreated, registerResponse{UUID: uuid})
}
func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "Cannot read body")
utils.WriteError(w, http.StatusBadRequest, "Cannot read body")
return
}
var req webLoginRequest
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
utils.WriteError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if req.Username == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "Username and password are required")
utils.WriteError(w, http.StatusBadRequest, "Username and password are required")
return
}
@@ -224,7 +223,7 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) {
).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.UUID)
if err != nil || !auth.VerifyPassword(req.Password, user.PasswordHash) {
writeError(w, http.StatusUnauthorized, "Invalid credentials")
utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
@@ -237,11 +236,11 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) {
token, token, user.ID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to create session")
utils.WriteError(w, http.StatusInternalServerError, "Failed to create session")
return
}
writeJSON(w, http.StatusOK, webLoginResponse{
utils.WriteJSON(w, http.StatusOK, webLoginResponse{
Token: token,
UUID: user.UUID,
Username: user.Username,
@@ -256,39 +255,39 @@ func (h *Handler) uploadSkin(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxSkinSize)
if err := r.ParseMultipartForm(maxSkinSize); err != nil {
writeError(w, http.StatusBadRequest, "Cannot parse form (max 1 MB)")
utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 1 MB)")
return
}
file, _, err := r.FormFile("skin")
if err != nil {
writeError(w, http.StatusBadRequest, "No skin file provided")
utils.WriteError(w, http.StatusBadRequest, "No skin file provided")
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "Cannot read skin file")
utils.WriteError(w, http.StatusBadRequest, "Cannot read skin file")
return
}
if !isValidSkinPNG(data) {
writeError(w, http.StatusBadRequest, "Invalid skin: must be a valid Minecraft skin PNG (64x32, 64x64, 128x128, or 128x64)")
utils.WriteError(w, http.StatusBadRequest, "Invalid skin: must be a valid Minecraft skin PNG (64x32, 64x64, 128x128, or 128x64)")
return
}
hash := sha1Hex(data)
hash := utils.SHA1Bytes(data)
// Store in CAS.
destDir := filepath.Join(h.cfg.SkinsDir, hash[:2])
if err := os.MkdirAll(destDir, 0o755); err != nil {
writeError(w, http.StatusInternalServerError, "Failed to store skin")
utils.WriteError(w, http.StatusInternalServerError, "Failed to store skin")
return
}
dest := filepath.Join(destDir, hash+".png")
if err := os.WriteFile(dest, data, 0o644); err != nil {
writeError(w, http.StatusInternalServerError, "Failed to write skin")
utils.WriteError(w, http.StatusInternalServerError, "Failed to write skin")
return
}
@@ -300,11 +299,11 @@ func (h *Handler) uploadSkin(w http.ResponseWriter, r *http.Request) {
userID, hash,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to update profile")
utils.WriteError(w, http.StatusInternalServerError, "Failed to update profile")
return
}
writeJSON(w, http.StatusOK, map[string]string{"hash": hash})
utils.WriteJSON(w, http.StatusOK, map[string]string{"hash": hash})
}
func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) {
@@ -315,40 +314,40 @@ func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxCapeSize)
if err := r.ParseMultipartForm(maxCapeSize); err != nil {
writeError(w, http.StatusBadRequest, "Cannot parse form (max 2 MB)")
utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 2 MB)")
return
}
file, _, err := r.FormFile("cape")
if err != nil {
writeError(w, http.StatusBadRequest, "No cape file provided")
utils.WriteError(w, http.StatusBadRequest, "No cape file provided")
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "Cannot read cape file")
utils.WriteError(w, http.StatusBadRequest, "Cannot read cape file")
return
}
// Validate PNG header.
if len(data) < 8 || string(data[:8]) != "\x89PNG\r\n\x1a\n" {
writeError(w, http.StatusBadRequest, "Invalid PNG file")
utils.WriteError(w, http.StatusBadRequest, "Invalid PNG file")
return
}
hash := sha1Hex(data)
hash := utils.SHA1Bytes(data)
// Store in skins dir alongside skins (same CAS layout).
destDir := filepath.Join(h.cfg.SkinsDir, hash[:2])
if err := os.MkdirAll(destDir, 0o755); err != nil {
writeError(w, http.StatusInternalServerError, "Failed to store cape")
utils.WriteError(w, http.StatusInternalServerError, "Failed to store cape")
return
}
dest := filepath.Join(destDir, hash+".png")
if err := os.WriteFile(dest, data, 0o644); err != nil {
writeError(w, http.StatusInternalServerError, "Failed to write cape")
utils.WriteError(w, http.StatusInternalServerError, "Failed to write cape")
return
}
@@ -360,11 +359,11 @@ func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) {
userID, hash,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to update profile")
utils.WriteError(w, http.StatusInternalServerError, "Failed to update profile")
return
}
writeJSON(w, http.StatusOK, map[string]string{"hash": hash})
utils.WriteJSON(w, http.StatusOK, map[string]string{"hash": hash})
}
func (h *Handler) deleteSkin(w http.ResponseWriter, r *http.Request) {
@@ -378,11 +377,11 @@ func (h *Handler) deleteSkin(w http.ResponseWriter, r *http.Request) {
userID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to remove skin")
utils.WriteError(w, http.StatusInternalServerError, "Failed to remove skin")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "skin removed"})
utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "skin removed"})
}
func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) {
@@ -396,11 +395,11 @@ func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) {
userID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Failed to remove cape")
utils.WriteError(w, http.StatusInternalServerError, "Failed to remove cape")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "cape removed"})
utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "cape removed"})
}
// authenticateRequest extracts and validates the Bearer token from the request,
@@ -408,7 +407,7 @@ func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) {
func (h *Handler) authenticateRequest(w http.ResponseWriter, r *http.Request) int {
token := auth.ExtractBearer(r.Header.Get("Authorization"))
if token == "" {
writeError(w, http.StatusUnauthorized, "Missing authorization token")
utils.WriteError(w, http.StatusUnauthorized, "Missing authorization token")
return 0
}
@@ -419,7 +418,7 @@ func (h *Handler) authenticateRequest(w http.ResponseWriter, r *http.Request) in
token,
).Scan(&userID)
if err != nil {
writeError(w, http.StatusUnauthorized, "Invalid or expired token")
utils.WriteError(w, http.StatusUnauthorized, "Invalid or expired token")
return 0
}
return userID
@@ -430,7 +429,7 @@ func (h *Handler) launcherLatest(w http.ResponseWriter, r *http.Request) {
archParam := r.URL.Query().Get("arch")
if osParam == "" || archParam == "" {
writeError(w, http.StatusBadRequest, "os and arch query parameters are required")
utils.WriteError(w, http.StatusBadRequest, "os and arch query parameters are required")
return
}
@@ -443,14 +442,14 @@ func (h *Handler) launcherLatest(w http.ResponseWriter, r *http.Request) {
).Scan(&release.Version, &release.FilePath, &release.SHA256, &release.IsMandatory)
if err != nil {
writeError(w, http.StatusNotFound, "No release found for this platform")
utils.WriteError(w, http.StatusNotFound, "No release found for this platform")
return
}
downloadURL := fmt.Sprintf("%s/files/launcher/%s/%s/%s/%s",
h.cfg.BaseURL, release.Version, osParam, archParam, filepath.Base(release.FilePath))
writeJSON(w, http.StatusOK, launcherLatestResponse{
utils.WriteJSON(w, http.StatusOK, launcherLatestResponse{
Version: release.Version,
Downloads: map[string]string{osParam + "_" + archParam: downloadURL},
SHA256: release.SHA256,
@@ -463,7 +462,7 @@ func (h *Handler) serversList(w http.ResponseWriter, r *http.Request) {
`SELECT slug, name, minecraft_version, server_ip FROM modpacks
WHERE is_active = true ORDER BY created_at DESC`)
if err != nil {
writeError(w, http.StatusInternalServerError, "Database error")
utils.WriteError(w, http.StatusInternalServerError, "Database error")
return
}
defer rows.Close()
@@ -477,13 +476,13 @@ func (h *Handler) serversList(w http.ResponseWriter, r *http.Request) {
servers = append(servers, s)
}
writeJSON(w, http.StatusOK, serversResponse{Servers: servers})
utils.WriteJSON(w, http.StatusOK, serversResponse{Servers: servers})
}
func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
if slug == "" {
writeError(w, http.StatusBadRequest, "Instance slug is required")
utils.WriteError(w, http.StatusBadRequest, "Instance slug is required")
return
}
@@ -494,7 +493,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) {
slug,
).Scan(&exists)
if err != nil || !exists {
writeError(w, http.StatusNotFound, "Instance not found")
utils.WriteError(w, http.StatusNotFound, "Instance not found")
return
}
@@ -502,7 +501,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) {
manifestPath := filepath.Join(h.cfg.CASDir, "..", "manifests", slug, "manifest.json")
data, err := os.ReadFile(manifestPath)
if err != nil {
writeError(w, http.StatusNotFound, "Manifest not found")
utils.WriteError(w, http.StatusNotFound, "Manifest not found")
return
}
@@ -513,7 +512,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) {
func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) {
uuid := r.PathValue("uuid")
if uuid == "" {
writeError(w, http.StatusBadRequest, "UUID is required")
utils.WriteError(w, http.StatusBadRequest, "UUID is required")
return
}
@@ -528,7 +527,7 @@ func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) {
uuid,
).Scan(&p.UUID, &p.Username, &skinHash, &capeHash, &isSlim)
if err != nil {
writeError(w, http.StatusNotFound, "Player not found")
utils.WriteError(w, http.StatusNotFound, "Player not found")
return
}
@@ -540,7 +539,7 @@ func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) {
}
}
writeJSON(w, http.StatusOK, p)
utils.WriteJSON(w, http.StatusOK, p)
}
func (h *Handler) serveSkin(w http.ResponseWriter, r *http.Request) {
@@ -572,11 +571,6 @@ func (h *Handler) serveSkin(w http.ResponseWriter, r *http.Request) {
// ── Helpers ───────────────────────────────────────────────────
func sha1Hex(data []byte) string {
h := sha1.Sum(data)
return hex.EncodeToString(h[:])
}
// isValidSkinPNG checks that data is a valid PNG with accepted Minecraft skin dimensions.
func isValidSkinPNG(data []byte) bool {
if len(data) < 8 || string(data[:8]) != "\x89PNG\r\n\x1a\n" {
@@ -602,13 +596,3 @@ func deref(s *string) string {
}
return *s
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, errorResponse{Error: msg})
}