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