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"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue