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,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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue