package auth import ( "encoding/json" "errors" "net/http" "strings" "github.com/gin-gonic/gin" ) type Handler struct { service *Service } func NewHandler(service *Service) *Handler { return &Handler{service: service} } func (h *Handler) RegisterRoutes(router gin.IRouter) { router.POST("/api/auth/register", h.Register) router.POST("/api/auth/login", h.Login) router.POST("/api/auth/logout", h.Logout) router.GET("/api/auth/me", h.Me) } func (h *Handler) RegisterProtectedRoutes(router gin.IRouter, require gin.HandlerFunc) { router.PATCH("/api/auth/me", require, h.UpdateMe) router.POST("/api/auth/password", require, h.ChangePassword) } type credentialsRequest struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` } type profileRequest struct { Email string `json:"email"` Name string `json:"name"` StudioName string `json:"studioName"` Tagline string `json:"tagline"` WebsiteURL string `json:"websiteUrl"` InstagramURL string `json:"instagramUrl"` } type passwordRequest struct { CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` } // Register godoc // @Summary Register a photographer account // @Tags authentication // @Accept json // @Produce json // @Param request body credentialsRequest true "Account details" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/auth/register [post] func (h *Handler) Register(c *gin.Context) { var request credentialsRequest if !decodeJSON(c, &request) { return } user, err := h.service.Register(c.Request.Context(), request.Email, request.Password, request.Name) if err != nil { if errors.Is(err, ErrEmailTaken) { writeJSON(c, http.StatusConflict, map[string]string{"error": "email is already registered"}) return } writeJSON(c, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } h.service.SetSession(c, user) writeJSON(c, http.StatusCreated, map[string]User{"user": user}) } // Login godoc // @Summary Sign in a photographer // @Tags authentication // @Accept json // @Produce json // @Param request body credentialsRequest true "Account credentials" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/auth/login [post] func (h *Handler) Login(c *gin.Context) { var request credentialsRequest if !decodeJSON(c, &request) { return } user, err := h.service.Login(c.Request.Context(), request.Email, request.Password) if err != nil { if errors.Is(err, ErrInvalidCredentials) { writeJSON(c, http.StatusUnauthorized, map[string]string{"error": "invalid email or password"}) return } writeJSON(c, http.StatusInternalServerError, map[string]string{"error": "could not sign in"}) return } h.service.SetSession(c, user) writeJSON(c, http.StatusOK, map[string]User{"user": user}) } // Logout godoc // @Summary Sign out the current photographer // @Tags authentication // @Produce json // @Success 200 {object} map[string]string // @Router /api/auth/logout [post] func (h *Handler) Logout(c *gin.Context) { h.service.ClearSession(c) writeJSON(c, http.StatusOK, map[string]string{"status": "ok"}) } // Me godoc // @Summary Get the current photographer // @Tags authentication // @Produce json // @Security studioSession // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Router /api/auth/me [get] func (h *Handler) Me(c *gin.Context) { user, err := h.service.UserFromRequest(c.Request.Context(), c.Request) if err != nil { writeJSON(c, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } writeJSON(c, http.StatusOK, map[string]User{"user": user}) } // UpdateMe godoc // @Summary Update the current photographer profile // @Tags authentication // @Accept json // @Produce json // @Security studioSession // @Param request body profileRequest true "Profile details" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/auth/me [patch] func (h *Handler) UpdateMe(c *gin.Context) { user, ok := UserFromContext(c) if !ok { writeJSON(c, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var request profileRequest if !decodeJSON(c, &request) { return } updated, err := h.service.UpdateProfile(c.Request.Context(), user.ID, request.Email, request.Name, request.StudioName, request.Tagline, request.WebsiteURL, request.InstagramURL) if err != nil { if errors.Is(err, ErrEmailTaken) { writeJSON(c, http.StatusConflict, map[string]string{"error": "email is already registered"}) return } if strings.HasPrefix(err.Error(), "enter ") || strings.HasPrefix(err.Error(), "name ") { writeJSON(c, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(c, http.StatusInternalServerError, map[string]string{"error": "could not update profile"}) return } writeJSON(c, http.StatusOK, map[string]User{"user": updated}) } // ChangePassword godoc // @Summary Change the current photographer password // @Tags authentication // @Accept json // @Produce json // @Security studioSession // @Param request body passwordRequest true "Password details" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/auth/password [post] func (h *Handler) ChangePassword(c *gin.Context) { user, ok := UserFromContext(c) if !ok { writeJSON(c, http.StatusUnauthorized, map[string]string{"error": "authentication required"}) return } var request passwordRequest if !decodeJSON(c, &request) { return } if err := h.service.ChangePassword(c.Request.Context(), user.ID, request.CurrentPassword, request.NewPassword); err != nil { if errors.Is(err, ErrCurrentPassword) { writeJSON(c, http.StatusBadRequest, map[string]string{"error": "current password is incorrect"}) return } if strings.HasPrefix(err.Error(), "new password") { writeJSON(c, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(c, http.StatusInternalServerError, map[string]string{"error": "could not update password"}) return } writeJSON(c, http.StatusOK, map[string]string{"status": "ok"}) } func decodeJSON(c *gin.Context, target any) bool { if !strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") { writeJSON(c, http.StatusUnsupportedMediaType, map[string]string{"error": "content type must be application/json"}) return false } decoder := json.NewDecoder(http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { writeJSON(c, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"}) return false } return true } func writeJSON(c *gin.Context, status int, value any) { c.JSON(status, value) }