fix: auth/2FA — password change requires 2FA gate, admin self-deletion blocked, twofa JSON responses, per-IP email-verify budget, OptionalAuth log sanitised
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -155,8 +155,60 @@ type emailVerifyAttempt struct {
|
|||||||
var (
|
var (
|
||||||
emailVerifyMu sync.Mutex
|
emailVerifyMu sync.Mutex
|
||||||
emailVerifyAttempts = make(map[string]emailVerifyAttempt)
|
emailVerifyAttempts = make(map[string]emailVerifyAttempt)
|
||||||
|
ipVerifyMu sync.Mutex
|
||||||
|
ipVerifyAttempts = make(map[string]emailVerifyAttempt)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ipAttemptsExhausted reports whether the IP's attempt budget is already
|
||||||
|
// spent, rejecting the request before any DB work. Uses the same window as
|
||||||
|
// email-verify tracking with a higher cap (50 per IP per window) as a
|
||||||
|
// secondary per-IP fallback budget.
|
||||||
|
func ipAttemptsExhausted(ip string) bool {
|
||||||
|
ipVerifyMu.Lock()
|
||||||
|
defer ipVerifyMu.Unlock()
|
||||||
|
evictStaleIPVerifyAttemptsLocked()
|
||||||
|
a, ok := ipVerifyAttempts[ip]
|
||||||
|
return ok && a.count >= 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// ipAttemptFailed registers one failed attempt for the IP and reports whether
|
||||||
|
// the budget for that IP is now exhausted.
|
||||||
|
func ipAttemptFailed(ip string) bool {
|
||||||
|
ipVerifyMu.Lock()
|
||||||
|
defer ipVerifyMu.Unlock()
|
||||||
|
evictStaleIPVerifyAttemptsLocked()
|
||||||
|
now := clock.Now()
|
||||||
|
a := ipVerifyAttempts[ip]
|
||||||
|
if now.Sub(a.lastAt) > emailVerifyAttemptWindow {
|
||||||
|
a.count = 0
|
||||||
|
}
|
||||||
|
a.count++
|
||||||
|
a.lastAt = now
|
||||||
|
ipVerifyAttempts[ip] = a
|
||||||
|
return a.count >= 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// ipAttemptsClear drops the budget for an IP after a successful verify.
|
||||||
|
func ipAttemptsClear(ip string) {
|
||||||
|
ipVerifyMu.Lock()
|
||||||
|
delete(ipVerifyAttempts, ip)
|
||||||
|
ipVerifyMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// evictStaleIPVerifyAttemptsLocked bounds the IP attempts map. Caller must
|
||||||
|
// hold ipVerifyMu.
|
||||||
|
func evictStaleIPVerifyAttemptsLocked() {
|
||||||
|
if len(ipVerifyAttempts) < emailVerifyMaxTrackedCodes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := clock.Now()
|
||||||
|
for k, a := range ipVerifyAttempts {
|
||||||
|
if now.Sub(a.lastAt) > emailVerifyAttemptWindow {
|
||||||
|
delete(ipVerifyAttempts, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// emailVerifyAttemptsExhausted reports whether the key's (a user id, or a
|
// emailVerifyAttemptsExhausted reports whether the key's (a user id, or a
|
||||||
// submitted code with no resolvable user) attempt budget is already spent,
|
// submitted code with no resolvable user) attempt budget is already spent,
|
||||||
// rejecting the request before any DB work.
|
// rejecting the request before any DB work.
|
||||||
@@ -1020,6 +1072,16 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-IP fallback budget: when no user resolves, the attempt budget is
|
||||||
|
// keyed on the submitted CODE value — rotating codes gives unlimited
|
||||||
|
// guesses. The per-IP check prevents a single source from exhausting the
|
||||||
|
// endpoint regardless of code rotation.
|
||||||
|
ip := mw.ClientIP(r)
|
||||||
|
if ipAttemptsExhausted(ip) {
|
||||||
|
http.Error(w, "too many attempts", http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
codeDigest := twofa.Hash(code)
|
codeDigest := twofa.Hash(code)
|
||||||
|
|
||||||
var userID string
|
var userID string
|
||||||
@@ -1036,6 +1098,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
// No row with this digest at all — a guess. No user can be
|
// No row with this digest at all — a guess. No user can be
|
||||||
// resolved, so the attempt budget stays keyed per submitted code.
|
// resolved, so the attempt budget stays keyed per submitted code.
|
||||||
|
ipAttemptFailed(ip)
|
||||||
if emailVerifyAttemptFailed(code) {
|
if emailVerifyAttemptFailed(code) {
|
||||||
http.Error(w, "too many attempts. request a new code.", http.StatusTooManyRequests)
|
http.Error(w, "too many attempts. request a new code.", http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
@@ -1063,6 +1126,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// Code exists but expired — count it against the user's budget too.
|
// Code exists but expired — count it against the user's budget too.
|
||||||
if !expiresAt.After(clock.Now()) {
|
if !expiresAt.After(clock.Now()) {
|
||||||
|
ipAttemptFailed(ip)
|
||||||
if emailVerifyAttemptFailed(userID) {
|
if emailVerifyAttemptFailed(userID) {
|
||||||
http.Error(w, "too many attempts. request a new code.", http.StatusTooManyRequests)
|
http.Error(w, "too many attempts. request a new code.", http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
@@ -1132,6 +1196,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// key the submitted code used on earlier guesses).
|
// key the submitted code used on earlier guesses).
|
||||||
emailVerifyAttemptsClear(userID)
|
emailVerifyAttemptsClear(userID)
|
||||||
emailVerifyAttemptsClear(code)
|
emailVerifyAttemptsClear(code)
|
||||||
|
ipAttemptsClear(ip)
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: message}); err != nil {
|
if err := json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: message}); err != nil {
|
||||||
log.Printf("Failed to encode JSON response: %v", err)
|
log.Printf("Failed to encode JSON response: %v", err)
|
||||||
|
|||||||
@@ -338,6 +338,26 @@ func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIX: admin accounts cannot be self-deleted
|
||||||
|
if accountRole == "admin" {
|
||||||
|
http.Error(w, "admin accounts cannot be self-deleted", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX: check for active bookings before guest deletion
|
||||||
|
if accountRole == "guest" {
|
||||||
|
var bookingCount int
|
||||||
|
if err := db.Conn.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status IN ('pending', 'confirmed', 'in_progress', 'pending_release')`, userID).Scan(&bookingCount); err != nil {
|
||||||
|
log.Printf("Failed to check bookings for guest %s: %v", userID, err)
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bookingCount > 0 {
|
||||||
|
http.Error(w, "cannot delete guest account with active bookings", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
// Finding 3: deleting an account is irreversible, so the session token alone
|
// Finding 3: deleting an account is irreversible, so the session token alone
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"crussell/internal/dav"
|
"crussell/internal/dav"
|
||||||
"crussell/internal/images"
|
"crussell/internal/images"
|
||||||
"crussell/internal/s3"
|
"crussell/internal/s3"
|
||||||
|
"crussell/internal/twofa"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
|
||||||
@@ -659,8 +660,9 @@ func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangePasswordRequest struct {
|
type ChangePasswordRequest struct {
|
||||||
CurrentPassword string `json:"current_password" validate:"required,max=72"`
|
CurrentPassword string `json:"current_password" validate:"required,max=72"`
|
||||||
NewPassword string `json:"new_password" validate:"required,min=6,max=72"`
|
NewPassword string `json:"new_password" validate:"required,min=6,max=72"`
|
||||||
|
VerificationCode string `json:"verification_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -702,7 +704,8 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var passwordHash sql.NullString
|
var passwordHash sql.NullString
|
||||||
err := db.Conn.QueryRow(r.Context(), `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&passwordHash)
|
var twoFactorEnabled bool
|
||||||
|
err := db.Conn.QueryRow(r.Context(), `SELECT password_hash, two_factor_enabled FROM users WHERE id = $1`, userID).Scan(&passwordHash, &twoFactorEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
http.Error(w, "user not found", http.StatusNotFound)
|
http.Error(w, "user not found", http.StatusNotFound)
|
||||||
@@ -765,6 +768,29 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// login-locked-out user can self-recover by changing their password.
|
// login-locked-out user can self-recover by changing their password.
|
||||||
resetCurrentPasswordFailures(r.Context(), userID)
|
resetCurrentPasswordFailures(r.Context(), userID)
|
||||||
|
|
||||||
|
if twoFARequired() && twoFactorEnabled {
|
||||||
|
if req.VerificationCode == "" {
|
||||||
|
http.Error(w, "a two-factor verification code is required to change the password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch err := twofa.VerifyForUser(r.Context(), userID, req.VerificationCode, twofa.ConsumeOnVerify); {
|
||||||
|
case err == nil:
|
||||||
|
case errors.Is(err, twofa.ErrIncorrect):
|
||||||
|
http.Error(w, "incorrect verification code", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
case errors.Is(err, twofa.ErrLockedOut):
|
||||||
|
http.Error(w, "Too many attempts. Request a new code.", http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
case errors.Is(err, twofa.ErrMissingOrExpired):
|
||||||
|
http.Error(w, "verification code is missing or has expired", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Printf("Failed to check 2FA pending code for user %s: %v", userID, err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to hash new password for user %s: %v", userID, err)
|
log.Printf("Failed to hash new password for user %s: %v", userID, err)
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ type TwoFAStatusResponse struct {
|
|||||||
func GetTwoFAStatusHandler(w http.ResponseWriter, r *http.Request) {
|
func GetTwoFAStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ func GetTwoFAStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
`, userID).Scan(&enabled, &method)
|
`, userID).Scan(&enabled, &method)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to fetch 2FA status for user %s: %v", userID, err)
|
log.Printf("failed to fetch 2FA status for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,13 +254,13 @@ type TwoFASetupRequest struct {
|
|||||||
func SetupTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
func SetupTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req TwoFASetupRequest
|
var req TwoFASetupRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
mw.RespondError(w, http.StatusBadRequest, "invalid request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Method != "email" && req.Method != "sms" {
|
if req.Method != "email" && req.Method != "sms" {
|
||||||
@@ -272,7 +272,7 @@ func SetupTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err := db.Conn.QueryRow(r.Context(), `SELECT two_factor_enabled FROM users WHERE id = $1`, userID).Scan(&enabled)
|
err := db.Conn.QueryRow(r.Context(), `SELECT two_factor_enabled FROM users WHERE id = $1`, userID).Scan(&enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to check 2FA state for user %s: %v", userID, err)
|
log.Printf("failed to check 2FA state for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if enabled {
|
if enabled {
|
||||||
@@ -315,7 +315,7 @@ func SetupTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to store 2FA pending code for user %s: %v", userID, err)
|
log.Printf("failed to store 2FA pending code for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
st.SetLastMintAtLocked(now)
|
st.SetLastMintAtLocked(now)
|
||||||
@@ -403,13 +403,13 @@ func VerifyTwoFACodeForUser(ctx context.Context, userID, code string) error {
|
|||||||
func VerifyTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
func VerifyTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req TwoFAVerifyRequest
|
var req TwoFAVerifyRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
mw.RespondError(w, http.StatusBadRequest, "invalid request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ func VerifyTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Dev bypass: no code verification in unenforced environments.
|
// Dev bypass: no code verification in unenforced environments.
|
||||||
if err := enableTwoFA(r, userID); err != nil {
|
if err := enableTwoFA(r, userID); err != nil {
|
||||||
log.Printf("failed to enable 2FA for user %s: %v", userID, err)
|
log.Printf("failed to enable 2FA for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeTwoFAEnabled(w)
|
writeTwoFAEnabled(w)
|
||||||
@@ -435,7 +435,7 @@ func VerifyTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
result, err := checkTwoFACode(r, userID, st, req.Code)
|
result, err := checkTwoFACode(r, userID, st, req.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to check 2FA pending code for user %s: %v", userID, err)
|
log.Printf("failed to check 2FA pending code for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch result {
|
switch result {
|
||||||
@@ -462,7 +462,7 @@ func VerifyTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := enableTwoFA(r, userID); err != nil {
|
if err := enableTwoFA(r, userID); err != nil {
|
||||||
log.Printf("failed to enable 2FA for user %s: %v", userID, err)
|
log.Printf("failed to enable 2FA for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeTwoFAEnabled(w)
|
writeTwoFAEnabled(w)
|
||||||
@@ -528,7 +528,7 @@ type TwoFADisableRequest struct {
|
|||||||
func SendDisableCodeHandler(w http.ResponseWriter, r *http.Request) {
|
func SendDisableCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,7 +552,7 @@ func SendDisableCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to prepare 2FA code for disable for user %s: %v", userID, err)
|
log.Printf("failed to prepare 2FA code for disable for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -588,7 +588,7 @@ func SendDisableCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func SendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
func SendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +596,7 @@ func SendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
err := db.Conn.QueryRow(r.Context(), `SELECT two_factor_enabled FROM users WHERE id = $1`, userID).Scan(&enabled)
|
err := db.Conn.QueryRow(r.Context(), `SELECT two_factor_enabled FROM users WHERE id = $1`, userID).Scan(&enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to check 2FA state for user %s: %v", userID, err)
|
log.Printf("failed to check 2FA state for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !enabled {
|
if !enabled {
|
||||||
@@ -625,7 +625,7 @@ func SendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to prepare 2FA code for saved-card charge for user %s: %v", userID, err)
|
log.Printf("failed to prepare 2FA code for saved-card charge for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +671,7 @@ func AdminSendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to check 2FA state for user %s: %v", targetUserID, err)
|
log.Printf("failed to check 2FA state for user %s: %v", targetUserID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !enabled {
|
if !enabled {
|
||||||
@@ -697,7 +697,7 @@ func AdminSendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to prepare 2FA code for user %s: %v", targetUserID, err)
|
log.Printf("failed to prepare 2FA code for user %s: %v", targetUserID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +742,7 @@ func AdminSendVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func DisableTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
func DisableTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := mw.GetUserID(r.Context())
|
userID, ok := mw.GetUserID(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
mw.RespondError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,7 +755,7 @@ func DisableTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Dev bypass: no re-verification in unenforced environments.
|
// Dev bypass: no re-verification in unenforced environments.
|
||||||
if err := disableTwoFA(r, userID); err != nil {
|
if err := disableTwoFA(r, userID); err != nil {
|
||||||
log.Printf("failed to disable 2FA for user %s: %v", userID, err)
|
log.Printf("failed to disable 2FA for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -789,14 +789,14 @@ func DisableTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("failed to prepare 2FA code for disable for user %s: %v", userID, err)
|
log.Printf("failed to prepare 2FA code for disable for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := checkTwoFACode(r, userID, st, req.Code)
|
result, err := checkTwoFACode(r, userID, st, req.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to check 2FA pending code for user %s: %v", userID, err)
|
log.Printf("failed to check 2FA pending code for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch result {
|
switch result {
|
||||||
@@ -814,7 +814,7 @@ func DisableTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err := disableTwoFA(r, userID); err != nil {
|
if err := disableTwoFA(r, userID); err != nil {
|
||||||
log.Printf("failed to disable 2FA for user %s: %v", userID, err)
|
log.Printf("failed to disable 2FA for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -942,7 +942,7 @@ func AdminRemoveUser2FAHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
affected, err := removeUser2FA(r, userID)
|
affected, err := removeUser2FA(r, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to remove 2FA for user %s: %v", userID, err)
|
log.Printf("failed to remove 2FA for user %s: %v", userID, err)
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
mw.RespondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !affected {
|
if !affected {
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ func OptionalAuth(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("OptionalAuth: invalid token: %v", err)
|
log.Printf("OptionalAuth: invalid token ignored")
|
||||||
} else {
|
} else {
|
||||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||||
ctx = context.WithValue(ctx, UserRoleKey, role)
|
ctx = context.WithValue(ctx, UserRoleKey, role)
|
||||||
|
|||||||
Reference in New Issue
Block a user