package auth import ( "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/claims" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "net/http" "time" "github.com/go-chi/jwtauth/v5" ) type Handler struct { Token *jwtauth.JWTAuth GetUseCase *useCases.GetUserUseCase ListUseCase *useCases.ListUsersUseCase CreateUseCase *useCases.CreateUserUseCase AuthenticateUseCase *useCases.AuthenticateUserUseCase VerifyUseCase *useCases.VerifyUserUseCase CancelUseCase *useCases.CancelRegistrationUseCase RequestPasswordResetUseCase *useCases.RequestPasswordResetUseCase ResetPasswordUseCase *useCases.ResetPasswordUseCase IsProduction bool } type RegisterCredentials struct { Username string `json:"username" validate:"required,min=3,max=20"` Password string `json:"password" validate:"required,min=8"` Email string `json:"email" validate:"required,email"` } type LoginCredentials struct { Username string `json:"username" validate:"required,min=3,max=20"` Password string `json:"password" validate:"required,min=8"` } type VerifyCredentials struct { Code string `json:"code" validate:"required"` } type RequestPasswordResetCredentials struct { Email string `json:"email" validate:"required,email"` } type ResetPasswordCredentials struct { Email string `json:"email" validate:"required,email"` Code string `json:"code" validate:"required"` Password string `json:"password" validate:"required,min=8"` } type PublicUser struct { ID uint32 `json:"id"` Username string `json:"username"` Email string `json:"email"` IsVerified bool `json:"is_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func newPublicUser(u user.User) PublicUser { return PublicUser{ ID: u.ID, Username: u.Username, Email: u.Email, IsVerified: u.IsVerified, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, } } func (h *Handler) removeJwtCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: "jwt", Value: "", Expires: time.Now(), MaxAge: -1, Secure: h.IsProduction, HttpOnly: true, Path: "/", }) } func NewHandler(token *jwtauth.JWTAuth, get *useCases.GetUserUseCase, list *useCases.ListUsersUseCase, create *useCases.CreateUserUseCase, authenticate *useCases.AuthenticateUserUseCase, verify *useCases.VerifyUserUseCase, cancel *useCases.CancelRegistrationUseCase, requestPasswordReset *useCases.RequestPasswordResetUseCase, resetPassword *useCases.ResetPasswordUseCase, isProduction bool) *Handler { return &Handler{ Token: token, GetUseCase: get, ListUseCase: list, CreateUseCase: create, AuthenticateUseCase: authenticate, VerifyUseCase: verify, CancelUseCase: cancel, RequestPasswordResetUseCase: requestPasswordReset, ResetPasswordUseCase: resetPassword, IsProduction: isProduction, } } func (h *Handler) SetJwtCookie(w *http.ResponseWriter, u user.User) error { _, tokenString, err := h.Token.Encode(map[string]any{"user_id": u.ID, "token_version": u.TokenVersion}) if err != nil { return err } http.SetCookie(*w, &http.Cookie{ Name: "jwt", Value: tokenString, Expires: time.Now().Add(24 * time.Hour), Secure: h.IsProduction, HttpOnly: true, Path: "/", }) return nil } func (h *Handler) IsUserTableEmpty() bool { users := h.ListUseCase.ListUsers() return len(users) == 0 } func (h *Handler) IsLoggedIn(r *http.Request) bool { token, err := jwtauth.VerifyRequest( h.Token, r, jwtauth.TokenFromCookie, jwtauth.TokenFromHeader, jwtauth.TokenFromQuery) return token != nil && err == nil } func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var credentials LoginCredentials err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } err = validator.Validate.Struct(credentials) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } dbUser, err := h.AuthenticateUseCase.Authenticate(credentials.Username, credentials.Password) if err != nil { http.Error(w, apperr.LogsErrorMessage, http.StatusForbidden) return } _ = h.SetJwtCookie(&w, dbUser) message, _ := json.Marshal(map[string]any{"message": "User logged in! HTTPonly jwt cookie created"}) _, _ = w.Write(message) } func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { var credentials RegisterCredentials err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } err = validator.Validate.Struct(credentials) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } createdUser, err := h.CreateUseCase.CreateUser(useCases.CreateUserCommand{ Username: credentials.Username, Password: credentials.Password, Email: credentials.Email, }) if err != nil { if errors.Is(err, user.ErrRegistrationClosed) { http.Error(w, apperr.RegistrationClosedErrorMessage, http.StatusForbidden) return } http.Error(w, apperr.RegistrationFailedErrorMessage, http.StatusInternalServerError) return } _ = h.SetJwtCookie(&w, createdUser) message, _ := json.Marshal(map[string]any{"message": "User registered! HTTPonly jwt cookie created"}) _, _ = w.Write(message) } func (h *Handler) Verify(w http.ResponseWriter, r *http.Request) { var credentials VerifyCredentials err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } err = validator.Validate.Struct(credentials) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } verifiedUser, err := h.VerifyUseCase.Verify(userId, credentials.Code) if err != nil { if errors.Is(err, user.ErrUserNotFound) { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } if errors.Is(err, user.ErrInvalidVerificationCode) || errors.Is(err, user.ErrVerificationCodeExpired) { http.Error(w, apperr.VerificationFailedErrorMessage, http.StatusForbidden) return } http.Error(w, "Internal server error", http.StatusInternalServerError) return } body, err := json.Marshal(newPublicUser(verifiedUser)) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } _, _ = w.Write(body) } func (h *Handler) CancelRegistration(w http.ResponseWriter, r *http.Request) { userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } err := h.CancelUseCase.CancelRegistration(userId) if err != nil { if errors.Is(err, user.ErrUserNotFound) { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } if errors.Is(err, user.ErrAlreadyVerified) { http.Error(w, apperr.AlreadyVerifiedErrorMessage, http.StatusForbidden) return } http.Error(w, "Internal server error", http.StatusInternalServerError) return } h.removeJwtCookie(w) message, _ := json.Marshal(map[string]any{"message": "Registration cancelled! Account deleted and HTTPonly jwt cookie cleared"}) _, _ = w.Write(message) } func (h *Handler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) { var credentials RequestPasswordResetCredentials err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } err = validator.Validate.Struct(credentials) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } _ = h.RequestPasswordResetUseCase.RequestPasswordReset(credentials.Email) message, _ := json.Marshal(map[string]any{"message": apperr.PasswordResetRequestedMessage}) _, _ = w.Write(message) } func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { var credentials ResetPasswordCredentials err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } err = validator.Validate.Struct(credentials) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } err = h.ResetPasswordUseCase.ResetPassword(credentials.Email, credentials.Code, credentials.Password) if err != nil { if errors.Is(err, user.ErrInvalidPasswordResetCode) || errors.Is(err, user.ErrPasswordResetCodeExpired) { http.Error(w, apperr.PasswordResetFailedErrorMessage, http.StatusForbidden) return } http.Error(w, "Internal server error", http.StatusInternalServerError) return } message, _ := json.Marshal(map[string]any{"message": "Your password has been reset. You can now log in."}) _, _ = w.Write(message) } func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) { h.removeJwtCookie(w) message, _ := json.Marshal(map[string]any{"message": "User logged out! HTTPonly jwt cookie deleted"}) _, _ = w.Write(message) }