ultisuite-backend/internal/api/admin/validate.go
R3D347HR4Y 621b0099d6
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(deploy): enhance Nginx configuration and API integration for UltiAI
- Updated .env.example to include new configuration options for the UltiAI branding and API endpoints.
- Enhanced Nginx configuration to support new API routes for the MCP and WebSocket connections.
- Introduced sub-filters for branding adjustments in Nginx responses.
- Added new JavaScript patch for API endpoint adjustments.
- Implemented tests for new API functionalities and improved error handling in the AI gateway.
2026-06-15 00:22:23 +02:00

223 lines
7.5 KiB
Go

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
}
func validateGroupID(groupID string) *apivalidate.ValidationError {
if strings.TrimSpace(groupID) == "" {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "groupID", Message: "required"})
}
return nil
}
func validateGroupIDFilter(raw string) (string, *apivalidate.ValidationError) {
groupID := strings.TrimSpace(raw)
if groupID == "" {
return "", nil
}
return groupID, nil
}
func validateCreateUserGroup(req *createUserGroupRequest) *apivalidate.ValidationError {
if strings.TrimSpace(req.Name) == "" {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "name", Message: "required"})
}
if len(strings.TrimSpace(req.Name)) > 120 {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "name", Message: "must be at most 120 characters"})
}
return nil
}
func validateUpdateUserGroup(req *updateUserGroupRequest) *apivalidate.ValidationError {
if req.Name == nil && req.Description == nil {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "group", Message: "at least one field is required"})
}
if req.Name != nil {
if strings.TrimSpace(*req.Name) == "" {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "name", Message: "required"})
}
if len(strings.TrimSpace(*req.Name)) > 120 {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "name", Message: "must be at most 120 characters"})
}
}
return nil
}
func validateBulkUsers(req *bulkUsersRequest) *apivalidate.ValidationError {
if len(req.UserIDs) == 0 {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "user_ids", Message: "required"})
}
action := strings.ToLower(strings.TrimSpace(req.Action))
switch action {
case "disable", "reactivate", "delete", "add_to_group", "remove_from_group":
if action == "add_to_group" || action == "remove_from_group" {
if strings.TrimSpace(req.GroupID) == "" {
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "group_id", Message: "required"})
}
}
case "set_role":
if _, ok := permission.ParseAccountRole(req.Role); !ok {
return apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "role",
Message: "must be one of: admin,user,guest,suspended",
})
}
default:
return apivalidate.NewValidationError(apivalidate.FieldDetail{Field: "action", Message: "invalid"})
}
return nil
}