Compare commits
5 Commits
9bf2a15045
...
3fee913c1c
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fee913c1c | |||
| 0e5d98cff7 | |||
| 2dcb1e7735 | |||
| 1cdfe9fefc | |||
| 9e2657c709 |
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httprate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -99,8 +100,16 @@ func main() {
|
||||
|
||||
// --- Публичные роуты ---
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Post("/register", userHandler.Register)
|
||||
r.Post("/login", authHandler.Login)
|
||||
// Rate limiting: 100 requests per minute for general API
|
||||
r.Use(httprate.LimitByIP(100, time.Minute))
|
||||
|
||||
// Auth endpoints: stricter limit (10 requests per minute)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(httprate.LimitByIP(10, time.Minute))
|
||||
r.Post("/register", userHandler.Register)
|
||||
r.Post("/login", authHandler.Login)
|
||||
})
|
||||
|
||||
r.Get("/servers", serverHandler.GetServers)
|
||||
|
||||
r.Route("/launcher", func(r chi.Router) {
|
||||
@@ -109,9 +118,13 @@ func main() {
|
||||
})
|
||||
})
|
||||
r.Route("/authserver", func(r chi.Router) {
|
||||
// Stricter rate limit for auth server (10 req/min)
|
||||
r.Use(httprate.LimitByIP(10, time.Minute))
|
||||
r.Post("/authenticate", authHandler.Authenticate)
|
||||
})
|
||||
r.Route("/sessionserver/session/minecraft", func(r chi.Router) {
|
||||
// Rate limit for session endpoints (60 req/min)
|
||||
r.Use(httprate.LimitByIP(60, time.Minute))
|
||||
r.Post("/join", authHandler.Join)
|
||||
r.Get("/profile/{uuid}", profileHandler.GetProfile)
|
||||
})
|
||||
@@ -134,7 +147,10 @@ func main() {
|
||||
})
|
||||
|
||||
r.Route("/modpacks", func(r chi.Router) {
|
||||
r.Get("/", modpackHandler.GetModpacks)
|
||||
r.Post("/import", modpackHandler.ImportModpack)
|
||||
r.Post("/update", modpackHandler.UpdateModpack)
|
||||
r.Get("/versions", modpackHandler.GetModpackVersions)
|
||||
})
|
||||
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
|
||||
15
go.mod
15
go.mod
@@ -5,17 +5,26 @@ go 1.24.1
|
||||
require (
|
||||
github.com/Tnze/go-mc v1.20.2
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-chi/httprate v0.15.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
||||
36
go.sum
36
go.sum
@@ -3,8 +3,20 @@ github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
|
||||
github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -19,19 +31,27 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/utils"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
@@ -27,6 +28,13 @@ func (h *AuthHandler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErrors := utils.ValidateStruct(req); validationErrors != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.Service.Authenticate(r.Context(), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrInvalidCredentials) {
|
||||
@@ -57,6 +65,13 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErrors := utils.ValidateStruct(req); validationErrors != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Service.ValidateJoinRequest(r.Context(), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrInvalidCredentials) {
|
||||
@@ -84,6 +99,13 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErrors := utils.ValidateStruct(req); validationErrors != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
token, user, err := h.Service.LoginUser(r.Context(), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrInvalidCredentials) {
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core/importer"
|
||||
@@ -203,3 +205,256 @@ func (h *ModpackHandler) updateJobStatus(ctx context.Context, jobID int, status
|
||||
msg, _ := json.Marshal(update)
|
||||
h.Hub.BroadcastMessage(msg)
|
||||
}
|
||||
|
||||
// ModpackVersionResponse представляет версию модпака для ответа API
|
||||
type ModpackVersionResponse struct {
|
||||
FileID int `json:"file_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
FileName string `json:"file_name"`
|
||||
FileDate string `json:"file_date"`
|
||||
GameVersions []string `json:"game_versions"`
|
||||
}
|
||||
|
||||
// GetModpackVersions возвращает список доступных версий для модпака по URL
|
||||
func (h *ModpackHandler) GetModpackVersions(w http.ResponseWriter, r *http.Request) {
|
||||
pageURL := r.URL.Query().Get("url")
|
||||
if pageURL == "" {
|
||||
http.Error(w, "Missing 'url' query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("CURSEFORGE_API_KEY")
|
||||
if apiKey == "" {
|
||||
http.Error(w, "CurseForge API key not configured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cfImporter := importer.NewCurseForgeImporter("", apiKey)
|
||||
|
||||
// Извлекаем slug из URL
|
||||
parsedURL, err := parseModpackURL(pageURL)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid URL: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Ищем проект по slug
|
||||
projectID, err := cfImporter.FindModpackBySlug(parsedURL)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Modpack not found: %v", err), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем список файлов (версий)
|
||||
files, err := cfImporter.GetModpackFiles(projectID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to get versions: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Преобразуем в ответ
|
||||
var versions []ModpackVersionResponse
|
||||
for _, f := range files {
|
||||
versions = append(versions, ModpackVersionResponse{
|
||||
FileID: f.ID,
|
||||
DisplayName: f.DisplayName,
|
||||
FileName: f.FileName,
|
||||
FileDate: f.FileDate,
|
||||
GameVersions: f.GameVersions,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(versions)
|
||||
}
|
||||
|
||||
// parseModpackURL извлекает slug из URL страницы CurseForge
|
||||
func parseModpackURL(rawURL string) (string, error) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// URL вида https://www.curseforge.com/minecraft/modpacks/all-the-mods-9
|
||||
// path.Base вернет "all-the-mods-9"
|
||||
slug := path.Base(parsed.Path)
|
||||
if slug == "" || slug == "." || slug == "/" {
|
||||
return "", fmt.Errorf("could not extract modpack slug from URL")
|
||||
}
|
||||
return slug, nil
|
||||
}
|
||||
|
||||
// GetModpacks возвращает список всех модпаков для админки
|
||||
func (h *ModpackHandler) GetModpacks(w http.ResponseWriter, r *http.Request) {
|
||||
modpacks, err := h.ModpackRepo.GetAllModpacks(r.Context())
|
||||
if err != nil {
|
||||
slog.Error("Failed to get modpacks", "error", err)
|
||||
http.Error(w, "Failed to get modpacks", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(modpacks)
|
||||
}
|
||||
|
||||
// UpdateJobParams содержит параметры для фоновой задачи обновления
|
||||
type UpdateJobParams struct {
|
||||
ImporterType string
|
||||
ImportMethod string
|
||||
SourceURL string
|
||||
TempZipPath string
|
||||
ModpackID int
|
||||
ModpackName string
|
||||
MCVersion string
|
||||
}
|
||||
|
||||
// UpdateModpack обрабатывает запрос на обновление модпака
|
||||
func (h *ModpackHandler) UpdateModpack(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(512 << 20); err != nil {
|
||||
http.Error(w, "File too large", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
modpackName := r.FormValue("modpackName")
|
||||
if modpackName == "" {
|
||||
http.Error(w, "Missing modpack name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем модпак по имени
|
||||
modpack, err := h.ModpackRepo.GetModpackByName(r.Context(), modpackName)
|
||||
if err != nil {
|
||||
http.Error(w, "Modpack not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
params := UpdateJobParams{
|
||||
ImporterType: r.FormValue("importerType"),
|
||||
ImportMethod: r.FormValue("importMethod"),
|
||||
SourceURL: r.FormValue("sourceUrl"),
|
||||
ModpackID: modpack.ID,
|
||||
ModpackName: modpackName,
|
||||
MCVersion: r.FormValue("mcVersion"),
|
||||
}
|
||||
|
||||
if params.MCVersion == "" {
|
||||
params.MCVersion = modpack.MinecraftVersion
|
||||
}
|
||||
|
||||
// Обработка загрузки файла
|
||||
if params.ImportMethod == "file" {
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid file upload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tempFile, err := os.CreateTemp("", "modpack-update-*.zip")
|
||||
if err != nil {
|
||||
http.Error(w, "Could not create temp file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tempFile, file); err != nil {
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
http.Error(w, "Could not save temp file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tempFile.Close()
|
||||
params.TempZipPath = tempFile.Name()
|
||||
}
|
||||
|
||||
// Создаем задачу в БД
|
||||
jobID, err := h.JobRepo.CreateJob(r.Context())
|
||||
if err != nil {
|
||||
if params.ImportMethod == "file" {
|
||||
os.Remove(params.TempZipPath)
|
||||
}
|
||||
http.Error(w, fmt.Sprintf("Failed to create job: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Запускаем worker
|
||||
go h.processUpdateJob(jobID, params)
|
||||
|
||||
// Отправляем ответ
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"message": "Update job started",
|
||||
})
|
||||
}
|
||||
|
||||
// processUpdateJob выполняет обновление модпака в фоне
|
||||
func (h *ModpackHandler) processUpdateJob(jobID int, params UpdateJobParams) {
|
||||
ctx := context.Background()
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 10, "")
|
||||
|
||||
// Очистка временного файла по завершении
|
||||
if params.TempZipPath != "" {
|
||||
defer os.Remove(params.TempZipPath)
|
||||
}
|
||||
|
||||
storagePath := os.Getenv("MODPACKS_STORAGE_PATH")
|
||||
var imp importer.ModpackImporter
|
||||
|
||||
// Настройка импортера
|
||||
switch params.ImporterType {
|
||||
case "simple":
|
||||
imp = &importer.SimpleZipImporter{StoragePath: storagePath}
|
||||
case "curseforge":
|
||||
apiKey := os.Getenv("CURSEFORGE_API_KEY")
|
||||
if apiKey == "" {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "CurseForge API key missing")
|
||||
return
|
||||
}
|
||||
imp = importer.NewCurseForgeImporter(storagePath, apiKey)
|
||||
case "modrinth":
|
||||
imp = importer.NewModrinthImporter(storagePath)
|
||||
default:
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "Invalid importer type")
|
||||
return
|
||||
}
|
||||
|
||||
// Загрузка по URL если нужно
|
||||
if params.ImportMethod == "url" {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusDownloading, 20, "Downloading from URL...")
|
||||
|
||||
if cfImporter, ok := imp.(*importer.CurseForgeImporter); ok {
|
||||
zipPath, err := cfImporter.DownloadModpackFromURL(params.SourceURL)
|
||||
if err != nil {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Download failed: %v", err))
|
||||
return
|
||||
}
|
||||
params.TempZipPath = zipPath
|
||||
defer os.Remove(zipPath)
|
||||
} else {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, "URL import not supported for this type")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 40, "Processing modpack files...")
|
||||
|
||||
// Импорт файлов
|
||||
files, err := imp.Import(params.TempZipPath)
|
||||
if err != nil {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Import failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusProcessing, 80, "Updating database...")
|
||||
|
||||
// Обновление в БД
|
||||
err = h.ModpackRepo.UpdateModpackTx(ctx, params.ModpackID, params.MCVersion, files)
|
||||
if err != nil {
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusFailed, 0, fmt.Sprintf("Database update failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
h.updateJobStatus(ctx, jobID, models.JobStatusCompleted, 100, "Success")
|
||||
|
||||
// Запуск Janitor-а
|
||||
go h.JanitorService.CleanOrphanedFiles(context.Background())
|
||||
}
|
||||
|
||||
480
internal/api/openapi.yaml
Normal file
480
internal/api/openapi.yaml
Normal file
@@ -0,0 +1,480 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Minecraft Platform API
|
||||
description: API for Minecraft Server Platform handling auth, skins, modpacks, and servers.
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: Local development server
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
paths:
|
||||
# --- Public Auth & User Registration ---
|
||||
/api/register:
|
||||
post:
|
||||
summary: Register a new user
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [username, email, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
responses:
|
||||
201:
|
||||
description: User registered successfully
|
||||
400:
|
||||
description: Validation error
|
||||
|
||||
/api/login:
|
||||
post:
|
||||
summary: Login user
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [email, password]
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
responses:
|
||||
200:
|
||||
description: Login successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
user:
|
||||
$ref: '#/components/schemas/User'
|
||||
401:
|
||||
description: Invalid credentials
|
||||
|
||||
# --- Auth Server (Launcher Integration) ---
|
||||
/authserver/authenticate:
|
||||
post:
|
||||
summary: Authenticate (Launcher flow)
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Authenticated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
accessToken:
|
||||
type: string
|
||||
clientToken:
|
||||
type: string
|
||||
selectedProfile:
|
||||
$ref: '#/components/schemas/GameProfile'
|
||||
|
||||
# --- Session Server ---
|
||||
/sessionserver/session/minecraft/join:
|
||||
post:
|
||||
summary: Join server (Client-side)
|
||||
tags: [Auth]
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
accessToken:
|
||||
type: string
|
||||
selectedProfile:
|
||||
type: string
|
||||
serverId:
|
||||
type: string
|
||||
responses:
|
||||
204:
|
||||
description: Joined successfully
|
||||
|
||||
/sessionserver/session/minecraft/profile/{uuid}:
|
||||
get:
|
||||
summary: Get user profile (Server-side check)
|
||||
tags: [User]
|
||||
security: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: uuid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Profile data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GameProfile'
|
||||
|
||||
# --- User Endpoints ---
|
||||
/api/user/me:
|
||||
get:
|
||||
summary: Get current user info
|
||||
tags: [User]
|
||||
responses:
|
||||
200:
|
||||
description: Current user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
/api/user/skin:
|
||||
post:
|
||||
summary: Upload skin
|
||||
tags: [User]
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
200:
|
||||
description: Skin uploaded
|
||||
|
||||
# --- Servers ---
|
||||
/api/servers:
|
||||
get:
|
||||
summary: Get game servers list
|
||||
tags: [Servers]
|
||||
security: []
|
||||
responses:
|
||||
200:
|
||||
description: List of servers
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GameServer'
|
||||
|
||||
# --- Launcher ---
|
||||
/api/launcher/modpacks/summary:
|
||||
get:
|
||||
summary: Get modpacks summary (for launcher)
|
||||
tags: [Launcher]
|
||||
security: []
|
||||
responses:
|
||||
200:
|
||||
description: List of modpacks
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
/api/launcher/modpacks/{name}/manifest:
|
||||
get:
|
||||
summary: Get modpack manifest
|
||||
tags: [Launcher]
|
||||
security: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Modpack manifest
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
hash:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
|
||||
# --- Admin / Modpacks ---
|
||||
/api/admin/modpacks:
|
||||
get:
|
||||
summary: Get all modpacks (Admin)
|
||||
tags: [Admin]
|
||||
responses:
|
||||
200:
|
||||
description: List of full modpack details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Modpack'
|
||||
|
||||
/api/admin/modpacks/import:
|
||||
post:
|
||||
summary: Import new modpack
|
||||
tags: [Admin]
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
mcVersion:
|
||||
type: string
|
||||
importerType:
|
||||
type: string
|
||||
enum: [simple, curseforge, modrinth]
|
||||
importMethod:
|
||||
type: string
|
||||
enum: [file, url]
|
||||
sourceUrl:
|
||||
type: string
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
202:
|
||||
description: Job started
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
job_id:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
|
||||
/api/admin/modpacks/update:
|
||||
post:
|
||||
summary: Update existing modpack
|
||||
tags: [Admin]
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
modpackName:
|
||||
type: string
|
||||
mcVersion:
|
||||
type: string
|
||||
importerType:
|
||||
type: string
|
||||
importMethod:
|
||||
type: string
|
||||
sourceUrl:
|
||||
type: string
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
202:
|
||||
description: Job started
|
||||
|
||||
/api/admin/modpacks/versions:
|
||||
get:
|
||||
summary: Get modpack versions (CurseForge)
|
||||
tags: [Admin]
|
||||
parameters:
|
||||
- in: query
|
||||
name: url
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: List of versions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
file_id:
|
||||
type: integer
|
||||
display_name:
|
||||
type: string
|
||||
game_versions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
# --- Admin / Users ---
|
||||
/api/admin/users:
|
||||
get:
|
||||
summary: Get all users
|
||||
tags: [Admin]
|
||||
responses:
|
||||
200:
|
||||
description: List of users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
/api/admin/users/{id}/role:
|
||||
patch:
|
||||
summary: Update user role
|
||||
tags: [Admin]
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
enum: [user, admin]
|
||||
responses:
|
||||
200:
|
||||
description: Role updated
|
||||
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
username:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
GameProfile:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
properties:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
signature:
|
||||
type: string
|
||||
|
||||
GameServer:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
address:
|
||||
type: string
|
||||
is_enabled:
|
||||
type: boolean
|
||||
motd:
|
||||
type: string
|
||||
player_count:
|
||||
type: integer
|
||||
max_players:
|
||||
type: integer
|
||||
version_name:
|
||||
type: string
|
||||
ping_proxy_server:
|
||||
type: integer
|
||||
bluemap_url:
|
||||
type: string
|
||||
|
||||
Modpack:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
minecraft_version:
|
||||
type: string
|
||||
is_active:
|
||||
type: boolean
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/core"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/utils"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
@@ -23,13 +25,18 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// --- ДОБАВЛЕНО ЛОГИРОВАНИЕ ---
|
||||
log.Printf("[Handler] Received registration request for username: '%s', email: '%s'", req.Username, req.Email)
|
||||
if validationErrors := utils.ValidateStruct(req); validationErrors != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Received registration request", "username", req.Username, "email", req.Email)
|
||||
|
||||
err := h.Service.RegisterNewUser(r.Context(), req)
|
||||
if err != nil {
|
||||
// --- ДОБАВЛЕНО ЛОГИРОВАНИЕ ОШИБКИ ---
|
||||
log.Printf("[Handler] Service returned error: %v", err)
|
||||
slog.Error("Service returned error", "error", err)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, database.ErrUserExists):
|
||||
@@ -42,8 +49,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// --- ДОБАВЛЕНО ЛОГИРОВАНИЕ УСПЕХА ---
|
||||
log.Printf("[Handler] User '%s' registered successfully.", req.Username)
|
||||
slog.Info("User registered successfully", "username", req.Username)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ func (i *CurseForgeImporter) getFileInfo(projectID, fileID int) (*CurseForgeFile
|
||||
return &fileInfo, nil
|
||||
}
|
||||
|
||||
// findModpackBySlug ищет ID проекта по его "слагу" (части URL).
|
||||
func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) {
|
||||
// FindModpackBySlug ищет ID проекта по его "слагу" (части URL).
|
||||
func (i *CurseForgeImporter) FindModpackBySlug(slug string) (int, error) {
|
||||
apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/search?gameId=432&slug=%s", url.QueryEscape(slug))
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
|
||||
if err != nil {
|
||||
@@ -113,30 +113,40 @@ func (i *CurseForgeImporter) findModpackBySlug(slug string) (int, error) {
|
||||
|
||||
// getLatestModpackFileURL находит URL для скачивания последнего файла проекта.
|
||||
func (i *CurseForgeImporter) getLatestModpackFileURL(projectID int) (string, error) {
|
||||
files, err := i.GetModpackFiles(projectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return "", fmt.Errorf("no files found for projectID %d", projectID)
|
||||
}
|
||||
|
||||
latestFile := files[0]
|
||||
return latestFile.DownloadURL, nil
|
||||
}
|
||||
|
||||
// GetModpackFiles возвращает список файлов (версий) для проекта.
|
||||
func (i *CurseForgeImporter) GetModpackFiles(projectID int) ([]CurseForgeFileData, error) {
|
||||
apiURL := fmt.Sprintf("https://api.curseforge.com/v1/mods/%d/files", projectID)
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("x-api-key", i.APIKey)
|
||||
|
||||
resp, err := i.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var filesResp CurseForgeFilesResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&filesResp); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(filesResp.Data) == 0 {
|
||||
return "", fmt.Errorf("no files found for projectID %d", projectID)
|
||||
}
|
||||
|
||||
latestFile := filesResp.Data[0]
|
||||
return latestFile.DownloadURL, nil
|
||||
return filesResp.Data, nil
|
||||
}
|
||||
|
||||
// DownloadModpackFromURL скачивает zip-архив модпака по URL страницы CurseForge.
|
||||
@@ -148,7 +158,7 @@ func (i *CurseForgeImporter) DownloadModpackFromURL(pageURL string) (string, err
|
||||
slug := path.Base(parsedURL.Path)
|
||||
log.Printf("Importer: Extracted slug '%s' from URL", slug)
|
||||
|
||||
projectID, err := i.findModpackBySlug(slug)
|
||||
projectID, err := i.FindModpackBySlug(slug)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -31,16 +31,22 @@ type CurseForgeSearchResponse struct {
|
||||
|
||||
// CurseForgeFilesResponse - ответ от эндпоинта получения файлов проекта
|
||||
type CurseForgeFilesResponse struct {
|
||||
Data []struct {
|
||||
ID int `json:"id"`
|
||||
FileName string `json:"fileName"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
} `json:"data"`
|
||||
Data []CurseForgeFileData `json:"data"`
|
||||
Pagination struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
// CurseForgeFileData - данные о файле
|
||||
type CurseForgeFileData struct {
|
||||
ID int `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
FileName string `json:"fileName"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
FileDate string `json:"fileDate"`
|
||||
GameVersions []string `json:"gameVersions"`
|
||||
}
|
||||
|
||||
// CurseForgeFile представляет полную информацию о файле с API.
|
||||
type CurseForgeFile struct {
|
||||
Data struct {
|
||||
|
||||
@@ -126,3 +126,85 @@ func (r *ModpackRepository) GetModpacksSummary(ctx context.Context) ([]models.Mo
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// GetAllModpacks возвращает список всех модпаков для админки.
|
||||
func (r *ModpackRepository) GetAllModpacks(ctx context.Context) ([]models.Modpack, error) {
|
||||
query := `
|
||||
SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at
|
||||
FROM modpacks
|
||||
ORDER BY name`
|
||||
|
||||
rows, err := r.DB.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var modpacks []models.Modpack
|
||||
for rows.Next() {
|
||||
var m models.Modpack
|
||||
if err := rows.Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modpacks = append(modpacks, m)
|
||||
}
|
||||
|
||||
return modpacks, nil
|
||||
}
|
||||
|
||||
// UpdateModpackTx обновляет файлы модпака в транзакции: удаляет старые, добавляет новые.
|
||||
func (r *ModpackRepository) UpdateModpackTx(ctx context.Context, modpackID int, mcVersion string, files []models.ModpackFile) error {
|
||||
tx, err := r.DB.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// Обновляем версию Minecraft и updated_at
|
||||
_, err = tx.Exec(ctx,
|
||||
"UPDATE modpacks SET minecraft_version = $1, updated_at = NOW() WHERE id = $2",
|
||||
mcVersion, modpackID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Удаляем старые файлы
|
||||
_, err = tx.Exec(ctx, "DELETE FROM modpack_files WHERE modpack_id = $1", modpackID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Добавляем новые файлы
|
||||
rows := make([][]interface{}, len(files))
|
||||
for i, f := range files {
|
||||
rows[i] = []interface{}{modpackID, f.RelativePath, f.FileHash, f.FileSize, f.DownloadURL}
|
||||
}
|
||||
|
||||
_, err = tx.CopyFrom(
|
||||
ctx,
|
||||
pgx.Identifier{"modpack_files"},
|
||||
[]string{"modpack_id", "relative_path", "file_hash", "file_size", "download_url"},
|
||||
pgx.CopyFromRows(rows),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// GetModpackByName возвращает модпак по имени.
|
||||
func (r *ModpackRepository) GetModpackByName(ctx context.Context, name string) (*models.Modpack, error) {
|
||||
query := `
|
||||
SELECT id, name, display_name, minecraft_version, is_active, created_at, updated_at
|
||||
FROM modpacks
|
||||
WHERE name = $1`
|
||||
|
||||
var m models.Modpack
|
||||
err := r.DB.QueryRow(ctx, query, name).Scan(&m.ID, &m.Name, &m.DisplayName, &m.MinecraftVersion, &m.IsActive, &m.CreatedAt, &m.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ type Agent struct {
|
||||
// AuthenticateRequest - это тело запроса на /authserver/authenticate
|
||||
type AuthenticateRequest struct {
|
||||
Agent Agent `json:"agent"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
ClientToken string `json:"clientToken"`
|
||||
}
|
||||
|
||||
@@ -70,15 +70,15 @@ type SessionProfileResponse struct {
|
||||
|
||||
// JoinRequest - это тело запроса на /sessionserver/session/minecraft/join
|
||||
type JoinRequest struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
SelectedProfile string `json:"selectedProfile"` // UUID пользователя без дефисов
|
||||
ServerID string `json:"serverId"`
|
||||
AccessToken string `json:"accessToken" validate:"required"`
|
||||
SelectedProfile string `json:"selectedProfile" validate:"required"` // UUID пользователя без дефисов
|
||||
ServerID string `json:"serverId" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginRequest - это тело запроса на /api/login
|
||||
type LoginRequest struct {
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
Login string `json:"login" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse - это тело успешного ответа с JWT
|
||||
|
||||
@@ -20,9 +20,9 @@ type User struct {
|
||||
|
||||
// RegisterRequest определяет структуру JSON-запроса на регистрацию
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username" validate:"required,min=3,max=16,alphanum"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
}
|
||||
type Profile struct {
|
||||
ID int `json:"-"`
|
||||
|
||||
47
internal/utils/validator.go
Normal file
47
internal/utils/validator.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
func init() {
|
||||
validate = validator.New()
|
||||
}
|
||||
|
||||
// ValidationErrorResponse represents the structure of validation errors returned to the client
|
||||
type ValidationErrorResponse struct {
|
||||
Errors map[string]string `json:"errors"`
|
||||
}
|
||||
|
||||
// ValidateStruct validates a struct based on its tags using go-playground/validator
|
||||
func ValidateStruct(s interface{}) *ValidationErrorResponse {
|
||||
err := validate.Struct(s)
|
||||
if err != nil {
|
||||
var errorsMap = make(map[string]string)
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
// Simpler error messages for now. Can be enhanced with universal-translator.
|
||||
fieldName := strings.ToLower(err.Field())
|
||||
switch err.Tag() {
|
||||
case "required":
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' is required", fieldName)
|
||||
case "email":
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be a valid email", fieldName)
|
||||
case "min":
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be at least %s characters long", fieldName, err.Param())
|
||||
case "max":
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be at most %s characters long", fieldName, err.Param())
|
||||
case "alphanum":
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' must contain only alphanumeric characters", fieldName)
|
||||
default:
|
||||
errorsMap[fieldName] = fmt.Sprintf("Field '%s' failed validation on '%s' tag", fieldName, err.Tag())
|
||||
}
|
||||
}
|
||||
return &ValidationErrorResponse{Errors: errorsMap}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user