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:
@@ -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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user