Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,73 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidLoginRequest = errors.New("invalid login request")
|
||||
ErrMissingEmail = errors.New("email is required")
|
||||
ErrInvalidEmail = errors.New("invalid email format")
|
||||
ErrMissingPassword = errors.New("password is required")
|
||||
)
|
||||
|
||||
// LoginRequestDTO represents the login request payload
|
||||
type LoginRequestDTO struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// Validate validates the login request
|
||||
// CWE-20: Improper Input Validation - Validates email format before authentication
|
||||
func (dto *LoginRequestDTO) Validate() error {
|
||||
// Validate email format
|
||||
validator := validation.NewValidator()
|
||||
if err := validator.ValidateEmail(dto.Email, "email"); err != nil {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
|
||||
// Normalize email (lowercase, trim whitespace)
|
||||
dto.Email = strings.ToLower(strings.TrimSpace(dto.Email))
|
||||
|
||||
// Validate password (non-empty)
|
||||
if strings.TrimSpace(dto.Password) == "" {
|
||||
return ErrMissingPassword
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseLoginRequest parses and validates a login request from HTTP request body
|
||||
func ParseLoginRequest(r *http.Request) (*LoginRequestDTO, error) {
|
||||
// CWE-436: Validate Content-Type before parsing
|
||||
if err := httpvalidation.RequireJSONContentType(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidLoginRequest
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse JSON
|
||||
var dto LoginRequestDTO
|
||||
if err := json.Unmarshal(body, &dto); err != nil {
|
||||
return nil, ErrInvalidLoginRequest
|
||||
}
|
||||
|
||||
// Validate
|
||||
if err := dto.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidRefreshRequest = errors.New("invalid refresh token request")
|
||||
ErrMissingRefreshToken = errors.New("refresh token is required")
|
||||
)
|
||||
|
||||
// RefreshTokenRequestDTO represents the refresh token request payload
|
||||
type RefreshTokenRequestDTO struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// Validate validates the refresh token request
|
||||
// CWE-20: Improper Input Validation - Validates refresh token presence
|
||||
func (dto *RefreshTokenRequestDTO) Validate() error {
|
||||
// Validate refresh token (non-empty)
|
||||
if strings.TrimSpace(dto.RefreshToken) == "" {
|
||||
return ErrMissingRefreshToken
|
||||
}
|
||||
|
||||
// Normalize token (trim whitespace)
|
||||
dto.RefreshToken = strings.TrimSpace(dto.RefreshToken)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRefreshTokenRequest parses and validates a refresh token request from HTTP request body
|
||||
func ParseRefreshTokenRequest(r *http.Request) (*RefreshTokenRequestDTO, error) {
|
||||
// CWE-436: Validate Content-Type before parsing
|
||||
if err := httpvalidation.RequireJSONContentType(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRefreshRequest
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse JSON
|
||||
var dto RefreshTokenRequestDTO
|
||||
if err := json.Unmarshal(body, &dto); err != nil {
|
||||
return nil, ErrInvalidRefreshRequest
|
||||
}
|
||||
|
||||
// Validate
|
||||
if err := dto.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/dto/gateway/register_dto.go
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// RegisterRequest is the HTTP request for user registration
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
Timezone string `json:"timezone,omitempty"` // Optional: defaults to "UTC" if not provided
|
||||
|
||||
// Consent fields
|
||||
AgreeTermsOfService bool `json:"agree_terms_of_service"`
|
||||
AgreePromotions bool `json:"agree_promotions"`
|
||||
AgreeToTrackingAcrossThirdPartyAppsAndServices bool `json:"agree_to_tracking_across_third_party_apps_and_services"`
|
||||
}
|
||||
|
||||
// ValidationErrors represents validation errors in RFC 9457 format
|
||||
type ValidationErrors struct {
|
||||
Errors map[string][]string
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (v *ValidationErrors) Error() string {
|
||||
if len(v.Errors) == 0 {
|
||||
return ""
|
||||
}
|
||||
// For backward compatibility with error logging, format as string
|
||||
var messages []string
|
||||
for field, errs := range v.Errors {
|
||||
for _, err := range errs {
|
||||
messages = append(messages, fmt.Sprintf("%s: %s", field, err))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("validation errors: %v", messages)
|
||||
}
|
||||
|
||||
// Validate validates the registration request fields
|
||||
// CWE-20: Improper Input Validation - Comprehensive email validation and normalization
|
||||
// Returns all validation errors grouped together in RFC 9457 format
|
||||
func (r *RegisterRequest) Validate() error {
|
||||
v := validation.NewValidator()
|
||||
emailValidator := validation.NewEmailValidator()
|
||||
validationErrors := make(map[string][]string)
|
||||
|
||||
// Validate and normalize email
|
||||
normalizedEmail, err := emailValidator.ValidateAndNormalize(r.Email, "email")
|
||||
if err != nil {
|
||||
// Extract just the error message without the field name prefix
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["email"] = append(validationErrors["email"], errMsg)
|
||||
} else {
|
||||
r.Email = normalizedEmail
|
||||
}
|
||||
|
||||
// Validate password (non-empty, will be validated for strength in use case)
|
||||
if err := v.ValidateRequired(r.Password, "password"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["password"] = append(validationErrors["password"], errMsg)
|
||||
} else if err := v.ValidateLength(r.Password, "password", 8, 128); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["password"] = append(validationErrors["password"], errMsg)
|
||||
}
|
||||
|
||||
// Validate confirm password
|
||||
if err := v.ValidateRequired(r.ConfirmPassword, "confirm_password"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["confirm_password"] = append(validationErrors["confirm_password"], errMsg)
|
||||
} else if r.Password != r.ConfirmPassword {
|
||||
// Only check if passwords match if both are provided
|
||||
validationErrors["confirm_password"] = append(validationErrors["confirm_password"], "Passwords do not match")
|
||||
}
|
||||
|
||||
// Validate first name
|
||||
firstName, err := v.ValidateAndSanitizeString(r.FirstName, "first_name", 1, 100)
|
||||
if err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["first_name"] = append(validationErrors["first_name"], errMsg)
|
||||
} else {
|
||||
r.FirstName = firstName
|
||||
if err := v.ValidateNoHTML(r.FirstName, "first_name"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["first_name"] = append(validationErrors["first_name"], errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate last name
|
||||
lastName, err := v.ValidateAndSanitizeString(r.LastName, "last_name", 1, 100)
|
||||
if err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["last_name"] = append(validationErrors["last_name"], errMsg)
|
||||
} else {
|
||||
r.LastName = lastName
|
||||
if err := v.ValidateNoHTML(r.LastName, "last_name"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["last_name"] = append(validationErrors["last_name"], errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate tenant name
|
||||
tenantName, err := v.ValidateAndSanitizeString(r.TenantName, "tenant_name", 1, 100)
|
||||
if err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["tenant_name"] = append(validationErrors["tenant_name"], errMsg)
|
||||
} else {
|
||||
r.TenantName = tenantName
|
||||
if err := v.ValidateNoHTML(r.TenantName, "tenant_name"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["tenant_name"] = append(validationErrors["tenant_name"], errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate consent: Terms of Service is REQUIRED
|
||||
if !r.AgreeTermsOfService {
|
||||
validationErrors["agree_terms_of_service"] = append(validationErrors["agree_terms_of_service"], "Must agree to terms of service")
|
||||
}
|
||||
|
||||
// Note: AgreePromotions and AgreeToTrackingAcrossThirdPartyAppsAndServices
|
||||
// are optional (defaults to false if not provided)
|
||||
|
||||
// Return all errors grouped together in RFC 9457 format
|
||||
if len(validationErrors) > 0 {
|
||||
return &ValidationErrors{Errors: validationErrors}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractErrorMessage extracts the error message after the field name prefix
|
||||
// Example: "email: invalid email format" -> "Invalid email format"
|
||||
func extractErrorMessage(fullError string) string {
|
||||
// Find the colon separator
|
||||
colonIndex := -1
|
||||
for i, char := range fullError {
|
||||
if char == ':' {
|
||||
colonIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colonIndex == -1 {
|
||||
// No colon found, capitalize first letter and return
|
||||
if len(fullError) > 0 {
|
||||
return string(fullError[0]-32) + fullError[1:]
|
||||
}
|
||||
return fullError
|
||||
}
|
||||
|
||||
// Extract message after colon and trim spaces
|
||||
message := fullError[colonIndex+1:]
|
||||
if len(message) > 0 && message[0] == ' ' {
|
||||
message = message[1:]
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
if len(message) > 0 {
|
||||
firstChar := message[0]
|
||||
if firstChar >= 'a' && firstChar <= 'z' {
|
||||
message = string(firstChar-32) + message[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// RegisterResponse is the HTTP response after successful registration
|
||||
type RegisterResponse struct {
|
||||
// User details
|
||||
UserID string `json:"user_id"`
|
||||
UserEmail string `json:"user_email"`
|
||||
UserName string `json:"user_name"`
|
||||
UserRole string `json:"user_role"`
|
||||
|
||||
// Tenant details
|
||||
TenantID string `json:"tenant_id"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
|
||||
// Authentication tokens
|
||||
SessionID string `json:"session_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
AccessExpiry time.Time `json:"access_expiry"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
RefreshExpiry time.Time `json:"refresh_expiry"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package page
|
||||
|
||||
// DeleteRequest represents the delete pages request
|
||||
type DeleteRequest struct {
|
||||
PageIDs []string `json:"page_ids"`
|
||||
}
|
||||
|
||||
// DeleteResponse represents the delete pages response
|
||||
type DeleteResponse struct {
|
||||
DeletedCount int `json:"deleted_count"`
|
||||
DeindexedCount int `json:"deindexed_count"`
|
||||
FailedPages []string `json:"failed_pages,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package page
|
||||
|
||||
// SearchRequest represents the search pages request
|
||||
type SearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
Filter string `json:"filter,omitempty"`
|
||||
}
|
||||
|
||||
// SearchResponse represents the search pages response
|
||||
type SearchResponse struct {
|
||||
Hits []map[string]interface{} `json:"hits"`
|
||||
Query string `json:"query"`
|
||||
ProcessingTimeMs int64 `json:"processing_time_ms"`
|
||||
TotalHits int64 `json:"total_hits"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package page
|
||||
|
||||
import "time"
|
||||
|
||||
// StatusResponse represents the sync status response
|
||||
type StatusResponse struct {
|
||||
SiteID string `json:"site_id"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
PublishedPages int64 `json:"published_pages"`
|
||||
DraftPages int64 `json:"draft_pages"`
|
||||
LastSyncedAt time.Time `json:"last_synced_at"`
|
||||
PagesIndexedMonth int64 `json:"pages_indexed_month"`
|
||||
SearchRequestsMonth int64 `json:"search_requests_month"`
|
||||
LastResetAt time.Time `json:"last_reset_at"`
|
||||
SearchIndexStatus string `json:"search_index_status"`
|
||||
SearchIndexDocCount int64 `json:"search_index_doc_count"`
|
||||
}
|
||||
|
||||
// PageDetailsResponse represents the page details response
|
||||
type PageDetailsResponse struct {
|
||||
PageID string `json:"page_id"`
|
||||
Title string `json:"title"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
PostType string `json:"post_type"`
|
||||
Author string `json:"author"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
IndexedAt time.Time `json:"indexed_at"`
|
||||
MeilisearchDocID string `json:"meilisearch_doc_id"`
|
||||
IsIndexed bool `json:"is_indexed"`
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// Allowed page statuses
|
||||
var AllowedPageStatuses = []string{"publish", "draft", "pending", "private", "trash"}
|
||||
|
||||
// Allowed post types
|
||||
var AllowedPostTypes = []string{"post", "page", "attachment", "custom"}
|
||||
|
||||
// SyncPageInput represents a single page to sync in the request
|
||||
type SyncPageInput struct {
|
||||
PageID string `json:"page_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
PostType string `json:"post_type"`
|
||||
Author string `json:"author"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
}
|
||||
|
||||
// Validate validates a single page input
|
||||
func (p *SyncPageInput) Validate() error {
|
||||
v := validation.NewValidator()
|
||||
|
||||
// Validate page ID (required)
|
||||
if err := v.ValidateRequired(p.PageID, "page_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.ValidateLength(p.PageID, "page_id", 1, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate title
|
||||
title, err := v.ValidateAndSanitizeString(p.Title, "title", 1, 500)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Title = title
|
||||
|
||||
// Validate content (optional but has max length if provided)
|
||||
if p.Content != "" {
|
||||
if err := v.ValidateLength(p.Content, "content", 0, 1000000); err != nil { // 1MB limit
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate excerpt (optional but has max length if provided)
|
||||
if p.Excerpt != "" {
|
||||
if err := v.ValidateLength(p.Excerpt, "excerpt", 0, 1000); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
if err := v.ValidateURL(p.URL, "url"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate status (enum)
|
||||
if err := v.ValidateEnum(p.Status, "status", AllowedPageStatuses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate post type (enum)
|
||||
if err := v.ValidateEnum(p.PostType, "post_type", AllowedPostTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate author
|
||||
author, err := v.ValidateAndSanitizeString(p.Author, "author", 1, 255)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Author = author
|
||||
if err := v.ValidateNoHTML(p.Author, "author"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncRequest represents the sync pages request
|
||||
type SyncRequest struct {
|
||||
Pages []SyncPageInput `json:"pages"`
|
||||
}
|
||||
|
||||
// Validate validates the sync request
|
||||
func (r *SyncRequest) Validate() error {
|
||||
// Check pages array is not empty
|
||||
if len(r.Pages) == 0 {
|
||||
return fmt.Errorf("pages: array cannot be empty")
|
||||
}
|
||||
|
||||
// Validate maximum number of pages in a single request
|
||||
if len(r.Pages) > 1000 {
|
||||
return fmt.Errorf("pages: cannot sync more than 1000 pages at once")
|
||||
}
|
||||
|
||||
// Validate each page
|
||||
for i, page := range r.Pages {
|
||||
if err := page.Validate(); err != nil {
|
||||
return fmt.Errorf("pages[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncResponse represents the sync pages response
|
||||
type SyncResponse struct {
|
||||
SyncedCount int `json:"synced_count"`
|
||||
IndexedCount int `json:"indexed_count"`
|
||||
FailedPages []string `json:"failed_pages,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// CreateRequest represents the HTTP request for creating a site
|
||||
// Note: Domain will be extracted from SiteURL by the backend
|
||||
type CreateRequest struct {
|
||||
SiteURL string `json:"site_url"`
|
||||
}
|
||||
|
||||
// ValidationErrors represents validation errors in RFC 9457 format
|
||||
type ValidationErrors struct {
|
||||
Errors map[string][]string
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (v *ValidationErrors) Error() string {
|
||||
if len(v.Errors) == 0 {
|
||||
return ""
|
||||
}
|
||||
// For backward compatibility with error logging, format as string
|
||||
var messages []string
|
||||
for field, errs := range v.Errors {
|
||||
for _, err := range errs {
|
||||
messages = append(messages, fmt.Sprintf("%s: %s", field, err))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("validation errors: %v", messages)
|
||||
}
|
||||
|
||||
// Validate validates the create site request fields
|
||||
// Returns all validation errors grouped together in RFC 9457 format
|
||||
func (r *CreateRequest) Validate() error {
|
||||
v := validation.NewValidator()
|
||||
validationErrors := make(map[string][]string)
|
||||
|
||||
// Validate site URL (required)
|
||||
if err := v.ValidateURL(r.SiteURL, "site_url"); err != nil {
|
||||
errMsg := extractErrorMessage(err.Error())
|
||||
validationErrors["site_url"] = append(validationErrors["site_url"], errMsg)
|
||||
}
|
||||
|
||||
// Return all errors grouped together in RFC 9457 format
|
||||
if len(validationErrors) > 0 {
|
||||
return &ValidationErrors{Errors: validationErrors}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractErrorMessage extracts the error message after the field name prefix
|
||||
// Example: "domain: invalid domain format" -> "Invalid domain format"
|
||||
func extractErrorMessage(fullError string) string {
|
||||
// Find the colon separator
|
||||
colonIndex := -1
|
||||
for i, char := range fullError {
|
||||
if char == ':' {
|
||||
colonIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colonIndex == -1 {
|
||||
// No colon found, capitalize first letter and return
|
||||
if len(fullError) > 0 {
|
||||
return string(fullError[0]-32) + fullError[1:]
|
||||
}
|
||||
return fullError
|
||||
}
|
||||
|
||||
// Extract message after colon and trim spaces
|
||||
message := fullError[colonIndex+1:]
|
||||
if len(message) > 0 && message[0] == ' ' {
|
||||
message = message[1:]
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
if len(message) > 0 {
|
||||
firstChar := message[0]
|
||||
if firstChar >= 'a' && firstChar <= 'z' {
|
||||
message = string(firstChar-32) + message[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// CreateResponse represents the HTTP response after creating a site
|
||||
type CreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
SiteURL string `json:"site_url"`
|
||||
APIKey string `json:"api_key"` // Only returned once at creation
|
||||
Status string `json:"status"`
|
||||
VerificationToken string `json:"verification_token"`
|
||||
SearchIndexName string `json:"search_index_name"`
|
||||
VerificationInstructions string `json:"verification_instructions"` // DNS TXT record setup instructions
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package site
|
||||
|
||||
import "time"
|
||||
|
||||
// GetResponse represents the HTTP response for getting a site
|
||||
type GetResponse struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Domain string `json:"domain"`
|
||||
SiteURL string `json:"site_url"`
|
||||
APIKeyPrefix string `json:"api_key_prefix"`
|
||||
APIKeyLastFour string `json:"api_key_last_four"`
|
||||
Status string `json:"status"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
SearchIndexName string `json:"search_index_name"`
|
||||
TotalPagesIndexed int64 `json:"total_pages_indexed"`
|
||||
LastIndexedAt time.Time `json:"last_indexed_at,omitempty"`
|
||||
PluginVersion string `json:"plugin_version,omitempty"`
|
||||
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||||
SearchRequestsCount int64 `json:"search_requests_count"`
|
||||
MonthlyPagesIndexed int64 `json:"monthly_pages_indexed"`
|
||||
LastResetAt time.Time `json:"last_reset_at"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package site
|
||||
|
||||
import "time"
|
||||
|
||||
// ListResponse represents the HTTP response for listing sites
|
||||
type ListResponse struct {
|
||||
Sites []SiteListItem `json:"sites"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// SiteListItem represents a site in the list
|
||||
type SiteListItem struct {
|
||||
ID string `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TotalPagesIndexed int64 `json:"total_pages_indexed"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package site
|
||||
|
||||
import "time"
|
||||
|
||||
// RotateAPIKeyResponse represents the HTTP response after rotating an API key
|
||||
type RotateAPIKeyResponse struct {
|
||||
NewAPIKey string `json:"new_api_key"` // New API key (only returned once)
|
||||
OldKeyLastFour string `json:"old_key_last_four"`
|
||||
RotatedAt time.Time `json:"rotated_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package tenant
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// CreateRequest represents the HTTP request for creating a tenant
|
||||
type CreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
// Validate validates the create tenant request
|
||||
// CWE-20: Improper Input Validation
|
||||
func (r *CreateRequest) Validate() error {
|
||||
validator := validation.NewValidator()
|
||||
|
||||
// Validate name: 3-100 chars, printable, no HTML
|
||||
if err := validator.ValidateRequired(r.Name, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validator.ValidateLength(r.Name, "name", 3, 100); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validator.ValidatePrintable(r.Name, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validator.ValidateNoHTML(r.Name, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate slug: uses existing slug validation (lowercase, hyphens, 3-63 chars)
|
||||
if err := validator.ValidateSlug(r.Slug, "slug"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sanitize inputs
|
||||
r.Name = validator.SanitizeString(r.Name)
|
||||
r.Slug = validator.SanitizeString(r.Slug)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateResponse represents the HTTP response after creating a tenant
|
||||
type CreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package tenant
|
||||
|
||||
import "time"
|
||||
|
||||
// GetResponse represents the HTTP response when retrieving a tenant
|
||||
type GetResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package user
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateRequest is the HTTP request for creating a user
|
||||
type CreateRequest struct {
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
|
||||
// CreateResponse is the HTTP response after creating a user
|
||||
type CreateResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package user
|
||||
|
||||
import "time"
|
||||
|
||||
// GetResponse is the HTTP response for getting a user
|
||||
type GetResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue