* 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>
86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
const defaultCLIConfigPath = ".multica/config.json"
|
|
|
|
// CLIConfig holds persistent CLI settings.
|
|
type CLIConfig struct {
|
|
ServerURL string `json:"server_url,omitempty"`
|
|
WorkspaceID string `json:"workspace_id,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
// CLIConfigPath returns the default path for the CLI config file.
|
|
func CLIConfigPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve CLI config path: %w", err)
|
|
}
|
|
return filepath.Join(home, defaultCLIConfigPath), nil
|
|
}
|
|
|
|
// LoadCLIConfig reads the CLI config from disk.
|
|
func LoadCLIConfig() (CLIConfig, error) {
|
|
path, err := CLIConfigPath()
|
|
if err != nil {
|
|
return CLIConfig{}, err
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return CLIConfig{}, nil
|
|
}
|
|
return CLIConfig{}, fmt.Errorf("read CLI config: %w", err)
|
|
}
|
|
var cfg CLIConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return CLIConfig{}, fmt.Errorf("parse CLI config: %w", err)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// LoadWorkspaceIDFromDaemonConfig reads workspace_id from ~/.multica/daemon.json
|
|
// as a fallback when it's not set in the CLI config.
|
|
func LoadWorkspaceIDFromDaemonConfig() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(home, ".multica/daemon.json"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var cfg struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
}
|
|
if json.Unmarshal(data, &cfg) != nil {
|
|
return ""
|
|
}
|
|
return cfg.WorkspaceID
|
|
}
|
|
|
|
// SaveCLIConfig writes the CLI config to disk.
|
|
func SaveCLIConfig(cfg CLIConfig) error {
|
|
path, err := CLIConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return fmt.Errorf("create CLI config directory: %w", err)
|
|
}
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("encode CLI config: %w", err)
|
|
}
|
|
if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil {
|
|
return fmt.Errorf("write CLI config: %w", err)
|
|
}
|
|
return nil
|
|
}
|