package admin import ( "net/mail" "slices" "strconv" "strings" "github.com/ultisuite/ulti-backend/internal/api/apivalidate" "github.com/ultisuite/ulti-backend/internal/permission" ) const maxQuotaRequestBody = 4 << 10 type setQuotaRequest struct { MailMaxStorageBytes *int64 `json:"mail_max_storage_bytes"` DriveMaxStorageBytes *int64 `json:"drive_max_storage_bytes"` PhotosMaxStorageBytes *int64 `json:"photos_max_storage_bytes"` } func validateSetQuota(req *setQuotaRequest) *apivalidate.ValidationError { if req.MailMaxStorageBytes == nil && req.DriveMaxStorageBytes == nil && req.PhotosMaxStorageBytes == nil { return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "quota", Message: "at least one quota field is required"}) } details := make([]apivalidate.FieldDetail, 0, 3) if req.MailMaxStorageBytes != nil && *req.MailMaxStorageBytes < 0 { details = append(details, apivalidate.FieldDetail{Field: "mail_max_storage_bytes", Message: "must be non-negative"}) } if req.DriveMaxStorageBytes != nil && *req.DriveMaxStorageBytes < 0 { details = append(details, apivalidate.FieldDetail{Field: "drive_max_storage_bytes", Message: "must be non-negative"}) } if req.PhotosMaxStorageBytes != nil && *req.PhotosMaxStorageBytes < 0 { details = append(details, apivalidate.FieldDetail{Field: "photos_max_storage_bytes", Message: "must be non-negative"}) } if len(details) > 0 { return apivalidate.NewValidationError(details...) } return nil } type createUserRequest struct { ExternalID string `json:"external_id"` Email string `json:"email"` Name string `json:"name"` } func validateCreateUser(req *createUserRequest) *apivalidate.ValidationError { details := make([]apivalidate.FieldDetail, 0, 2) if strings.TrimSpace(req.ExternalID) == "" { details = append(details, apivalidate.FieldDetail{Field: "external_id", Message: "required"}) } if _, err := mail.ParseAddress(strings.TrimSpace(req.Email)); err != nil { details = append(details, apivalidate.FieldDetail{Field: "email", Message: "must be valid"}) } if len(details) > 0 { return apivalidate.NewValidationError(details...) } return nil } type inviteUserRequest struct { Email string `json:"email"` Name string `json:"name"` } func validateInviteUser(req *inviteUserRequest) *apivalidate.ValidationError { if _, err := mail.ParseAddress(strings.TrimSpace(req.Email)); err != nil { return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "email", Message: "must be valid"}) } return nil } type updateUserRequest struct { Email *string `json:"email"` Name *string `json:"name"` } func validateUpdateUser(req *updateUserRequest) *apivalidate.ValidationError { if req.Email == nil && req.Name == nil { return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "user", Message: "at least one field is required"}) } if req.Email != nil { if _, err := mail.ParseAddress(strings.TrimSpace(*req.Email)); err != nil { return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "email", Message: "must be valid"}) } } return nil } func validateExportFormat(raw string) (string, *apivalidate.ValidationError) { format := strings.TrimSpace(raw) if format == "" { return "ndjson", nil } if !slices.Contains([]string{"ndjson", "csv"}, format) { return "", apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "format", Message: "must be one of: ndjson,csv"}) } return format, nil } func validateExportLimit(raw string) (int, *apivalidate.ValidationError) { val := strings.TrimSpace(raw) if val == "" { return 5000, nil } limit, err := strconv.Atoi(val) if err != nil || limit < 1 || limit > 10000 { return 0, apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "limit", Message: "must be between 1 and 10000"}) } return limit, nil } func validateStatus(raw string) (string, *apivalidate.ValidationError) { status := strings.ToLower(strings.TrimSpace(raw)) if status == "" { return "", nil } if !slices.Contains([]string{"active", "disabled", "invited"}, status) { return "", apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "status", Message: "must be one of: active,disabled,invited"}) } return status, nil } func validateUserID(userID string) *apivalidate.ValidationError { if strings.TrimSpace(userID) == "" { return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "userID", Message: "required"}) } return nil } type setUserRoleRequest struct { Role string `json:"role"` } func validateSetUserRole(req *setUserRoleRequest) *apivalidate.ValidationError { if _, ok := permission.ParseAccountRole(req.Role); !ok { return apivalidate.NewValidationError(apivalidate.FieldDetail{ Field: "role", Message: "must be one of: admin,user,guest,suspended", }) } return nil } func validateAccountRoleFilter(raw string) (string, *apivalidate.ValidationError) { role := strings.ToLower(strings.TrimSpace(raw)) if role == "" { return "", nil } if _, ok := permission.ParseAccountRole(role); !ok { return "", apivalidate.NewValidationError(apivalidate.FieldDetail{ Field: "role", Message: "must be one of: admin,user,guest,suspended", }) } return role, nil }