initial commit
This commit is contained in:
363
internal/delivery/http/platform/asset.go
Normal file
363
internal/delivery/http/platform/asset.go
Normal 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
36
internal/delivery/http/platform/landing.go
Normal file
36
internal/delivery/http/platform/landing.go
Normal 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)
|
||||
}
|
||||
106
internal/delivery/http/platform/overview.go
Normal file
106
internal/delivery/http/platform/overview.go
Normal 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)
|
||||
}
|
||||
274
internal/delivery/http/platform/profile.go
Normal file
274
internal/delivery/http/platform/profile.go
Normal 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)
|
||||
}
|
||||
34
internal/delivery/http/platform/profilerole.go
Normal file
34
internal/delivery/http/platform/profilerole.go
Normal 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)
|
||||
}
|
||||
163
internal/delivery/http/platform/public.go
Normal file
163
internal/delivery/http/platform/public.go
Normal 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)
|
||||
}
|
||||
34
internal/delivery/http/platform/skill.go
Normal file
34
internal/delivery/http/platform/skill.go
Normal 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)
|
||||
}
|
||||
185
internal/delivery/http/platform/specialist.go
Normal file
185
internal/delivery/http/platform/specialist.go
Normal 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)
|
||||
}
|
||||
}
|
||||
141
internal/delivery/http/platform/user.go
Normal file
141
internal/delivery/http/platform/user.go
Normal 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)
|
||||
}
|
||||
58
internal/delivery/http/platform/utils.go
Normal file
58
internal/delivery/http/platform/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user