98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
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) {
|
|
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
|
|
}
|