106 lines
2.9 KiB
Go

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)
}
}()
}