753 lines
26 KiB
Go
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
|
|
}
|
|
}
|