multica/server/internal/service/email.go
LinYushen 5c9c2f69fd
feat(auth): email verification login and personal access tokens
* 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>
2026-03-26 14:32:30 +08:00

54 lines
1.2 KiB
Go

package service
import (
"fmt"
"os"
"github.com/resend/resend-go/v2"
)
type EmailService struct {
client *resend.Client
fromEmail string
}
func NewEmailService() *EmailService {
apiKey := os.Getenv("RESEND_API_KEY")
from := os.Getenv("RESEND_FROM_EMAIL")
if from == "" {
from = "noreply@multica.ai"
}
var client *resend.Client
if apiKey != "" {
client = resend.NewClient(apiKey)
}
return &EmailService{
client: client,
fromEmail: from,
}
}
func (s *EmailService) SendVerificationCode(to, code string) error {
if s.client == nil {
fmt.Printf("[DEV] Verification code for %s: %s\n", to, code)
return nil
}
params := &resend.SendEmailRequest{
From: s.fromEmail,
To: []string{to},
Subject: "Your Multica verification code",
Html: fmt.Sprintf(
`<div style="font-family: sans-serif; max-width: 400px; margin: 0 auto;">
<h2>Your verification code</h2>
<p style="font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 24px 0;">%s</p>
<p>This code expires in 10 minutes.</p>
<p style="color: #666; font-size: 14px;">If you didn't request this code, you can safely ignore this email.</p>
</div>`, code),
}
_, err := s.client.Emails.Send(params)
return err
}