Compare commits
1 Commits
42f2b68848
...
5e609017f0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e609017f0 |
@@ -34,7 +34,7 @@ func main() {
|
|||||||
if keyPath == "" {
|
if keyPath == "" {
|
||||||
log.Fatal("RSA_PRIVATE_KEY_PATH environment variable is not set")
|
log.Fatal("RSA_PRIVATE_KEY_PATH environment variable is not set")
|
||||||
}
|
}
|
||||||
domain := os.Getenv("APP_DOMAIN") // Нам нужен домен для генерации URL
|
domain := os.Getenv("APP_DOMAIN")
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
log.Fatal("APP_DOMAIN environment variable is not set")
|
log.Fatal("APP_DOMAIN environment variable is not set")
|
||||||
}
|
}
|
||||||
@@ -68,12 +68,10 @@ func main() {
|
|||||||
|
|
||||||
// --- Защищенные роуты ---
|
// --- Защищенные роуты ---
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
// Применяем нашу middleware ко всем роутам в этой группе
|
|
||||||
r.Use(api.AuthMiddleware)
|
r.Use(api.AuthMiddleware)
|
||||||
|
|
||||||
r.Route("/api/user", func(r chi.Router) {
|
r.Route("/api/user", func(r chi.Router) {
|
||||||
r.Post("/skin", profileHandler.UploadSkin)
|
r.Post("/skin", profileHandler.UploadSkin)
|
||||||
// Здесь будут другие эндпоинты для управления профилем
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
15
go.mod
15
go.mod
@@ -3,15 +3,18 @@ module gitea.mrixs.me/minecraft-platform/backend
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Tnze/go-mc v1.20.2 // indirect
|
github.com/Tnze/go-mc v1.20.2
|
||||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
|
golang.org/x/crypto v0.39.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
7
go.sum
7
go.sum
@@ -1,6 +1,8 @@
|
|||||||
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
|
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
|
||||||
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
|
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
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/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
@@ -15,10 +17,13 @@ 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/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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
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/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/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.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.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 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
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 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
@@ -27,3 +32,5 @@ golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
|||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err := h.Service.ValidateJoinRequest(r.Context(), req)
|
err := h.Service.ValidateJoinRequest(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Yggdrasil ожидает 403 Forbidden при невалидной сессии
|
|
||||||
if errors.Is(err, core.ErrInvalidCredentials) {
|
if errors.Is(err, core.ErrInvalidCredentials) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
@@ -75,7 +74,6 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// В случае успеха возвращаем пустой ответ со статусом 204
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey - это тип для ключей контекста, чтобы избежать коллизий.
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
const UserIDContextKey = contextKey("userID")
|
const UserIDContextKey = contextKey("userID")
|
||||||
@@ -24,7 +23,7 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
if tokenString == authHeader { // Префикс "Bearer " не найден
|
if tokenString == authHeader {
|
||||||
http.Error(w, "Invalid token format", http.StatusUnauthorized)
|
http.Error(w, "Invalid token format", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -36,7 +35,6 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
// Проверяем, что метод подписи HMAC
|
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, jwt.ErrSignatureInvalid
|
return nil, jwt.ErrSignatureInvalid
|
||||||
}
|
}
|
||||||
@@ -54,7 +52,6 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем user_id из claims. JWT хранит числа как float64.
|
|
||||||
userIDFloat, ok := claims["user_id"].(float64)
|
userIDFloat, ok := claims["user_id"].(float64)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Invalid user_id in token", http.StatusUnauthorized)
|
http.Error(w, "Invalid user_id in token", http.StatusUnauthorized)
|
||||||
@@ -62,7 +59,6 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
userID := int(userIDFloat)
|
userID := int(userIDFloat)
|
||||||
|
|
||||||
// Добавляем userID в контекст запроса для использования в хендлере
|
|
||||||
ctx := context.WithValue(r.Context(), UserIDContextKey, userID)
|
ctx := context.WithValue(r.Context(), UserIDContextKey, userID)
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ func (h *ProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
profile, err := h.Service.GetSignedProfile(r.Context(), playerUUID)
|
profile, err := h.Service.GetSignedProfile(r.Context(), playerUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, database.ErrUserNotFound) {
|
if errors.Is(err, database.ErrUserNotFound) {
|
||||||
// Yggdrasil возвращает 204 No Content, если профиль не найден
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,17 +39,15 @@ func (h *ProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ProfileHandler) UploadSkin(w http.ResponseWriter, r *http.Request) {
|
func (h *ProfileHandler) UploadSkin(w http.ResponseWriter, r *http.Request) {
|
||||||
// Получаем userID из контекста, который был добавлен middleware
|
|
||||||
userID, ok := r.Context().Value(UserIDContextKey).(int)
|
userID, ok := r.Context().Value(UserIDContextKey).(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Could not get user ID from context", http.StatusInternalServerError)
|
http.Error(w, "Could not get user ID from context", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ограничиваем размер загружаемого файла (например, 16KB)
|
r.ParseMultipartForm(256 << 10) // 256KB
|
||||||
r.ParseMultipartForm(16 << 10) // 16KB
|
|
||||||
|
|
||||||
file, header, err := r.FormFile("skin") // "skin" - это имя поля в форме
|
file, header, err := r.FormFile("skin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Invalid file upload", http.StatusBadRequest)
|
http.Error(w, "Invalid file upload", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -59,7 +56,6 @@ func (h *ProfileHandler) UploadSkin(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err = h.Service.UpdateUserSkin(r.Context(), userID, file, header)
|
err = h.Service.UpdateUserSkin(r.Context(), userID, file, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Можно добавить более детальную обработку ошибок
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,20 +23,16 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
err := h.Service.RegisterNewUser(r.Context(), req)
|
err := h.Service.RegisterNewUser(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Определяем, какую ошибку вернуть клиенту
|
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, database.ErrUserExists):
|
case errors.Is(err, database.ErrUserExists):
|
||||||
http.Error(w, err.Error(), http.StatusConflict) // 409
|
http.Error(w, err.Error(), http.StatusConflict)
|
||||||
case errors.Is(err, core.ErrInvalidUsername), errors.Is(err, core.ErrInvalidEmail), errors.Is(err, core.ErrPasswordTooShort):
|
case errors.Is(err, core.ErrInvalidUsername), errors.Is(err, core.ErrInvalidEmail), errors.Is(err, core.ErrPasswordTooShort):
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest) // 400
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
default:
|
default:
|
||||||
// Логируем внутреннюю ошибку, но не показываем ее клиенту
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
// log.Printf("internal server error: %v", err)
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError) // 500
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 11 из ТЗ: Возвращаем 201 Created
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,34 +24,28 @@ type AuthService struct {
|
|||||||
|
|
||||||
// Authenticate проверяет учетные данные и возвращает данные для ответа Yggdrasil.
|
// Authenticate проверяет учетные данные и возвращает данные для ответа Yggdrasil.
|
||||||
func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateRequest) (*models.AuthenticateResponse, error) {
|
func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateRequest) (*models.AuthenticateResponse, error) {
|
||||||
// 1. Найти пользователя по имени
|
|
||||||
user, err := s.UserRepo.GetUserByUsername(ctx, req.Username)
|
user, err := s.UserRepo.GetUserByUsername(ctx, req.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, database.ErrUserNotFound) {
|
if errors.Is(err, database.ErrUserNotFound) {
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
return nil, err // Другая ошибка БД
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Сравнить хеш пароля из БД с паролем из запроса
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Если хеши не совпадают, bcrypt возвращает ошибку
|
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Сгенерировать новый accessToken
|
|
||||||
accessToken := uuid.New().String()
|
accessToken := uuid.New().String()
|
||||||
|
|
||||||
// 4. Сохранить токен в БД
|
|
||||||
err = s.UserRepo.CreateAccessToken(ctx, user.ID, accessToken, req.ClientToken)
|
err = s.UserRepo.CreateAccessToken(ctx, user.ID, accessToken, req.ClientToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Сформировать ответ согласно спецификации Yggdrasil
|
|
||||||
profile := models.ProfileInfo{
|
profile := models.ProfileInfo{
|
||||||
ID: strings.ReplaceAll(user.UUID.String(), "-", ""), // UUID без дефисов
|
ID: strings.ReplaceAll(user.UUID.String(), "-", ""),
|
||||||
Name: user.Username,
|
Name: user.Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,26 +65,21 @@ func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateR
|
|||||||
|
|
||||||
// LoginUser проверяет учетные данные и генерирует JWT для веб-сессии.
|
// LoginUser проверяет учетные данные и генерирует JWT для веб-сессии.
|
||||||
func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (string, *models.User, error) {
|
func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (string, *models.User, error) {
|
||||||
// 1. Найти пользователя по логину (username или email)
|
|
||||||
user, err := s.UserRepo.GetUserByLogin(ctx, req.Login)
|
user, err := s.UserRepo.GetUserByLogin(ctx, req.Login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, database.ErrUserNotFound) {
|
if errors.Is(err, database.ErrUserNotFound) {
|
||||||
return "", nil, ErrInvalidCredentials
|
return "", nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
return "", nil, err // Другая ошибка БД
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Сравнить хеш пароля
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, ErrInvalidCredentials
|
return "", nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Создать JWT
|
|
||||||
// Устанавливаем срок действия токена, например, 72 часа
|
|
||||||
expirationTime := time.Now().Add(72 * time.Hour)
|
expirationTime := time.Now().Add(72 * time.Hour)
|
||||||
|
|
||||||
// Создаем claims (полезная нагрузка токена)
|
|
||||||
claims := &jwt.MapClaims{
|
claims := &jwt.MapClaims{
|
||||||
"exp": expirationTime.Unix(),
|
"exp": expirationTime.Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
@@ -98,17 +87,14 @@ func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (s
|
|||||||
"role": user.Role,
|
"role": user.Role,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем токен с указанием алгоритма подписи и claims
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
// Подписываем токен нашим секретным ключом
|
|
||||||
jwtSecret := os.Getenv("JWT_SECRET_KEY")
|
jwtSecret := os.Getenv("JWT_SECRET_KEY")
|
||||||
tokenString, err := token.SignedString([]byte(jwtSecret))
|
tokenString, err := token.SignedString([]byte(jwtSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скрываем хеш пароля перед отправкой данных пользователя на клиент
|
|
||||||
user.PasswordHash = ""
|
user.PasswordHash = ""
|
||||||
|
|
||||||
return tokenString, user, nil
|
return tokenString, user, nil
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex" // Для преобразования хеша в строку
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png" // Для валидации PNG
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
@@ -45,7 +45,6 @@ func NewProfileService(repo *database.UserRepository, keyPath, domain string) (*
|
|||||||
|
|
||||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Попробуем PKCS#8, если PKCS#1 не удался
|
|
||||||
key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes)
|
key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
if errPkcs8 != nil {
|
if errPkcs8 != nil {
|
||||||
return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8)
|
return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8)
|
||||||
@@ -71,7 +70,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Формируем структуру со свойствами текстур
|
|
||||||
textures := models.Textures{}
|
textures := models.Textures{}
|
||||||
if profile.SkinHash != "" {
|
if profile.SkinHash != "" {
|
||||||
textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)}
|
textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)}
|
||||||
@@ -88,14 +86,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
|
|||||||
Textures: textures,
|
Textures: textures,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Маршализация в JSON и кодирование в Base64
|
|
||||||
valueJSON, err := json.Marshal(propValue)
|
valueJSON, err := json.Marshal(propValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
valueBase64 := base64.StdEncoding.EncodeToString(valueJSON)
|
valueBase64 := base64.StdEncoding.EncodeToString(valueJSON)
|
||||||
|
|
||||||
// 3. Подпись
|
|
||||||
hasher := sha1.New()
|
hasher := sha1.New()
|
||||||
hasher.Write([]byte(valueBase64))
|
hasher.Write([]byte(valueBase64))
|
||||||
hashed := hasher.Sum(nil)
|
hashed := hasher.Sum(nil)
|
||||||
@@ -106,7 +102,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
|
|||||||
}
|
}
|
||||||
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
|
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
|
||||||
|
|
||||||
// 4. Формирование итогового ответа
|
|
||||||
response := &models.SessionProfileResponse{
|
response := &models.SessionProfileResponse{
|
||||||
ID: profileID,
|
ID: profileID,
|
||||||
Name: user.Username,
|
Name: user.Username,
|
||||||
@@ -124,15 +119,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
|
|||||||
|
|
||||||
// UpdateUserSkin обрабатывает загрузку, валидацию и сохранение файла скина.
|
// UpdateUserSkin обрабатывает загрузку, валидацию и сохранение файла скина.
|
||||||
func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file multipart.File, header *multipart.FileHeader) error {
|
func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file multipart.File, header *multipart.FileHeader) error {
|
||||||
// 1. Читаем файл в память
|
|
||||||
fileBytes, err := io.ReadAll(file)
|
fileBytes, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read file: %w", err)
|
return fmt.Errorf("failed to read file: %w", err)
|
||||||
}
|
}
|
||||||
// Возвращаем указатель файла в начало, чтобы его можно было прочитать снова
|
|
||||||
file.Seek(0, 0)
|
file.Seek(0, 0)
|
||||||
|
|
||||||
// 2. Валидация PNG 64x64
|
|
||||||
config, err := png.DecodeConfig(file)
|
config, err := png.DecodeConfig(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("invalid PNG file")
|
return errors.New("invalid PNG file")
|
||||||
@@ -141,20 +133,16 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu
|
|||||||
return errors.New("skin must be 64x64 pixels")
|
return errors.New("skin must be 64x64 pixels")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Вычисляем SHA1 хеш
|
|
||||||
hasher := sha1.New()
|
hasher := sha1.New()
|
||||||
hasher.Write(fileBytes)
|
hasher.Write(fileBytes)
|
||||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
|
||||||
// 4. Сохраняем файл на диск
|
|
||||||
// Путь к хранилищу текстур должен быть конфигурируемым
|
|
||||||
storagePath := os.Getenv("TEXTURES_STORAGE_PATH")
|
storagePath := os.Getenv("TEXTURES_STORAGE_PATH")
|
||||||
if storagePath == "" {
|
if storagePath == "" {
|
||||||
return errors.New("textures storage path not configured")
|
return errors.New("textures storage path not configured")
|
||||||
}
|
}
|
||||||
filePath := fmt.Sprintf("%s/%s", storagePath, hash)
|
filePath := fmt.Sprintf("%s/%s", storagePath, hash)
|
||||||
|
|
||||||
// Создаем файл только если его еще нет
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
outFile, err := os.Create(filePath)
|
outFile, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,6 +156,5 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Сохраняем хеш в БД
|
|
||||||
return s.UserRepo.UpdateSkinHash(ctx, userID, hash)
|
return s.UserRepo.UpdateSkinHash(ctx, userID, hash)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,22 @@ import (
|
|||||||
|
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||||
"github.com/Tnze/go-mc/bot/basic"
|
"github.com/Tnze/go-mc/bot"
|
||||||
"github.com/Tnze/go-mc/net"
|
"github.com/Tnze/go-mc/chat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type pingResponse struct {
|
||||||
|
Description chat.Message `json:"description"`
|
||||||
|
Players struct {
|
||||||
|
Max int `json:"max"`
|
||||||
|
Online int `json:"online"`
|
||||||
|
} `json:"players"`
|
||||||
|
Version struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Protocol int `json:"protocol"`
|
||||||
|
} `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerPoller struct {
|
type ServerPoller struct {
|
||||||
Repo *database.ServerRepository
|
Repo *database.ServerRepository
|
||||||
}
|
}
|
||||||
@@ -47,29 +59,19 @@ func (p *ServerPoller) pollAllServers(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) {
|
func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) {
|
||||||
resp, delay, err := net.PingAndListTimeout(server.Address, 5*time.Second)
|
resp, delay, err := bot.PingAndList(server.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err)
|
log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var status basic.ServerList
|
var status pingResponse
|
||||||
if err := json.Unmarshal(resp, &status); err != nil {
|
if err := json.Unmarshal(resp, &status); err != nil {
|
||||||
log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err)
|
log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOTD может быть сложным объектом, извлекаем текст
|
motdText := status.Description.String()
|
||||||
var motdText string
|
|
||||||
if s, ok := status.Description.(string); ok {
|
|
||||||
motdText = s
|
|
||||||
} else {
|
|
||||||
if m, ok := status.Description.(map[string]interface{}); ok {
|
|
||||||
if t, ok := m["text"].(string); ok {
|
|
||||||
motdText = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData := &models.ServerStatus{
|
updateData := &models.ServerStatus{
|
||||||
StatusJSON: string(resp),
|
StatusJSON: string(resp),
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ var (
|
|||||||
ErrPasswordTooShort = errors.New("password is too short (minimum 8 characters)")
|
ErrPasswordTooShort = errors.New("password is too short (minimum 8 characters)")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Регулярное выражение для валидации username
|
|
||||||
var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
|
var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
|
||||||
|
|
||||||
// Регулярное выражение для валидации email (упрощенное, но эффективное)
|
|
||||||
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
@@ -31,7 +29,6 @@ type UserService struct {
|
|||||||
|
|
||||||
// RegisterNewUser выполняет полный алгоритм регистрации
|
// RegisterNewUser выполняет полный алгоритм регистрации
|
||||||
func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRequest) error {
|
func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRequest) error {
|
||||||
// Шаг 2 из ТЗ: Валидация
|
|
||||||
if !usernameRegex.MatchString(req.Username) {
|
if !usernameRegex.MatchString(req.Username) {
|
||||||
return ErrInvalidUsername
|
return ErrInvalidUsername
|
||||||
}
|
}
|
||||||
@@ -42,13 +39,11 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe
|
|||||||
return ErrPasswordTooShort
|
return ErrPasswordTooShort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 5 из ТЗ: Генерация хеша пароля
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) // Стоимость 12, как в ТЗ
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 6 из ТЗ: Генерация UUID
|
|
||||||
userUUID, err := uuid.NewRandom()
|
userUUID, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -59,16 +54,14 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe
|
|||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
PasswordHash: string(passwordHash),
|
PasswordHash: string(passwordHash),
|
||||||
Role: "user", // По умолчанию
|
Role: "user",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вызываем метод репозитория для сохранения в БД
|
|
||||||
return s.Repo.CreateUserTx(ctx, user)
|
return s.Repo.CreateUserTx(ctx, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateJoinRequest проверяет запрос на присоединение к серверу.
|
// ValidateJoinRequest проверяет запрос на присоединение к серверу.
|
||||||
func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRequest) error {
|
func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRequest) error {
|
||||||
// Преобразуем UUID из строки без дефисов в стандартный формат
|
|
||||||
var uuidStr string
|
var uuidStr string
|
||||||
if len(req.SelectedProfile) == 32 {
|
if len(req.SelectedProfile) == 32 {
|
||||||
uuidStr = fmt.Sprintf("%s-%s-%s-%s-%s",
|
uuidStr = fmt.Sprintf("%s-%s-%s-%s-%s",
|
||||||
@@ -87,18 +80,14 @@ func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRe
|
|||||||
return fmt.Errorf("failed to parse profile UUID: %w", err)
|
return fmt.Errorf("failed to parse profile UUID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем токен в базе данных
|
|
||||||
err = s.UserRepo.ValidateAccessToken(ctx, req.AccessToken, userUUID)
|
err = s.UserRepo.ValidateAccessToken(ctx, req.AccessToken, userUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, database.ErrTokenNotFound) {
|
if errors.Is(err, database.ErrTokenNotFound) {
|
||||||
// Возвращаем ту же ошибку, что и при неверных кредах, чтобы не давать лишней информации
|
|
||||||
return ErrInvalidCredentials
|
return ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// В ТЗ указано "привязка serverId". В простой реализации это может быть просто логирование
|
|
||||||
// или запись в кеш (например, Redis). Для начала, просто прохождение валидации достаточно.
|
|
||||||
log.Printf("User %s successfully joined server with serverId %s", userUUID, req.ServerID)
|
log.Printf("User %s successfully joined server with serverId %s", userUUID, req.ServerID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
_ "github.com/jackc/pgx/v5/stdlib" // Регистрируем pgx драйвер
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connect устанавливает соединение с базой данных PostgreSQL
|
// Connect устанавливает соединение с базой данных PostgreSQL
|
||||||
@@ -20,7 +20,6 @@ func Connect() *sql.DB {
|
|||||||
log.Fatalf("Unable to connect to database: %v\n", err)
|
log.Fatalf("Unable to connect to database: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что соединение действительно установлено
|
|
||||||
if err = db.Ping(); err != nil {
|
if err = db.Ping(); err != nil {
|
||||||
log.Fatalf("Unable to ping database: %v\n", err)
|
log.Fatalf("Unable to ping database: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Гарантируем откат транзакции в случае любой ошибки
|
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Шаг 4 из ТЗ: Проверка уникальности
|
|
||||||
var exists bool
|
var exists bool
|
||||||
err = tx.QueryRowContext(ctx,
|
err = tx.QueryRowContext(ctx,
|
||||||
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)",
|
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)",
|
||||||
@@ -38,7 +36,6 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er
|
|||||||
return ErrUserExists
|
return ErrUserExists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 7 из ТЗ: INSERT в таблицу users. Получаем ID нового пользователя.
|
|
||||||
var newUserID int
|
var newUserID int
|
||||||
err = tx.QueryRowContext(ctx,
|
err = tx.QueryRowContext(ctx,
|
||||||
"INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
"INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||||
@@ -48,22 +45,18 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 9 из ТЗ: INSERT в таблицу profiles
|
|
||||||
_, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID)
|
_, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 10 из ТЗ: Коммитим транзакцию
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUserNotFound = errors.New("user not found") // Новая ошибка
|
ErrUserNotFound = errors.New("user not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// GetUserByUsername находит пользователя по его имени.
|
// GetUserByUsername находит пользователя по его имени.
|
||||||
// Возвращает полную структуру User, включая хеш пароля для проверки.
|
// Возвращает полную структуру User, включая хеш пароля для проверки.
|
||||||
func (r *UserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
func (r *UserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||||
@@ -97,7 +90,7 @@ func (r *UserRepository) CreateAccessToken(ctx context.Context, userID int, acce
|
|||||||
func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUID) (*models.User, *models.Profile, error) {
|
func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUID) (*models.User, *models.Profile, error) {
|
||||||
user := &models.User{UUID: userUUID}
|
user := &models.User{UUID: userUUID}
|
||||||
profile := &models.Profile{}
|
profile := &models.Profile{}
|
||||||
var skinHash, capeHash sql.NullString // Используем NullString для полей, которые могут быть NULL
|
var skinHash, capeHash sql.NullString
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT u.id, u.username, p.skin_hash, p.cape_hash
|
SELECT u.id, u.username, p.skin_hash, p.cape_hash
|
||||||
@@ -127,8 +120,7 @@ func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ...
|
ErrTokenNotFound = errors.New("access token not found or invalid")
|
||||||
ErrTokenNotFound = errors.New("access token not found or invalid") // Новая ошибка
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateAccessToken проверяет, действителен ли токен для данного пользователя.
|
// ValidateAccessToken проверяет, действителен ли токен для данного пользователя.
|
||||||
@@ -169,7 +161,7 @@ func (r *UserRepository) UpdateSkinHash(ctx context.Context, userID int, skinHas
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if rowsAffected == 0 {
|
if rowsAffected == 0 {
|
||||||
return ErrUserNotFound // Если профиль для user_id не найден
|
return ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -180,7 +172,6 @@ func (r *UserRepository) GetUserByLogin(ctx context.Context, login string) (*mod
|
|||||||
user := &models.User{}
|
user := &models.User{}
|
||||||
var userUUID string
|
var userUUID string
|
||||||
|
|
||||||
// Ищем по username ИЛИ по email
|
|
||||||
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1"
|
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1"
|
||||||
err := r.DB.QueryRowContext(ctx, query, login).Scan(
|
err := r.DB.QueryRowContext(ctx, query, login).Scan(
|
||||||
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt,
|
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt,
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ type JoinRequest struct {
|
|||||||
|
|
||||||
// LoginRequest - это тело запроса на /api/login
|
// LoginRequest - это тело запроса на /api/login
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
// Позволяем логиниться как по username, так и по email
|
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
@@ -85,5 +84,5 @@ type LoginRequest struct {
|
|||||||
// LoginResponse - это тело успешного ответа с JWT
|
// LoginResponse - это тело успешного ответа с JWT
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
User *User `json:"user"` // Отдаем информацию о пользователе
|
User *User `json:"user"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
|
|
||||||
// User представляет структуру пользователя в таблице 'users'
|
// User представляет структуру пользователя в таблице 'users'
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"-"` // Скрываем в JSON
|
ID int `json:"-"`
|
||||||
UUID uuid.UUID `json:"uuid"`
|
UUID uuid.UUID `json:"uuid"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"-"` // Пароль никогда не отдаем
|
PasswordHash string `json:"-"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
Reference in New Issue
Block a user