diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-07-29 20:04:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-07-29 20:04:09 +0900 |
| commit | 648613e24c8afe5fd3c599def61b33ccf7bcb96c (patch) | |
| tree | f368aa1ef1d734d3096c9129e17d6af11d1041a6 | |
| parent | d73fd8bf5bf589a4a391c867e980761fadb647ce (diff) | |
| download | iosdc-japan-2025-albatross-648613e24c8afe5fd3c599def61b33ccf7bcb96c.tar.gz iosdc-japan-2025-albatross-648613e24c8afe5fd3c599def61b33ccf7bcb96c.tar.zst iosdc-japan-2025-albatross-648613e24c8afe5fd3c599def61b33ccf7bcb96c.zip | |
feat: authenticate WebSocket connection by short-lived access token
| -rw-r--r-- | backend/api/generated.go | 143 | ||||
| -rw-r--r-- | backend/api/handlers.go | 24 | ||||
| -rw-r--r-- | backend/auth/jwt.go | 15 | ||||
| -rw-r--r-- | backend/game/http.go | 25 | ||||
| -rw-r--r-- | backend/main.go | 1 | ||||
| -rw-r--r-- | frontend/app/.server/api/schema.d.ts | 53 | ||||
| -rw-r--r-- | frontend/app/components/GolfPlayApp.tsx | 14 | ||||
| -rw-r--r-- | frontend/app/components/GolfWatchApp.tsx | 14 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 49 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 49 | ||||
| -rw-r--r-- | openapi.yaml | 34 |
11 files changed, 342 insertions, 79 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go index 7e64929..b6563c9 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -212,6 +212,11 @@ type PostLoginJSONBody struct { Username string `json:"username"` } +// GetTokenParams defines parameters for GetToken. +type GetTokenParams struct { + Authorization string `json:"Authorization"` +} + // PostLoginJSONRequestBody defines body for PostLogin for application/json ContentType. type PostLoginJSONRequestBody PostLoginJSONBody @@ -588,6 +593,9 @@ type ServerInterface interface { // User login // (POST /login) PostLogin(ctx echo.Context) error + // Get a short-lived access token + // (GET /token) + GetToken(ctx echo.Context, params GetTokenParams) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -679,6 +687,37 @@ func (w *ServerInterfaceWrapper) PostLogin(ctx echo.Context) error { return err } +// GetToken converts echo context to params. +func (w *ServerInterfaceWrapper) GetToken(ctx echo.Context) error { + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params GetTokenParams + + headers := ctx.Request().Header + // ------------- Required header parameter "Authorization" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { + var Authorization string + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n)) + } + + err = runtime.BindStyledParameterWithOptions("simple", "Authorization", valueList[0], &Authorization, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter Authorization: %s", err)) + } + + params.Authorization = Authorization + } else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found")) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetToken(ctx, params) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -710,6 +749,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/games/:game_id", wrapper.GetGamesGameId) router.POST(baseURL+"/login", wrapper.PostLogin) + router.GET(baseURL+"/token", wrapper.GetToken) } @@ -802,6 +842,36 @@ func (response PostLogin401JSONResponse) VisitPostLoginResponse(w http.ResponseW return json.NewEncoder(w).Encode(response) } +type GetTokenRequestObject struct { + Params GetTokenParams +} + +type GetTokenResponseObject interface { + VisitGetTokenResponse(w http.ResponseWriter) error +} + +type GetToken200JSONResponse struct { + Token string `json:"token"` +} + +func (response GetToken200JSONResponse) VisitGetTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetToken403JSONResponse struct { + Message string `json:"message"` +} + +func (response GetToken403JSONResponse) VisitGetTokenResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // List games @@ -813,6 +883,9 @@ type StrictServerInterface interface { // User login // (POST /login) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) + // Get a short-lived access token + // (GET /token) + GetToken(ctx context.Context, request GetTokenRequestObject) (GetTokenResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -907,31 +980,57 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error { return nil } +// GetToken operation middleware +func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error { + var request GetTokenRequestObject + + request.Params = params + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetToken(ctx.Request().Context(), request.(GetTokenRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetToken") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetTokenResponseObject); ok { + return validResponse.VisitGetTokenResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/9xYbW/bNhD+Kxo3oBsg+C1B0PlblnVZig4z6g37UAQGLZ1tZhSp8qg6XqD/PpDUi2XJ", - "luwI27B+SBPp7vjc3XMv4gsJZBRLAUIjmb4QDDYQUfvrPY3A/B8rGYPSDOzTkGHM6W4hsrfwTKOYA5la", - "eW9MfKJ3sfkbtWJiTVKfhImimkmxQAikCLGid3UzKlSY0LAGZXTWNIIFCyui4ybBWMklh8gIfqNgRabk", - "62Hp0zBzaDjLxFKfoKZKQ7igumL9++ubm7fXb0eNcFBT7fwVSUSmn0jAJUJIfLKlTDOxXoDQysSofGLP", - "IQYhxFQByU42QbH+uV9WTDDcQEge/b1gFuYPgpn6RMHnhCkIDYo8SjlAv5qfhtA/Fibl8gkCbZwzmZtx", - "ugP1CyDStXVUCvh1RaafToe1pjqf3JHUP1PpbjIn6WMTEvPmcjB3k/k7odXuIkQfgYaXad7JEI77Y9/W", - "64pq2sbhY9ZmdMclDU0qXW5NVQvUZEpiKz4NJjgNzLlthLJvfYemE1UOINT8CjJvS2rHign97ZufgXPp", - "e1upePjVm+9akVlDXSG5rNfAnIgOWI1O4ekKwhHoHBDKavQHwhTjq0p5lrWus6tgPrmb2/Z3iea7Zwg+", - "AiZcH6miqkw/tVSx2V5ROAmm8AyBchh6r6tGODVPMZCqWl5jM79Ewjldmj+1SuDYPEtwf6BhEgSAWB1D", - "+cM29zJzfgaoq4c5vXrLYGawW/rKudx/7g6A1Bw8d2s5gJSrd4XjarG3MFtz3YKcL0H9h7gCol4Z5u0Z", - "K16d0E79GJo/qA42Fy5MVV27MT02mj27f9fUuzfhmqrbYi7RbOrfzeYvZmSjuROM3Dp5S8neFqGTIHrc", - "hPysoDp8Dx32iULPP71Ancphf0nqNGD3U9XzhO0AqN6pu4be//emsbEQglJVeh2Rk0m1KZIK/1rjvE+p", - "g7FfmC/wdE7EKwdUs72OJOtvRJ2G8Y/OqPfb4+cev8N5LzfC+1FCE3VYIMUipnpTVRmyiK4Bh09yIwZP", - "8bpRFRc0jJioaK4ox7IallJyoMJIJ9hQb5OrpsIxonUvDJTWfOan7Bmp3Z4UuJsiPCtXuIPwAgaKxZrJ", - "qsPktw1Dj6FHvXx/a2r17lWnhqOZ5ge+Z6iaLuKad0gXA2fJr2CvO21MMLGS9jPWnU1u+ZJqJRE9A0wJ", - "yr0tLL3b2QPxyRdQaMNARoPxYGQwyxgEjRmZkqvBaDAiPjGcsoEbrmnkQrgGWxQmqvb66iEkU3IP+t4K", - "GBVFI9Cg0K5FhlnkcwL2C97xodqnbKsoP773SyrT3gANQZXqt4neSMX+sseT/ci5Jl4zWUT50QhjLAU6", - "XyajUdZ5NAjrFo1jzgJrefiEjiWlvSqZipAwDRF26YVluyNUKbprvDTEI9mtcJd8YKg9ufKcRuqT69HV", - "K3yJymW5JOxPUi1ZGILwimy3Ujc31MWHwr61gkkUUbXLfcscS/2Me8OX7EY1bWWh+fEQHuGibZMFl8pb", - "2lYW/ZeJ2U68evRvbYj/Z9S5B+3RzDFDHS7XbrbFEhsYM5OoP1gRBwVQ/yDdzeCF0Ygp4laq8OBzIns6", - "nlw1DZZXzspsJBZHNwewysa011ao5Z9wMFGfzb/B3s/2Nc4a6ZL9udu2VwnnO48megNCG6gQOjqP+6bz", - "g/hCOQu9QEFozqIce6Vzbj/PpieVV6SzyvDfEZTnaJ2mafp3AAAA///2zTVTJhwAAA==", + "H4sIAAAAAAAC/9xYbW/bNhD+Kxo3oBugxY5TFJ2/ZVmXpegwo+mwD0Vg0OLZZkaRKo9q4hX67wNJvViW", + "bMmOsAXthzSR7o7P3T33In4hkYoTJUEaJNMvBKM1xNT9ek1jsP8nWiWgDQf3lHFMBN3MZf4WHmmcCCBT", + "Jx+ck5CYTWL/RqO5XJEsJCzV1HAl5wiRkgxrehevxqUKlwZWoK3OisYw56wmet4mmGi1EBBbwe80LMmU", + "fDuqfBrlDo1muVgWEjRUG2BzamrWf3r56tXrl6/HrXDQUOP9lWlMph9JJBQCIyF5oNxwuZqDNNrGqHri", + "ziEWISRUA8lPtkFx/vlfllxyXAMjd+FWMEvzO8HMQqLhU8o1MIuiiFIBMKznpyX0d6VJtbiHyFjnbOZm", + "gm5A/w6IdOUcVRL+WJLpx8NhbajeTq5IFh6pdDW5JdldGxL75nQwV5PbN9LozUmI3gNlp2leKQb7/XFv", + "m3VFDe3i8D5rM7oRijKbSp9bW9USDZmSxIlPowlOI3tuF6Hc29Cj6UWVHQgNv6Lc24raiebSfP/iNxBC", + "hcGD0oJ98+KHTmTOUF9IPusNMAeiA06jV3j6gvAEOgaEdhrDgbDF+KRSnuWt6+gquJ1c3br2d4rmm0eI", + "3gOmwuyporrMMLVUs9ldUTiJpvAIkfYYBq+rVjgNTzFSul5e53Z+yVQIurB/Gp3CvnmW4vZAwzSKALE+", + "hoqHXe7l5sIcUF8PC3oNlsHcYL/0VXN5+NztAGk4eOzWsgOpUO8Lx9fiYGF25voFuViChg9xDUSzMuzb", + "I1a8JqG9+j40f1ETrU9cmOq6bmO6azV7dP9uqPdvwg1Vv8WcotnWv9vNn8zIVnMHGPng5R0lB1uEDoIY", + "cBMK84Lq8T202ydKvfDwAnUoh8MlqdeA3U7VwBO2B6Bmp+4b+vD/m8bWAgOt6/TaI6fSelMkNf51xnmb", + "UjtjvzRf4umdiCcOqHZ7PUk23Ig6DOM/nVFvH/afu/8O561ay+AXBW3U4ZGS84SadV1lxGO6Ahzdq7U8", + "u09Wrao4pyzmsqa5pAKralgoJYBKK51iS71NLtoKx4o2vbBQOvNZnLJlpHF7UuJui/CsWuF2wgsYaZ4Y", + "ruoOkw9rjgHHgAbF/tbW6v2rXg3HcCN2fM9RtV3Ete+QPgbeUljD3nTamuByqdxnrD+bXIoFNVohBhaY", + "llQED7AILmc3JCSfQaMLAxmfnZ+NLWaVgKQJJ1NycTY+G5OQWE65wI1WNPYhXIErChtVd311w8iUXIO5", + "dgJWRdMYDGh0a5FlFvmUgvuC93yo9ynXKqqP7+2SyrXXQBnoSv0yNWul+T/ueLIdOd/EGybLKN9ZYUyU", + "RO/LZDzOO48B6dyiSSJ45CyP7tGzpLJXJ1MZEm4gxj69sGp3hGpNN62XhrgnuzXuknccTaCWgdfIQvJy", + "fPEEX+JqWa4I+6vSC84YyKDMdid1C0N9fCjtOyuYxjHVm8K33LEszLk3+pLfqGadLLQ/btgeLro2WXKp", + "uqXtZNFzJmY38ZrRv3Qh/sqocw0moLljljpCrfxsSxS2MGam0LxzIh4KoPlZ+ZvBE6ORUMQHpdnO50T+", + "9Hxy0TZYnjgr85FYHt0ewDobs0FboVF/w85EfbT/zrZ+dq9xzkif7N/6bXuZCrEJaGrWII2FCszT+Xxo", + "Ot/Iz1RwFkQamD2LChyUzoX9IpuB0kGZzjrD/0TQgae1Y3gZ+n0t8YMTaG+Gz3W0PjM+fWXtEddKmx8F", + "/wwsoM7zwMcqy7Ls3wAAAP//jcdHSHceAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handlers.go b/backend/api/handlers.go index f50558d..c4810a0 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -50,6 +50,17 @@ func (h *ApiHandler) PostLogin(ctx context.Context, request PostLoginRequestObje }, nil } +func (h *ApiHandler) GetToken(ctx context.Context, request GetTokenRequestObject) (GetTokenResponseObject, error) { + user := ctx.Value("user").(*auth.JWTClaims) + newToken, err := auth.NewShortLivedJWT(user) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return GetToken200JSONResponse{ + Token: newToken, + }, nil +} + func (h *ApiHandler) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) { user := ctx.Value("user").(*auth.JWTClaims) playerId := request.Params.PlayerId @@ -190,23 +201,10 @@ func setupJWTFromAuthorizationHeader(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) } - c.Set("user", claims) c.SetRequest(c.Request().WithContext(context.WithValue(c.Request().Context(), "user", claims))) return nil } -func NewEchoJWTMiddleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - err := setupJWTFromAuthorizationHeader(c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - return next(c) - } - } -} - func NewJWTMiddleware() StrictMiddlewareFunc { return func(handler StrictHandlerFunc, operationID string) StrictHandlerFunc { if operationID == "PostLogin" { diff --git a/backend/auth/jwt.go b/backend/auth/jwt.go index 0b92155..b7abc68 100644 --- a/backend/auth/jwt.go +++ b/backend/auth/jwt.go @@ -33,6 +33,21 @@ func NewJWT(user *db.User) (string, error) { return token.SignedString([]byte("TODO")) } +func NewShortLivedJWT(claims *JWTClaims) (string, error) { + newClaims := &JWTClaims{ + UserID: claims.UserID, + Username: claims.Username, + DisplayName: claims.DisplayName, + IconPath: claims.IconPath, + IsAdmin: claims.IsAdmin, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims) + return token.SignedString([]byte("TODO")) +} + func ParseJWT(token string) (*JWTClaims, error) { claims := new(JWTClaims) t, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { diff --git a/backend/game/http.go b/backend/game/http.go index 1ac77b0..8cf7322 100644 --- a/backend/game/http.go +++ b/backend/game/http.go @@ -5,7 +5,8 @@ import ( "strconv" "github.com/labstack/echo/v4" - // "github.com/nsfisis/iosdc-2024-albatross/backend/auth" + + "github.com/nsfisis/iosdc-2024-albatross/backend/auth" ) type sockHandler struct { @@ -19,11 +20,13 @@ func newSockHandler(hubs *GameHubs) *sockHandler { } func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error { - // user := c.Get("user").(*auth.JWTClaims) - // if user == nil { - // return echo.NewHTTPError(http.StatusUnauthorized) - // } + jwt := c.QueryParam("token") + claims, err := auth.ParseJWT(jwt) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } // TODO: check user permission + gameId := c.Param("gameId") gameIdInt, err := strconv.Atoi(gameId) if err != nil { @@ -39,11 +42,19 @@ func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error { if foundHub == nil { return echo.NewHTTPError(http.StatusNotFound, "Game not found") } - return servePlayerWs(foundHub, c.Response(), c.Request(), 1) + return servePlayerWs(foundHub, c.Response(), c.Request(), claims.UserID) } func (h *sockHandler) HandleSockGolfWatch(c echo.Context) error { - // TODO: auth + jwt := c.QueryParam("token") + claims, err := auth.ParseJWT(jwt) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + if !claims.IsAdmin { + return echo.NewHTTPError(http.StatusForbidden, "Permission denied") + } + gameId := c.Param("gameId") gameIdInt, err := strconv.Atoi(gameId) if err != nil { diff --git a/backend/main.go b/backend/main.go index fe113dc..91caa73 100644 --- a/backend/main.go +++ b/backend/main.go @@ -71,7 +71,6 @@ func main() { } defer gameHubs.Close() sockGroup := e.Group("/sock") - // sockGroup.Use(api.NewEchoJWTMiddleware()) sockHandler := gameHubs.SockHandler() sockGroup.GET("/golf/:gameId/play", func(c echo.Context) error { return sockHandler.HandleSockGolfPlay(c) diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 1d3313e..cd409f7 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -64,6 +64,59 @@ export interface paths { patch?: never; trace?: never; }; + "/token": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a short-lived access token */ + get: { + parameters: { + query?: never; + header: { + Authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully authenticated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example xxxxx.xxxxx.xxxxx */ + token: string; + }; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Forbidden operation */ + message: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/games": { parameters: { query?: never; diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx index 13afb22..3cb512a 100644 --- a/frontend/app/components/GolfPlayApp.tsx +++ b/frontend/app/components/GolfPlayApp.tsx @@ -15,12 +15,18 @@ type Problem = components["schemas"]["Problem"]; type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished"; -export default function GolfPlayApp({ game }: { game: Game }) { - // const socketUrl = `wss://t.nil.ninja/iosdc/2024/sock/golf/${game.game_id}/play`; +export default function GolfPlayApp({ + game, + sockToken, +}: { + game: Game; + sockToken: string; +}) { + // const socketUrl = `wss://t.nil.ninja/iosdc/2024/sock/golf/${game.game_id}/play?token=${sockToken}`; const socketUrl = process.env.NODE_ENV === "development" - ? `ws://localhost:8002/sock/golf/${game.game_id}/play` - : `ws://api-server/sock/golf/${game.game_id}/play`; + ? `ws://localhost:8002/sock/golf/${game.game_id}/play?token=${sockToken}` + : `ws://api-server/sock/golf/${game.game_id}/play?token=${sockToken}`; const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket<WebSocketMessage>(socketUrl, {}); diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx index bcd1f0f..00ad005 100644 --- a/frontend/app/components/GolfWatchApp.tsx +++ b/frontend/app/components/GolfWatchApp.tsx @@ -14,12 +14,18 @@ type Problem = components["schemas"]["Problem"]; type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished"; -export default function GolfWatchApp({ game }: { game: Game }) { - // const socketUrl = `wss://t.nil.ninja/iosdc/2024/sock/golf/${game.game_id}/play`; +export default function GolfWatchApp({ + game, + sockToken, +}: { + game: Game; + sockToken: string; +}) { + // const socketUrl = `wss://t.nil.ninja/iosdc/2024/sock/golf/${game.game_id}/watch?token=${sockToken}`; const socketUrl = process.env.NODE_ENV === "development" - ? `ws://localhost:8002/sock/golf/${game.game_id}/play` - : `ws://api-server/sock/golf/${game.game_id}/play`; + ? `ws://localhost:8002/sock/golf/${game.game_id}/watch?token=${sockToken}` + : `ws://api-server/sock/golf/${game.game_id}/watch?token=${sockToken}`; const { lastJsonMessage, readyState } = useWebSocket<WebSocketMessage>( socketUrl, diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index bda563f..78ce585 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -8,26 +8,47 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const { token } = await isAuthenticated(request, { failureRedirect: "/login", }); - const { data, error } = await apiClient.GET("/games/{game_id}", { - params: { - path: { - game_id: Number(params.gameId), + + const fetchGame = async () => { + const { data, error } = await apiClient.GET("/games/{game_id}", { + params: { + path: { + game_id: Number(params.gameId), + }, + header: { + Authorization: `Bearer ${token}`, + }, }, - header: { - Authorization: `Bearer ${token}`, + }); + if (error) { + throw new Error(error.message); + } + return data; + }; + + const fetchSockToken = async () => { + const { data, error } = await apiClient.GET("/token", { + params: { + header: { + Authorization: `Bearer ${token}`, + }, }, - }, - }); - if (error) { - throw new Error(error.message); - } + }); + if (error) { + throw new Error(error.message); + } + return data.token; + }; + + const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); return { - game: data, + game, + sockToken, }; } export default function GolfPlay() { - const { game } = useLoaderData<typeof loader>(); + const { game, sockToken } = useLoaderData<typeof loader>(); - return <GolfPlayApp game={game} />; + return <GolfPlayApp game={game} sockToken={sockToken} />; } diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index e1cb5d7..28c17cc 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -8,26 +8,47 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const { token } = await isAuthenticated(request, { failureRedirect: "/login", }); - const { data, error } = await apiClient.GET("/games/{game_id}", { - params: { - path: { - game_id: Number(params.gameId), + + const fetchGame = async () => { + const { data, error } = await apiClient.GET("/games/{game_id}", { + params: { + path: { + game_id: Number(params.gameId), + }, + header: { + Authorization: `Bearer ${token}`, + }, }, - header: { - Authorization: `Bearer ${token}`, + }); + if (error) { + throw new Error(error.message); + } + return data; + }; + + const fetchSockToken = async () => { + const { data, error } = await apiClient.GET("/token", { + params: { + header: { + Authorization: `Bearer ${token}`, + }, }, - }, - }); - if (error) { - throw new Error(error.message); - } + }); + if (error) { + throw new Error(error.message); + } + return data.token; + }; + + const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); return { - game: data, + game, + sockToken, }; } export default function GolfWatch() { - const { game } = useLoaderData<typeof loader>(); + const { game, sockToken } = useLoaderData<typeof loader>(); - return <GolfWatchApp game={game} />; + return <GolfWatchApp game={game} sockToken={sockToken} />; } diff --git a/openapi.yaml b/openapi.yaml index fa6c8d0..739f013 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -47,6 +47,40 @@ paths: example: "Invalid credentials" required: - message + /token: + get: + summary: Get a short-lived access token + parameters: + - in: header + name: Authorization + schema: + type: string + required: true + responses: + '200': + description: Successfully authenticated + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: "xxxxx.xxxxx.xxxxx" + required: + - token + '403': + description: Forbidden + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Forbidden operation" + required: + - message /games: get: summary: List games |
