Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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"`
}