118 lines
3.4 KiB
Go
118 lines
3.4 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
|
|
postAuthRateLimit = 100 // req/min
|
|
)
|
|
|
|
type SvcConfig struct {
|
|
JWTSecret string
|
|
CSRFSecret string
|
|
IsProd bool
|
|
IsRegEnabled 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: getRateLimiter(authRateLimit),
|
|
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(getRateLimiter(postAuthRateLimit).Handler) // Base limit applied globally
|
|
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)
|
|
}
|
|
|
|
// Generate a new rate limiter that responds to surpassing clients with HTTP 429 + JSON error message.
|
|
func getRateLimiter(requestLimit int) *httprate.RateLimiter {
|
|
return httprate.NewRateLimiter(requestLimit, time.Minute, httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
|
// NOTE: Error message mustn't contain a dot due to one being added client-side anyway
|
|
respondError(w, http.StatusTooManyRequests, "You're rate-limited, please slow down")
|
|
}))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}()
|
|
}
|