inboxer/src/internal/auth/auth.go
cclohmar 766e3e3de6 Phase 4: Polish, settings fix, 7-way AI classification & empty-inbox guarantee (Version: 2026-04.5)
- Fixed settings form silently dropping fields (multipart/form-data parse)
- Fixed IMAP test connection (multipart parse + raw field logging)
- Added IMAPUsername field throughout (model, settings, handlers, worker)
- Replaced smtp.SendMail with custom sendMail (explicit HELO + STARTTLS)
- Added From header to OTP/Welcome emails (RFC5322 compliance)
- Worker now processes ALL INBOX emails (FetchBatch instead of FetchUnseen)
- Fixed go build . compiling debug scripts instead of src/cmd/main.go
- Added Notifications, Finance, Social classification folders (7 total)
- Refined AI prompt with precise category descriptions
- Added session guardrail to AGENTS.md
2026-04-23 18:35:30 +00:00

141 lines
No EOL
3.3 KiB
Go

package auth
import (
"fmt"
"log"
"sync"
"time"
)
// AuthService provides authentication functionality
type AuthService struct {
otpStore OTPStore
smtpSender *SMTPSender
sessionManager *SessionManager
mu sync.RWMutex
}
// OTPStore defines the interface for storing and retrieving OTPs
type OTPStore interface {
StoreOTP(email, otp string, expiry time.Time) error
GetOTP(email string) (string, time.Time, error)
DeleteOTP(email string) error
}
// InMemoryOTPStore is a simple in-memory OTP store for development
type InMemoryOTPStore struct {
store map[string]struct {
otp string
expiry time.Time
}
mu sync.RWMutex
}
// NewInMemoryOTPStore creates a new in-memory OTP store
func NewInMemoryOTPStore() *InMemoryOTPStore {
return &InMemoryOTPStore{
store: make(map[string]struct {
otp string
expiry time.Time
}),
}
}
// StoreOTP stores an OTP for the given email
func (s *InMemoryOTPStore) StoreOTP(email, otp string, expiry time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
s.store[email] = struct {
otp string
expiry time.Time
}{otp: otp, expiry: expiry}
return nil
}
// GetOTP retrieves the OTP for the given email
func (s *InMemoryOTPStore) GetOTP(email string) (string, time.Time, error) {
s.mu.RLock()
defer s.mu.RUnlock()
entry, exists := s.store[email]
if !exists {
return "", time.Time{}, fmt.Errorf("OTP not found for email: %s", email)
}
return entry.otp, entry.expiry, nil
}
// DeleteOTP removes the OTP for the given email
func (s *InMemoryOTPStore) DeleteOTP(email string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.store, email)
return nil
}
// NewAuthService creates a new authentication service
func NewAuthService(smtpSender *SMTPSender, sessionManager *SessionManager, otpStore OTPStore) *AuthService {
return &AuthService{
otpStore: otpStore,
smtpSender: smtpSender,
sessionManager: sessionManager,
}
}
// RequestOTP generates and sends an OTP to the given email
func (as *AuthService) RequestOTP(email string) error {
// Generate OTP
otpPlain, otp, err := GenerateOTP()
if err != nil {
return fmt.Errorf("failed to generate OTP: %w", err)
}
// Store OTP hash and expiry
err = as.otpStore.StoreOTP(email, otp.Hash, otp.Expiry)
if err != nil {
return fmt.Errorf("failed to store OTP: %w", err)
}
// Send OTP via email
err = as.smtpSender.SendOTP(email, otpPlain)
if err != nil {
log.Printf("OTP for %s: %s (SMTP failed: %v)", email, otpPlain, err)
// Keep the OTP stored so developer can use it for testing
return nil // Don't fail — allow dev access via logs
}
log.Printf("OTP sent to %s", email)
return nil
}
// VerifyOTP verifies the provided OTP for the given email
func (as *AuthService) VerifyOTP(email, userOTP string) (bool, error) {
// Retrieve stored OTP
storedOTPHash, expiry, err := as.otpStore.GetOTP(email)
if err != nil {
return false, fmt.Errorf("OTP not found or expired: %w", err)
}
// Create OTP struct for verification
storedOTP := &OTP{
Hash: storedOTPHash,
Expiry: expiry,
}
// Verify OTP
if !VerifyOTP(userOTP, storedOTP) {
return false, nil
}
// Clean up OTP after successful verification
as.otpStore.DeleteOTP(email)
return true, nil
}
// GetSessionManager returns the session manager
func (as *AuthService) GetSessionManager() *SessionManager {
return as.sessionManager
}