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) }