Add Supabase JWT authorization middleware, config parsing, and WebSocket token verification

This commit is contained in:
2026-07-17 22:46:26 +04:00
parent 9c9632343d
commit a5d2ee32c6
8 changed files with 753 additions and 395 deletions
+138 -42
View File
@@ -38,6 +38,7 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
@@ -51,6 +52,7 @@ import (
// the // @... annotations on the handlers.
_ "github.com/x0gp/server/docs"
"github.com/x0gp/server/internal/auth"
"github.com/x0gp/server/internal/catalog"
"github.com/x0gp/server/internal/clans"
"github.com/x0gp/server/internal/config"
@@ -288,35 +290,40 @@ func main() {
// Start WebRTC service
webrtcSvc.Start(ctx, &wg)
apiMux := http.NewServeMux()
apiMux.HandleFunc("/api/version", versionHandler())
apiMux.HandleFunc("/api/catalog", catalogHandler(cat))
apiMux.HandleFunc("/api/tracks", tracksRouter(cat))
apiMux.HandleFunc("/api/tracks/", trackByIDHandler(cat))
apiMux.HandleFunc("/api/tracks/calendar", trackCalendarRouter(cat))
apiMux.HandleFunc("/api/tracks/calendar/", deleteTrackCalendarHandler(cat))
apiMux.HandleFunc("/api/cars", carsRouter(cat))
apiMux.HandleFunc("/api/cars/", carByIDHandler(cat))
apiMux.HandleFunc("/api/races", racesListHandler(racesSvc))
apiMux.HandleFunc("/api/races/last", racesGetLastHandler(racesSvc))
apiMux.HandleFunc("/api/races/", racesGetHandler(racesSvc))
apiMux.HandleFunc("/api/races/upcoming", racesUpcomingHandler(racesSvc))
apiMux.HandleFunc("/api/races/queue", racesQueueRouter(racesSvc))
apiMux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc))
apiMux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc))
apiMux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc))
apiMux.HandleFunc("/api/leaderboard", leaderboardHandler(racesSvc))
apiMux.HandleFunc("/api/leaderboard/tracks", calendarLeaderboardHandler(racesSvc))
apiMux.HandleFunc("/api/clans", clansRouter(clansSvc))
apiMux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
apiMux.HandleFunc("/api/drivers", driversRouter(driversSvc))
apiMux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc))
apiMux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
apiMux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
apiMux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())
authMW := auth.Middleware(cfg.JWTSecret, cfg.DevMode)
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler(hub, engine))
mux.HandleFunc("/api/version", versionHandler())
mux.HandleFunc("/stats", statsHandler(hub, engine))
mux.HandleFunc("/api/catalog", catalogHandler(cat))
mux.HandleFunc("/api/tracks", tracksRouter(cat))
mux.HandleFunc("/api/tracks/", trackByIDHandler(cat))
mux.HandleFunc("/api/tracks/calendar", trackCalendarRouter(cat))
mux.HandleFunc("/api/tracks/calendar/", deleteTrackCalendarHandler(cat))
mux.HandleFunc("/api/cars", carsRouter(cat))
mux.HandleFunc("/api/cars/", carByIDHandler(cat))
mux.HandleFunc("/api/races", racesListHandler(racesSvc))
mux.HandleFunc("/api/races/last", racesGetLastHandler(racesSvc))
mux.HandleFunc("/api/races/", racesGetHandler(racesSvc))
mux.HandleFunc("/api/races/upcoming", racesUpcomingHandler(racesSvc))
mux.HandleFunc("/api/races/queue", racesQueueRouter(racesSvc))
mux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc))
mux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc))
mux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc))
mux.HandleFunc("/api/leaderboard", leaderboardHandler(racesSvc))
mux.HandleFunc("/api/leaderboard/tracks", calendarLeaderboardHandler(racesSvc))
mux.HandleFunc("/api/clans", clansRouter(clansSvc))
mux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
mux.HandleFunc("/api/drivers", driversRouter(driversSvc))
mux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc))
mux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
mux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
mux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())
mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, videoSvc, logger))
mux.Handle("/api/", authMW(apiMux))
mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, clansSvc, videoSvc, logger))
// Static client files (resolving TODO)
mux.Handle("/", http.FileServer(http.Dir("./web")))
@@ -485,7 +492,7 @@ func statsHandler(h *Hub, e *control.Engine) http.HandlerFunc {
// @Tags websocket
// @Success 101 {object} transport.Envelope "Switching protocols. Subsequent frames follow the envelope schema."
// @Router /ws [get]
func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, videoSvc *UDPVideoService, logger *slog.Logger) http.HandlerFunc {
func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, videoSvc *UDPVideoService, logger *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
@@ -503,7 +510,7 @@ func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Servi
"remote", r.RemoteAddr,
)
go writePump(client, conn, logger)
go readPump(cfg, client, conn, e, h, cat, lobbySvc, driversSvc, videoSvc, logger)
go readPump(cfg, client, conn, e, h, cat, lobbySvc, driversSvc, clansSvc, videoSvc, logger)
}
}
@@ -545,7 +552,7 @@ func writePump(c *realtime.Client, conn *websocket.Conn, logger *slog.Logger) {
}
}
func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, videoSvc *UDPVideoService, logger *slog.Logger) {
func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, videoSvc *UDPVideoService, logger *slog.Logger) {
defer func() {
h.Unregister(c)
logger.Info("client disconnected", "client_id", c.ID)
@@ -577,7 +584,7 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
switch env.Type {
case transport.TypeClientHello:
handleClientHello(c, e, cfg, lobbySvc, driversSvc, env)
handleClientHello(c, e, cfg, lobbySvc, driversSvc, clansSvc, env)
case transport.TypeJoinRace:
handleJoinRace(c, e, lobbySvc, cat, env)
case transport.TypeLeaveRace:
@@ -608,7 +615,7 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
// Message handlers --------------------------------------------------------
func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config, lobbySvc *lobby.Service, driversSvc *drivers.Service, env *transport.Envelope) {
func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, env *transport.Envelope) {
hello := transport.ServerHello{
SessionID: c.ID,
RaceTick: 0,
@@ -619,24 +626,113 @@ func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config
hello.Config.MaxCars = 4
c.SessionID = c.ID
// Extract client_id (DriverID) if present in payload
payload, _ := env.Payload.(map[string]any)
if payload != nil {
if clientID, ok := payload["client_id"].(string); ok && clientID != "" {
c.DriverID = clientID
// Pre-populate driver in the lobby
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
if drv, err := driversSvc.Get(ctx, clientID); err == nil {
_, _ = lobbySvc.AddDriver(clientID, drv.Name, "")
lobbySvc.SetDriverProfile(clientID, drv.Nickname, drv.AvatarURL, drv.ClanID, "")
}
cancel()
var clientID string
var email string
if cfg.DevMode {
// In devMode, client_id is optional and can be arbitrary
if cid, ok := payload["client_id"].(string); ok && cid != "" {
clientID = cid
} else {
clientID = c.ID
}
} else {
// In production, require valid Supabase JWT
tokenStr, _ := payload["token"].(string)
if tokenStr == "" {
sendError(c, "unauthorized", "missing auth token in client_hello")
c.Close()
return
}
claims, err := auth.ValidateJWT(tokenStr, cfg.JWTSecret)
if err != nil {
sendError(c, "unauthorized", fmt.Sprintf("invalid auth token: %v", err))
c.Close()
return
}
clientID = claims.DriverID
email = claims.Email
}
c.DriverID = clientID
// Resolve/ensure profile and join lobby
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
drv, clanTag, err := ensureDriverProfile(ctx, clientID, email, driversSvc, clansSvc)
if err == nil {
_, _ = lobbySvc.AddDriver(clientID, drv.Name, "")
lobbySvc.SetDriverProfile(clientID, drv.Nickname, drv.AvatarURL, drv.ClanID, clanTag)
} else {
// Fallback placeholder profile if database error
_, _ = lobbySvc.AddDriver(clientID, "Driver", "")
lobbySvc.SetDriverProfile(clientID, "DRV", "", "", "")
}
c.Send <- mustEncode(transport.TypeServerHello, hello)
}
func ensureDriverProfile(ctx context.Context, id string, email string, driversSvc *drivers.Service, clansSvc *clans.Service) (drivers.Driver, string, error) {
drv, err := driversSvc.Get(ctx, id)
if err == nil {
var clanTag string
if drv.ClanID != "" {
if cl, err := clansSvc.Get(ctx, drv.ClanID); err == nil {
clanTag = cl.Tag
}
}
return drv, clanTag, nil
}
// Profile not found: auto-create a default profile
name := "Driver"
if email != "" {
if idx := strings.Index(email, "@"); idx > 0 {
name = email[:idx]
}
}
// Generate unique 3-letter nickname
nick := "DRV"
if len(name) >= 3 {
candidate := strings.ToUpper(name[:3])
if isAlphaOnly(candidate) {
nick = candidate
}
}
// Check if nickname already exists
for i := 0; i < 20; i++ {
_, err := driversSvc.GetByNickname(ctx, nick)
if err != nil && errors.Is(err, drivers.ErrNotFound) {
break
}
nick = fmt.Sprintf("D%02d", i)
}
created, err := driversSvc.Create(ctx, drivers.CreateInput{
ID: id,
Nickname: nick,
Name: name,
})
if err != nil {
return drivers.Driver{}, "", err
}
return created, "", nil
}
func isAlphaOnly(s string) bool {
for _, c := range s {
if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
return false
}
}
return true
}
func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service, cat *catalog.Service, env *transport.Envelope) {
payload, _ := env.Payload.(map[string]any)
slot := 0