package service import ( "context" "net/http" "time" "git.umbrella.haus/ae/qnote/internal/data" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" "github.com/jackc/pgx/v5" "github.com/rs/zerolog/log" ) const authRateLimit = 5 // req/min type SvcConfig struct { JWTSecret string CSRFSecret string IsProd bool Domain string FrontendURL string } func (sc *SvcConfig) allowedOrigins() []string { allowed := []string{sc.FrontendURL} log.Debug().Msgf("CORS allowedOrigins: %v", allowed) return allowed } func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error { r := chi.NewRouter() if !config.IsProd { log.Warn().Msg("Running in *INSECURE* development mode") } authRouter := authResource{ Config: config, RateLimiter: httprate.NewRateLimiter(authRateLimit, time.Minute), Users: q, Tokens: q, } notesRouter := notesResource{ Config: config, Notes: q, // Wrapped (to be unit testable with mock DB) RawQueries: q, // Passed separately to allow tx. usage DB: conn, } // Global middlewares r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(loggerMiddleware(&log.Logger)) r.Use(httprate.LimitByIP(100, time.Minute)) // Base limit for all routes r.Use(cors.Handler(cors.Options{ AllowedOrigins: config.allowedOrigins(), AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Csrf-Token"}, ExposedHeaders: []string{"List", "X-Csrf-Token"}, AllowCredentials: true, MaxAge: 300, })) r.Use(middleware.Recoverer) r.Use(middleware.AllowContentType("application/json")) // Cleanup workers scheduleArtifactCleanup(context.Background(), q) // Routes grouped by functionality (we must prefix the API routes with `/api` // as the domain will be the same for the front and back ends) r.Route("/api", func(r chi.Router) { r.Mount("/auth", authRouter.Routes()) r.Mount("/notes", notesRouter.Routes()) r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { respondJSON(w, http.StatusOK, map[string]string{ "message": "pong", }) }) }) log.Info().Msg("Starting server on :8080") return http.ListenAndServe(":8080", r) } // Start worker that automatically cleans up the `notes` (cascading to `note_versions`) and // `refresh_tokens` tables from expired (or revoked) entries. The tasks run once during // initialization and then once an hour until the backend is shutdown. func scheduleArtifactCleanup(ctx context.Context, q *data.Queries) { cleanupNotes(ctx, q) cleanupRefreshTokens(ctx, q) log.Info().Msg("Scheduled database artifact cleanup to run once an hour") ticker := time.NewTicker(1 * time.Hour) go func() { for range ticker.C { cleanupCtx := context.Background() cleanupNotes(cleanupCtx, q) cleanupRefreshTokens(cleanupCtx, q) } }() }