initial commit
This commit is contained in:
468
internal/delivery/http/platform/auth.go
Normal file
468
internal/delivery/http/platform/auth.go
Normal file
@@ -0,0 +1,468 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user