diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-13 23:46:16 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-13 23:46:16 +0900 |
| commit | 7258ca81812a24edd382438ce6e9ebc538549427 (patch) | |
| tree | 9bbc034be62777a2412d871211188268d7c56da4 | |
| parent | 7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (diff) | |
| download | phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.gz phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.tar.zst phperkaigi-2026-albatross-7258ca81812a24edd382438ce6e9ebc538549427.zip | |
feat(auth): store JWT in HTTP-only cookie instead of JS-accessible cookie
Prevent XSS-based token theft by making the JWT inaccessible to
JavaScript. The backend now sets/clears the cookie via Set-Cookie
headers, and the frontend retrieves user info from /api/me instead
of decoding the JWT directly.
- Add JWTCookieMiddleware to parse cookie and inject claims into context
- Add /me and /logout endpoints to OpenAPI spec and handlers
- Update PostLogin to return user object + Set-Cookie header
- Replace Authorization header auth with cookie-based auth throughout
- Rewrite frontend auth to use /api/me instead of jwt-decode
- Remove jwt-decode dependency
- Configure CORS with credentials for local dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| -rw-r--r-- | backend/api/auth_middleware.go | 32 | ||||
| -rw-r--r-- | backend/api/generated.go | 462 | ||||
| -rw-r--r-- | backend/api/handler.go | 90 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 80 | ||||
| -rw-r--r-- | backend/gen/api/handler_wrapper_gen.go | 24 | ||||
| -rw-r--r-- | backend/main.go | 8 | ||||
| -rw-r--r-- | frontend/app/api/client.ts | 37 | ||||
| -rw-r--r-- | frontend/app/api/schema.d.ts | 112 | ||||
| -rw-r--r-- | frontend/app/auth.ts | 42 | ||||
| -rw-r--r-- | frontend/app/components/ProtectedRoute.tsx | 6 | ||||
| -rw-r--r-- | frontend/app/components/PublicOnlyRoute.tsx | 6 | ||||
| -rw-r--r-- | frontend/app/hooks/useAuth.ts | 63 | ||||
| -rw-r--r-- | frontend/app/pages/DashboardPage.tsx | 9 | ||||
| -rw-r--r-- | frontend/app/pages/GolfPlayPage.tsx | 9 | ||||
| -rw-r--r-- | frontend/app/pages/GolfWatchPage.tsx | 9 | ||||
| -rw-r--r-- | frontend/app/pages/TournamentPage.tsx | 5 | ||||
| -rw-r--r-- | frontend/package-lock.json | 10 | ||||
| -rw-r--r-- | frontend/package.json | 1 | ||||
| -rw-r--r-- | openapi/api-server.yaml | 51 |
19 files changed, 524 insertions, 532 deletions
diff --git a/backend/api/auth_middleware.go b/backend/api/auth_middleware.go new file mode 100644 index 0000000..97f8946 --- /dev/null +++ b/backend/api/auth_middleware.go @@ -0,0 +1,32 @@ +package api + +import ( + "context" + + "github.com/labstack/echo/v4" + + "albatross-2026-backend/auth" +) + +type contextKey struct{} + +func JWTCookieMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + cookie, err := c.Cookie("albatross_token") + if err != nil { + return next(c) + } + claims, err := auth.ParseJWT(cookie.Value) + if err != nil { + return next(c) + } + ctx := context.WithValue(c.Request().Context(), contextKey{}, claims) + c.SetRequest(c.Request().WithContext(ctx)) + return next(c) + } +} + +func GetJWTClaimsFromContext(ctx context.Context) (*auth.JWTClaims, bool) { + claims, ok := ctx.Value(contextKey{}).(*auth.JWTClaims) + return claims, ok +} diff --git a/backend/api/generated.go b/backend/api/generated.go index 3c65e2f..636d761 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -122,9 +122,6 @@ type User struct { Username string `json:"username"` } -// HeaderAuthorization defines model for header_authorization. -type HeaderAuthorization = string - // PathGameID defines model for path_game_id. type PathGameID = int @@ -140,51 +137,16 @@ type NotFound = Error // Unauthorized defines model for Unauthorized. type Unauthorized = Error -// GetGamesParams defines parameters for GetGames. -type GetGamesParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// GetGameParams defines parameters for GetGame. -type GetGameParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - // PostGamePlayCodeJSONBody defines parameters for PostGamePlayCode. type PostGamePlayCodeJSONBody struct { Code string `json:"code"` } -// PostGamePlayCodeParams defines parameters for PostGamePlayCode. -type PostGamePlayCodeParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// GetGamePlayLatestStateParams defines parameters for GetGamePlayLatestState. -type GetGamePlayLatestStateParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - // PostGamePlaySubmitJSONBody defines parameters for PostGamePlaySubmit. type PostGamePlaySubmitJSONBody struct { Code string `json:"code"` } -// PostGamePlaySubmitParams defines parameters for PostGamePlaySubmit. -type PostGamePlaySubmitParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// GetGameWatchLatestStatesParams defines parameters for GetGameWatchLatestStates. -type GetGameWatchLatestStatesParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// GetGameWatchRankingParams defines parameters for GetGameWatchRanking. -type GetGameWatchRankingParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - // PostLoginJSONBody defines parameters for PostLogin. type PostLoginJSONBody struct { Password string `json:"password"` @@ -193,12 +155,11 @@ type PostLoginJSONBody struct { // GetTournamentParams defines parameters for GetTournament. type GetTournamentParams struct { - Game1 int `form:"game1" json:"game1"` - Game2 int `form:"game2" json:"game2"` - Game3 int `form:"game3" json:"game3"` - Game4 int `form:"game4" json:"game4"` - Game5 int `form:"game5" json:"game5"` - Authorization HeaderAuthorization `json:"Authorization"` + Game1 int `form:"game1" json:"game1"` + Game2 int `form:"game2" json:"game2"` + Game3 int `form:"game3" json:"game3"` + Game4 int `form:"game4" json:"game4"` + Game5 int `form:"game5" json:"game5"` } // PostGamePlayCodeJSONRequestBody defines body for PostGamePlayCode for application/json ContentType. @@ -214,28 +175,34 @@ type PostLoginJSONRequestBody PostLoginJSONBody type ServerInterface interface { // List games // (GET /games) - GetGames(ctx echo.Context, params GetGamesParams) error + GetGames(ctx echo.Context) error // Get a game // (GET /games/{game_id}) - GetGame(ctx echo.Context, gameID PathGameID, params GetGameParams) error + GetGame(ctx echo.Context, gameID PathGameID) error // Post the latest code // (POST /games/{game_id}/play/code) - PostGamePlayCode(ctx echo.Context, gameID PathGameID, params PostGamePlayCodeParams) error + PostGamePlayCode(ctx echo.Context, gameID PathGameID) error // Get the latest execution result for player // (GET /games/{game_id}/play/latest_state) - GetGamePlayLatestState(ctx echo.Context, gameID PathGameID, params GetGamePlayLatestStateParams) error + GetGamePlayLatestState(ctx echo.Context, gameID PathGameID) error // Submit the answer // (POST /games/{game_id}/play/submit) - PostGamePlaySubmit(ctx echo.Context, gameID PathGameID, params PostGamePlaySubmitParams) error + PostGamePlaySubmit(ctx echo.Context, gameID PathGameID) error // Get all the latest game states of the main players // (GET /games/{game_id}/watch/latest_states) - GetGameWatchLatestStates(ctx echo.Context, gameID PathGameID, params GetGameWatchLatestStatesParams) error + GetGameWatchLatestStates(ctx echo.Context, gameID PathGameID) error // Get the latest player ranking // (GET /games/{game_id}/watch/ranking) - GetGameWatchRanking(ctx echo.Context, gameID PathGameID, params GetGameWatchRankingParams) error + GetGameWatchRanking(ctx echo.Context, gameID PathGameID) error // User login // (POST /login) PostLogin(ctx echo.Context) error + // User logout + // (POST /logout) + PostLogout(ctx echo.Context) error + // Get current user + // (GET /me) + GetMe(ctx echo.Context) error // Get tournament bracket data // (GET /tournament) GetTournament(ctx echo.Context, params GetTournamentParams) error @@ -250,30 +217,8 @@ type ServerInterfaceWrapper struct { func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { var err error - // Parameter object where we will unmarshal all parameters from the context - var params GetGamesParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.GetGames(ctx, params) + err = w.Handler.GetGames(ctx) return err } @@ -288,30 +233,8 @@ func (w *ServerInterfaceWrapper) GetGame(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params GetGameParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.GetGame(ctx, gameID, params) + err = w.Handler.GetGame(ctx, gameID) return err } @@ -326,30 +249,8 @@ func (w *ServerInterfaceWrapper) PostGamePlayCode(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params PostGamePlayCodeParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.PostGamePlayCode(ctx, gameID, params) + err = w.Handler.PostGamePlayCode(ctx, gameID) return err } @@ -364,30 +265,8 @@ func (w *ServerInterfaceWrapper) GetGamePlayLatestState(ctx echo.Context) error return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params GetGamePlayLatestStateParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.GetGamePlayLatestState(ctx, gameID, params) + err = w.Handler.GetGamePlayLatestState(ctx, gameID) return err } @@ -402,30 +281,8 @@ func (w *ServerInterfaceWrapper) PostGamePlaySubmit(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params PostGamePlaySubmitParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.PostGamePlaySubmit(ctx, gameID, params) + err = w.Handler.PostGamePlaySubmit(ctx, gameID) return err } @@ -440,30 +297,8 @@ func (w *ServerInterfaceWrapper) GetGameWatchLatestStates(ctx echo.Context) erro return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params GetGameWatchLatestStatesParams - - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.GetGameWatchLatestStates(ctx, gameID, params) + err = w.Handler.GetGameWatchLatestStates(ctx, gameID) return err } @@ -478,39 +313,35 @@ func (w *ServerInterfaceWrapper) GetGameWatchRanking(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err)) } - // Parameter object where we will unmarshal all parameters from the context - var params GetGameWatchRankingParams + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetGameWatchRanking(ctx, gameID) + return err +} - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n)) - } +// PostLogin converts echo context to params. +func (w *ServerInterfaceWrapper) PostLogin(ctx echo.Context) error { + var err error - 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)) - } + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostLogin(ctx) + return err +} - params.Authorization = Authorization - } else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found")) - } +// PostLogout converts echo context to params. +func (w *ServerInterfaceWrapper) PostLogout(ctx echo.Context) error { + var err error // Invoke the callback with all the unmarshaled arguments - err = w.Handler.GetGameWatchRanking(ctx, gameID, params) + err = w.Handler.PostLogout(ctx) return err } -// PostLogin converts echo context to params. -func (w *ServerInterfaceWrapper) PostLogin(ctx echo.Context) error { +// GetMe converts echo context to params. +func (w *ServerInterfaceWrapper) GetMe(ctx echo.Context) error { var err error // Invoke the callback with all the unmarshaled arguments - err = w.Handler.PostLogin(ctx) + err = w.Handler.GetMe(ctx) return err } @@ -555,25 +386,6 @@ func (w *ServerInterfaceWrapper) GetTournament(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game5: %s", err)) } - headers := ctx.Request().Header - // ------------- Required header parameter "Authorization" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found { - var Authorization HeaderAuthorization - 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.GetTournament(ctx, params) return err @@ -615,6 +427,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/games/:game_id/watch/latest_states", wrapper.GetGameWatchLatestStates) router.GET(baseURL+"/games/:game_id/watch/ranking", wrapper.GetGameWatchRanking) router.POST(baseURL+"/login", wrapper.PostLogin) + router.POST(baseURL+"/logout", wrapper.PostLogout) + router.GET(baseURL+"/me", wrapper.GetMe) router.GET(baseURL+"/tournament", wrapper.GetTournament) } @@ -628,7 +442,6 @@ type NotFoundJSONResponse Error type UnauthorizedJSONResponse Error type GetGamesRequestObject struct { - Params GetGamesParams } type GetGamesResponseObject interface { @@ -666,7 +479,6 @@ func (response GetGames403JSONResponse) VisitGetGamesResponse(w http.ResponseWri type GetGameRequestObject struct { GameID PathGameID `json:"game_id"` - Params GetGameParams } type GetGameResponseObject interface { @@ -713,7 +525,6 @@ func (response GetGame404JSONResponse) VisitGetGameResponse(w http.ResponseWrite type PostGamePlayCodeRequestObject struct { GameID PathGameID `json:"game_id"` - Params PostGamePlayCodeParams Body *PostGamePlayCodeJSONRequestBody } @@ -758,7 +569,6 @@ func (response PostGamePlayCode404JSONResponse) VisitPostGamePlayCodeResponse(w type GetGamePlayLatestStateRequestObject struct { GameID PathGameID `json:"game_id"` - Params GetGamePlayLatestStateParams } type GetGamePlayLatestStateResponseObject interface { @@ -805,7 +615,6 @@ func (response GetGamePlayLatestState404JSONResponse) VisitGetGamePlayLatestStat type PostGamePlaySubmitRequestObject struct { GameID PathGameID `json:"game_id"` - Params PostGamePlaySubmitParams Body *PostGamePlaySubmitJSONRequestBody } @@ -850,7 +659,6 @@ func (response PostGamePlaySubmit404JSONResponse) VisitPostGamePlaySubmitRespons type GetGameWatchLatestStatesRequestObject struct { GameID PathGameID `json:"game_id"` - Params GetGameWatchLatestStatesParams } type GetGameWatchLatestStatesResponseObject interface { @@ -897,7 +705,6 @@ func (response GetGameWatchLatestStates404JSONResponse) VisitGetGameWatchLatestS type GetGameWatchRankingRequestObject struct { GameID PathGameID `json:"game_id"` - Params GetGameWatchRankingParams } type GetGameWatchRankingResponseObject interface { @@ -951,7 +758,7 @@ type PostLoginResponseObject interface { } type PostLogin200JSONResponse struct { - Token string `json:"token"` + User User `json:"user"` } func (response PostLogin200JSONResponse) VisitPostLoginResponse(w http.ResponseWriter) error { @@ -970,6 +777,57 @@ func (response PostLogin401JSONResponse) VisitPostLoginResponse(w http.ResponseW return json.NewEncoder(w).Encode(response) } +type PostLogoutRequestObject struct { +} + +type PostLogoutResponseObject interface { + VisitPostLogoutResponse(w http.ResponseWriter) error +} + +type PostLogout200Response struct { +} + +func (response PostLogout200Response) VisitPostLogoutResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type PostLogout401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response PostLogout401JSONResponse) VisitPostLogoutResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetMeRequestObject struct { +} + +type GetMeResponseObject interface { + VisitGetMeResponse(w http.ResponseWriter) error +} + +type GetMe200JSONResponse struct { + User User `json:"user"` +} + +func (response GetMe200JSONResponse) VisitGetMeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetMe401JSONResponse struct{ UnauthorizedJSONResponse } + +func (response GetMe401JSONResponse) VisitGetMeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + type GetTournamentRequestObject struct { Params GetTournamentParams } @@ -1042,6 +900,12 @@ type StrictServerInterface interface { // User login // (POST /login) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) + // User logout + // (POST /logout) + PostLogout(ctx context.Context, request PostLogoutRequestObject) (PostLogoutResponseObject, error) + // Get current user + // (GET /me) + GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) // Get tournament bracket data // (GET /tournament) GetTournament(ctx context.Context, request GetTournamentRequestObject) (GetTournamentResponseObject, error) @@ -1060,11 +924,9 @@ type strictHandler struct { } // GetGames operation middleware -func (sh *strictHandler) GetGames(ctx echo.Context, params GetGamesParams) error { +func (sh *strictHandler) GetGames(ctx echo.Context) error { var request GetGamesRequestObject - request.Params = params - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.GetGames(ctx.Request().Context(), request.(GetGamesRequestObject)) } @@ -1085,11 +947,10 @@ func (sh *strictHandler) GetGames(ctx echo.Context, params GetGamesParams) error } // GetGame operation middleware -func (sh *strictHandler) GetGame(ctx echo.Context, gameID PathGameID, params GetGameParams) error { +func (sh *strictHandler) GetGame(ctx echo.Context, gameID PathGameID) error { var request GetGameRequestObject request.GameID = gameID - request.Params = params handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.GetGame(ctx.Request().Context(), request.(GetGameRequestObject)) @@ -1111,11 +972,10 @@ func (sh *strictHandler) GetGame(ctx echo.Context, gameID PathGameID, params Get } // PostGamePlayCode operation middleware -func (sh *strictHandler) PostGamePlayCode(ctx echo.Context, gameID PathGameID, params PostGamePlayCodeParams) error { +func (sh *strictHandler) PostGamePlayCode(ctx echo.Context, gameID PathGameID) error { var request PostGamePlayCodeRequestObject request.GameID = gameID - request.Params = params var body PostGamePlayCodeJSONRequestBody if err := ctx.Bind(&body); err != nil { @@ -1143,11 +1003,10 @@ func (sh *strictHandler) PostGamePlayCode(ctx echo.Context, gameID PathGameID, p } // GetGamePlayLatestState operation middleware -func (sh *strictHandler) GetGamePlayLatestState(ctx echo.Context, gameID PathGameID, params GetGamePlayLatestStateParams) error { +func (sh *strictHandler) GetGamePlayLatestState(ctx echo.Context, gameID PathGameID) error { var request GetGamePlayLatestStateRequestObject request.GameID = gameID - request.Params = params handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.GetGamePlayLatestState(ctx.Request().Context(), request.(GetGamePlayLatestStateRequestObject)) @@ -1169,11 +1028,10 @@ func (sh *strictHandler) GetGamePlayLatestState(ctx echo.Context, gameID PathGam } // PostGamePlaySubmit operation middleware -func (sh *strictHandler) PostGamePlaySubmit(ctx echo.Context, gameID PathGameID, params PostGamePlaySubmitParams) error { +func (sh *strictHandler) PostGamePlaySubmit(ctx echo.Context, gameID PathGameID) error { var request PostGamePlaySubmitRequestObject request.GameID = gameID - request.Params = params var body PostGamePlaySubmitJSONRequestBody if err := ctx.Bind(&body); err != nil { @@ -1201,11 +1059,10 @@ func (sh *strictHandler) PostGamePlaySubmit(ctx echo.Context, gameID PathGameID, } // GetGameWatchLatestStates operation middleware -func (sh *strictHandler) GetGameWatchLatestStates(ctx echo.Context, gameID PathGameID, params GetGameWatchLatestStatesParams) error { +func (sh *strictHandler) GetGameWatchLatestStates(ctx echo.Context, gameID PathGameID) error { var request GetGameWatchLatestStatesRequestObject request.GameID = gameID - request.Params = params handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.GetGameWatchLatestStates(ctx.Request().Context(), request.(GetGameWatchLatestStatesRequestObject)) @@ -1227,11 +1084,10 @@ func (sh *strictHandler) GetGameWatchLatestStates(ctx echo.Context, gameID PathG } // GetGameWatchRanking operation middleware -func (sh *strictHandler) GetGameWatchRanking(ctx echo.Context, gameID PathGameID, params GetGameWatchRankingParams) error { +func (sh *strictHandler) GetGameWatchRanking(ctx echo.Context, gameID PathGameID) error { var request GetGameWatchRankingRequestObject request.GameID = gameID - request.Params = params handler := func(ctx echo.Context, request interface{}) (interface{}, error) { return sh.ssi.GetGameWatchRanking(ctx.Request().Context(), request.(GetGameWatchRankingRequestObject)) @@ -1281,6 +1137,52 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error { return nil } +// PostLogout operation middleware +func (sh *strictHandler) PostLogout(ctx echo.Context) error { + var request PostLogoutRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.PostLogout(ctx.Request().Context(), request.(PostLogoutRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostLogout") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(PostLogoutResponseObject); ok { + return validResponse.VisitPostLogoutResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + +// GetMe operation middleware +func (sh *strictHandler) GetMe(ctx echo.Context) error { + var request GetMeRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetMe(ctx.Request().Context(), request.(GetMeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetMe") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetMeResponseObject); ok { + return validResponse.VisitGetMeResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // GetTournament operation middleware func (sh *strictHandler) GetTournament(ctx echo.Context, params GetTournamentParams) error { var request GetTournamentRequestObject @@ -1309,33 +1211,33 @@ func (sh *strictHandler) GetTournament(ctx echo.Context, params GetTournamentPar // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+xa62/bNhD/VwRuQL+osfNY0GWf0q0tOnRF0AeGoQgEWjrbTClSJak6XqH/fThSsl6U", - "LadOtxTrh8KWeMd7/O7HO8ZfSCzTTAoQRpOLLySjiqZgQNlvS6AJqIjmZikV+5saJgU+Z4JclC9JSARN", - "gVyQy9aqkCj4lDMFCbkwKoeQ6HgJKUVxs85QQBvFxIIURUgyapbRgqYQsWSzAT6s1VdvRyhmwsACFClQ", - "tQKdSaHBOvSUJm/gUw7a4LdYCgPCfqRZxllsTZ/caOdlrfdHBXNyQX6Y1MGauLd68kwpWW6VgI4Vy1yU", - "cK9AlZsVIXku1YwlCYj737neqgjJa2mey1wk97/ta2mCud2qCMl7UaEGvsHWrd3wdSmBCp0QYlvJDJRh", - "DgopaE0XgB/hlqYZR+S8FJ8pZ3XeQg9Wa/h92Ci53iyUsxuIbcKf3UKco31vDTW53RNEnqKYkAIQyLkQ", - "qDUkOo9j0JqEZKWkWERU6JWtLcNSkDkaguFgHCKw7lhhfLn5jqBXgvLywXXYcKtW33EnJC9sdXWDkzCd", - "cbqORPm2VoXrg2OfpiRXNqeRhliKRLfkTs+nYa88Q9Io+c3S48GF7nEdxuPPaEiac8PQWuh47V737GQ6", - "yvIZZ3FrV0cl5eKZlByoLZ+UMhE57dYjZiDVu3D6XjurS3VUKbrG75mSMw7pLvGrchni2FBlIImoaVn7", - "89n5+ZOzJ9N+UENy+3ghH9dPz896qK2ptA5rMy5hO/+e1NaudCLkK4RX1IA2CBysBA/aZqBNpGOpINL5", - "LGVmu8ci55zOeknbGgGsn6QDZYiXMni0BM5lsJKKJ49+8eHFGtZG6AgrXO7Kut/KaR2a6CbL2l1ZEQ7G", - "arOdLwNXNe46dd5k0WZs3i2ZDpgOaFAnuhcZTsUirzi0rMlsmaEtKzY37Wp0L3o6SvWjSEDbl9GdM2mY", - "4R3JMjI+RuvkoWFopal9CjXi0TbVl5E3VHxkYvFMGLXup2WshwMYbETX0eJIuhpAuicRI6r0DrxUWluD", - "vYPwwWi+k7lCqnL9ReecpyZewnjurnX9gZJ9Gu+2AKX+7XY5XT3jRp9/LjTHYzNZLo88GR3WfrKf9pOx", - "2ldMCAfCbesGzihfWK0de/Qsv8ulCH6T4O0GYikiO2m0RCYspQvQkxu5FEc32WKgkaBJytrMOadcexsJ", - "TmfA25toQ+fzMWWca1A9mJyc+oKNS/sRQDd2Mly1S0NJrxPY+Fw51M8PqmViLu1A5hiXXPIZNUpqHVRt", - "arCCWXB59ZKE5DMo7dr46dHJ0RS9kBkImjFyQU6PpkdT4iZEm+YJIsMVD9hyRwzY7uRlgv0p2E7DtiiN", - "WfaDH9n1kol31i2uOwPkyXS61zTTL/fxTGRb813041T6s9CekV4xbQI5D5xEEZKz6SCfbHyetCcrFDrd", - "LdQYQO1pkaYUjzlnQrl/EZapnHwpa73YldQD5TTcKde6jrgHDIzLvCfToxJ9aUP8zTKMEme7JTb3EG1I", - "vAAT0NJgDyQmSD6Tqh/KpPag40q66eKK0/Wvrlv+12Birw2eymT9FQi5Y3/rmxv8cGlfoBV+eLcx9dbd", - "IMxzztdBniXUVGD5zyMM4RGYJQTczqGBDcww1tyqSFej6jZCQsS56dZNtt8DP20c30ZQ3ZG+Cz6nZAxZ", - "/SVzVWUGPQqc6MMhrwayoBrlAwU65yaYSxWUQ80w4NyQM47e3rq1/xPc/RLcZu58IDh0sLBQLG+OvXBb", - "4RDaIridffSfKNKgOP39cJz9RJOEoQjlV60Ve5FfH4MeNhzXpF9y3mSUmhE1tu74JqVMBNV16wPq8fZ0", - "bBi/yl2djUJuec32XYC24feo8bF1xbhrjKyUj8Holc1RUIk8yKM66/iAcONy4S5zhs/hV3bJoc7BjGq9", - "kqp9r7N5enxySgbugr7igkdUc3S59VccoHf02siP0Plrwy3+O2r8v9MVp2QMWlsHOyIQhEFT73y4t2D1", - "XoMKHHAshkzrJnqInxr31QdjJvuTiU85qHX7NxPH+/1iYoumk4NpOj2YprODafppP03XB66JJmzG/XnC", - "UxObV2MKo9YUJNTQh8TjteUzReOPUHlQFMU/AQAA//9Rv7470SQAAA==", + "H4sIAAAAAAAC/+xaW2/buBL+KwTPAfqixs7lBD3Zp7TbFl20hdELFouiEGhpbDOlSJWk6noL/ffFkJJ1", + "t+XELRJg3yKJM5yZ75vhDOMfNFJJqiRIa+jVD5oyzRKwoIsnuwqXLIGQx/jMJb1yL2lAJUuAXtHya0A1", + "fM24hpheWZ1BQE20goShmN2kuJRLC0vQNM9zXG1SJQ24fZ6y+B18zcBYfIqUtCDdnyxNBY+Y5UpOboyS", + "+K7S+18NC3pF/zOpfJj4r2byXGtVbBWDiTRPUQm9wr2ILjbLA/pC6TmPY5A/f+dqqzygb5V9oTIZ//xt", + "3ypLFm6rPKAfJcvsSmn+N/yCrRu74edCAhV6ISSZViloyz0VEjCGLQH/hO8sSQUy55X8xgSvcAtKShmr", + "uVxST6iSfp+2Sj5vF6r5DUQO8OffIcrQvveW2cztCTJLUEwqCUjkTErUGlCTRREYQwO61kouQybNGjRu", + "zxNQGRqC4eACQnDuOGH8uH1G0mvJRPHic1Bzq1LfciegL112tYMTc5MKtgll8bVShevJaZ+mONMO09BA", + "pGRsGnLnl9Ogk54BraX8dunp4EL/ugrj6Tc0JMmE5WgttLz2nzt2chOm2VzwqLGrLyXF4rlSAphLn4Rx", + "GXrtziNuITH7ePrReKsLdUxrtsHnVKu5gGSf+KxYhjy2TFuIQ2Yb1v7/4vLyycWTaTeoAf3+eKkeV28v", + "LzqsrUppFdZ6XIIm/j3QVq60ItSXCK+ZBWOROJgJPWybg7GhiZSG0GTzhNvdHstMCDbvgLYzApg/cYvK", + "EK0UebQCIRRZKy3iR7/18cUZ1mToCCs8dkXe76xprTLRBsvZXVoRDMZqu10fArOKd608r1fRemw+rLgh", + "3BBGKqA7kRFMLrOyhhY5ma5StGXNF7aZjf5DR0ehflQRMO5jeGskLbeiJVlEpq+itXCoGVpqap5CtXg0", + "Te1D5B2TX7hcPpdWb7qwjPVwgIO16PqyOLJcDTC9B4gRWXqLulRYW5G9xfDBaH5QmcZS5fuL1jnPbLSC", + "8bW70vUGJbtlvN0CFPp32+V1dYwbff750JyORbJYHvYgOqz97DDtZ2O1r7mUnoS71g2cUX1hdXYc0LP8", + "oVaS/K6gtxuIlAzdpNEQmfCELcFMbtRKntyky4FGgsUJb1bOBROmt5EQbA6iuYmxbLEYk8aZAd2hydl5", + "X7BxaTcC6MbeClfuUlPS6QS2PpcOdfFBtVwulBvIfMWl12LOrFbGkLJNJWuYk+vZKxrQb6CNb+OnJ+cn", + "U/RCpSBZyukVPT+Znkyx32B25WCeIDN88oBLd+SA605exdifgus0DG1NfmfT6UFjSDdPx5cQ11Pvqxte", + "ZX/4msPNa24sUQviJfKAXkwHC8HW50lzJEKh8/1CtcnRlfkkYXg+eROK/fOgwGDyo0jSfB8aDr9q3v/U", + "b0a1ZNK4D8g/Hx3LcQj2IDYKsGsXql+GFEpc7JfYXgQ0oX0JlrDC4B5oJ5j9k7IhSZXpQXmmfHs/E2zz", + "zLerd4bbzd9PVby5A9K3bBT7GvB+2Js3UXk/TZvceO9H8UUmxIZkacxsCfq9ZwrCTOwKiHADHXGBGeaM", + "XxWacubbVSCQOX5M9CPifaoXWwd2FYz2jNsmkVcypnj8pTJdRhg9Il704RSTGkOgnG2JBpMJSxZKk6LL", + "HyaO7/rHlZv3fu2/BWdcwdkOVA+ETx5eR6niSrSXNmucrhoFZ2+D+CeK1EqOuX81x/3F4pijCBOzxoqD", + "ilGXSz3VaVwzei1EPcOrCmWwRcUvCeOSlPeBD6gHOtCxYR5qf7czioHFPdC9Il/N/lHjTuMua9/YUyof", + "w7WZizUpRR7kEZi2fEDaCLX0twbD59trt+RY51LKjFkr3bxA2L49PTunA5cOd7hJkOXcV2x9hwPtll5n", + "ZuzVZ4/5owjaOFuRdCAtWnfr87XBJLSNeK6UtFGZ3csb/+/CAxsDoZZLiAnKHtFypw5NT3ZOAG+APjDg", + "n2Vag7QEBYi77DpC1LByRDXFPnS2cbc9FMLaDXjnKHE/pviagd40f01xethvKYJhTWdH03R+NE0XR9P0", + "v8M0HfcwbsI/7h8XHV7XlIxhd6WJxMyyh3TwVpbPNYu+QOlBnuf/BAAA//9b1VG2giQAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index d2883a9..3b04665 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "errors" "log" "net/http" @@ -12,12 +13,14 @@ import ( "github.com/oapi-codegen/nullable" "albatross-2026-backend/auth" + "albatross-2026-backend/config" "albatross-2026-backend/db" ) type Handler struct { - q *db.Queries - hub GameHubInterface + q *db.Queries + hub GameHubInterface + conf *config.Config } type GameHubInterface interface { @@ -25,6 +28,18 @@ type GameHubInterface interface { EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error } +type postLoginCookieResponse struct { + cookie http.Cookie + body PostLogin200JSONResponse +} + +func (r postLoginCookieResponse) VisitPostLoginResponse(w http.ResponseWriter) error { + http.SetCookie(w, &r.cookie) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + return json.NewEncoder(w).Encode(r.body) +} + func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password @@ -44,7 +59,7 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) }, nil } - user, err := h.q.GetUserByID(ctx, int32(userID)) + dbUser, err := h.q.GetUserByID(ctx, int32(userID)) if err != nil { return PostLogin401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ @@ -53,13 +68,76 @@ func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) }, nil } - jwt, err := auth.NewJWT(&user) + jwt, err := auth.NewJWT(&dbUser) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return PostLogin200JSONResponse{ - Token: jwt, + return postLoginCookieResponse{ + cookie: http.Cookie{ + Name: "albatross_token", + Value: jwt, + Path: h.conf.BasePath, + MaxAge: 86400, + HttpOnly: true, + Secure: !h.conf.IsLocal, + SameSite: http.SameSiteLaxMode, + }, + body: PostLogin200JSONResponse{ + User: User{ + UserID: int(dbUser.UserID), + Username: dbUser.Username, + DisplayName: dbUser.DisplayName, + IconPath: dbUser.IconPath, + IsAdmin: dbUser.IsAdmin, + Label: toNullable(dbUser.Label), + }, + }, + }, nil +} + +func (h *Handler) GetMe(ctx context.Context, _ GetMeRequestObject, claims *auth.JWTClaims) (GetMeResponseObject, error) { + dbUser, err := h.q.GetUserByID(ctx, int32(claims.UserID)) + if err != nil { + return GetMe401JSONResponse{ + UnauthorizedJSONResponse: UnauthorizedJSONResponse{ + Message: "Unauthorized", + }, + }, nil + } + return GetMe200JSONResponse{ + User: User{ + UserID: int(dbUser.UserID), + Username: dbUser.Username, + DisplayName: dbUser.DisplayName, + IconPath: dbUser.IconPath, + IsAdmin: dbUser.IsAdmin, + Label: toNullable(dbUser.Label), + }, + }, nil +} + +type postLogoutCookieResponse struct { + cookie http.Cookie +} + +func (r postLogoutCookieResponse) VisitPostLogoutResponse(w http.ResponseWriter) error { + http.SetCookie(w, &r.cookie) + w.WriteHeader(200) + return nil +} + +func (h *Handler) PostLogout(_ context.Context, _ PostLogoutRequestObject, _ *auth.JWTClaims) (PostLogoutResponseObject, error) { + return postLogoutCookieResponse{ + cookie: http.Cookie{ + Name: "albatross_token", + Value: "", + Path: h.conf.BasePath, + MaxAge: -1, + HttpOnly: true, + Secure: !h.conf.IsLocal, + SameSite: http.SameSiteLaxMode, + }, }, nil } diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index cfa9575..b88ddb2 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -4,10 +4,8 @@ package api import ( "context" - "errors" - "strings" - "albatross-2026-backend/auth" + "albatross-2026-backend/config" "albatross-2026-backend/db" ) @@ -17,31 +15,19 @@ type HandlerWrapper struct { impl Handler } -func NewHandler(queries *db.Queries, hub GameHubInterface) *HandlerWrapper { +func NewHandler(queries *db.Queries, hub GameHubInterface, conf *config.Config) *HandlerWrapper { return &HandlerWrapper{ impl: Handler{ - q: queries, - hub: hub, + q: queries, + hub: hub, + conf: conf, }, } } -func parseJWTClaimsFromAuthorizationHeader(authorization string) (*auth.JWTClaims, error) { - const prefix = "Bearer " - if !strings.HasPrefix(authorization, prefix) { - return nil, errors.New("invalid authorization header") - } - token := authorization[len(prefix):] - claims, err := auth.ParseJWT(token) - if err != nil { - return nil, err - } - return claims, nil -} - func (h *HandlerWrapper) GetGame(ctx context.Context, request GetGameRequestObject) (GetGameResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetGame401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -52,8 +38,8 @@ func (h *HandlerWrapper) GetGame(ctx context.Context, request GetGameRequestObje } func (h *HandlerWrapper) GetGamePlayLatestState(ctx context.Context, request GetGamePlayLatestStateRequestObject) (GetGamePlayLatestStateResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetGamePlayLatestState401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -64,8 +50,8 @@ func (h *HandlerWrapper) GetGamePlayLatestState(ctx context.Context, request Get } func (h *HandlerWrapper) GetGameWatchLatestStates(ctx context.Context, request GetGameWatchLatestStatesRequestObject) (GetGameWatchLatestStatesResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetGameWatchLatestStates401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -76,8 +62,8 @@ func (h *HandlerWrapper) GetGameWatchLatestStates(ctx context.Context, request G } func (h *HandlerWrapper) GetGameWatchRanking(ctx context.Context, request GetGameWatchRankingRequestObject) (GetGameWatchRankingResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetGameWatchRanking401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -88,8 +74,8 @@ func (h *HandlerWrapper) GetGameWatchRanking(ctx context.Context, request GetGam } func (h *HandlerWrapper) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetGames401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -99,9 +85,21 @@ func (h *HandlerWrapper) GetGames(ctx context.Context, request GetGamesRequestOb return h.impl.GetGames(ctx, request, user) } +func (h *HandlerWrapper) GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { + return GetMe401JSONResponse{ + UnauthorizedJSONResponse: UnauthorizedJSONResponse{ + Message: "Unauthorized", + }, + }, nil + } + return h.impl.GetMe(ctx, request, user) +} + func (h *HandlerWrapper) GetTournament(ctx context.Context, request GetTournamentRequestObject) (GetTournamentResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return GetTournament401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -112,8 +110,8 @@ func (h *HandlerWrapper) GetTournament(ctx context.Context, request GetTournamen } func (h *HandlerWrapper) PostGamePlayCode(ctx context.Context, request PostGamePlayCodeRequestObject) (PostGamePlayCodeResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return PostGamePlayCode401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -124,8 +122,8 @@ func (h *HandlerWrapper) PostGamePlayCode(ctx context.Context, request PostGameP } func (h *HandlerWrapper) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySubmitRequestObject) (PostGamePlaySubmitResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return PostGamePlaySubmit401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", @@ -138,3 +136,15 @@ func (h *HandlerWrapper) PostGamePlaySubmit(ctx context.Context, request PostGam func (h *HandlerWrapper) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { return h.impl.PostLogin(ctx, request) } + +func (h *HandlerWrapper) PostLogout(ctx context.Context, request PostLogoutRequestObject) (PostLogoutResponseObject, error) { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { + return PostLogout401JSONResponse{ + UnauthorizedJSONResponse: UnauthorizedJSONResponse{ + Message: "Unauthorized", + }, + }, nil + } + return h.impl.PostLogout(ctx, request, user) +} diff --git a/backend/gen/api/handler_wrapper_gen.go b/backend/gen/api/handler_wrapper_gen.go index 1aeaba7..5a5ce2d 100644 --- a/backend/gen/api/handler_wrapper_gen.go +++ b/backend/gen/api/handler_wrapper_gen.go @@ -104,10 +104,8 @@ package api import ( "context" - "errors" - "strings" - "albatross-2026-backend/auth" + "albatross-2026-backend/config" "albatross-2026-backend/db" ) @@ -117,33 +115,21 @@ type HandlerWrapper struct { impl Handler } -func NewHandler(queries *db.Queries, hub GameHubInterface) *HandlerWrapper { +func NewHandler(queries *db.Queries, hub GameHubInterface, conf *config.Config) *HandlerWrapper { return &HandlerWrapper{ impl: Handler{ q: queries, hub: hub, + conf: conf, }, } } -func parseJWTClaimsFromAuthorizationHeader(authorization string) (*auth.JWTClaims, error) { - const prefix = "Bearer " - if !strings.HasPrefix(authorization, prefix) { - return nil, errors.New("invalid authorization header") - } - token := authorization[len(prefix):] - claims, err := auth.ParseJWT(token) - if err != nil { - return nil, err - } - return claims, nil -} - {{ range . }} func (h *HandlerWrapper) {{ .Name }}(ctx context.Context, request {{ .Name }}RequestObject) ({{ .Name }}ResponseObject, error) { {{ if .RequiresLogin -}} - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { + user, ok := GetJWTClaimsFromContext(ctx) + if !ok { return {{ .Name }}401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Unauthorized", diff --git a/backend/main.go b/backend/main.go index 40fb8f0..29edfdb 100644 --- a/backend/main.go +++ b/backend/main.go @@ -73,8 +73,9 @@ func main() { apiGroup := e.Group(conf.BasePath + "api") apiGroup.Use(ratelimit.LoginRateLimitMiddleware(loginRL)) + apiGroup.Use(api.JWTCookieMiddleware) apiGroup.Use(oapimiddleware.OapiRequestValidator(openAPISpec)) - apiHandler := api.NewHandler(queries, gameHub) + apiHandler := api.NewHandler(queries, gameHub, conf) api.RegisterHandlers(apiGroup, api.NewStrictHandler(apiHandler, nil)) adminHandler := admin.NewHandler(queries, conf) @@ -97,7 +98,10 @@ func main() { }) // Allow access from dev server. - e.Use(middleware.CORS()) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"http://localhost:5173"}, + AllowCredentials: true, + })) } go gameHub.Run() diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index 86f2506..26c20d1 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -9,6 +9,7 @@ const apiOrigin = const client = createClient<paths>({ baseUrl: `${apiOrigin}${API_BASE_PATH}`, + credentials: "include", }); export async function apiLogin(username: string, password: string) { @@ -22,15 +23,20 @@ export async function apiLogin(username: string, password: string) { return data; } -class AuthenticatedApiClient { - constructor(public readonly token: string) {} +export async function apiLogout() { + const { error } = await client.POST("/logout"); + if (error) throw new Error(error.message); +} +export async function apiGetMe() { + const { data, error } = await client.GET("/me"); + if (error) return null; + return data; +} + +class AuthenticatedApiClient { async getGames() { - const { data, error } = await client.GET("/games", { - params: { - header: this._getAuthorizationHeader(), - }, - }); + const { data, error } = await client.GET("/games"); if (error) throw new Error(error.message); return data; } @@ -38,7 +44,6 @@ class AuthenticatedApiClient { async getGame(gameId: number) { const { data, error } = await client.GET("/games/{game_id}", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }); @@ -51,7 +56,6 @@ class AuthenticatedApiClient { "/games/{game_id}/play/latest_state", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }, @@ -63,7 +67,6 @@ class AuthenticatedApiClient { async postGamePlayCode(gameId: number, code: string) { const { error } = await client.POST("/games/{game_id}/play/code", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, body: { code }, @@ -74,7 +77,6 @@ class AuthenticatedApiClient { async postGamePlaySubmit(gameId: number, code: string) { const { data, error } = await client.POST("/games/{game_id}/play/submit", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, body: { code }, @@ -86,7 +88,6 @@ class AuthenticatedApiClient { async getGameWatchRanking(gameId: number) { const { data, error } = await client.GET("/games/{game_id}/watch/ranking", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }); @@ -99,7 +100,6 @@ class AuthenticatedApiClient { "/games/{game_id}/watch/latest_states", { params: { - header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, }, @@ -117,21 +117,18 @@ class AuthenticatedApiClient { ) { const { data, error } = await client.GET("/tournament", { params: { - header: this._getAuthorizationHeader(), query: { game1, game2, game3, game4, game5 }, }, }); if (error) throw new Error(error.message); return data; } - - _getAuthorizationHeader() { - return { Authorization: `Bearer ${this.token}` }; - } } -export function createApiClient(token: string) { - return new AuthenticatedApiClient(token); +const apiClient = new AuthenticatedApiClient(); + +export function createApiClient() { + return apiClient; } export const ApiClientContext = createContext<AuthenticatedApiClient | null>( diff --git a/frontend/app/api/schema.d.ts b/frontend/app/api/schema.d.ts index 04bfc10..6f9e270 100644 --- a/frontend/app/api/schema.d.ts +++ b/frontend/app/api/schema.d.ts @@ -21,6 +21,40 @@ export interface paths { patch?: never; trace?: never; }; + "/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** User logout */ + post: operations["postLogout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get current user */ + get: operations["getMe"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/games": { parameters: { query?: never; @@ -291,7 +325,6 @@ export interface components { }; }; parameters: { - header_authorization: string; path_game_id: number; }; requestBodies: never; @@ -325,20 +358,59 @@ export interface operations { }; content: { "application/json": { - /** @example xxxxx.xxxxx.xxxxx */ - token: string; + user: components["schemas"]["User"]; }; }; }; 401: components["responses"]["Unauthorized"]; }; }; - getGames: { + postLogout: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully logged out */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; }; + 401: components["responses"]["Unauthorized"]; + }; + }; + getMe: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + user: components["schemas"]["User"]; + }; + }; + }; + 401: components["responses"]["Unauthorized"]; + }; + }; + getGames: { + parameters: { + query?: never; + header?: never; path?: never; cookie?: never; }; @@ -362,9 +434,7 @@ export interface operations { getGame: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -391,9 +461,7 @@ export interface operations { getGamePlayLatestState: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -420,9 +488,7 @@ export interface operations { postGamePlayCode: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -452,9 +518,7 @@ export interface operations { postGamePlaySubmit: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -484,9 +548,7 @@ export interface operations { getGameWatchRanking: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -513,9 +575,7 @@ export interface operations { getGameWatchLatestStates: { parameters: { query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path: { game_id: components["parameters"]["path_game_id"]; }; @@ -550,9 +610,7 @@ export interface operations { game4: number; game5: number; }; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; + header?: never; path?: never; cookie?: never; }; diff --git a/frontend/app/auth.ts b/frontend/app/auth.ts index 7a3d10d..769ac27 100644 --- a/frontend/app/auth.ts +++ b/frontend/app/auth.ts @@ -1,45 +1,3 @@ -import { type JwtPayload, jwtDecode } from "jwt-decode"; import type { components } from "./api/schema"; export type User = components["schemas"]["User"]; - -const COOKIE_NAME = "albatross_token"; - -export function getToken(): string | null { - const match = document.cookie - .split("; ") - .find((row) => row.startsWith(`${COOKIE_NAME}=`)); - if (!match) return null; - return match.split("=").slice(1).join("="); -} - -export function setToken(token: string): void { - document.cookie = `${COOKIE_NAME}=${token}; path=/; SameSite=Lax`; -} - -export function clearToken(): void { - document.cookie = `${COOKIE_NAME}=; path=/; SameSite=Lax; max-age=0`; -} - -export function getUserFromToken(): User | null { - const token = getToken(); - if (!token) return null; - try { - return jwtDecode<User & JwtPayload>(token); - } catch { - return null; - } -} - -export function isTokenExpired(): boolean { - const token = getToken(); - if (!token) return true; - try { - const decoded = jwtDecode<JwtPayload>(token); - if (decoded.exp == null) return false; - // If the token will expire in less than an hour, treat it as expired. - return new Date((decoded.exp - 3600) * 1000) < new Date(); - } catch { - return true; - } -} diff --git a/frontend/app/components/ProtectedRoute.tsx b/frontend/app/components/ProtectedRoute.tsx index 3aeaebc..b943696 100644 --- a/frontend/app/components/ProtectedRoute.tsx +++ b/frontend/app/components/ProtectedRoute.tsx @@ -6,7 +6,11 @@ export default function ProtectedRoute({ }: { children: React.ReactNode; }) { - const { isLoggedIn } = useAuth(); + const { isLoggedIn, isLoading } = useAuth(); + + if (isLoading) { + return null; + } if (!isLoggedIn) { return <Redirect to="/login" />; diff --git a/frontend/app/components/PublicOnlyRoute.tsx b/frontend/app/components/PublicOnlyRoute.tsx index 2527918..7b3ef9d 100644 --- a/frontend/app/components/PublicOnlyRoute.tsx +++ b/frontend/app/components/PublicOnlyRoute.tsx @@ -6,7 +6,11 @@ export default function PublicOnlyRoute({ }: { children: React.ReactNode; }) { - const { isLoggedIn } = useAuth(); + const { isLoggedIn, isLoading } = useAuth(); + + if (isLoading) { + return null; + } if (isLoggedIn) { return <Redirect to="/dashboard" />; diff --git a/frontend/app/hooks/useAuth.ts b/frontend/app/hooks/useAuth.ts index 8762734..7913a0e 100644 --- a/frontend/app/hooks/useAuth.ts +++ b/frontend/app/hooks/useAuth.ts @@ -1,58 +1,33 @@ -import { useCallback, useSyncExternalStore } from "react"; -import { apiLogin } from "../api/client"; -import { - type User, - clearToken, - getToken, - getUserFromToken, - isTokenExpired, - setToken, -} from "../auth"; - -// Simple external store to trigger re-renders when auth state changes. -let authVersion = 0; -const listeners = new Set<() => void>(); - -function subscribe(callback: () => void) { - listeners.add(callback); - return () => listeners.delete(callback); -} - -function getSnapshot() { - return authVersion; -} - -function notifyAuthChange() { - authVersion++; - for (const listener of listeners) { - listener(); - } -} +import { useCallback, useEffect, useState } from "react"; +import { apiGetMe, apiLogin, apiLogout } from "../api/client"; +import type { User } from "../auth"; export function useAuth(): { user: User | null; - token: string | null; isLoggedIn: boolean; + isLoading: boolean; login: (username: string, password: string) => Promise<void>; - logout: () => void; + logout: () => Promise<void>; } { - useSyncExternalStore(subscribe, getSnapshot); + const [user, setUser] = useState<User | null>(null); + const [isLoading, setIsLoading] = useState(true); - const token = getToken(); - const isExpired = isTokenExpired(); - const user = isExpired ? null : getUserFromToken(); - const isLoggedIn = user !== null && !isExpired; + useEffect(() => { + apiGetMe() + .then((data) => setUser(data?.user ?? null)) + .catch(() => setUser(null)) + .finally(() => setIsLoading(false)); + }, []); const login = useCallback(async (username: string, password: string) => { - const { token } = await apiLogin(username, password); - setToken(token); - notifyAuthChange(); + const { user } = await apiLogin(username, password); + setUser(user); }, []); - const logout = useCallback(() => { - clearToken(); - notifyAuthChange(); + const logout = useCallback(async () => { + await apiLogout(); + setUser(null); }, []); - return { user, token: isLoggedIn ? token : null, isLoggedIn, login, logout }; + return { user, isLoggedIn: user !== null, isLoading, login, logout }; } diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx index c81014d..708a867 100644 --- a/frontend/app/pages/DashboardPage.tsx +++ b/frontend/app/pages/DashboardPage.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react"; import { useLocation } from "wouter"; import { createApiClient } from "../api/client"; import type { components } from "../api/schema"; -import { getToken } from "../auth"; import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption"; import NavigateLink from "../components/NavigateLink"; import UserIcon from "../components/UserIcon"; @@ -22,17 +21,15 @@ export default function DashboardPage() { const [loading, setLoading] = useState(true); useEffect(() => { - const token = getToken(); - if (!token) return; - const apiClient = createApiClient(token); + const apiClient = createApiClient(); apiClient .getGames() .then(({ games }) => setGames(games)) .finally(() => setLoading(false)); }, []); - function handleLogout() { - logout(); + async function handleLogout() { + await logout(); navigate("/"); } diff --git a/frontend/app/pages/GolfPlayPage.tsx b/frontend/app/pages/GolfPlayPage.tsx index 3fddbf8..c183ac8 100644 --- a/frontend/app/pages/GolfPlayPage.tsx +++ b/frontend/app/pages/GolfPlayPage.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from "react"; import { useLocation } from "wouter"; import { ApiClientContext, createApiClient } from "../api/client"; import type { components } from "../api/schema"; -import { getToken } from "../auth"; import GolfPlayApp from "../components/GolfPlayApp"; import { APP_NAME } from "../config"; import { useAuth } from "../hooks/useAuth"; @@ -29,9 +28,7 @@ export default function GolfPlayPage({ gameId }: { gameId: string }) { ); useEffect(() => { - const token = getToken(); - if (!token) return; - const apiClient = createApiClient(token); + const apiClient = createApiClient(); Promise.all([ apiClient.getGame(gameIdNum), apiClient.getGamePlayLatestState(gameIdNum), @@ -57,11 +54,9 @@ export default function GolfPlayPage({ gameId }: { gameId: string }) { ); } - const token = getToken()!; - return ( <JotaiProvider store={store}> - <ApiClientContext.Provider value={createApiClient(token)}> + <ApiClientContext.Provider value={createApiClient()}> <GolfPlayApp key={game.game_id} game={game} diff --git a/frontend/app/pages/GolfWatchPage.tsx b/frontend/app/pages/GolfWatchPage.tsx index 317f860..519a030 100644 --- a/frontend/app/pages/GolfWatchPage.tsx +++ b/frontend/app/pages/GolfWatchPage.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from "react"; import { useLocation } from "wouter"; import { ApiClientContext, createApiClient } from "../api/client"; import type { components } from "../api/schema"; -import { getToken } from "../auth"; import GolfWatchApp from "../components/GolfWatchApp"; import { APP_NAME } from "../config"; import { usePageTitle } from "../hooks/usePageTitle"; @@ -31,9 +30,7 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) { ); useEffect(() => { - const token = getToken(); - if (!token) return; - const apiClient = createApiClient(token); + const apiClient = createApiClient(); Promise.all([ apiClient.getGame(gameIdNum), apiClient.getGameWatchRanking(gameIdNum), @@ -61,11 +58,9 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) { ); } - const token = getToken()!; - return ( <JotaiProvider store={store}> - <ApiClientContext.Provider value={createApiClient(token)}> + <ApiClientContext.Provider value={createApiClient()}> <GolfWatchApp key={game.game_id} game={game} diff --git a/frontend/app/pages/TournamentPage.tsx b/frontend/app/pages/TournamentPage.tsx index 43ea790..404fa2d 100644 --- a/frontend/app/pages/TournamentPage.tsx +++ b/frontend/app/pages/TournamentPage.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; import { createApiClient } from "../api/client"; import type { components } from "../api/schema"; -import { getToken } from "../auth"; import BorderedContainer from "../components/BorderedContainer"; import UserIcon from "../components/UserIcon"; import { APP_NAME } from "../config"; @@ -241,9 +240,7 @@ export default function TournamentPage() { setPlayerIDs(pIDs); - const token = getToken(); - if (!token) return; - const apiClient = createApiClient(token); + const apiClient = createApiClient(); apiClient .getTournament(game1, game2, game3, game4, game5) .then(({ tournament }) => setTournament(tournament)) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cd06b43..b4799d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,6 @@ "@fortawesome/react-fontawesome": "^0.2.2", "hast-util-to-jsx-runtime": "^2.3.6", "jotai": "^2.12.1", - "jwt-decode": "^4.0.0", "openapi-fetch": "^0.13.4", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -5364,15 +5363,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index c024f7a..b7b5f09 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "@fortawesome/react-fontawesome": "^0.2.2", "hast-util-to-jsx-runtime": "^2.3.6", "jotai": "^2.12.1", - "jwt-decode": "^4.0.0", "openapi-fetch": "^0.13.4", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml index 8709db2..15b75b6 100644 --- a/openapi/api-server.yaml +++ b/openapi/api-server.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Albatross internal web API - version: 0.2.0 + version: 0.3.0 paths: /login: post: @@ -31,19 +31,43 @@ paths: schema: type: object properties: - token: - type: string - example: "xxxxx.xxxxx.xxxxx" + user: + $ref: '#/components/schemas/User' required: - - token + - user + '401': + $ref: '#/components/responses/Unauthorized' + /logout: + post: + operationId: postLogout + summary: User logout + responses: + '200': + description: Successfully logged out + '401': + $ref: '#/components/responses/Unauthorized' + /me: + get: + operationId: getMe + summary: Get current user + responses: + '200': + description: Current user info + content: + application/json: + schema: + type: object + properties: + user: + $ref: '#/components/schemas/User' + required: + - user '401': $ref: '#/components/responses/Unauthorized' /games: get: operationId: getGames summary: List games - parameters: - - $ref: '#/components/parameters/header_authorization' responses: '200': description: List of games @@ -67,7 +91,6 @@ paths: operationId: getGame summary: Get a game parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' responses: '200': @@ -92,7 +115,6 @@ paths: operationId: getGamePlayLatestState summary: Get the latest execution result for player parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' responses: '200': @@ -117,7 +139,6 @@ paths: operationId: postGamePlayCode summary: Post the latest code parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' requestBody: required: true @@ -145,7 +166,6 @@ paths: operationId: postGamePlaySubmit summary: Submit the answer parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' requestBody: required: true @@ -173,7 +193,6 @@ paths: operationId: getGameWatchRanking summary: Get the latest player ranking parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' responses: '200': @@ -200,7 +219,6 @@ paths: operationId: getGameWatchLatestStates summary: Get all the latest game states of the main players parameters: - - $ref: '#/components/parameters/header_authorization' - $ref: '#/components/parameters/path_game_id' responses: '200': @@ -227,7 +245,6 @@ paths: operationId: getTournament summary: Get tournament bracket data parameters: - - $ref: '#/components/parameters/header_authorization' - in: query name: game1 schema: @@ -273,12 +290,6 @@ paths: $ref: '#/components/responses/NotFound' components: parameters: - header_authorization: - in: header - name: Authorization - schema: - type: string - required: true path_game_id: in: path name: game_id |
