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,122 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/gateway/hello_handler.go
package gateway
import (
"encoding/json"
"fmt"
"html"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
)
// HelloHandler handles the hello endpoint for authenticated users
type HelloHandler struct {
logger *zap.Logger
}
// ProvideHelloHandler creates a new HelloHandler
func ProvideHelloHandler(logger *zap.Logger) *HelloHandler {
return &HelloHandler{
logger: logger,
}
}
// HelloRequest represents the request body for the hello endpoint
type HelloRequest struct {
Name string `json:"name"`
}
// HelloResponse represents the response for the hello endpoint
type HelloResponse struct {
Message string `json:"message"`
}
// Handle handles the HTTP request for the hello endpoint
// Security: CWE-20, CWE-79, CWE-117 - Comprehensive input validation and sanitization
func (h *HelloHandler) Handle(w http.ResponseWriter, r *http.Request) {
// M-2: Enforce strict Content-Type validation
// CWE-436: Validate Content-Type before parsing
if err := httpvalidation.ValidateJSONContentTypeStrict(r); err != nil {
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
httperror.ProblemBadRequest(w, err.Error())
return
}
// Parse request body
var req HelloRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Warn("invalid request body", zap.Error(err))
httperror.ProblemBadRequest(w, "invalid request body")
return
}
// H-1: Comprehensive input validation
// CWE-20: Improper Input Validation
validator := validation.NewValidator()
// Validate required
if err := validator.ValidateRequired(req.Name, "name"); err != nil {
httperror.ProblemBadRequest(w, err.Error())
return
}
// Validate length (1-100 characters is reasonable for a name)
if err := validator.ValidateLength(req.Name, "name", 1, 100); err != nil {
httperror.ProblemBadRequest(w, err.Error())
return
}
// Validate printable characters only
if err := validator.ValidatePrintable(req.Name, "name"); err != nil {
httperror.ProblemBadRequest(w, err.Error())
return
}
// M-1: Validate no HTML tags (XSS prevention)
// CWE-79: Cross-site Scripting
if err := validator.ValidateNoHTML(req.Name, "name"); err != nil {
httperror.ProblemBadRequest(w, err.Error())
return
}
// Sanitize input
req.Name = validator.SanitizeString(req.Name)
// H-2: Fix log injection vulnerability
// CWE-117: Improper Output Neutralization for Logs
// Hash the name to prevent log injection and protect PII
nameHash := logger.HashString(req.Name)
// L-1: Extract user ID from context for correlation
// Get authenticated user info from JWT context
userID := "unknown"
if uid := r.Context().Value(constants.SessionUserID); uid != nil {
if userIDUint, ok := uid.(uint64); ok {
userID = fmt.Sprintf("%d", userIDUint)
}
}
h.logger.Info("hello endpoint accessed",
zap.String("user_id", userID),
zap.String("name_hash", nameHash))
// M-1: HTML-escape the name to prevent XSS in any context
// CWE-79: Cross-site Scripting
safeName := html.EscapeString(req.Name)
// Create response with sanitized output
response := HelloResponse{
Message: fmt.Sprintf("Hello, %s! Welcome to MaplePress Backend.", safeName),
}
// Write response
httpresponse.OK(w, response)
}

View file

@ -0,0 +1,183 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/gateway/login_handler.go
package gateway
import (
"errors"
"fmt"
"net/http"
"go.uber.org/zap"
gatewaydto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/gateway"
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/gateway"
securityeventservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/securityevent"
gatewayuc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/ratelimit"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/clientip"
)
// LoginHandler handles HTTP requests for user login
type LoginHandler struct {
loginService gatewaysvc.LoginService
loginRateLimiter ratelimit.LoginRateLimiter
securityEventLogger securityeventservice.Logger
ipExtractor *clientip.Extractor
logger *zap.Logger
}
// NewLoginHandler creates a new login handler
// CWE-307: Integrates rate limiting and account lockout protection
// CWE-778: Integrates security event logging for audit trails
func NewLoginHandler(
loginService gatewaysvc.LoginService,
loginRateLimiter ratelimit.LoginRateLimiter,
securityEventLogger securityeventservice.Logger,
ipExtractor *clientip.Extractor,
logger *zap.Logger,
) *LoginHandler {
return &LoginHandler{
loginService: loginService,
loginRateLimiter: loginRateLimiter,
securityEventLogger: securityEventLogger,
ipExtractor: ipExtractor,
logger: logger.Named("login-handler"),
}
}
// ProvideLoginHandler creates a new LoginHandler for dependency injection
func ProvideLoginHandler(
loginService gatewaysvc.LoginService,
loginRateLimiter ratelimit.LoginRateLimiter,
securityEventLogger securityeventservice.Logger,
ipExtractor *clientip.Extractor,
logger *zap.Logger,
) *LoginHandler {
return NewLoginHandler(loginService, loginRateLimiter, securityEventLogger, ipExtractor, logger)
}
// Handle processes POST /api/v1/login requests
// CWE-307: Implements rate limiting and account lockout protection against brute force attacks
func (h *LoginHandler) Handle(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling login request")
// Parse and validate request
dto, err := gatewaydto.ParseLoginRequest(r)
if err != nil {
h.logger.Warn("invalid login request", zap.Error(err))
httperror.ProblemBadRequest(w, err.Error())
return
}
// CWE-348: Extract client IP securely with trusted proxy validation
clientIP := h.ipExtractor.Extract(r)
// CWE-307: Check rate limits and account lockout BEFORE attempting authentication
allowed, isLocked, remainingAttempts, err := h.loginRateLimiter.CheckAndRecordAttempt(
r.Context(),
dto.Email,
clientIP,
)
if err != nil {
// Log error but continue (fail open)
h.logger.Error("rate limiter error",
logger.EmailHash(dto.Email),
zap.String("ip", clientIP),
zap.Error(err))
}
// Account is locked - return error immediately
if isLocked {
h.logger.Warn("login attempt on locked account",
logger.EmailHash(dto.Email),
logger.SafeEmail("email_redacted", dto.Email),
zap.String("ip", clientIP))
// Add Retry-After header (30 minutes)
w.Header().Set("Retry-After", "1800")
httperror.ProblemTooManyRequests(w, "Account temporarily locked due to too many failed login attempts. Please try again later.")
return
}
// IP rate limit exceeded - return error immediately
if !allowed {
h.logger.Warn("login rate limit exceeded",
logger.EmailHash(dto.Email),
zap.String("ip", clientIP))
// CWE-778: Log security event for IP rate limit
h.securityEventLogger.LogIPRateLimitExceeded(r.Context(), clientIP)
// Add Retry-After header (15 minutes)
w.Header().Set("Retry-After", "900")
httperror.ProblemTooManyRequests(w, "Too many login attempts from this IP address. Please try again later.")
return
}
// Execute login
response, err := h.loginService.Login(r.Context(), &gatewaysvc.LoginInput{
Email: dto.Email,
Password: dto.Password,
})
if err != nil {
if errors.Is(err, gatewayuc.ErrInvalidCredentials) {
// CWE-307: Record failed login attempt for account lockout tracking
if err := h.loginRateLimiter.RecordFailedAttempt(r.Context(), dto.Email, clientIP); err != nil {
h.logger.Error("failed to record failed login attempt",
logger.EmailHash(dto.Email),
zap.String("ip", clientIP),
zap.Error(err))
}
// CWE-532: Log with redacted email (security event logging)
h.logger.Warn("login failed: invalid credentials",
logger.EmailHash(dto.Email),
logger.SafeEmail("email_redacted", dto.Email),
zap.String("ip", clientIP),
zap.Int("remaining_attempts", remainingAttempts-1))
// CWE-778: Log security event for failed login
redactor := logger.NewSensitiveFieldRedactor()
h.securityEventLogger.LogFailedLogin(r.Context(), redactor.HashForLogging(dto.Email), clientIP, remainingAttempts-1)
// Include remaining attempts in error message to help legitimate users
errorMsg := "Invalid email or password."
if remainingAttempts <= 3 {
errorMsg = fmt.Sprintf("Invalid email or password. %d attempts remaining before account lockout.", remainingAttempts-1)
}
httperror.ProblemUnauthorized(w, errorMsg)
return
}
h.logger.Error("login failed", zap.Error(err))
httperror.ProblemInternalServerError(w, "Failed to process login. Please try again later.")
return
}
// CWE-307: Record successful login (resets failed attempt counters)
if err := h.loginRateLimiter.RecordSuccessfulLogin(r.Context(), dto.Email, clientIP); err != nil {
// Log error but don't fail the login
h.logger.Error("failed to reset login counters after successful login",
logger.EmailHash(dto.Email),
zap.String("ip", clientIP),
zap.Error(err))
}
// CWE-532: Log with safe identifiers only (no PII)
h.logger.Info("login successful",
zap.String("user_id", response.UserID),
zap.String("tenant_id", response.TenantID),
logger.EmailHash(response.UserEmail),
zap.String("ip", clientIP))
// CWE-778: Log security event for successful login
redactor := logger.NewSensitiveFieldRedactor()
h.securityEventLogger.LogSuccessfulLogin(r.Context(), redactor.HashForLogging(dto.Email), clientIP)
// Return response with pretty JSON
httpresponse.OK(w, response)
}

View file

@ -0,0 +1,68 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/gateway/me_handler.go
package gateway
import (
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
)
// MeHandler handles the /me endpoint for getting authenticated user profile
type MeHandler struct {
logger *zap.Logger
}
// ProvideMeHandler creates a new MeHandler
func ProvideMeHandler(logger *zap.Logger) *MeHandler {
return &MeHandler{
logger: logger,
}
}
// MeResponse represents the user profile response
type MeResponse struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
TenantID string `json:"tenant_id"`
}
// Handle handles the HTTP request for the /me endpoint
func (h *MeHandler) Handle(w http.ResponseWriter, r *http.Request) {
// Extract user info from context (set by JWT middleware)
userUUID, ok := r.Context().Value(constants.SessionUserUUID).(string)
if !ok || userUUID == "" {
h.logger.Error("user UUID not found in context")
httperror.ProblemUnauthorized(w, "Authentication required")
return
}
userEmail, _ := r.Context().Value(constants.SessionUserEmail).(string)
userName, _ := r.Context().Value(constants.SessionUserName).(string)
userRole, _ := r.Context().Value(constants.SessionUserRole).(string)
tenantUUID, _ := r.Context().Value(constants.SessionTenantID).(string)
// CWE-532: Use redacted email for logging
h.logger.Info("/me endpoint accessed",
zap.String("user_id", userUUID),
logger.EmailHash(userEmail),
logger.SafeEmail("email_redacted", userEmail))
// Create response
response := MeResponse{
UserID: userUUID,
Email: userEmail,
Name: userName,
Role: userRole,
TenantID: tenantUUID,
}
// Write response with pretty JSON
httpresponse.OK(w, response)
}

View file

@ -0,0 +1,80 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/gateway/refresh_handler.go
package gateway
import (
"net/http"
"go.uber.org/zap"
gatewaydto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/gateway"
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
)
// RefreshTokenHandler handles HTTP requests for token refresh
type RefreshTokenHandler struct {
refreshTokenService gatewaysvc.RefreshTokenService
logger *zap.Logger
}
// NewRefreshTokenHandler creates a new refresh token handler
func NewRefreshTokenHandler(
refreshTokenService gatewaysvc.RefreshTokenService,
logger *zap.Logger,
) *RefreshTokenHandler {
return &RefreshTokenHandler{
refreshTokenService: refreshTokenService,
logger: logger.Named("refresh-token-handler"),
}
}
// ProvideRefreshTokenHandler creates a new RefreshTokenHandler for dependency injection
func ProvideRefreshTokenHandler(
refreshTokenService gatewaysvc.RefreshTokenService,
logger *zap.Logger,
) *RefreshTokenHandler {
return NewRefreshTokenHandler(refreshTokenService, logger)
}
// Handle processes POST /api/v1/refresh requests
// CWE-613: Validates session still exists before issuing new tokens
func (h *RefreshTokenHandler) Handle(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling token refresh request")
// Parse and validate request
dto, err := gatewaydto.ParseRefreshTokenRequest(r)
if err != nil {
h.logger.Warn("invalid refresh token request", zap.Error(err))
httperror.ProblemBadRequest(w, err.Error())
return
}
// Execute token refresh
response, err := h.refreshTokenService.RefreshToken(r.Context(), &gatewaysvc.RefreshTokenInput{
RefreshToken: dto.RefreshToken,
})
if err != nil {
h.logger.Warn("token refresh failed", zap.Error(err))
// Return appropriate error based on error message
switch err.Error() {
case "invalid or expired refresh token":
httperror.ProblemUnauthorized(w, "Invalid or expired refresh token. Please log in again.")
case "session not found or expired":
httperror.ProblemUnauthorized(w, "Session has expired or been invalidated. Please log in again.")
default:
httperror.ProblemInternalServerError(w, "Failed to refresh token. Please try again later.")
}
return
}
// CWE-532: Log with safe identifiers only (no PII)
h.logger.Info("token refresh successful",
zap.String("user_id", response.UserID),
zap.String("tenant_id", response.TenantID),
zap.String("session_id", response.SessionID))
// Return response with pretty JSON
httpresponse.OK(w, response)
}

View file

@ -0,0 +1,185 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/gateway/register_handler.go
package gateway
import (
"encoding/json"
"net/http"
"strings"
"go.uber.org/zap"
gatewaydto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/gateway"
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/clientip"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/validation"
)
// RegisterHandler handles user registration HTTP requests
type RegisterHandler struct {
service gatewaysvc.RegisterService
ipExtractor *clientip.Extractor
logger *zap.Logger
}
// ProvideRegisterHandler creates a new RegisterHandler
func ProvideRegisterHandler(
service gatewaysvc.RegisterService,
ipExtractor *clientip.Extractor,
logger *zap.Logger,
) *RegisterHandler {
return &RegisterHandler{
service: service,
ipExtractor: ipExtractor,
logger: logger,
}
}
// Handle handles the HTTP request for user registration
func (h *RegisterHandler) Handle(w http.ResponseWriter, r *http.Request) {
// CWE-436: Validate Content-Type before parsing to prevent interpretation conflicts
if err := httpvalidation.RequireJSONContentType(r); err != nil {
h.logger.Warn("invalid content type",
zap.String("content_type", r.Header.Get("Content-Type")))
httperror.ProblemBadRequest(w, err.Error())
return
}
// Parse request body
var req gatewaydto.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Warn("invalid request body", zap.Error(err))
httperror.ProblemBadRequest(w, "Invalid request body format. Please check your JSON syntax.")
return
}
// CWE-20: Comprehensive input validation
if err := req.Validate(); err != nil {
h.logger.Warn("registration request validation failed", zap.Error(err))
// Check if it's a structured validation error (RFC 9457 format)
if validationErr, ok := err.(*gatewaydto.ValidationErrors); ok {
httperror.ValidationError(w, validationErr.Errors, "One or more validation errors occurred")
return
}
// Fallback for non-structured errors
httperror.ProblemBadRequest(w, err.Error())
return
}
// CWE-348: Extract IP address securely with X-Forwarded-For validation
// Only trusts X-Forwarded-For if request comes from configured trusted proxies
ipAddress := h.ipExtractor.Extract(r)
// Default timezone to UTC if not provided
timezone := req.Timezone
if timezone == "" {
timezone = "UTC"
h.logger.Debug("timezone not provided, defaulting to UTC")
}
// Generate tenant slug from tenant name
validator := validation.NewValidator()
tenantSlug := validator.GenerateSlug(req.TenantName)
h.logger.Debug("generated tenant slug from name",
zap.String("tenant_name", req.TenantName),
zap.String("tenant_slug", tenantSlug))
// Map DTO to service input
input := &gatewaysvc.RegisterInput{
Email: req.Email,
Password: req.Password,
FirstName: req.FirstName,
LastName: req.LastName,
TenantName: req.TenantName,
TenantSlug: tenantSlug,
Timezone: timezone,
// Consent fields
AgreeTermsOfService: req.AgreeTermsOfService,
AgreePromotions: req.AgreePromotions,
AgreeToTrackingAcrossThirdPartyAppsAndServices: req.AgreeToTrackingAcrossThirdPartyAppsAndServices,
// IP address for audit trail
CreatedFromIPAddress: ipAddress,
}
// Call service
output, err := h.service.Register(r.Context(), input)
if err != nil {
// CWE-532: Log with redacted sensitive information
h.logger.Error("failed to register user",
zap.Error(err),
logger.EmailHash(req.Email),
logger.SafeEmail("email_redacted", req.Email),
logger.TenantSlugHash(tenantSlug),
logger.SafeTenantSlug("tenant_slug_redacted", tenantSlug))
// Check for specific errors
errMsg := err.Error()
switch {
case errMsg == "user already exists":
// CWE-203: Return generic message to prevent user enumeration
httperror.ProblemConflict(w, "Registration failed. The provided information is already in use.")
case errMsg == "tenant already exists":
// CWE-203: Return generic message to prevent tenant slug enumeration
// Prevents attackers from discovering valid tenant slugs for reconnaissance
httperror.ProblemConflict(w, "Registration failed. The provided information is already in use.")
case errMsg == "must agree to terms of service":
httperror.ProblemBadRequest(w, "You must agree to the terms of service to create an account.")
case errMsg == "password must be at least 8 characters":
httperror.ProblemBadRequest(w, "Password must be at least 8 characters long.")
// CWE-521: Password breach checking
case strings.Contains(errMsg, "data breaches"):
httperror.ProblemBadRequest(w, "This password has been found in data breaches and cannot be used. Please choose a different password.")
// CWE-521: Granular password strength errors for better user experience
case errMsg == "password must contain at least one uppercase letter (A-Z)":
httperror.ProblemBadRequest(w, "Password must contain at least one uppercase letter (A-Z).")
case errMsg == "password must contain at least one lowercase letter (a-z)":
httperror.ProblemBadRequest(w, "Password must contain at least one lowercase letter (a-z).")
case errMsg == "password must contain at least one number (0-9)":
httperror.ProblemBadRequest(w, "Password must contain at least one number (0-9).")
case errMsg == "password must contain at least one special character (!@#$%^&*()_+-=[]{}; etc.)":
httperror.ProblemBadRequest(w, "Password must contain at least one special character (!@#$%^&*()_+-=[]{}; etc.).")
case errMsg == "password must contain uppercase, lowercase, number, and special character":
httperror.ProblemBadRequest(w, "Password must contain uppercase, lowercase, number, and special character.")
case errMsg == "invalid email format":
httperror.ProblemBadRequest(w, "Invalid email format. Please provide a valid email address.")
case errMsg == "tenant slug must contain only lowercase letters, numbers, and hyphens":
httperror.ProblemBadRequest(w, "Tenant name must contain only lowercase letters, numbers, and hyphens.")
default:
httperror.ProblemInternalServerError(w, "Failed to register user. Please try again later.")
}
return
}
// CWE-532: Log with safe identifiers (no PII)
h.logger.Info("user registered successfully",
zap.String("user_id", output.UserID),
zap.String("tenant_id", output.TenantID),
logger.EmailHash(output.UserEmail))
// Map to response DTO
response := gatewaydto.RegisterResponse{
UserID: output.UserID,
UserEmail: output.UserEmail,
UserName: output.UserName,
UserRole: output.UserRole,
TenantID: output.TenantID,
TenantName: output.TenantName,
TenantSlug: output.TenantSlug,
SessionID: output.SessionID,
AccessToken: output.AccessToken,
AccessExpiry: output.AccessExpiry,
RefreshToken: output.RefreshToken,
RefreshExpiry: output.RefreshExpiry,
CreatedAt: output.CreatedAt,
}
// Write response
httpresponse.Created(w, response)
}