refactor(backend): migrate db.DB to db.Conn PoolProxy across all handlers
Replace direct *pgxpool.Pool usage with PoolProxy wrapper across the entire backend: - db.DB renamed to db.Conn (*pgxpool.Pool -> *PoolProxy) - JWT functions now accept context.Context instead of using context.Background() - Handler DB calls route through PoolProxy for per-test transaction support - Fixture/helper/testdb functions accept Querier interface for decoupling - Query ordering fixed in bookings handlers: COUNT after data query to avoid pgx conn busy - Time truncation fixed: time.Date instead of Truncate(24*time.Hour) for week start calc - testmain_test.go files updated with SeedBaseline and NewPoolProxy Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crussell/auth"
|
||||
"crussell/db"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -29,9 +28,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var (
|
||||
titleCaser = cases.Title(language.English)
|
||||
)
|
||||
|
||||
|
||||
const maxLoginInProgress = 20
|
||||
|
||||
@@ -173,8 +170,10 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
req.Phone = strings.TrimSpace(phone)
|
||||
|
||||
// Convert names to title case
|
||||
req.FirstName = titleCaser.String(strings.ToLower(req.FirstName))
|
||||
req.LastName = titleCaser.String(strings.ToLower(req.LastName))
|
||||
// Create per-call caser (cases.Caser is not goroutine-safe)
|
||||
tc := cases.Title(language.English)
|
||||
req.FirstName = tc.String(strings.ToLower(req.FirstName))
|
||||
req.LastName = tc.String(strings.ToLower(req.LastName))
|
||||
|
||||
// Parse date of birth
|
||||
dob, err := time.Parse("2006-01-02", req.DateOfBirth)
|
||||
@@ -203,7 +202,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// Look up referrer by referral code
|
||||
err := db.DB.QueryRow(r.Context(),
|
||||
err := db.Conn.QueryRow(r.Context(),
|
||||
"SELECT id FROM users WHERE referral_code = $1", req.ReferralCode).Scan(&referrerID)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid referral code", http.StatusBadRequest)
|
||||
@@ -218,7 +217,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "server error", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -319,8 +318,8 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
|
||||
var userID, passwordHash, role string
|
||||
ctx := context.Background()
|
||||
err := db.DB.QueryRow(ctx, `
|
||||
ctx := r.Context()
|
||||
err := db.Conn.QueryRow(ctx, `
|
||||
SELECT id, password_hash, account_role
|
||||
FROM users
|
||||
WHERE email = $1 AND account_type = 'email'
|
||||
@@ -334,7 +333,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if account is locked
|
||||
var failedAttempts int
|
||||
var lockedUntil *time.Time
|
||||
err = db.DB.QueryRow(r.Context(), `SELECT failed_attempts, locked_until FROM users WHERE id = $1`, userID).Scan(&failedAttempts, &lockedUntil)
|
||||
err = db.Conn.QueryRow(r.Context(), `SELECT failed_attempts, locked_until FROM users WHERE id = $1`, userID).Scan(&failedAttempts, &lockedUntil)
|
||||
if err == nil && lockedUntil != nil && time.Now().Before(*lockedUntil) {
|
||||
http.Error(w, "account is temporarily locked. try again later.", http.StatusTooManyRequests)
|
||||
log.Printf("LOGIN_AUDIT: locked account attempt - user=%s ip=%s", userID, middleware.GetClientIP(r.Context()))
|
||||
@@ -369,7 +368,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Increment failed attempts in DB with progressive lockout
|
||||
var newFailed int
|
||||
var newLockedUntil *time.Time
|
||||
db.DB.QueryRow(r.Context(), `
|
||||
db.Conn.QueryRow(r.Context(), `
|
||||
UPDATE users
|
||||
SET failed_attempts = failed_attempts + 1,
|
||||
locked_until = CASE
|
||||
@@ -395,7 +394,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// On success, clear lockout and update last_login
|
||||
// TODO: Password reset flow (MVP #4 in Future Work doc) must also clear
|
||||
// failed_attempts and locked_until — a locked-out user can't call this handler.
|
||||
db.DB.Exec(r.Context(), `UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, userID)
|
||||
db.Conn.Exec(r.Context(), `UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, userID)
|
||||
|
||||
// Generate JWT
|
||||
tokenString, jti, err := auth.GenerateToken(userID, role)
|
||||
@@ -405,7 +404,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Generate refresh token for token rotation
|
||||
refreshToken, err := auth.GenerateRefreshToken(userID, role)
|
||||
refreshToken, err := auth.GenerateRefreshToken(r.Context(), userID, role)
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate refresh token: %v", err)
|
||||
http.Error(w, "could not generate refresh token", http.StatusInternalServerError)
|
||||
@@ -428,7 +427,7 @@ func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Verify user still exists and role hasn't changed
|
||||
var currentRole string
|
||||
err := db.DB.QueryRow(r.Context(), `
|
||||
err := db.Conn.QueryRow(r.Context(), `
|
||||
SELECT account_role FROM users WHERE id = $1
|
||||
`, userID).Scan(¤tRole)
|
||||
|
||||
@@ -444,7 +443,7 @@ func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Revoke the old token's JTI before issuing a new one (rotation)
|
||||
if oldJTI != "" {
|
||||
auth.RevokeJTI(oldJTI, time.Now().Add(90*24*time.Hour)) // match refresh token lifetime
|
||||
auth.RevokeJTI(r.Context(), oldJTI, time.Now().Add(90*24*time.Hour)) // match refresh token lifetime
|
||||
}
|
||||
|
||||
// Generate new token
|
||||
@@ -455,7 +454,7 @@ func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Also issue a refresh token (opaque, stored in DB)
|
||||
refreshToken, err := auth.GenerateRefreshToken(userID, currentRole)
|
||||
refreshToken, err := auth.GenerateRefreshToken(r.Context(), userID, currentRole)
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate refresh token: %v", err)
|
||||
http.Error(w, "could not generate refresh token", http.StatusInternalServerError)
|
||||
@@ -479,7 +478,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Revoke the JTI — match the access token lifetime (1 hour)
|
||||
auth.RevokeJTI(jti, time.Now().Add(1*time.Hour))
|
||||
auth.RevokeJTI(r.Context(), jti, time.Now().Add(1*time.Hour))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
@@ -516,7 +515,7 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var userID string
|
||||
err := db.DB.QueryRow(r.Context(),
|
||||
err := db.Conn.QueryRow(r.Context(),
|
||||
"SELECT id FROM users WHERE LOWER(email) = $1", email,
|
||||
).Scan(&userID)
|
||||
if err != nil {
|
||||
@@ -533,7 +532,7 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
expiresAt := time.Now().Add(24 * time.Hour)
|
||||
|
||||
var code string
|
||||
err = db.DB.QueryRow(r.Context(),
|
||||
err = db.Conn.QueryRow(r.Context(),
|
||||
`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
|
||||
userID, expiresAt,
|
||||
).Scan(&code)
|
||||
@@ -543,8 +542,6 @@ func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: Verification code for %s: %s (expires at %s)", email, code, expiresAt.Format(time.RFC3339))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"})
|
||||
}
|
||||
@@ -570,7 +567,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var purpose string
|
||||
var expiresAt time.Time
|
||||
|
||||
err := db.DB.QueryRow(r.Context(),
|
||||
err := db.Conn.QueryRow(r.Context(),
|
||||
`SELECT user_id, purpose, expires_at FROM verification_codes
|
||||
WHERE code = $1 AND used_at IS NULL AND expires_at > NOW()`,
|
||||
code,
|
||||
@@ -579,7 +576,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
// Check if code exists but was already used or expired
|
||||
var checkUsedAt *time.Time
|
||||
checkErr := db.DB.QueryRow(r.Context(),
|
||||
checkErr := db.Conn.QueryRow(r.Context(),
|
||||
`SELECT used_at FROM verification_codes WHERE code = $1`, code,
|
||||
).Scan(&checkUsedAt)
|
||||
if checkErr != nil {
|
||||
@@ -601,7 +598,7 @@ func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := db.DB.Begin(r.Context())
|
||||
tx, err := db.Conn.Begin(r.Context())
|
||||
if err != nil {
|
||||
log.Printf("Failed to start transaction: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
|
||||
Reference in New Issue
Block a user