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,130 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/admin/account_status_handler.go
package admin
import (
"net/http"
"time"
"go.uber.org/zap"
"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/validation"
)
// AccountStatusHandler handles HTTP requests for checking account lock status
type AccountStatusHandler struct {
loginRateLimiter ratelimit.LoginRateLimiter
logger *zap.Logger
}
// NewAccountStatusHandler creates a new account status handler
func NewAccountStatusHandler(
loginRateLimiter ratelimit.LoginRateLimiter,
logger *zap.Logger,
) *AccountStatusHandler {
return &AccountStatusHandler{
loginRateLimiter: loginRateLimiter,
logger: logger.Named("account-status-handler"),
}
}
// ProvideAccountStatusHandler creates a new AccountStatusHandler for dependency injection
func ProvideAccountStatusHandler(
loginRateLimiter ratelimit.LoginRateLimiter,
logger *zap.Logger,
) *AccountStatusHandler {
return NewAccountStatusHandler(loginRateLimiter, logger)
}
// AccountStatusResponse represents the account status response
type AccountStatusResponse struct {
Email string `json:"email"`
IsLocked bool `json:"is_locked"`
FailedAttempts int `json:"failed_attempts"`
RemainingTime string `json:"remaining_time,omitempty"`
RemainingSeconds int `json:"remaining_seconds,omitempty"`
}
// Handle processes GET /api/v1/admin/account-status?email=user@example.com requests
// This endpoint allows administrators to check if an account is locked and get details
func (h *AccountStatusHandler) Handle(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling account status request")
// CWE-20: Validate email query parameter
email, err := validation.ValidateQueryEmail(r, "email")
if err != nil {
h.logger.Warn("invalid email query parameter", zap.Error(err))
httperror.ProblemBadRequest(w, err.Error())
return
}
// Check if account is locked
locked, remainingTime, err := h.loginRateLimiter.IsAccountLocked(r.Context(), email)
if err != nil {
h.logger.Error("failed to check account lock status",
logger.EmailHash(email),
zap.Error(err))
httperror.ProblemInternalServerError(w, "Failed to check account status")
return
}
// Get failed attempts count
failedAttempts, err := h.loginRateLimiter.GetFailedAttempts(r.Context(), email)
if err != nil {
h.logger.Error("failed to get failed attempts",
logger.EmailHash(email),
zap.Error(err))
// Continue with locked status even if we can't get attempt count
failedAttempts = 0
}
response := &AccountStatusResponse{
Email: email,
IsLocked: locked,
FailedAttempts: failedAttempts,
}
if locked {
response.RemainingTime = formatDuration(remainingTime)
response.RemainingSeconds = int(remainingTime.Seconds())
}
h.logger.Info("account status checked",
logger.EmailHash(email),
zap.Bool("is_locked", locked),
zap.Int("failed_attempts", failedAttempts))
httpresponse.OK(w, response)
}
// formatDuration formats a duration into a human-readable string
func formatDuration(d time.Duration) string {
if d < 0 {
return "0s"
}
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
if hours > 0 {
return formatWithUnit(hours, "hour") + " " + formatWithUnit(minutes, "minute")
}
if minutes > 0 {
return formatWithUnit(minutes, "minute") + " " + formatWithUnit(seconds, "second")
}
return formatWithUnit(seconds, "second")
}
func formatWithUnit(value int, unit string) string {
if value == 0 {
return ""
}
if value == 1 {
return "1 " + unit
}
return string(rune(value)) + " " + unit + "s"
}

View file

@ -0,0 +1,149 @@
// File Path: monorepo/cloud/maplepress-backend/internal/interface/http/handler/admin/unlock_account_handler.go
package admin
import (
"encoding/json"
"io"
"net/http"
"go.uber.org/zap"
securityeventservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/securityevent"
"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/validation"
)
// UnlockAccountHandler handles HTTP requests for unlocking locked accounts
type UnlockAccountHandler struct {
loginRateLimiter ratelimit.LoginRateLimiter
securityEventLogger securityeventservice.Logger
logger *zap.Logger
}
// NewUnlockAccountHandler creates a new unlock account handler
func NewUnlockAccountHandler(
loginRateLimiter ratelimit.LoginRateLimiter,
securityEventLogger securityeventservice.Logger,
logger *zap.Logger,
) *UnlockAccountHandler {
return &UnlockAccountHandler{
loginRateLimiter: loginRateLimiter,
securityEventLogger: securityEventLogger,
logger: logger.Named("unlock-account-handler"),
}
}
// ProvideUnlockAccountHandler creates a new UnlockAccountHandler for dependency injection
func ProvideUnlockAccountHandler(
loginRateLimiter ratelimit.LoginRateLimiter,
securityEventLogger securityeventservice.Logger,
logger *zap.Logger,
) *UnlockAccountHandler {
return NewUnlockAccountHandler(loginRateLimiter, securityEventLogger, logger)
}
// UnlockAccountRequest represents the unlock account request payload
type UnlockAccountRequest struct {
Email string `json:"email"`
}
// UnlockAccountResponse represents the unlock account response
type UnlockAccountResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Email string `json:"email"`
}
// Handle processes POST /api/v1/admin/unlock-account requests
// This endpoint allows administrators to manually unlock accounts that have been
// locked due to excessive failed login attempts
func (h *UnlockAccountHandler) Handle(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling unlock account request")
// Parse request body
body, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Warn("failed to read request body", zap.Error(err))
httperror.ProblemBadRequest(w, "Invalid request body")
return
}
defer r.Body.Close()
var req UnlockAccountRequest
if err := json.Unmarshal(body, &req); err != nil {
h.logger.Warn("failed to parse request body", zap.Error(err))
httperror.ProblemBadRequest(w, "Invalid JSON")
return
}
// CWE-20: Comprehensive email validation
emailValidator := validation.NewEmailValidator()
normalizedEmail, err := emailValidator.ValidateAndNormalize(req.Email, "email")
if err != nil {
h.logger.Warn("invalid email", zap.Error(err))
httperror.ProblemBadRequest(w, err.Error())
return
}
req.Email = normalizedEmail
// Check if account is currently locked
locked, remainingTime, err := h.loginRateLimiter.IsAccountLocked(r.Context(), req.Email)
if err != nil {
h.logger.Error("failed to check account lock status",
logger.EmailHash(req.Email),
zap.Error(err))
httperror.ProblemInternalServerError(w, "Failed to check account status")
return
}
if !locked {
h.logger.Info("account not locked - nothing to do",
logger.EmailHash(req.Email))
httpresponse.OK(w, &UnlockAccountResponse{
Success: true,
Message: "Account is not locked",
Email: req.Email,
})
return
}
// Unlock the account
if err := h.loginRateLimiter.UnlockAccount(r.Context(), req.Email); err != nil {
h.logger.Error("failed to unlock account",
logger.EmailHash(req.Email),
zap.Error(err))
httperror.ProblemInternalServerError(w, "Failed to unlock account")
return
}
// Get admin user ID from context (set by JWT middleware)
// TODO: Extract admin user ID from JWT claims when authentication is added
adminUserID := "admin" // Placeholder until JWT middleware is integrated
// Log security event
redactor := logger.NewSensitiveFieldRedactor()
if err := h.securityEventLogger.LogAccountUnlocked(
r.Context(),
redactor.HashForLogging(req.Email),
adminUserID,
); err != nil {
h.logger.Error("failed to log security event",
logger.EmailHash(req.Email),
zap.Error(err))
// Don't fail the request if logging fails
}
h.logger.Info("account unlocked successfully",
logger.EmailHash(req.Email),
logger.SafeEmail("email_redacted", req.Email),
zap.Duration("was_locked_for", remainingTime))
httpresponse.OK(w, &UnlockAccountResponse{
Success: true,
Message: "Account unlocked successfully",
Email: req.Email,
})
}