- Backend: Add server-side validation with go-playground/validator/v10 - Frontend: Add toast notifications for API errors with @zerodevx/svelte-toast - UI: Complete Tokyo Night theme redesign with modern animations - Docs: Update COMPLETE_DOCUMENTATION.md and README.md with all enhancements - Validation: Email format, password strength, and input sanitization - UX: Real-time error feedback, loading states, and responsive design
167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookmann/internal/database"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v4"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type AuthHandler struct {
|
|
db *database.Queries
|
|
jwtKey []byte
|
|
}
|
|
|
|
func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler {
|
|
return &AuthHandler{
|
|
db: db,
|
|
jwtKey: []byte(jwtSecret),
|
|
}
|
|
}
|
|
|
|
type RegisterRequest struct {
|
|
Email string `json:"email" validate:"required,email"`
|
|
Username string `json:"username" validate:"required,min=3,max=50"`
|
|
Password string `json:"password" validate:"required,min=6"`
|
|
}
|
|
|
|
type LoginRequest struct {
|
|
Login string `json:"login" validate:"required"` // email or username
|
|
Password string `json:"password" validate:"required"`
|
|
}
|
|
|
|
type AuthResponse struct {
|
|
Token string `json:"token"`
|
|
User UserProfile `json:"user"`
|
|
}
|
|
|
|
type UserProfile struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
// Register handles POST /api/auth/register
|
|
func (h *AuthHandler) Register(c echo.Context) error {
|
|
var req RegisterRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Check if user already exists
|
|
if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
|
|
return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"})
|
|
}
|
|
|
|
if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil {
|
|
return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
|
|
}
|
|
|
|
// Create user
|
|
user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{
|
|
Email: req.Email,
|
|
Username: req.Username,
|
|
PasswordHash: string(hashedPassword),
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Generate JWT
|
|
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, AuthResponse{
|
|
Token: token,
|
|
User: UserProfile{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Username: user.Username,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Login handles POST /api/auth/login
|
|
func (h *AuthHandler) Login(c echo.Context) error {
|
|
var req LoginRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Get user by email or username
|
|
user, err := h.db.GetUserByEmailOrUsername(c.Request().Context(), req.Login)
|
|
if err != nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
|
}
|
|
|
|
// Check password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
|
}
|
|
|
|
// Generate JWT
|
|
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, AuthResponse{
|
|
Token: token,
|
|
User: UserProfile{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Username: user.Username,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetProfile handles GET /api/auth/profile
|
|
func (h *AuthHandler) GetProfile(c echo.Context) error {
|
|
userID := c.Get("user_id").(string)
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
|
}
|
|
|
|
user, err := h.db.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, UserProfile{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Username: user.Username,
|
|
})
|
|
}
|
|
|
|
func (h *AuthHandler) generateJWT(userID string) (string, error) {
|
|
claims := jwt.MapClaims{
|
|
"user_id": userID,
|
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(h.jwtKey)
|
|
}
|