aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
committernsfisis <nsfisis@gmail.com>2026-02-13 23:46:16 +0900
commit7258ca81812a24edd382438ce6e9ebc538549427 (patch)
tree9bbc034be62777a2412d871211188268d7c56da4
parent7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (diff)
downloadphperkaigi-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.go32
-rw-r--r--backend/api/generated.go462
-rw-r--r--backend/api/handler.go90
-rw-r--r--backend/api/handler_wrapper.go80
-rw-r--r--backend/gen/api/handler_wrapper_gen.go24
-rw-r--r--backend/main.go8
-rw-r--r--frontend/app/api/client.ts37
-rw-r--r--frontend/app/api/schema.d.ts112
-rw-r--r--frontend/app/auth.ts42
-rw-r--r--frontend/app/components/ProtectedRoute.tsx6
-rw-r--r--frontend/app/components/PublicOnlyRoute.tsx6
-rw-r--r--frontend/app/hooks/useAuth.ts63
-rw-r--r--frontend/app/pages/DashboardPage.tsx9
-rw-r--r--frontend/app/pages/GolfPlayPage.tsx9
-rw-r--r--frontend/app/pages/GolfWatchPage.tsx9
-rw-r--r--frontend/app/pages/TournamentPage.tsx5
-rw-r--r--frontend/package-lock.json10
-rw-r--r--frontend/package.json1
-rw-r--r--openapi/api-server.yaml51
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{
- "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",
+ "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",
}
// 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