188 lines
7.7 KiB
Go
188 lines
7.7 KiB
Go
// Package api provides HTTP routing and handlers for the persona-api service.
|
|
package api
|
|
|
|
import (
|
|
"time"
|
|
|
|
emailpkg "git.threesix.ai/jordan/persona-community-5/pkg/email"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/app"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/auth"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/middleware"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/queue"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/realtime"
|
|
"git.threesix.ai/jordan/persona-community-5/pkg/storage"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/api/handlers"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/config"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
|
|
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/service"
|
|
)
|
|
|
|
// RegisterRoutes registers all HTTP routes for the service.
|
|
// Routes are mounted under /api/persona-api to match the ingress path routing.
|
|
// This allows the monorepo to expose multiple services under a single domain:
|
|
// - https://domain/api/persona-api/health
|
|
// - https://domain/api/persona-api/examples
|
|
// - https://domain/api/persona-api/events?channel=user:123 (SSE)
|
|
func RegisterRoutes(application *app.App, deps *Dependencies) {
|
|
logger := application.Logger()
|
|
cfg := config.Load()
|
|
|
|
// Initialize handlers with injected services
|
|
healthHandler := handlers.NewHealth(logger)
|
|
exampleHandler := handlers.NewExample(deps.ExampleService, logger)
|
|
authHandler := handlers.NewAuth(deps.AuthService, logger)
|
|
generateHandler := handlers.NewGenerate(deps.Queue, deps.JobReader, deps.SSEHub, logger)
|
|
chatHandler := handlers.NewChat(deps.Queue, deps.SSEHub, logger)
|
|
mediaHandler := handlers.NewMedia(deps.Store, deps.MediaRepo, logger)
|
|
albumHandler := handlers.NewAlbum(deps.AlbumService, logger)
|
|
personaHandler := handlers.NewPersona(deps.PersonaService, logger)
|
|
|
|
// Build and mount OpenAPI spec
|
|
spec := NewServiceSpec()
|
|
application.EnableDocs(spec)
|
|
|
|
// JWT validator for protected routes
|
|
jwtValidator := auth.NewJWTValidator(auth.JWTConfig{
|
|
Secret: []byte(cfg.JWTSecret),
|
|
Issuer: "persona-community-5",
|
|
})
|
|
|
|
// Dev email preview (development only — not mounted in production).
|
|
if cfg.AppConfig.Environment == "development" && deps.EmailRenderer != nil {
|
|
devHandler := emailpkg.NewDevHandler(deps.EmailRenderer)
|
|
application.Router().Get("/dev/emails", devHandler.List)
|
|
application.Router().Get("/dev/emails/{purpose}", devHandler.Preview)
|
|
}
|
|
|
|
// Register API routes under /api/{service-name} to match ingress path routing.
|
|
// The ingress routes /api/persona-api/* to this service.
|
|
application.Route("/api/persona-api", func(r app.Router) {
|
|
r.Get("/health", healthHandler.Check)
|
|
|
|
// ----- Public auth routes (rate-limited) -----
|
|
// Auth attempts: 20/min per IP (login, register, verify, reset).
|
|
authAttemptLimit := middleware.RateLimit(middleware.RateLimitConfig{Requests: 20, Window: time.Minute})
|
|
// Code sends: 5/min per IP (prevents email bombing via OTP/magic-link/forgot-password).
|
|
codeSendLimit := middleware.RateLimit(middleware.RateLimitConfig{Requests: 5, Window: time.Minute})
|
|
|
|
r.Group(func(r app.Router) {
|
|
r.Use(authAttemptLimit)
|
|
r.Post("/auth/login", app.Wrap(authHandler.Login))
|
|
r.Post("/auth/register", app.Wrap(authHandler.Register))
|
|
r.Post("/auth/otp/verify", app.Wrap(authHandler.VerifyOTP))
|
|
r.Post("/auth/magic-link/verify", app.Wrap(authHandler.VerifyMagicLink))
|
|
r.Post("/auth/reset-password", app.Wrap(authHandler.ResetPassword))
|
|
})
|
|
r.Group(func(r app.Router) {
|
|
r.Use(codeSendLimit)
|
|
r.Post("/auth/otp/send", app.Wrap(authHandler.SendOTP))
|
|
r.Post("/auth/magic-link", app.Wrap(authHandler.SendMagicLink))
|
|
r.Post("/auth/forgot-password", app.Wrap(authHandler.ForgotPassword))
|
|
})
|
|
|
|
// Refresh accepts expired tokens (still validates signature).
|
|
// The service layer checks session validity to prevent abuse.
|
|
r.Group(func(r app.Router) {
|
|
r.Use(auth.Middleware(auth.MiddlewareConfig{
|
|
Validator: jwtValidator,
|
|
AllowExpired: true,
|
|
}))
|
|
r.Post("/auth/refresh", app.Wrap(authHandler.RefreshToken))
|
|
})
|
|
|
|
// Session checker for revocation enforcement.
|
|
sessionChecker := deps.AuthService.CheckSession
|
|
|
|
// ----- Protected auth routes -----
|
|
r.Group(func(r app.Router) {
|
|
r.Use(auth.Middleware(auth.MiddlewareConfig{
|
|
Validator: jwtValidator,
|
|
}))
|
|
r.Use(auth.SessionCheck(sessionChecker))
|
|
|
|
r.Get("/auth/me", app.Wrap(authHandler.Me))
|
|
r.Put("/auth/me", app.Wrap(authHandler.UpdateMe))
|
|
r.Post("/auth/change-password", app.Wrap(authHandler.ChangePassword))
|
|
r.Post("/auth/logout", app.Wrap(authHandler.Logout))
|
|
r.Post("/auth/verify-email/send", app.Wrap(authHandler.SendVerifyEmail))
|
|
r.Post("/auth/verify-email", app.Wrap(authHandler.VerifyEmail))
|
|
r.Get("/auth/sessions", app.Wrap(authHandler.ListSessions))
|
|
r.Delete("/auth/sessions", app.Wrap(authHandler.RevokeAllSessions))
|
|
r.Delete("/auth/sessions/{id}", app.Wrap(authHandler.RevokeSession))
|
|
})
|
|
|
|
// ----- SSE Events -----
|
|
// Server-Sent Events for async job updates (generation progress, etc.)
|
|
r.Mount("/events", generateHandler.Events())
|
|
|
|
// ----- Example routes -----
|
|
// Public routes (no auth required)
|
|
r.Get("/examples", app.Wrap(exampleHandler.List))
|
|
r.Get("/examples/{id}", app.Wrap(exampleHandler.Get))
|
|
|
|
// Protected routes (auth required when enabled)
|
|
r.Group(func(r app.Router) {
|
|
if cfg.AuthEnabled {
|
|
r.Use(auth.Middleware(auth.MiddlewareConfig{
|
|
Validator: jwtValidator,
|
|
}))
|
|
}
|
|
|
|
r.Post("/examples", app.Wrap(exampleHandler.Create))
|
|
r.Put("/examples/{id}", app.Wrap(exampleHandler.Update))
|
|
r.Delete("/examples/{id}", app.Wrap(exampleHandler.Delete))
|
|
})
|
|
|
|
// ----- Chat + Generate + Media routes (auth required) -----
|
|
// Auth is required because SSE events are delivered to user:<userId> channels.
|
|
// Without a real user identity, events go to user:anonymous and never reach the client.
|
|
r.Group(func(r app.Router) {
|
|
r.Use(auth.Middleware(auth.MiddlewareConfig{
|
|
Validator: jwtValidator,
|
|
}))
|
|
r.Use(auth.SessionCheck(sessionChecker))
|
|
|
|
// Chat messaging
|
|
r.Post("/chat/messages", app.Wrap(chatHandler.SendMessage))
|
|
|
|
// Media generation (all queue-based, returns 202)
|
|
r.Post("/generate/image", app.Wrap(generateHandler.GenerateImage))
|
|
r.Post("/generate/video", app.Wrap(generateHandler.GenerateVideo))
|
|
r.Post("/generate/text", app.Wrap(generateHandler.GenerateText))
|
|
r.Get("/generate/jobs/{id}", app.Wrap(generateHandler.GetJobStatus))
|
|
|
|
// Media library (upload, list, delete)
|
|
r.Mount("/media", mediaHandler.Routes())
|
|
|
|
// Album generation (anchor + shots)
|
|
r.Get("/albums", app.Wrap(albumHandler.List))
|
|
r.Post("/albums", app.Wrap(albumHandler.Create))
|
|
r.Get("/albums/{id}", app.Wrap(albumHandler.Get))
|
|
r.Delete("/albums/{id}", app.Wrap(albumHandler.Delete))
|
|
r.Post("/albums/{id}/anchor", app.Wrap(albumHandler.GenerateAnchor))
|
|
r.Post("/albums/{id}/shots", app.Wrap(albumHandler.GenerateAllShots))
|
|
r.Post("/albums/{id}/shots/{index}", app.Wrap(albumHandler.GenerateShot))
|
|
r.Delete("/albums/{id}/shots/{index}", app.Wrap(albumHandler.ResetShot))
|
|
|
|
// Persona CRUD (Create enqueues generate_spec, returns 202)
|
|
r.Post("/personas", app.Wrap(personaHandler.Create))
|
|
r.Get("/personas", app.Wrap(personaHandler.List))
|
|
r.Get("/personas/{id}", app.Wrap(personaHandler.GetByID))
|
|
})
|
|
})
|
|
}
|
|
|
|
// Dependencies holds all service dependencies for route registration.
|
|
type Dependencies struct {
|
|
ExampleService *service.ExampleService
|
|
AuthService *service.AuthService
|
|
AlbumService *service.AlbumService
|
|
PersonaService *service.PersonaService
|
|
Queue queue.Producer
|
|
JobReader queue.JobReader
|
|
SSEHub *realtime.SSEHub
|
|
Store storage.Store
|
|
MediaRepo port.MediaRepository
|
|
EmailRenderer *emailpkg.Renderer
|
|
}
|