Files
2026-04-10 18:25:21 +03:30

469 lines
14 KiB
Go

package platform
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
"base/internal/application/auth"
"base/internal/dto"
"base/internal/pkg/oauth"
)
// RegisterWithCredentials godoc
// @Summary register with credentials
// @Description register a new user with email and password
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "register request"
// @Success 200 {object} dto.TokenResponse "token response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/register [post]
func (ctl *Controller) RegisterWithCredentials(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "RegisterWithCredentials").
Logger()
var req dto.RegisterRequest
if !ctl.validateRequest(c, &req) {
return
}
tokens, err := ctl.authService.RegisterWithCredentials(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to register user")
switch {
case errors.Is(err, auth.ErrUserAlreadyExists):
r := dto.Conflict().WithMessage("user already exists")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
})
c.JSON(r.Status, r)
}
// LoginWithCredentials godoc
// @Summary login with credentials
// @Description login with email and password
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.LoginRequest true "login request"
// @Success 200 {object} dto.TokenResponse "token response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 401 {object} dto.ErrorResponse "invalid credentials"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/login [post]
func (ctl *Controller) LoginWithCredentials(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "LoginWithCredentials").
Logger()
var req dto.LoginRequest
if !ctl.validateRequest(c, &req) {
return
}
tokens, err := ctl.authService.LoginWithCredentials(
c.Request.Context(),
req.Email,
req.Password,
)
if err != nil {
lg.Error().Err(err).Msg("failed to login")
switch {
case errors.Is(err, auth.ErrInvalidCredentials):
r := dto.Unauthorized().WithMessage("invalid credentials")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
})
c.JSON(r.Status, r)
}
// RefreshToken godoc
// @Summary refresh token
// @Description refresh access token using refresh token
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.RefreshTokenRequest true "refresh token request"
// @Success 200 {object} dto.TokenResponse "token response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 401 {object} dto.ErrorResponse "invalid refresh token"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/refresh-token [post]
func (ctl *Controller) RefreshToken(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "RefreshToken").
Logger()
var req dto.RefreshTokenRequest
if !ctl.validateRequest(c, &req) {
return
}
tokens, err := ctl.authService.RefreshToken(
c.Request.Context(),
req.RefreshToken,
)
if err != nil {
lg.Error().Err(err).Msg("failed to refresh token")
switch {
case errors.Is(err, auth.ErrInvalidRefreshToken):
r := dto.Unauthorized().WithMessage("invalid refresh token")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
})
c.JSON(r.Status, r)
}
// GetOauthRedirectURL godoc
// @Summary get oauth redirect url
// @Description get OAuth redirect URL for the specified provider
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.OAuthRedirectURLRequest true "oauth redirect url request"
// @Success 200 {object} dto.OAuthRedirectURLResponse "oauth redirect url response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/oauth/redirect-url [post]
func (ctl *Controller) GetOauthRedirectURL(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "GetOauthRedirectURL").
Logger()
var req dto.OAuthRedirectURLRequest
if !ctl.validateRequest(c, &req) {
return
}
redirectURL, err := ctl.authService.GetOAuthRedirectURL(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to get OAuth redirect URL")
r := dto.BadRequest().WithMessage(err.Error())
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(dto.OAuthRedirectURLResponse{
RedirectURL: redirectURL,
})
c.JSON(r.Status, r)
}
// OauthCallbackGET handles OAuth redirect from provider (GET with code, state in query).
// Compatible with OAuth 2.0 flow where provider redirects to redirect_uri?code=...&state=...
// Route: GET /api/v1/auth/oauth/callback/:provider
func (ctl *Controller) OauthCallbackGET(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "OauthCallbackGET").
Logger()
providerStr := c.Param("provider")
provider, err := oauth.ParseProvider(providerStr)
if err != nil {
r := dto.BadRequest().WithMessage("invalid provider")
c.JSON(r.Status, r)
return
}
code := c.Query("code")
if code == "" {
r := dto.BadRequest().WithMessage("code is required")
c.JSON(r.Status, r)
return
}
req := dto.OAuthCallbackRequest{Provider: provider, Code: code}
response, err := ctl.authService.OAuthCallback(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to handle OAuth callback")
msg := err.Error()
if errors.Is(err, oauth.ErrMockNotEnabled) {
msg = "OAuth mock is not enabled - set oauth.mock.enabled=true and oauth.mock.base_url for local development"
}
r := dto.BadRequest().WithMessage(msg)
c.JSON(r.Status, r)
return
}
// If success_redirect in query, redirect with tokens in fragment (OAuth-compatible)
if redirectTo := c.Query("success_redirect"); redirectTo != "" {
u, err := url.Parse(redirectTo)
if err == nil {
u.Fragment = fmt.Sprintf("access_token=%s&refresh_token=%s&is_new_user=%t",
response.AccessToken, response.RefreshToken, response.IsNewUser)
c.Redirect(http.StatusFound, u.String())
return
}
}
r := dto.OK().WithData(dto.OAuthCallbackResponse{
AccessToken: response.AccessToken,
RefreshToken: response.RefreshToken,
IsNewUser: response.IsNewUser,
})
c.JSON(r.Status, r)
}
// OauthCallback handles OAuth callback via POST (e.g. frontend posting code).
// @Summary oauth callback
// @Description handle OAuth callback and authenticate user
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.OAuthCallbackRequest true "oauth callback request"
// @Success 200 {object} dto.OAuthCallbackResponse "oauth callback response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/oauth/callback [post]
func (ctl *Controller) OauthCallback(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "OauthCallback").
Logger()
var req dto.OAuthCallbackRequest
if !ctl.validateRequest(c, &req) {
return
}
response, err := ctl.authService.OAuthCallback(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to handle OAuth callback")
msg := err.Error()
if errors.Is(err, oauth.ErrMockNotEnabled) {
msg = "OAuth mock is not enabled - set oauth.mock.enabled=true and oauth.mock.base_url for local development"
}
r := dto.BadRequest().WithMessage(msg)
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(response)
c.JSON(r.Status, r)
}
// SendVerificationEmail godoc
// @Summary send verification email
// @Description send verification email to the authenticated user
// @Tags Public
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.SendVerificationEmailRequest true "send verification email request"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/send-verification-email [post]
func (ctl *Controller) SendVerificationEmail(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "SendVerificationEmail").
Logger()
var req dto.SendVerificationEmailRequest
if !ctl.validateRequest(c, &req) {
return
}
err := ctl.authService.SendVerificationEmail(c.Request.Context(), dto.SendVerificationEmailRequest{})
if err != nil {
lg.Error().Err(err).Msg("failed to send verification email")
switch {
case errors.Is(err, auth.ErrUserNotFound):
r := dto.NotFound().WithMessage("user not found")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrEmailAlreadyVerified):
r := dto.BadRequest().WithMessage("email already verified")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithMessage("verification email sent")
c.JSON(r.Status, r)
}
// VerifyAccount godoc
// @Summary verify account
// @Description verify account with verification code
// @Tags Public
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.VerifyAccountRequest true "verify account request"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/verify-account [post]
func (ctl *Controller) VerifyAccount(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "VerifyAccount").
Logger()
var req dto.VerifyAccountRequest
if !ctl.validateRequest(c, &req) {
return
}
err := ctl.authService.VerifyAccount(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to verify account")
switch {
case errors.Is(err, auth.ErrUserNotFound):
r := dto.NotFound().WithMessage("user not found")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrInvalidVerificationCode):
r := dto.BadRequest().WithMessage("invalid verification code")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrEmailAlreadyVerified):
r := dto.BadRequest().WithMessage("email already verified")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithMessage("account verified successfully")
c.JSON(r.Status, r)
}
// SendResetPasswordEmail godoc
// @Summary send reset password email
// @Description send password reset email
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.SendResetPasswordEmailRequest true "send reset password email request"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/send-reset-password-email [post]
func (ctl *Controller) SendResetPasswordEmail(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "SendResetPasswordEmail").
Logger()
var req dto.SendResetPasswordEmailRequest
if !ctl.validateRequest(c, &req) {
return
}
err := ctl.authService.SendResetPasswordEmail(c.Request.Context(), req)
if err != nil {
// TODO: we should handle for when user not exist, email service goes wrong and ...
lg.Error().Err(err).Msg("failed to send reset password email")
// Don't reveal if user exists or not for security
r := dto.OK().WithMessage("if the email exists, a reset password email has been sent")
c.JSON(r.Status, r)
return
}
r := dto.OK().WithMessage("if the email exists, a reset password email has been sent")
c.JSON(r.Status, r)
}
// ResetPassword godoc
// @Summary reset password
// @Description reset password with reset code
// @Tags Public
// @Accept json
// @Produce json
// @Param request body dto.ResetPasswordRequest true "reset password request"
// @Success 200 {object} dto.TokenResponse "token response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/auth/reset-password [post]
func (ctl *Controller) ResetPassword(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "ResetPassword").
Logger()
var req dto.ResetPasswordRequest
if !ctl.validateRequest(c, &req) {
return
}
tokens, err := ctl.authService.ResetPassword(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to reset password")
switch {
case errors.Is(err, auth.ErrUserNotFound):
r := dto.NotFound().WithMessage("user not found")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrInvalidVerificationCode):
r := dto.BadRequest().WithMessage("invalid reset code")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(dto.TokenResponse{
AccessToken: tokens.AccessToken,
RefreshToken: tokens.RefreshToken,
})
c.JSON(r.Status, r)
}