add auth
This commit is contained in:
+100
-3
@@ -12,7 +12,10 @@ import (
|
||||
"github.com/prabhupadasays/spsays-bff/internal/handler"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/middleware"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/service"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/worker"
|
||||
"github.com/prabhupadasays/spsays-bff/pkg/auth"
|
||||
"github.com/prabhupadasays/spsays-bff/pkg/mailer"
|
||||
"github.com/prabhupadasays/spsays-bff/pkg/shorturl"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -41,6 +44,12 @@ type Server struct {
|
||||
transcriptionH *handler.TranscriptionHandler
|
||||
searchH *handler.SearchHandler
|
||||
shortURLH *handler.ShortURLHandler
|
||||
authH *handler.AuthHandler
|
||||
userH *handler.UserHandler
|
||||
|
||||
// Auth state (nil when Postgres is unavailable — /auth and /users return 503)
|
||||
authIssuer *auth.Issuer
|
||||
userRepo *repository.UserRepo
|
||||
}
|
||||
|
||||
// New creates a new Server with all routes registered.
|
||||
@@ -86,10 +95,61 @@ func New(cfg *config.Config, logger *zap.Logger) *Server {
|
||||
// Connection failures degrade gracefully — the BFF stays up; only /share returns 503.
|
||||
s.initShortener(cfg, logger)
|
||||
|
||||
// Auth: depends on Postgres being up. initShortener already opened the pool,
|
||||
// so we reuse s.pg here. On a fresh DB the users + auth_tokens tables get
|
||||
// created by the migration runner that initShortener already invoked.
|
||||
s.initAuth(cfg, logger)
|
||||
|
||||
s.setupRouter()
|
||||
return s
|
||||
}
|
||||
|
||||
// initAuth wires the user repo, JWT issuer, mailer, R2 client, and the auth
|
||||
// + user handlers. If Postgres is unavailable, leaves the handlers nil and
|
||||
// the routes 503-out gracefully.
|
||||
func (s *Server) initAuth(cfg *config.Config, logger *zap.Logger) {
|
||||
if s.pg == nil {
|
||||
logger.Warn("auth: Postgres unavailable — /auth and /users will return 503")
|
||||
return
|
||||
}
|
||||
|
||||
userRepo := repository.NewUserRepo(s.pg)
|
||||
tokenRepo := repository.NewAuthTokenRepo(s.pg)
|
||||
|
||||
r2, err := repository.NewR2(repository.R2Config{
|
||||
AccountID: cfg.R2AccountID,
|
||||
AccessKeyID: cfg.R2AccessKeyID,
|
||||
SecretAccessKey: cfg.R2SecretAccessKey,
|
||||
Bucket: cfg.R2Bucket,
|
||||
PublicURL: cfg.R2PublicURL,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("auth: R2 unavailable — avatar uploads will return 503", zap.Error(err))
|
||||
}
|
||||
if r2 == nil {
|
||||
logger.Info("auth: R2 not configured — avatar presign endpoint will return 503")
|
||||
}
|
||||
|
||||
issuer := auth.NewIssuer(cfg.JWTSecret, cfg.JWTExpiration)
|
||||
mail := mailer.New(mailer.Config{
|
||||
Host: cfg.SMTPHost,
|
||||
Port: cfg.SMTPPort,
|
||||
Username: cfg.SMTPUsername,
|
||||
Password: cfg.SMTPPassword,
|
||||
From: cfg.SMTPFrom,
|
||||
}, logger)
|
||||
|
||||
authSvc := service.NewAuthService(userRepo, tokenRepo, issuer, mail, logger, cfg.FrontendURL)
|
||||
userSvc := service.NewUserService(userRepo, r2)
|
||||
|
||||
cookieOpts := auth.CookieOptions{Domain: cfg.CookieDomain, Secure: cfg.CookieSecure}
|
||||
|
||||
s.authIssuer = issuer
|
||||
s.userRepo = userRepo
|
||||
s.authH = handler.NewAuthHandler(authSvc, cookieOpts)
|
||||
s.userH = handler.NewUserHandler(userSvc)
|
||||
}
|
||||
|
||||
// initShortener wires Postgres + Redis + the shorturl handler/workers.
|
||||
// On connection failure it logs and leaves the components nil — the handler
|
||||
// short-circuits to 503 so the rest of the BFF keeps working.
|
||||
@@ -152,6 +212,15 @@ func (s *Server) Close() {
|
||||
s.logger.Info("all connections closed")
|
||||
}
|
||||
|
||||
// authUnavailable is the 503 fallback used when Postgres is down and the
|
||||
// auth handlers couldn't be wired up. Keeps every /auth/* and /users/* URL
|
||||
// returning a structured error rather than 404.
|
||||
func (s *Server) authUnavailable(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte(`{"success":false,"error":{"code":503,"message":"auth service unavailable"}}`))
|
||||
}
|
||||
|
||||
// injectBookCode is a middleware that sets "code" in chi's URL params
|
||||
// for explicit sub-routes like /books/sb and /books/cc where {code}
|
||||
// isn't a URL param but a fixed path prefix.
|
||||
@@ -367,12 +436,40 @@ func (s *Server) setupRouter() {
|
||||
// Search (Typesense)
|
||||
r.Get("/search", s.searchH.Search)
|
||||
|
||||
// User routes (PostgreSQL)
|
||||
// --- Auth ---
|
||||
// Public endpoints (no JWT required) for sign-up, log-in, password reset,
|
||||
// and email verification. The /auth/me + /auth/resend-verification routes
|
||||
// require a session and live under the protected block below.
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
// TODO: register auth handlers (login, register, refresh)
|
||||
if s.authH == nil {
|
||||
r.HandleFunc("/*", s.authUnavailable)
|
||||
return
|
||||
}
|
||||
r.Post("/signup", s.authH.SignUp)
|
||||
r.Post("/login", s.authH.LogIn)
|
||||
r.Post("/logout", s.authH.LogOut)
|
||||
r.Post("/forgot-password", s.authH.ForgotPassword)
|
||||
r.Post("/reset-password", s.authH.ResetPassword)
|
||||
r.Post("/verify-email", s.authH.VerifyEmail)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireAuth(s.authIssuer, s.userRepo))
|
||||
r.Get("/me", s.authH.Me)
|
||||
r.Post("/resend-verification", s.authH.ResendVerification)
|
||||
})
|
||||
})
|
||||
|
||||
// User profile + avatar — all protected.
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
// TODO: register user handlers
|
||||
if s.userH == nil {
|
||||
r.HandleFunc("/*", s.authUnavailable)
|
||||
return
|
||||
}
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireAuth(s.authIssuer, s.userRepo))
|
||||
r.Patch("/me", s.userH.UpdateProfile)
|
||||
r.Post("/me/avatar-presign", s.userH.AvatarPresign)
|
||||
})
|
||||
})
|
||||
r.Route("/collections", func(r chi.Router) {
|
||||
// TODO: register collection/bookmark handlers
|
||||
|
||||
Reference in New Issue
Block a user