// 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" ) func NewAuthController(log *zap.Logger, auth services.AuthService) Controller { return &controllerImpl{ Path: "/auth", Middleware: []gin.HandlerFunc{}, Methods: []ControllerMethod{ // @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] { HttpMethod: POST, Path: "/registrationBegin", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.RegistrationBeginRequest](c); if err != nil { return } _, err = auth.RegistrationBegin(request.Body); if err != nil { if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) { c.Status(http.StatusConflict) } else { c.Status(http.StatusInternalServerError) } return } c.Status(http.StatusOK) return }, }, // @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] { HttpMethod: POST, Path: "/registrationComplete", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.RegistrationCompleteRequest](c); if err != nil { return } response, err := 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] { HttpMethod: POST, Path: "/login", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.LoginRequest](c); if err != nil { return } response, err := 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" { HttpMethod: POST, Path: "/refresh", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.RefreshRequest](c); if err != nil { return } response, err := 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, 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" { HttpMethod: POST, Path: "/passwordResetBegin", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.PasswordResetBeginRequest](c); if err != nil { return } response, err := auth.PasswordResetBegin(request.Body) if err != nil { if errors.Is(err, errs.ErrTooManyRequests) { c.Status(http.StatusTooManyRequests) } else { c.Status(http.StatusInternalServerError) } return } c.JSON(http.StatusOK, response) }, }, // @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" { HttpMethod: POST, Path: "/passwordResetComplete", Authorization: enums.GuestRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.PasswordResetCompleteRequest](c); if err != nil { return } response, err := 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] { HttpMethod: POST, Path: "/changePassword", Authorization: enums.UserRole, Middleware: []gin.HandlerFunc{}, Function: func(c *gin.Context) { request, err := GetRequest[models.ChangePasswordRequest](c); if err != nil { return } response, err := 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.JSON(http.StatusOK, response) }, }, }, } }