Files
fm_be/internal/transport/http/handlers/master_settings_handler.go
2026-07-16 22:16:45 +07:00

753 lines
26 KiB
Go

package handlers
import (
"context"
"errors"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
filemanager "wucher/internal/domain/file_manager"
mastersettings "wucher/internal/domain/master_settings"
"wucher/internal/shared/pkg/apperrorsx"
"wucher/internal/shared/pkg/uuidv7"
requestdto "wucher/internal/transport/http/dto/request"
responsedto "wucher/internal/transport/http/dto/response"
"wucher/internal/transport/http/jsonapi"
"wucher/internal/transport/http/response"
"wucher/internal/transport/http/validators"
)
type MasterSettingsHandler struct {
svc mastersettings.Service
fileSvc filemanager.Service
storage filemanager.ObjectStorage
validate *validators.Validator
}
type managedFileAsset struct {
OriginalURL *string
ThumbnailURL *string
OriginalSizeBytes *int64
}
func NewMasterSettingsHandler(svc mastersettings.Service, fileSvc filemanager.Service, storage filemanager.ObjectStorage) *MasterSettingsHandler {
return &MasterSettingsHandler{
svc: svc,
fileSvc: fileSvc,
storage: storage,
validate: validators.New(),
}
}
// CreateMasterSettings godoc
// @Summary Create master settings
// @Description Create a master settings row.
// @Tags Master Settings
// @Accept json
// @Produce json
// @Param request body requestdto.MasterSettingsCreateRequest true "JSON:API master settings create request"
// @Success 201 {object} responsedto.MasterSettingsResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/create [post]
func (h *MasterSettingsHandler) Create(c *fiber.Ctx) error {
var req requestdto.MasterSettingsCreateRequest
if err := c.BodyParser(&req); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Invalid JSON",
Detail: safeInternalDetail(),
}})
}
if errs := h.validate.ValidateStruct(req); errs != nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs)
}
companyName := strings.TrimSpace(req.Data.Attributes.CompanyName)
if companyName == "" {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "company_name is required",
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/company_name"},
}})
}
logoFileID, statusCode, errObj := h.resolveLogoFileID(c.UserContext(), req.Data.Attributes.LogoFileUUID, "/data/attributes/logo_file_uuid")
if errObj != nil {
return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj})
}
coverFileID, statusCode, errObj := h.resolveCoverFileID(c.UserContext(), req.Data.Attributes.CoverFileUUID, "/data/attributes/cover_file_uuid")
if errObj != nil {
return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj})
}
row := &mastersettings.MasterSettings{
CompanyName: companyName,
Title: optionalTrimmedString(req.Data.Attributes.Title),
Subtitle: optionalTrimmedString(req.Data.Attributes.Subtitle),
LogoFileID: logoFileID,
CoverFileID: coverFileID,
LogoURL: nil,
CreatedBy: actorUserID(c),
UpdatedBy: actorUserID(c),
}
if err := h.svc.Create(c.UserContext(), row); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Create failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, nil, row.LogoFileID, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Create failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, nil, row.CoverFileID, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Create failed",
Detail: safeInternalDetail(),
}})
}
created, err := h.svc.GetByID(c.UserContext(), row.ID)
if err == nil && created != nil {
return response.Write(c, fiber.StatusCreated, h.masterSettingsResource(c, created))
}
return response.Write(c, fiber.StatusCreated, h.masterSettingsResource(c, row))
}
// GetMasterSettings godoc
// @Summary Get current master settings
// @Description Return singleton master settings object (latest active row).
// @Tags Master Settings
// @Produce json
// @Success 200 {object} responsedto.MasterSettingsResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/get [get]
func (h *MasterSettingsHandler) Get(c *fiber.Ctx) error {
rows, _, err := h.svc.List(c.UserContext(), "", "-updated_at", 1, 0)
if err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Get failed",
Detail: safeInternalDetail(),
}})
}
if len(rows) == 0 {
return response.Write(c, fiber.StatusOK, responsedto.MasterSettingsResponse{
Data: responsedto.MasterSettingsResource{
Type: "master_settings",
ID: "",
Attributes: responsedto.MasterSettingsAttributes{
CompanyName: "",
},
},
})
}
return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, &rows[0]))
}
// UpsertMicrosoftEntraSSO godoc
// @Summary Upsert Microsoft Entra SSO settings
// @Description Create or update Microsoft Entra SSO settings as master settings.
// @Tags Master Settings
// @Accept json
// @Produce json
// @Param request body requestdto.MasterSettingsMicrosoftEntraUpsertRequest true "JSON:API Microsoft Entra SSO upsert request"
// @Success 200 {object} responsedto.MasterSettingsMicrosoftEntraResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/sso/microsoft [put]
func (h *MasterSettingsHandler) UpsertMicrosoftEntraSSO(c *fiber.Ctx) error {
var req requestdto.MasterSettingsMicrosoftEntraUpsertRequest
if err := c.BodyParser(&req); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Invalid JSON",
Detail: safeInternalDetail(),
}})
}
if errs := h.validate.ValidateStruct(req); errs != nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs)
}
view, err := h.svc.UpsertMicrosoftEntraConfig(c.UserContext(), mastersettings.MicrosoftEntraConfigInput{
TenantID: req.Data.Attributes.TenantID,
ClientID: req.Data.Attributes.ClientID,
ClientSecret: req.Data.Attributes.ClientSecret,
RedirectURL: req.Data.Attributes.RedirectURL,
Authority: req.Data.Attributes.Authority,
Scopes: req.Data.Attributes.Scopes,
}, actorUserID(c))
if err != nil {
status := fiber.StatusBadRequest
title := "Upsert failed"
if isMicrosoftEntraValidationError(err) {
status = fiber.StatusUnprocessableEntity
title = "Validation error"
}
return writeMasterSettingsErrors(c, status, []jsonapi.ErrorObject{{
Status: strconv.Itoa(status),
Title: title,
Detail: safeInternalDetail(),
}})
}
return response.Write(c, fiber.StatusOK, h.microsoftEntraResource(view))
}
// GetMicrosoftEntraSSO godoc
// @Summary Get Microsoft Entra SSO settings
// @Description Get Microsoft Entra SSO settings from master settings.
// @Tags Master Settings
// @Produce json
// @Success 200 {object} responsedto.MasterSettingsMicrosoftEntraResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/sso/microsoft [get]
func (h *MasterSettingsHandler) GetMicrosoftEntraSSO(c *fiber.Ctx) error {
view, err := h.svc.GetMicrosoftEntraConfig(c.UserContext())
if err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Get failed",
Detail: safeInternalDetail(),
}})
}
if view == nil {
return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
Status: "404",
Title: "Not found",
Detail: "microsoft entra sso settings not found",
}})
}
return response.Write(c, fiber.StatusOK, h.microsoftEntraResource(view))
}
// UpdateMasterSettings godoc
// @Summary Update master settings (partial)
// @Description Patch master settings by ID.
// @Tags Master Settings
// @Accept json
// @Produce json
// @Param uuid path string true "Master Settings UUID (UUIDv7)"
// @Param request body requestdto.MasterSettingsUpdateRequest true "JSON:API master settings update request"
// @Success 200 {object} responsedto.MasterSettingsResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/update/{uuid} [patch]
func (h *MasterSettingsHandler) Update(c *fiber.Ctx) error {
uuidStr := c.Params("uuid")
id, err := uuidv7.ParseString(uuidStr)
if err != nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "uuid is invalid UUID",
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
}})
}
var req requestdto.MasterSettingsUpdateRequest
if err := c.BodyParser(&req); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Invalid JSON",
Detail: safeInternalDetail(),
}})
}
if errs := h.validate.ValidateStruct(req); errs != nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs)
}
if strings.TrimSpace(req.Data.ID) == "" {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "id is required",
Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
}})
}
if req.Data.ID != uuidStr {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "id does not match path uuid",
Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
}})
}
attrs := req.Data.Attributes
if attrs.CompanyName == nil &&
attrs.Title == nil &&
attrs.Subtitle == nil &&
attrs.LogoFileUUID == nil &&
attrs.CoverFileUUID == nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "at least one attribute must be provided",
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"},
}})
}
existing, err := h.svc.GetByID(c.UserContext(), id)
if err != nil || existing == nil {
return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
Status: "404",
Title: "Not found",
Detail: "master settings not found",
}})
}
prevLogoFileID := append([]byte(nil), existing.LogoFileID...)
prevCoverFileID := append([]byte(nil), existing.CoverFileID...)
if attrs.CompanyName != nil {
v := strings.TrimSpace(*attrs.CompanyName)
if v == "" {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "company_name cannot be empty",
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/company_name"},
}})
}
existing.CompanyName = v
}
if attrs.Title != nil {
existing.Title = strings.TrimSpace(*attrs.Title)
}
if attrs.Subtitle != nil {
existing.Subtitle = strings.TrimSpace(*attrs.Subtitle)
}
if attrs.LogoFileUUID != nil {
if hasBlankOptionalString(attrs.LogoFileUUID) {
existing.LogoFileID = nil
} else {
logoFileID, statusCode, errObj := h.resolveLogoFileID(c.UserContext(), attrs.LogoFileUUID, "/data/attributes/logo_file_uuid")
if errObj != nil {
return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj})
}
existing.LogoFileID = logoFileID
// Keep manual URL disabled once logo is managed by file manager metadata.
existing.LogoURL = nil
}
}
if attrs.CoverFileUUID != nil {
if hasBlankOptionalString(attrs.CoverFileUUID) {
existing.CoverFileID = nil
} else {
coverFileID, statusCode, errObj := h.resolveCoverFileID(c.UserContext(), attrs.CoverFileUUID, "/data/attributes/cover_file_uuid")
if errObj != nil {
return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj})
}
existing.CoverFileID = coverFileID
}
}
existing.UpdatedBy = actorUserID(c)
if err := h.svc.Update(c.UserContext(), existing); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Update failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevLogoFileID, existing.LogoFileID, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Update failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevCoverFileID, existing.CoverFileID, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Update failed",
Detail: safeInternalDetail(),
}})
}
updated, err := h.svc.GetByID(c.UserContext(), existing.ID)
if err == nil && updated != nil {
return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, updated))
}
return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, existing))
}
// DeleteMasterSettings godoc
// @Summary Delete master settings
// @Tags Master Settings
// @Produce json
// @Param uuid path string true "Master Settings UUID (UUIDv7)"
// @Success 200 {object} jsonapi.Resource
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Router /api/v1/master-settings/delete/{uuid} [delete]
func (h *MasterSettingsHandler) Delete(c *fiber.Ctx) error {
uuidStr := c.Params("uuid")
id, err := uuidv7.ParseString(uuidStr)
if err != nil {
return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
Status: "422",
Title: "Validation error",
Detail: "uuid is invalid UUID",
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
}})
}
existing, _ := h.svc.GetByID(c.UserContext(), id)
var prevLogoFileID []byte
var prevCoverFileID []byte
if existing != nil {
prevLogoFileID = append([]byte(nil), existing.LogoFileID...)
prevCoverFileID = append([]byte(nil), existing.CoverFileID...)
}
if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
Status: "404",
Title: "Not found",
Detail: "master settings not found",
}})
}
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Delete failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevLogoFileID, nil, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Delete failed",
Detail: safeInternalDetail(),
}})
}
if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevCoverFileID, nil, actorUserID(c)); err != nil {
return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "Delete failed",
Detail: safeInternalDetail(),
}})
}
return response.Write(c, fiber.StatusOK, jsonapi.Resource{
Type: "master_settings_delete",
Attributes: map[string]any{
"deleted": true,
},
})
}
func (h *MasterSettingsHandler) masterSettingsResource(c *fiber.Ctx, row *mastersettings.MasterSettings) responsedto.MasterSettingsResource {
logoFileUUID := uuidStringPtrOrNil(row.LogoFileID)
logoURL := h.resolveLogoURL(c, row, logoFileUUID)
logoThumbURL := h.resolveLogoThumbnailURL(c, row, logoFileUUID)
logoSize := h.resolveLogoOriginalSize(c, row, logoFileUUID)
coverFileUUID := uuidStringPtrOrNil(row.CoverFileID)
coverURL := h.resolveCoverURL(c, row, coverFileUUID)
coverThumbURL := h.resolveCoverThumbnailURL(c, row, coverFileUUID)
coverSize := h.resolveCoverOriginalSize(c, row, coverFileUUID)
return responsedto.MasterSettingsResource{
Type: "master_settings",
ID: idString(row.ID),
Attributes: responsedto.MasterSettingsAttributes{
CompanyName: row.CompanyName,
Title: row.Title,
Subtitle: row.Subtitle,
LogoFileUUID: logoFileUUID,
LogoURL: logoURL,
LogoThumbURL: logoThumbURL,
LogoSizeBytes: logoSize,
CoverFileUUID: coverFileUUID,
CoverURL: coverURL,
CoverThumbURL: coverThumbURL,
CoverSizeBytes: coverSize,
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
CreatedBy: uuidStringOrEmpty(row.CreatedBy),
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
DeletedAt: timeStringOrEmpty(row.DeletedAt),
DeletedBy: uuidStringOrEmpty(row.DeletedBy),
},
}
}
func optionalTrimmedString(v *string) string {
if v == nil {
return ""
}
return strings.TrimSpace(*v)
}
func (h *MasterSettingsHandler) resolveLogoURL(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *string {
legacyURL := strings.TrimSpace(masterSettingsDerefString(row.LogoURL))
if logoFileUUID == nil || strings.TrimSpace(*logoFileUUID) == "" {
if legacyURL == "" {
return nil
}
return stringPointer(legacyURL)
}
return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).OriginalURL
}
func (h *MasterSettingsHandler) resolveCoverURL(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *string {
return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).OriginalURL
}
func (h *MasterSettingsHandler) resolveManagedFileURL(c *fiber.Ctx, fileID []byte, fileUUID *string) *string {
return h.resolveManagedFileAsset(c, fileID, fileUUID).OriginalURL
}
func (h *MasterSettingsHandler) resolveLogoThumbnailURL(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *string {
if row == nil {
return nil
}
return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).ThumbnailURL
}
func (h *MasterSettingsHandler) resolveCoverThumbnailURL(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *string {
if row == nil {
return nil
}
return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).ThumbnailURL
}
func (h *MasterSettingsHandler) resolveLogoOriginalSize(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *int64 {
if row == nil {
return nil
}
return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).OriginalSizeBytes
}
func (h *MasterSettingsHandler) resolveCoverOriginalSize(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *int64 {
if row == nil {
return nil
}
return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).OriginalSizeBytes
}
func (h *MasterSettingsHandler) resolveManagedFileAsset(c *fiber.Ctx, fileID []byte, fileUUID *string) managedFileAsset {
if fileUUID == nil || strings.TrimSpace(*fileUUID) == "" {
return managedFileAsset{}
}
if h.fileSvc == nil {
return managedFileAsset{}
}
fileRow, err := h.fileSvc.GetFileByID(c.UserContext(), fileID)
if err != nil || fileRow == nil {
return managedFileAsset{}
}
resolved := resolveFileManagerDownloadURLs(c.UserContext(), h.storage, fileRow)
asset := managedFileAsset{
OriginalSizeBytes: int64Ptr(resolved.OriginalSizeBytes),
}
if url := strings.TrimSpace(resolved.OriginalURL); url != "" {
asset.OriginalURL = stringPointer(url)
}
if url := strings.TrimSpace(resolved.ThumbnailURL); url != "" {
asset.ThumbnailURL = stringPointer(url)
}
return asset
}
func stringPointer(v string) *string {
return &v
}
func masterSettingsDerefString(v *string) string {
if v == nil {
return ""
}
return *v
}
func uuidStringPtrOrNil(id []byte) *string {
v := idString(id)
if strings.TrimSpace(v) == "" {
return nil
}
return &v
}
func (h *MasterSettingsHandler) microsoftEntraResource(view *mastersettings.MicrosoftEntraConfigView) responsedto.MasterSettingsMicrosoftEntraResource {
if view == nil {
return responsedto.MasterSettingsMicrosoftEntraResource{Type: "master_settings_microsoft_entra_sso"}
}
return responsedto.MasterSettingsMicrosoftEntraResource{
Type: "master_settings_microsoft_entra_sso",
Attributes: responsedto.MasterSettingsMicrosoftEntraAttributes{
TenantID: view.TenantID,
ClientID: view.ClientID,
ClientSecretMasked: view.ClientSecretMasked,
RedirectURL: view.RedirectURL,
Authority: view.Authority,
Scopes: view.Scopes,
},
}
}
func isMicrosoftEntraValidationError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "MS_ENTRA_") ||
strings.Contains(msg, "required") ||
strings.Contains(msg, "valid absolute URL")
}
func (h *MasterSettingsHandler) resolveLogoFileID(ctx context.Context, raw *string, pointer string) ([]byte, int, *jsonapi.ErrorObject) {
return h.resolveManagedFileID(ctx, raw, pointer, "logo_file_uuid")
}
func (h *MasterSettingsHandler) resolveCoverFileID(ctx context.Context, raw *string, pointer string) ([]byte, int, *jsonapi.ErrorObject) {
return h.resolveManagedFileID(ctx, raw, pointer, "cover_file_uuid")
}
func (h *MasterSettingsHandler) resolveManagedFileID(ctx context.Context, raw *string, pointer, fieldName string) ([]byte, int, *jsonapi.ErrorObject) {
if raw == nil {
return nil, 0, nil
}
v := strings.TrimSpace(*raw)
if v == "" {
return nil, 0, nil
}
id, err := uuidv7.ParseString(v)
if err != nil {
return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fieldName + " is invalid UUID",
Source: &jsonapi.ErrorSource{Pointer: pointer},
}
}
if h.fileSvc == nil {
return nil, fiber.StatusServiceUnavailable, &jsonapi.ErrorObject{
Status: "503",
Title: "File manager unavailable",
Detail: "file manager service is not configured",
Source: &jsonapi.ErrorSource{Pointer: pointer},
}
}
fileRow, err := h.fileSvc.GetFileByID(ctx, id)
if err != nil {
return nil, fiber.StatusBadRequest, &jsonapi.ErrorObject{
Status: "400",
Title: "Validation error",
Detail: safeInternalDetail(),
Source: &jsonapi.ErrorSource{Pointer: pointer},
}
}
if fileRow == nil {
return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fieldName + " is not found",
Source: &jsonapi.ErrorSource{Pointer: pointer},
}
}
if !strings.EqualFold(strings.TrimSpace(fileRow.Status), filemanager.FileStatusReady) {
return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fieldName + " is not ready. finalize upload via /api/v1/file-manager/files/create",
Source: &jsonapi.ErrorSource{Pointer: pointer},
}
}
return id, 0, nil
}
func writeMasterSettingsErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
enriched := make([]jsonapi.ErrorObject, 0, len(errs))
statusStr := strconv.Itoa(status)
for i := range errs {
e := errs[i]
e.Status = statusStr
def := inferMasterSettingsDef(status, e.Title, e.Detail)
if strings.TrimSpace(e.Code) == "" {
e.Code = def.Code
}
if strings.TrimSpace(e.ErrorCode) == "" {
e.ErrorCode = def.ErrorCode
}
if strings.TrimSpace(e.Title) == "" {
e.Title = def.Title
}
e.Detail = def.Message
enriched = append(enriched, e)
}
return response.WriteErrors(c, status, enriched)
}
func inferMasterSettingsDef(status int, title, detail string) apperrorsx.Def {
lowerTitle := strings.ToLower(strings.TrimSpace(title))
lowerDetail := strings.ToLower(strings.TrimSpace(detail))
switch status {
case fiber.StatusNotFound:
return apperrorsx.ErrMasterSettingsNotFound
case fiber.StatusUnprocessableEntity:
switch {
case strings.Contains(lowerDetail, "uuid is invalid"):
return apperrorsx.ErrMasterSettingsInvalidUUID
case strings.Contains(lowerDetail, "id is required"):
return apperrorsx.ErrMasterSettingsIDRequired
case strings.Contains(lowerDetail, "id does not match"):
return apperrorsx.ErrMasterSettingsIDMismatch
case strings.Contains(lowerDetail, "at least one attribute"):
return apperrorsx.ErrMasterSettingsAttributesRequired
case strings.Contains(lowerDetail, "company_name is required"), strings.Contains(lowerDetail, "company_name cannot be empty"):
return apperrorsx.ErrMasterSettingsCompanyNameRequired
case strings.Contains(lowerDetail, "logo_file_uuid"):
return apperrorsx.ErrMasterSettingsLogoFileInvalid
case strings.Contains(lowerDetail, "cover_file_uuid"):
return apperrorsx.ErrMasterSettingsCoverFileInvalid
default:
return apperrorsx.ErrMasterSettingsInvalidPayload
}
case fiber.StatusBadRequest:
switch {
case strings.Contains(lowerTitle, "invalid json"):
return apperrorsx.ErrMasterSettingsInvalidJSON
case strings.Contains(lowerTitle, "create failed"):
return apperrorsx.ErrMasterSettingsCreateFailed
case strings.Contains(lowerTitle, "update failed"):
return apperrorsx.ErrMasterSettingsUpdateFailed
case strings.Contains(lowerTitle, "delete failed"):
return apperrorsx.ErrMasterSettingsDeleteFailed
case strings.Contains(lowerTitle, "get failed"):
return apperrorsx.ErrMasterSettingsGetFailed
case strings.Contains(lowerTitle, "upsert failed"):
return apperrorsx.ErrMasterSettingsMicrosoftUpsert
default:
return apperrorsx.ErrMasterSettingsInvalidPayload
}
default:
return apperrorsx.ErrInternal
}
}