mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-19 05:47:02 +00:00
Add Supabase JWT authorization middleware, config parsing, and WebSocket token verification
This commit is contained in:
+138
-42
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user