init push
This commit is contained in:
752
internal/transport/http/handlers/master_settings_handler.go
Normal file
752
internal/transport/http/handlers/master_settings_handler.go
Normal file
@@ -0,0 +1,752 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user