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) }