|
4 | 4 | "context" |
5 | 5 | "crypto/rand" |
6 | 6 | "crypto/sha256" |
| 7 | + "crypto/subtle" |
7 | 8 | "encoding/hex" |
8 | 9 | "encoding/json" |
9 | | - "fmt" |
10 | 10 | "log/slog" |
11 | 11 | "net/http" |
12 | 12 | "strconv" |
@@ -51,10 +51,32 @@ type SessionStore struct { |
51 | 51 |
|
52 | 52 | // NewSessionStore creates a new session store. |
53 | 53 | func NewSessionStore() *SessionStore { |
54 | | - return &SessionStore{ |
| 54 | + s := &SessionStore{ |
55 | 55 | sessions: make(map[string]*SessionData), |
56 | 56 | pendingTOTP: make(map[string]*PendingTOTP), |
57 | 57 | } |
| 58 | + |
| 59 | + // Periodically clean up expired sessions and pending TOTP tokens. |
| 60 | + go func() { |
| 61 | + ticker := time.NewTicker(10 * time.Minute) |
| 62 | + for range ticker.C { |
| 63 | + now := time.Now() |
| 64 | + s.mu.Lock() |
| 65 | + for token, data := range s.sessions { |
| 66 | + if now.After(data.Expiry) { |
| 67 | + delete(s.sessions, token) |
| 68 | + } |
| 69 | + } |
| 70 | + for token, pending := range s.pendingTOTP { |
| 71 | + if now.After(pending.Expiry) { |
| 72 | + delete(s.pendingTOTP, token) |
| 73 | + } |
| 74 | + } |
| 75 | + s.mu.Unlock() |
| 76 | + } |
| 77 | + }() |
| 78 | + |
| 79 | + return s |
58 | 80 | } |
59 | 81 |
|
60 | 82 | // Create generates a new session token for a user, valid for 24 hours. |
@@ -204,7 +226,7 @@ func authMiddleware(cfg *config.Config, database *db.DB, sessions *SessionStore, |
204 | 226 | if apiKey == "" { |
205 | 227 | apiKey = r.URL.Query().Get("apikey") |
206 | 228 | } |
207 | | - if apiKey == cfg.APIKey { |
| 229 | + if subtle.ConstantTimeCompare([]byte(apiKey), []byte(cfg.APIKey)) == 1 { |
208 | 230 | // API key users get admin-level access. |
209 | 231 | ctx := context.WithValue(r.Context(), ctxUserRole, "admin") |
210 | 232 | ctx = context.WithValue(ctx, ctxUsername, "api") |
@@ -490,10 +512,17 @@ func handleRegister(database *db.DB, sessions *SessionStore) http.HandlerFunc { |
490 | 512 | return |
491 | 513 | } |
492 | 514 |
|
493 | | - if len(req.Username) < 3 || len(req.Password) < 6 { |
| 515 | + if len(req.Username) < 3 || len(req.Username) > 64 { |
| 516 | + writeJSON(w, http.StatusBadRequest, map[string]interface{}{ |
| 517 | + "success": false, |
| 518 | + "error": "Username must be 3-64 characters", |
| 519 | + }) |
| 520 | + return |
| 521 | + } |
| 522 | + if len(req.Password) < 6 || len(req.Password) > 72 { |
494 | 523 | writeJSON(w, http.StatusBadRequest, map[string]interface{}{ |
495 | 524 | "success": false, |
496 | | - "error": "Username must be at least 3 characters, password at least 6", |
| 525 | + "error": "Password must be 6-72 characters", |
497 | 526 | }) |
498 | 527 | return |
499 | 528 | } |
@@ -737,9 +766,10 @@ func handleDeleteUser(database *db.DB) http.HandlerFunc { |
737 | 766 | } |
738 | 767 |
|
739 | 768 | if err := database.DeleteUser(id); err != nil { |
| 769 | + slog.Error("failed to delete user", "id", id, "error", err) |
740 | 770 | writeJSON(w, http.StatusNotFound, map[string]interface{}{ |
741 | 771 | "success": false, |
742 | | - "error": fmt.Sprintf("Failed to delete user: %s", err.Error()), |
| 772 | + "error": "Failed to delete user", |
743 | 773 | }) |
744 | 774 | return |
745 | 775 | } |
|
0 commit comments