mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-27 18:14:12 +00:00
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type LoginRequest struct {
|
|
Wallet string `json:"wallet"`
|
|
Chain string `json:"chain"`
|
|
Message string `json:"message"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
Wallet string `json:"wallet"`
|
|
Chain string `json:"chain"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
|
|
func LoginHandler(database *sql.DB, jwtSecret string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req LoginRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
req.Chain = strings.ToLower(req.Chain)
|
|
if req.Wallet == "" || req.Message == "" || req.Signature == "" {
|
|
jsonError(w, http.StatusBadRequest, "wallet, message, and signature are required")
|
|
return
|
|
}
|
|
if req.Chain != "sol" && req.Chain != "evm" {
|
|
jsonError(w, http.StatusBadRequest, "chain must be 'sol' or 'evm'")
|
|
return
|
|
}
|
|
|
|
// Validate message timestamp (anti-replay: within 5 minutes)
|
|
if err := validateMessageTimestamp(req.Message); err != nil {
|
|
jsonError(w, http.StatusBadRequest, fmt.Sprintf("message validation failed: %v", err))
|
|
return
|
|
}
|
|
|
|
// Verify signature
|
|
var verifyErr error
|
|
if req.Chain == "sol" {
|
|
verifyErr = VerifySolana(req.Wallet, req.Message, req.Signature)
|
|
} else {
|
|
verifyErr = VerifyEVM(req.Wallet, req.Message, req.Signature)
|
|
}
|
|
if verifyErr != nil {
|
|
jsonError(w, http.StatusUnauthorized, fmt.Sprintf("signature verification failed: %v", verifyErr))
|
|
return
|
|
}
|
|
|
|
// Upsert wallet
|
|
database.Exec(
|
|
`INSERT INTO wallets (wallet, chain) VALUES (?, ?)
|
|
ON CONFLICT(wallet) DO UPDATE SET last_seen = CURRENT_TIMESTAMP, chain = ?`,
|
|
req.Wallet, req.Chain, req.Chain,
|
|
)
|
|
|
|
// Generate JWT
|
|
token, expiresAt, err := GenerateToken(req.Wallet, req.Chain, jwtSecret)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "failed to generate token")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(LoginResponse{
|
|
Token: token,
|
|
Wallet: req.Wallet,
|
|
Chain: req.Chain,
|
|
ExpiresAt: expiresAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
}
|
|
|
|
func validateMessageTimestamp(message string) error {
|
|
// Extract timestamp from message format:
|
|
// "...Timestamp: 2026-03-21T12:00:00.000Z"
|
|
idx := strings.Index(message, "Timestamp: ")
|
|
if idx == -1 {
|
|
return fmt.Errorf("message does not contain a timestamp")
|
|
}
|
|
|
|
tsStr := strings.TrimSpace(message[idx+len("Timestamp: "):])
|
|
// Handle potential trailing content
|
|
if newline := strings.Index(tsStr, "\n"); newline != -1 {
|
|
tsStr = tsStr[:newline]
|
|
}
|
|
|
|
ts, err := time.Parse(time.RFC3339Nano, tsStr)
|
|
if err != nil {
|
|
ts, err = time.Parse("2006-01-02T15:04:05.000Z", tsStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid timestamp format: %s", tsStr)
|
|
}
|
|
}
|
|
|
|
if time.Since(ts) > 5*time.Minute {
|
|
return fmt.Errorf("message timestamp expired (older than 5 minutes)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func jsonError(w http.ResponseWriter, status int, msg string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
|
}
|