feat(auth): implement user registration endpoint
This commit is contained in:
42
internal/api/user_handler.go
Normal file
42
internal/api/user_handler.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
Service *core.UserService
|
||||
}
|
||||
|
||||
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Service.RegisterNewUser(r.Context(), req)
|
||||
if err != nil {
|
||||
// Определяем, какую ошибку вернуть клиенту
|
||||
switch {
|
||||
case errors.Is(err, database.ErrUserExists):
|
||||
http.Error(w, err.Error(), http.StatusConflict) // 409
|
||||
case errors.Is(err, core.ErrInvalidUsername), errors.Is(err, core.ErrInvalidEmail), errors.Is(err, core.ErrPasswordTooShort):
|
||||
http.Error(w, err.Error(), http.StatusBadRequest) // 400
|
||||
default:
|
||||
// Логируем внутреннюю ошибку, но не показываем ее клиенту
|
||||
// log.Printf("internal server error: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError) // 500
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Шаг 11 из ТЗ: Возвращаем 201 Created
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
65
internal/core/user_service.go
Normal file
65
internal/core/user_service.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidUsername = errors.New("invalid username format or length")
|
||||
ErrInvalidEmail = errors.New("invalid email format")
|
||||
ErrPasswordTooShort = errors.New("password is too short (minimum 8 characters)")
|
||||
)
|
||||
|
||||
// Регулярное выражение для валидации username
|
||||
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,}$`)
|
||||
|
||||
type UserService struct {
|
||||
Repo *database.UserRepository
|
||||
}
|
||||
|
||||
// RegisterNewUser выполняет полный алгоритм регистрации
|
||||
func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRequest) error {
|
||||
// Шаг 2 из ТЗ: Валидация
|
||||
if !usernameRegex.MatchString(req.Username) {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
if !emailRegex.MatchString(req.Email) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
return ErrPasswordTooShort
|
||||
}
|
||||
|
||||
// Шаг 5 из ТЗ: Генерация хеша пароля
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) // Стоимость 12, как в ТЗ
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Шаг 6 из ТЗ: Генерация UUID
|
||||
userUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
UUID: userUUID,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
PasswordHash: string(passwordHash),
|
||||
Role: "user", // По умолчанию
|
||||
}
|
||||
|
||||
// Вызываем метод репозитория для сохранения в БД
|
||||
return s.Repo.CreateUserTx(ctx, user)
|
||||
}
|
||||
30
internal/database/postgres.go
Normal file
30
internal/database/postgres.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // Регистрируем pgx драйвер
|
||||
)
|
||||
|
||||
// Connect устанавливает соединение с базой данных PostgreSQL
|
||||
func Connect() *sql.DB {
|
||||
connStr := os.Getenv("DATABASE_URL")
|
||||
if connStr == "" {
|
||||
log.Fatal("DATABASE_URL environment variable is not set")
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", connStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to connect to database: %v\n", err)
|
||||
}
|
||||
|
||||
// Проверяем, что соединение действительно установлено
|
||||
if err = db.Ping(); err != nil {
|
||||
log.Fatalf("Unable to ping database: %v\n", err)
|
||||
}
|
||||
|
||||
log.Println("Successfully connected to PostgreSQL!")
|
||||
return db
|
||||
}
|
||||
59
internal/database/user_repository.go
Normal file
59
internal/database/user_repository.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// File: backend/internal/database/user_repository.go
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserExists = errors.New("user with this username or email already exists")
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// CreateUserTx создает нового пользователя и его профиль в рамках одной транзакции
|
||||
func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) error {
|
||||
tx, err := r.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Гарантируем откат транзакции в случае любой ошибки
|
||||
defer tx.Rollback()
|
||||
|
||||
// Шаг 4 из ТЗ: Проверка уникальности
|
||||
var exists bool
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)",
|
||||
user.Username, user.Email).Scan(&exists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrUserExists
|
||||
}
|
||||
|
||||
// Шаг 7 из ТЗ: INSERT в таблицу users. Получаем ID нового пользователя.
|
||||
var newUserID int
|
||||
err = tx.QueryRowContext(ctx,
|
||||
"INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||
user.UUID, user.Username, user.Email, user.PasswordHash, user.Role,
|
||||
).Scan(&newUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Шаг 9 из ТЗ: INSERT в таблицу profiles
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Шаг 10 из ТЗ: Коммитим транзакцию
|
||||
return tx.Commit()
|
||||
}
|
||||
26
internal/models/user.go
Normal file
26
internal/models/user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// User представляет структуру пользователя в таблице 'users'
|
||||
type User struct {
|
||||
ID int `json:"-"` // Скрываем в JSON
|
||||
UUID uuid.UUID `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"-"` // Пароль никогда не отдаем
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// RegisterRequest определяет структуру JSON-запроса на регистрацию
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
Reference in New Issue
Block a user