package main import ( "context" "net/http" "strings" "time" ) const ( sessionHeaderToken = "X-Edut-Session" sessionHeaderExpiresAt = "X-Edut-Session-Expires-At" ) func (a *app) issueWalletSession(ctx context.Context, wallet, designationCode string) (walletSessionRecord, error) { _, _ = a.store.deleteExpiredWalletSessions(ctx, time.Now().UTC()) token, err := randomHex(24) if err != nil { return walletSessionRecord{}, err } now := time.Now().UTC() rec := walletSessionRecord{ SessionToken: token, Wallet: strings.ToLower(strings.TrimSpace(wallet)), DesignationCode: strings.TrimSpace(designationCode), ChainID: a.cfg.ChainID, IssuedAt: now, ExpiresAt: now.Add(a.cfg.WalletSessionTTL), LastSeenAt: &now, } if err := a.store.putWalletSession(ctx, rec); err != nil { return walletSessionRecord{}, err } return rec, nil } func sessionTokenFromRequest(r *http.Request) string { auth := strings.TrimSpace(r.Header.Get("Authorization")) if auth != "" { lower := strings.ToLower(auth) if strings.HasPrefix(lower, "bearer ") { token := strings.TrimSpace(auth[len("Bearer "):]) if token != "" { return token } } } if token := strings.TrimSpace(r.Header.Get(sessionHeaderToken)); token != "" { return token } return strings.TrimSpace(r.URL.Query().Get("session_token")) } func (a *app) enforceWalletSession(w http.ResponseWriter, r *http.Request, wallet string) bool { sessionToken := sessionTokenFromRequest(r) if sessionToken == "" { if a.cfg.RequireWalletSession { writeErrorCode(w, http.StatusUnauthorized, "wallet_session_required", "wallet session required") return false } return true } rec, err := a.store.getWalletSession(r.Context(), sessionToken) if err != nil { if err == errNotFound { writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session not found") return false } writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve wallet session") return false } if rec.RevokedAt != nil { writeErrorCode(w, http.StatusUnauthorized, "wallet_session_revoked", "wallet session revoked") return false } if rec.ChainID != a.cfg.ChainID { writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session chain mismatch") return false } now := time.Now().UTC() if !now.Before(rec.ExpiresAt.UTC()) { writeErrorCode(w, http.StatusUnauthorized, "wallet_session_expired", "wallet session expired") return false } if !strings.EqualFold(strings.TrimSpace(rec.Wallet), strings.TrimSpace(wallet)) { writeErrorCode(w, http.StatusForbidden, "wallet_session_mismatch", "wallet session does not match requested wallet") return false } if err := a.store.touchWalletSession(r.Context(), rec.SessionToken, now); err != nil && err != errNotFound { writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to update wallet session") return false } w.Header().Set(sessionHeaderToken, rec.SessionToken) w.Header().Set(sessionHeaderExpiresAt, rec.ExpiresAt.UTC().Format(time.RFC3339Nano)) return true }