* feat(auth): add email verification login flow with 401 auto-redirect Replace the old OAuth-based login with email verification codes: - Backend: send-code / verify-code endpoints, verification_codes table (migration 009), rate limiting, Resend email service - Frontend: two-step login UI (email → 6-digit OTP), auth store with sendCode/verifyCode - SDK: ApiClient gains onUnauthorized callback; 401 responses auto-clear token and redirect to /login - Fix login button staying disabled due to global isLoading state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): add brute-force protection, redirect loop guard, and expired code cleanup - VerifyCode: increment attempts on wrong code, reject after 5 failed tries (migration 010) - onUnauthorized: skip redirect if already on /login to prevent infinite loops - SendCode: best-effort cleanup of expired verification codes older than 1 hour Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): add master verification code for non-production environments Allow code "888888" to bypass email verification in non-production environments to simplify development and testing workflows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(auth): add personal access tokens for CLI and API authentication Add full-stack PAT support: users create tokens in Settings, CLI authenticates via `multica auth login`. Server stores SHA-256 hashes only. Auth middleware extended to accept both JWTs and PATs (distinguished by `mul_` prefix). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
44 lines
918 B
Go
44 lines
918 B
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
const defaultJWTSecret = "multica-dev-secret-change-in-production"
|
|
|
|
var (
|
|
jwtSecret []byte
|
|
jwtSecretOnce sync.Once
|
|
)
|
|
|
|
func JWTSecret() []byte {
|
|
jwtSecretOnce.Do(func() {
|
|
secret := os.Getenv("JWT_SECRET")
|
|
if secret == "" {
|
|
secret = defaultJWTSecret
|
|
}
|
|
jwtSecret = []byte(secret)
|
|
})
|
|
|
|
return jwtSecret
|
|
}
|
|
|
|
// GeneratePATToken creates a new personal access token: "mul_" + 40 random hex chars.
|
|
func GeneratePATToken() (string, error) {
|
|
b := make([]byte, 20) // 20 bytes = 40 hex chars
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("generate PAT token: %w", err)
|
|
}
|
|
return "mul_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// HashToken returns the hex-encoded SHA-256 hash of a token string.
|
|
func HashToken(token string) string {
|
|
h := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(h[:])
|
|
}
|