// Copyright (c) 2025 Nikolai Papin // // This file is part of Easywish // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package controllers import ( errs "easywish/internal/errors" "easywish/internal/models" "easywish/internal/services" "easywish/internal/utils" "easywish/internal/utils/enums" "errors" "net/http" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type AuthController struct { auth services.AuthService log *zap.Logger } func NewAuthController(log *zap.Logger, auth services.AuthService) Controller { ctrl := &AuthController{auth: auth, log: log} return &controllerImpl{ Path: "/auth", Middleware: []gin.HandlerFunc{}, Methods: []ControllerMethod{ { HttpMethod: POST, Path: "/registrationBegin", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.registrationBeginHandler, }, { HttpMethod: POST, Path: "/registrationComplete", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.registrationCompleteHandler, }, { HttpMethod: POST, Path: "/login", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.loginHandler, }, { HttpMethod: POST, Path: "/refresh", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.refreshHandler, }, { HttpMethod: POST, Path: "/passwordResetBegin", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.passwordResetBeginHandler, }, { HttpMethod: POST, Path: "/passwordResetComplete", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.passwordResetCompleteHandler, }, { HttpMethod: POST, Path: "/changePassword", Authorization: enums.UserRole, Middleware: []gin.HandlerFunc{}, Function: ctrl.changePasswordHandler, }, }, } } // @Summary Register an account // @Tags Auth // @Accept json // @Produce json // @Param request body models.RegistrationBeginRequest true " " // @Success 200 "Account is created and awaiting verification" // @Failure 409 "Username or email is already taken" // @Failure 429 "Too many recent registration attempts for this email" // @Router /auth/registrationBegin [post] func (ctrl *AuthController) registrationBeginHandler(c *gin.Context) { request, err := GetRequest[models.RegistrationBeginRequest](c) if err != nil { return } _, err = ctrl.auth.RegistrationBegin(request.Body) if err != nil { if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) { c.Status(http.StatusConflict) } else if errors.Is(err, errs.ErrTooManyRequests) { c.Status(http.StatusTooManyRequests) } else { c.Status(http.StatusInternalServerError) } return } c.Status(http.StatusOK) } // @Summary Confirm with code, finish creating the account // @Tags Auth // @Accept json // @Produce json // @Param request body models.RegistrationCompleteRequest true " " // @Success 200 {object} models.RegistrationCompleteResponse " " // @Failure 403 "Invalid email or verification code" // @Router /auth/registrationComplete [post] func (ctrl *AuthController) registrationCompleteHandler(c *gin.Context) { request, err := GetRequest[models.RegistrationCompleteRequest](c) if err != nil { return } response, err := ctrl.auth.RegistrationComplete(request.User, request.Body) if err != nil { if errors.Is(err, errs.ErrForbidden) { c.Status(http.StatusForbidden) } else if errors.Is(err, errs.ErrUnauthorized) { c.Status(http.StatusUnauthorized) } else { c.Status(http.StatusInternalServerError) } return } c.JSON(http.StatusOK, response) } // @Summary Acquire tokens via login credentials (and 2FA code if needed) // @Tags Auth // @Accept json // @Produce json // @Param request body models.LoginRequest true " " // @Success 200 {object} models.LoginResponse " " // @Failure 403 "Invalid login credentials" // @Router /auth/login [post] func (ctrl *AuthController) loginHandler(c *gin.Context) { request, err := GetRequest[models.LoginRequest](c) if err != nil { return } response, err := ctrl.auth.Login(request.User, request.Body) if err != nil { if errors.Is(err, errs.ErrForbidden) { c.Status(http.StatusForbidden) } else { c.Status(http.StatusInternalServerError) } return } c.JSON(http.StatusOK, response) } // @Summary Receive new tokens via refresh token // @Tags Auth // @Accept json // @Produce json // @Param request body models.RefreshRequest true " " // @Router /auth/refresh [post] // @Success 200 {object} models.RefreshResponse " " // @Failure 401 "Invalid refresh token" func (ctrl *AuthController) refreshHandler(c *gin.Context) { request, err := GetRequest[models.RefreshRequest](c) if err != nil { return } response, err := ctrl.auth.Refresh(request.Body) if err != nil { if utils.ErrorIsOneOf( err, errs.ErrTokenExpired, errs.ErrTokenInvalid, errs.ErrInvalidToken, errs.ErrWrongTokenType, errs.ErrSessionNotFound, errs.ErrSessionTerminated, ) { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // @Summary Request password reset email // @Tags Auth // @Accept json // @Produce json // @Param request body models.PasswordResetBeginRequest true " " // @Router /auth/passwordResetBegin [post] // @Success 200 "Reset code sent to the email if it is attached to an account" // @Failure 429 "Too many recent requests for this email" func (ctrl *AuthController) passwordResetBeginHandler(c *gin.Context) { request, err := GetRequest[models.PasswordResetBeginRequest](c) if err != nil { return } _, err = ctrl.auth.PasswordResetBegin(request.Body) if err != nil { if errors.Is(err, errs.ErrTooManyRequests) { c.Status(http.StatusTooManyRequests) } else { c.Status(http.StatusInternalServerError) } return } c.Status(http.StatusOK) } // @Summary Complete password reset via email code // @Tags Auth // @Accept json // @Produce json // @Param request body models.PasswordResetCompleteRequest true " " // @Router /auth/passwordResetComplete [post] // @Success 200 {object} models.PasswordResetCompleteResponse " " // @Success 403 "Wrong verification code or username" func (ctrl *AuthController) passwordResetCompleteHandler(c *gin.Context) { request, err := GetRequest[models.PasswordResetCompleteRequest](c) if err != nil { return } response, err := ctrl.auth.PasswordResetComplete(request.Body) if err != nil { if errors.Is(err, errs.ErrForbidden) { c.Status(http.StatusForbidden) } else { c.Status(http.StatusInternalServerError) } return } c.JSON(http.StatusOK, response) } // @Summary Set new password using the old password // @Tags Auth // @Accept json // @Produce json // @Security JWT // @Param request body models.ChangePasswordRequest true " " // @Success 200 "Password successfully changed" // @Failure 403 "Invalid old password" // @Router /auth/changePassword [post] func (ctrl *AuthController) changePasswordHandler(c *gin.Context) { request, err := GetRequest[models.ChangePasswordRequest](c) if err != nil { return } _, err = ctrl.auth.ChangePassword(request.Body, request.User) if err != nil { if errors.Is(err, errs.ErrForbidden) { c.Status(http.StatusForbidden) } else { c.Status(http.StatusInternalServerError) } return } c.Status(http.StatusOK) }