initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
package platform
import (
"errors"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
appAsset "base/internal/application/asset"
"base/internal/dto"
)
// ListAssetCategories godoc
// @Summary list asset categories
// @Description returns all asset categories
// @Tags Asset
// @Accept json
// @Produce json
// @Success 200 {object} dto.ListCategoriesResponse "list of categories"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/categories [get]
func (ctl *Controller) ListAssetCategories(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "ListAssetCategories").
Logger()
resp, err := ctl.assetService.ListCategories(c.Request.Context())
if err != nil {
lg.Error().Err(err).Msg("failed to list asset categories")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(resp)
c.JSON(r.Status, r)
}
// ListCategoriesWithPreview returns categories with up to 8 assets per category.
// @Summary list categories with preview assets
// @Description returns asset categories, each with up to N sample assets (default 8). Use for carousels and landing previews.
// @Tags Asset
// @Accept json
// @Produce json
// @Param request body dto.CategoriesPreviewRequest true "filter options"
// @Success 200 {object} dto.CategoriesPreviewResponse "categories with preview assets"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/categories/preview [post]
func (ctl *Controller) ListCategoriesWithPreview(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "ListCategoriesWithPreview").
Logger()
var req dto.CategoriesPreviewRequest
if !ctl.validateRequest(c, &req) {
return
}
if req.AssetsPerCategory == 0 {
req.AssetsPerCategory = 8
}
resp, err := ctl.assetService.GetCategoriesWithPreview(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to list categories with preview")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(resp).WithMessage("Asset categories with sample assets")
c.JSON(r.Status, r)
}
// ListAssetsByCategoryID returns paginated assets for a single category (Phase 2 of two-phase loading).
// @Summary list assets by category ID
// @Description returns paginated assets for the given category. Use after fetching categories from GET /assets/categories.
// @Tags Asset
// @Accept json
// @Produce json
// @Param id path string true "category UUID"
// @Param limit query int false "max items per page (default 10)"
// @Param page query int false "page number (default 1)"
// @Success 200 {object} dto.ListAssetsByCategoryIDResponse "paginated assets for category"
// @Failure 400 {object} dto.ErrorResponse "invalid category ID"
// @Failure 404 {object} dto.ErrorResponse "category not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/categories/{id}/assets [get]
func (ctl *Controller) ListAssetsByCategoryID(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "ListAssetsByCategoryID").
Logger()
categoryID, err := uuid.Parse(c.Param("id"))
if err != nil {
r := dto.BadRequest().WithMessage("invalid category ID")
c.JSON(r.Status, r)
return
}
limit, page := 10, 1
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
limit = n
}
}
if v := c.Query("page"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
page = n
}
}
resp, err := ctl.assetService.ListByCategoryID(c.Request.Context(), categoryID, limit, page)
if err != nil {
lg.Error().Err(err).Msg("failed to list assets by category")
switch {
case errors.Is(err, appAsset.ErrCategoryNotFound):
r := dto.NotFound().WithMessage("category not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(resp)
c.JSON(r.Status, r)
}
// CreateAsset godoc
// @Summary create asset
// @Description create a new asset
// @Tags Asset
// @Accept json
// @Produce json
// @Param request body dto.CreateAssetRequest true "create asset request"
// @Success 201 {object} dto.AssetResponse "asset response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "category not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets [post]
func (ctl *Controller) CreateAsset(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "CreateAsset").
Logger()
var req dto.CreateAssetRequest
if !ctl.validateRequest(c, &req) {
return
}
asset, err := ctl.assetService.Create(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to create asset")
switch {
case errors.Is(err, appAsset.ErrCategoryNotFound):
r := dto.NotFound().WithMessage("asset category not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError().WithMessage("failed to create asset")
c.JSON(r.Status, r)
}
return
}
r := dto.Created(asset)
c.JSON(r.Status, r)
}
// GetAsset godoc
// @Summary get asset by ID
// @Description get asset by ID
// @Tags Asset
// @Accept json
// @Produce json
// @Param id path string true "asset ID"
// @Success 200 {object} dto.AssetResponse "asset response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "asset not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/{id} [get]
func (ctl *Controller) GetAsset(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "GetAsset").
Logger()
var req dto.GetAssetRequest
if !ctl.validateRequest(c, &req) {
return
}
id, err := uuid.Parse(req.ID)
if err != nil {
lg.Error().Err(err).Msg("invalid asset ID")
r := dto.BadRequest().WithMessage("invalid asset ID")
c.JSON(r.Status, r)
return
}
asset, err := ctl.assetService.GetByID(c.Request.Context(), id)
if err != nil {
lg.Error().Err(err).Msg("failed to get asset")
switch {
case errors.Is(err, appAsset.ErrAssetNotFound):
r := dto.NotFound().WithMessage("asset not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(asset)
c.JSON(r.Status, r)
}
// UpdateAsset godoc
// @Summary update asset
// @Description update an existing asset
// @Tags Asset
// @Accept json
// @Produce json
// @Param id path string true "asset ID"
// @Param request body dto.UpdateAssetRequest true "update asset request"
// @Success 200 {object} dto.AssetResponse "asset response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "asset not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/{id} [put]
func (ctl *Controller) UpdateAsset(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "UpdateAsset").
Logger()
var req dto.UpdateAssetRequest
if !ctl.validateRequest(c, &req) {
return
}
asset, err := ctl.assetService.Update(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to update asset")
switch {
case errors.Is(err, appAsset.ErrAssetNotFound):
r := dto.NotFound().WithMessage("asset not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(asset)
c.JSON(r.Status, r)
}
// ListAssetsByProfile godoc
// @Summary list assets by profile ID
// @Description list all assets for a profile
// @Tags Asset
// @Accept json
// @Produce json
// @Param id path string true "profile ID"
// @Success 200 {object} dto.ListAssetsResponse "list assets response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles/{id}/assets [get]
func (ctl *Controller) ListAssetsByProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "ListAssetsByProfile").
Logger()
var req dto.ListAssetsByProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
profileID, err := uuid.Parse(req.ProfileID)
if err != nil {
lg.Error().Err(err).Msg("invalid profile ID")
r := dto.BadRequest().WithMessage("invalid profile ID")
c.JSON(r.Status, r)
return
}
assets, err := ctl.assetService.FindByProfileID(c.Request.Context(), profileID)
if err != nil {
lg.Error().Err(err).Msg("failed to list assets")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(assets)
c.JSON(r.Status, r)
}
// DeleteAsset godoc
// @Summary delete asset
// @Description delete an asset
// @Tags Asset
// @Accept json
// @Produce json
// @Param id path string true "asset ID"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "asset not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/assets/{id} [delete]
func (ctl *Controller) DeleteAsset(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "asset").
Str("handler", "DeleteAsset").
Logger()
var req dto.DeleteAssetRequest
if !ctl.validateRequest(c, &req) {
return
}
id, err := uuid.Parse(req.ID)
if err != nil {
lg.Error().Err(err).Msg("invalid asset ID")
r := dto.BadRequest().WithMessage("invalid asset ID")
c.JSON(r.Status, r)
return
}
if err := ctl.assetService.Delete(c.Request.Context(), id); err != nil {
lg.Error().Err(err).Msg("failed to delete asset")
switch {
case errors.Is(err, appAsset.ErrAssetNotFound):
r := dto.NotFound().WithMessage("asset not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithMessage("asset deleted successfully")
c.JSON(r.Status, r)
}

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

View File

@@ -0,0 +1,36 @@
package platform
import (
"github.com/gin-gonic/gin"
"base/internal/dto"
)
// GetLanding returns the landing page data.
// @Summary get landing page
// @Description returns landing page with categories, specialist roles, assets by category, specialists, and blogs
// @Tags Landing
// @Accept json
// @Produce json
// @Success 200 {object} dto.Landing "landing page data"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/landing [get]
func (ctl *Controller) GetLanding(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "landing").
Str("handler", "GetLanding").
Logger()
resp, err := ctl.landingService.GetLanding(c.Request.Context())
if err != nil {
lg.Error().Err(err).Msg("failed to get landing page")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(resp.Data).WithMessage(resp.Message)
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,106 @@
package platform
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"base/internal/domain/profile"
"base/internal/dto"
"base/internal/server/middleware"
)
// GetSpecialistOverview returns overview for specialist users with full asset details, profile, and skills.
// @Summary get specialist overview
// @Description get overview for specialist view with assets, profile, skills, recently joined, analytics
// @Tags Platform
// @Produce json
// @Security BearerAuth
// @Success 200 {object} dto.SpecialistOverviewFetchedResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse "profile not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/platform/overview/specialist [get]
func (ctl *Controller) GetSpecialistOverview(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "overview").
Str("handler", "Overview").
Logger()
userIDVal, exists := c.Get(middleware.UserIDKey)
if !exists {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userIDStr, ok := userIDVal.(string)
if !ok {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
r := dto.BadRequest().WithMessage("invalid user ID")
c.JSON(r.Status, r)
return
}
resp, err := ctl.specialistService.Overview(c.Request.Context(), userID)
if err != nil {
lg.Error().Err(err).Msg("failed to fetch overview")
switch {
case errors.Is(err, profile.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(resp)
c.JSON(r.Status, r)
}
// GetDiscoveryOverview returns overview for non-specialist users discovering assets and specialists.
// No profile required - callers browse latest assets and profiles.
// @Summary get discovery overview
// @Description overview for browsing users (latest assets, recently joined profiles, analytics). No profile required.
// @Tags Platform
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} dto.OverviewFetchedResponse "overview response"
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/platform/overview/discovery [get]
func (ctl *Controller) GetDiscoveryOverview(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "overview").
Str("handler", "GetDiscoveryOverview").
Logger()
if _, exists := c.Get(middleware.UserIDKey); !exists {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
overview, err := ctl.discoveryService.GetDiscoveryOverview(c.Request.Context())
if err != nil {
lg.Error().Err(err).Msg("failed to get discovery overview")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(overview)
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,274 @@
package platform
import (
profileDomian "base/internal/domain/profile"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"base/internal/dto"
)
// CreateProfile godoc
// @Summary create profile
// @Description create a new profile
// @Tags Profile
// @Accept json
// @Produce json
// @Param request body dto.CreateProfileRequest true "create profile request"
// @Success 201 {object} dto.ProfileResponse "profile response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles [post]
func (ctl *Controller) CreateProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "CreateProfile").
Logger()
var req dto.CreateProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
profile, err := ctl.profileService.Create(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to create profile")
r := dto.InternalServerError().WithMessage("failed to create profile")
c.JSON(r.Status, r)
return
}
r := dto.Created(profile)
c.JSON(r.Status, r)
}
// GetProfile godoc
// @Summary get profile by ID
// @Description get profile by ID
// @Tags Profile
// @Accept json
// @Produce json
// @Param id path string true "profile ID"
// @Success 200 {object} dto.ProfileResponse "profile response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "profile not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles/{id} [get]
func (ctl *Controller) GetProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "GetProfile").
Logger()
var req dto.GetProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
id, err := uuid.Parse(req.ID)
if err != nil {
lg.Error().Err(err).Msg("invalid profile ID")
r := dto.BadRequest().WithMessage("invalid profile ID")
c.JSON(r.Status, r)
return
}
profile, err := ctl.profileService.GetByID(c.Request.Context(), id)
if err != nil {
lg.Error().Err(err).Msg("failed to get profile")
switch {
case errors.Is(err, profileDomian.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(profile)
c.JSON(r.Status, r)
}
// GetProfileByHandle godoc
// @Summary get profile by handle
// @Description get profile by handle
// @Tags Profile
// @Accept json
// @Produce json
// @Param handle path string true "profile handle"
// @Success 200 {object} dto.ProfileResponse "profile response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "profile not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles/handle/{handle} [get]
func (ctl *Controller) GetProfileByHandle(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "GetProfileByHandle").
Logger()
var req dto.GetProfileByHandleRequest
if !ctl.validateRequest(c, &req) {
return
}
profile, err := ctl.profileService.GetByHandle(c.Request.Context(), req.Handle)
if err != nil {
lg.Error().Err(err).Msg("failed to get profile by handle")
switch {
case errors.Is(err, profileDomian.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(profile)
c.JSON(r.Status, r)
}
// UpdateProfile godoc
// @Summary update profile
// @Description update an existing profile
// @Tags Profile
// @Accept json
// @Produce json
// @Param id path string true "profile ID"
// @Param request body dto.UpdateProfileRequest true "update profile request"
// @Success 200 {object} dto.ProfileResponse "profile response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "profile not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles/{id} [put]
func (ctl *Controller) UpdateProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "UpdateProfile").
Logger()
var req dto.UpdateProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
profile, err := ctl.profileService.Update(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to update profile")
switch {
case errors.Is(err, profileDomian.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(profile)
c.JSON(r.Status, r)
}
// ListProfiles godoc
// @Summary list profiles
// @Description list profiles with filtering and pagination
// @Tags Profile
// @Accept json
// @Produce json
// @Param role_id query string false "role ID"
// @Param first_name query string false "first name"
// @Param last_name query string false "last name"
// @Param company query string false "company"
// @Param skill_name query string false "skill name"
// @Param page query int false "page number" default(1)
// @Param page_size query int false "page size" default(10)
// @Param sorted_by query string false "sort field"
// @Param ascending query bool false "ascending order" default(false)
// @Success 200 {object} dto.ListProfilesResponse "list profiles response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles [get]
func (ctl *Controller) ListProfiles(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "ListProfiles").
Logger()
var req dto.ListProfilesRequest
if !ctl.validateRequest(c, &req) {
return
}
profiles, err := ctl.profileService.List(c.Request.Context(), req)
if err != nil {
lg.Error().Err(err).Msg("failed to list profiles")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(profiles)
c.JSON(r.Status, r)
}
// DeleteProfile godoc
// @Summary delete profile
// @Description delete a profile
// @Tags Profile
// @Accept json
// @Produce json
// @Param id path string true "profile ID"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 404 {object} dto.ErrorResponse "profile not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/profiles/{id} [delete]
func (ctl *Controller) DeleteProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "profile").
Str("handler", "DeleteProfile").
Logger()
var req dto.DeleteProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
id, err := uuid.Parse(req.ID)
if err != nil {
lg.Error().Err(err).Msg("invalid profile ID")
r := dto.BadRequest().WithMessage("invalid profile ID")
c.JSON(r.Status, r)
return
}
err = ctl.profileService.Delete(c.Request.Context(), id)
if err != nil {
lg.Error().Err(err).Msg("failed to delete profile")
switch {
case errors.Is(err, profileDomian.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithMessage("profile deleted successfully")
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,34 @@
package platform
import (
"github.com/gin-gonic/gin"
"base/internal/dto"
)
// ListProfileRoles returns the list of profile roles for setup-profile.
// @Summary list profile roles
// @Description returns all profile roles (id, title) for platform - use role_id when calling setup-profile
// @Tags Platform
// @Produce json
// @Success 200 {array} dto.ProfileRole "list of profile roles"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/platform/profile-roles [get]
func (ctl *Controller) ListProfileRoles(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "platform").
Str("handler", "ListProfileRoles").
Logger()
roles, err := ctl.profileRoleService.List(c.Request.Context())
if err != nil {
lg.Error().Err(err).Msg("failed to list profile roles")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(roles)
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,163 @@
package platform
import (
"context"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"go.uber.org/fx"
"base/config"
appAsset "base/internal/application/asset"
appAuth "base/internal/application/auth"
appDiscovery "base/internal/application/discovery"
appLanding "base/internal/application/landing"
appProfile "base/internal/application/profile"
appProfileRole "base/internal/application/profilerole"
appSkill "base/internal/application/skill"
appSpecialist "base/internal/application/specialist"
"base/internal/server/middleware"
)
type Controller struct {
logger zerolog.Logger
middleware middleware.Middleware
config *config.AppConfig
e *gin.Engine
authService appAuth.Service
profileService appProfile.Service
profileRoleService appProfileRole.Service
skillService appSkill.Service
assetService appAsset.Service
discoveryService appDiscovery.Service
landingService appLanding.Service
specialistService appSpecialist.Service
}
type Param struct {
Logger zerolog.Logger
Engine *gin.Engine
Middleware middleware.Middleware
Config *config.AppConfig
AuthService appAuth.Service
ProfileService appProfile.Service
ProfileRoleService appProfileRole.Service
SkillService appSkill.Service
AssetService appAsset.Service
DiscoveryService appDiscovery.Service
LandingService appLanding.Service
SpecialistService appSpecialist.Service
fx.In
}
func New(lc fx.Lifecycle, param Param) *Controller {
c := &Controller{
logger: param.Logger,
e: param.Engine,
middleware: param.Middleware,
config: param.Config,
authService: param.AuthService,
profileService: param.ProfileService,
profileRoleService: param.ProfileRoleService,
skillService: param.SkillService,
assetService: param.AssetService,
discoveryService: param.DiscoveryService,
landingService: param.LandingService,
specialistService: param.SpecialistService,
}
lc.Append(
fx.Hook{
OnStart: func(ctx context.Context) error {
c.SetupRouter()
return nil
},
OnStop: func(ctx context.Context) error {
return nil
},
},
)
return c
}
func (ctl *Controller) SetupRouter() {
apiRouter := ctl.e.Group("/api")
ctl.registerRoutes(apiRouter.Group("/v1"))
ctl.registerSpecialistRoutes(apiRouter.Group("/specialists/v1"))
}
func (ctl *Controller) registerRoutes(router *gin.RouterGroup) {
authRouter := router.Group("/auth")
ctl.registerAuthRoutes(authRouter)
accountRouter := router.Group("/account")
ctl.registerAccountRoutes(accountRouter)
profileRouter := router.Group("/profiles")
ctl.registerProfileRoutes(profileRouter)
ctl.registerAssetRoutes(router)
platformRouter := router.Group("/platform")
ctl.registerPlatformRoutes(platformRouter)
landingRouter := router.Group("/landing")
ctl.registerLandingRoutes(landingRouter)
}
func (ctl *Controller) registerPlatformRoutes(platformRouter *gin.RouterGroup) {
protected := platformRouter.Use(ctl.middleware.AuthShield())
protected.GET("/profile-roles", ctl.ListProfileRoles)
protected.GET("/skills", ctl.ListSkills)
protected.GET("/overview/discovery", ctl.GetDiscoveryOverview)
protected.GET("/overview/specialist", ctl.GetSpecialistOverview)
protected.POST("/verify-account", ctl.VerifyAccount)
protected.POST("/setup-profile", ctl.SetupProfile)
}
func (ctl *Controller) registerLandingRoutes(landingRouter *gin.RouterGroup) {
landingRouter.GET("", ctl.GetLanding)
}
func (ctl *Controller) registerAuthRoutes(authRouter *gin.RouterGroup) {
authRouter.POST("/login", ctl.LoginWithCredentials)
authRouter.POST("/register", ctl.RegisterWithCredentials)
authRouter.POST("/refresh-token", ctl.RefreshToken)
authRouter.POST("/oauth/redirect-url", ctl.GetOauthRedirectURL)
authRouter.GET("/oauth/callback/:provider", ctl.OauthCallbackGET)
authRouter.POST("/oauth/callback", ctl.OauthCallback)
authRouter.POST("/send-reset-password-email", ctl.SendResetPasswordEmail)
authRouter.POST("/reset-password", ctl.ResetPassword)
// Protected routes
protectedRoutes := authRouter.Use(ctl.middleware.AuthShield())
protectedRoutes.POST("/send-verification-email", ctl.SendVerificationEmail)
}
func (ctl *Controller) registerAccountRoutes(accountRouter *gin.RouterGroup) {
protected := accountRouter.Use(ctl.middleware.AuthShield())
protected.GET("/info", ctl.GetUserInfo)
}
func (ctl *Controller) registerProfileRoutes(profileRouter *gin.RouterGroup) {
profileRouter.POST("", ctl.CreateProfile)
profileRouter.GET("", ctl.ListProfiles)
profileRouter.GET("/handle/:handle", ctl.GetProfileByHandle)
profileRouter.GET("/:id/assets", ctl.ListAssetsByProfile)
profileRouter.GET("/:id", ctl.GetProfile)
profileRouter.PUT("/:id", ctl.UpdateProfile)
profileRouter.DELETE("/:id", ctl.DeleteProfile)
}
func (ctl *Controller) registerAssetRoutes(router *gin.RouterGroup) {
assetRouter := router.Group("/assets")
assetRouter.GET("/categories", ctl.ListAssetCategories)
assetRouter.POST("/categories/preview", ctl.ListCategoriesWithPreview)
assetRouter.GET("/categories/:id/assets", ctl.ListAssetsByCategoryID)
assetRouter.POST("", ctl.CreateAsset)
assetRouter.GET("/:id", ctl.GetAsset)
assetRouter.PUT("/:id", ctl.UpdateAsset)
assetRouter.DELETE("/:id", ctl.DeleteAsset)
}

View File

@@ -0,0 +1,34 @@
package platform
import (
"github.com/gin-gonic/gin"
"base/internal/dto"
)
// ListSkills returns the list of skills for profile skill selection.
// @Summary list skills
// @Description returns all skills from the catalog for profile update skill selection
// @Tags Platform
// @Produce json
// @Success 200 {array} dto.Skill "list of skills"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/platform/skills [get]
func (ctl *Controller) ListSkills(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "platform").
Str("handler", "ListSkills").
Logger()
skills, err := ctl.skillService.List(c.Request.Context())
if err != nil {
lg.Error().Err(err).Msg("failed to list skills")
r := dto.InternalServerError()
c.JSON(r.Status, r)
return
}
r := dto.OK().WithData(skills)
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,185 @@
package platform
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"base/internal/domain/profile"
"base/internal/dto"
"base/internal/server/middleware"
)
func (ctl *Controller) registerSpecialistRoutes(router *gin.RouterGroup) {
protected := router.Use(ctl.middleware.AuthShield())
protected.PUT("/page-sections/hero", ctl.SpecialistUpdateHero)
protected.PUT("/page-sections/contact", ctl.SpecialistUpdateContact)
protected.PUT("/page-sections/skills", ctl.SpecialistUpdateSkills)
protected.GET("/page-sections", ctl.SpecialistGetPageSections)
protected.GET("/profile", ctl.SpecialistGetProfile)
}
// SpecialistUpdateHero updates the hero section of the specialist's profile.
// @Summary update hero section
// @Tags Specialist
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.HeroDTO true "hero section"
// @Success 200 {object} dto.SuccessResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/specialists/v1/page-sections/hero [put]
func (ctl *Controller) SpecialistUpdateHero(c *gin.Context) {
userID, err := getUserIDFromContext(c)
if err != nil {
return
}
var req dto.HeroDTO
if !ctl.validateRequest(c, &req) {
return
}
if err := ctl.specialistService.UpdateHero(c.Request.Context(), userID, req); err != nil {
ctl.handleSpecialistError(c, err)
return
}
r := dto.OK().WithMessage("hero updated")
c.JSON(r.Status, r)
}
// SpecialistUpdateContact updates the contact section.
// @Summary update contact section
// @Tags Specialist
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.ContactDTO true "contact section"
// @Success 200 {object} dto.SuccessResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/specialists/v1/page-sections/contact [put]
func (ctl *Controller) SpecialistUpdateContact(c *gin.Context) {
userID, err := getUserIDFromContext(c)
if err != nil {
return
}
var req dto.ContactDTO
if !ctl.validateRequest(c, &req) {
return
}
if err := ctl.specialistService.UpdateContact(c.Request.Context(), userID, req); err != nil {
ctl.handleSpecialistError(c, err)
return
}
r := dto.OK().WithMessage("contact updated")
c.JSON(r.Status, r)
}
// SpecialistUpdateSkills updates the skills section.
// @Summary update skills section
// @Tags Specialist
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.SkillsUpdateRequest true "skills section"
// @Success 200 {object} dto.SuccessResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/specialists/v1/page-sections/skills [put]
func (ctl *Controller) SpecialistUpdateSkills(c *gin.Context) {
userID, err := getUserIDFromContext(c)
if err != nil {
return
}
var req dto.SkillsUpdateRequest
if !ctl.validateRequest(c, &req) {
return
}
if err := ctl.specialistService.UpdateSkills(c.Request.Context(), userID, req); err != nil {
ctl.handleSpecialistError(c, err)
return
}
r := dto.OK().WithMessage("skills updated")
c.JSON(r.Status, r)
}
// SpecialistGetPageSections returns hero, contact, skills for the specialist.
// @Summary get page sections
// @Tags Specialist
// @Produce json
// @Security Bearer
// @Success 200 {object} dto.PageSectionsResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/specialists/v1/page-sections [get]
func (ctl *Controller) SpecialistGetPageSections(c *gin.Context) {
userID, err := getUserIDFromContext(c)
if err != nil {
return
}
resp, err := ctl.specialistService.GetPageSections(c.Request.Context(), userID)
if err != nil {
ctl.handleSpecialistError(c, err)
return
}
r := dto.OK().WithData(resp)
c.JSON(r.Status, r)
}
// SpecialistGetProfile returns the specialist's full profile.
// @Summary get specialist profile
// @Tags Specialist
// @Produce json
// @Security Bearer
// @Success 200 {object} dto.ProfileResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/specialists/v1/profile [get]
func (ctl *Controller) SpecialistGetProfile(c *gin.Context) {
userID, err := getUserIDFromContext(c)
if err != nil {
return
}
resp, err := ctl.specialistService.GetProfile(c.Request.Context(), userID)
if err != nil {
ctl.handleSpecialistError(c, err)
return
}
r := dto.OK().WithData(resp)
c.JSON(r.Status, r)
}
func getUserIDFromContext(c *gin.Context) (uuid.UUID, error) {
val, exists := c.Get(middleware.UserIDKey)
if !exists {
c.JSON(dto.Unauthorized().Status, dto.Unauthorized())
return uuid.Nil, errors.New("unauthorized")
}
str, ok := val.(string)
if !ok {
c.JSON(dto.Unauthorized().Status, dto.Unauthorized())
return uuid.Nil, errors.New("invalid user id type")
}
id, err := uuid.Parse(str)
if err != nil {
c.JSON(dto.BadRequest().Status, dto.BadRequest().WithMessage("invalid user ID"))
return uuid.Nil, err
}
return id, nil
}
func (ctl *Controller) handleSpecialistError(c *gin.Context, err error) {
switch {
case errors.Is(err, profile.ErrProfileNotFound):
r := dto.NotFound().WithMessage("profile not found")
c.JSON(r.Status, r)
default:
ctl.logger.Error().Err(err).Msg("specialist error")
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
}

View File

@@ -0,0 +1,141 @@
package platform
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"base/internal/application/auth"
"base/internal/dto"
"base/internal/server/middleware"
)
// SetupProfile godoc
// @Summary setup profile after registration
// @Description complete profile with handle, role, level, and short bio. Requires authentication.
// @Tags Platform
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.SetupProfileRequest true "setup profile request"
// @Success 200 {object} dto.SuccessResponse "success response"
// @Failure 400 {object} dto.ErrorResponse "invalid request"
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 409 {object} dto.ErrorResponse "profile already exists or handle already taken"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/user/platform/setup-profile [post]
func (ctl *Controller) SetupProfile(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "auth").
Str("handler", "SetupProfile").
Logger()
userIDVal, exists := c.Get(middleware.UserIDKey)
if !exists {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userIDStr, ok := userIDVal.(string)
if !ok {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
r := dto.BadRequest().WithMessage("invalid user ID")
c.JSON(r.Status, r)
return
}
var req dto.SetupProfileRequest
if !ctl.validateRequest(c, &req) {
return
}
err = ctl.authService.SetupProfile(c.Request.Context(), userID, req)
if err != nil {
lg.Error().Err(err).Msg("failed to setup profile")
switch {
case errors.Is(err, auth.ErrProfileAlreadyExists):
r := dto.Conflict().WithMessage("profile already exists")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrHandleAlreadyTaken):
r := dto.Conflict().WithMessage("handle already taken")
c.JSON(r.Status, r)
case errors.Is(err, auth.ErrUserNotFound):
r := dto.NotFound().WithMessage("user not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithMessage("profile created successfully")
c.JSON(r.Status, r)
}
// GetUserInfo godoc
// @Summary get account info
// @Description returns user and profile_id for the authenticated user
// @Tags Platform
// @Produce json
// @Security Bearer
// @Success 200 {object} dto.UserInfoResponse "account info"
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/platform/user/info [get]
func (ctl *Controller) GetUserInfo(c *gin.Context) {
lg := ctl.logger.With().
Str("module", "platform").
Str("router", "account").
Str("handler", "GetUserInfo").
Logger()
userIDVal, exists := c.Get(middleware.UserIDKey)
if !exists {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userIDStr, ok := userIDVal.(string)
if !ok {
r := dto.Unauthorized()
c.JSON(r.Status, r)
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
r := dto.BadRequest().WithMessage("invalid user ID")
c.JSON(r.Status, r)
return
}
info, err := ctl.authService.GetUserInfo(c.Request.Context(), userID)
if err != nil {
lg.Error().Err(err).Msg("failed to get account info")
switch {
case errors.Is(err, auth.ErrUserNotFound):
r := dto.NotFound().WithMessage("user not found")
c.JSON(r.Status, r)
default:
r := dto.InternalServerError()
c.JSON(r.Status, r)
}
return
}
r := dto.OK().WithData(info)
c.JSON(r.Status, r)
}

View File

@@ -0,0 +1,58 @@
package platform
import (
"base/internal/dto"
"base/pkg/helper"
"base/pkg/validation"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func shouldBindJSON(c *gin.Context) bool {
// Only bind JSON for methods that normally carry bodies
switch c.Request.Method {
case http.MethodPost,
http.MethodPut,
http.MethodPatch:
default:
return false
}
// Must actually be JSON
contentType := c.ContentType()
return contentType == "application/json" ||
strings.HasSuffix(contentType, "+json")
}
func (ctl *Controller) validateRequest(c *gin.Context, request dto.DTO) bool {
if err := c.ShouldBindUri(&request); err != nil {
ctl.logger.Error().Err(err).Msg("RequestBundErr")
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request path parameters"})
return false
}
if err := c.ShouldBindQuery(&request); err != nil {
ctl.logger.Error().Err(err).Msg("RequestBundErr")
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request query parameters"})
return false
}
if shouldBindJSON(c) {
if err := c.ShouldBindJSON(&request); err != nil {
ctl.logger.Error().Err(err).Msg("RequestBundErr")
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return false
}
}
validator := validation.NewGenericValidator()
validator.Validate(helper.StructToMap(request), request.Schema())
if validator.HasErrors() {
ctl.logger.Error().Any("request", request).Any("error", validator.GetErrors()).Msg("validatorHasErrors")
c.JSON(http.StatusBadRequest, gin.H{"errors": validator.GetErrors()})
return false
}
return true
}