feat(api): add POST /api/events handler

Discriminated-union JSON over three event types: play_started,
play_ended, play_skipped. Auth via existing RequireUser. play_started
returns play_event_id + session_id; the other two return { ok: true }.
Validates ownership: play_ended/play_skipped on another user's row
returns 403. Mount signature now takes EventsConfig and constructs
the playevents.Writer; server.New propagates it through.
This commit is contained in:
2026-04-26 00:12:06 -04:00
parent 0c7e1b0408
commit f4e73b81b4
8 changed files with 343 additions and 14 deletions
+13 -2
View File
@@ -6,17 +6,26 @@ package api
import (
"log/slog"
"time"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
"git.fabledsword.com/bvandeusen/minstrel/internal/config"
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
)
// Mount attaches /api/* handlers to r. Public endpoints (login) are outside
// RequireUser; everything else is gated by the middleware.
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger) {
h := &handlers{pool: pool, logger: logger}
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, cfg config.EventsConfig) {
w := playevents.NewWriter(
pool, logger,
time.Duration(cfg.SessionTimeoutMinutes)*time.Minute,
cfg.SkipMaxCompletionRatio,
cfg.SkipMaxDurationPlayedMs,
)
h := &handlers{pool: pool, logger: logger, events: w}
r.Route("/api", func(api chi.Router) {
api.Post("/auth/login", h.handleLogin)
@@ -33,6 +42,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger) {
authed.Get("/tracks/{id}/stream", h.handleGetStream)
authed.Get("/search", h.handleSearch)
authed.Get("/radio", h.handleRadio)
authed.Post("/events", h.handleEvents)
})
})
}
@@ -40,4 +50,5 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger) {
type handlers struct {
pool *pgxpool.Pool
logger *slog.Logger
events *playevents.Writer
}