aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--backend/api/generated.go443
-rw-r--r--backend/api/handlers.go181
-rw-r--r--backend/db/query.sql.go32
-rw-r--r--backend/game/models.go14
-rw-r--r--backend/query.sql10
-rw-r--r--frontend/.prettierignore1
-rw-r--r--frontend/app/.server/api/schema.d.ts198
-rw-r--r--frontend/app/routes/admin.dashboard.tsx3
-rw-r--r--frontend/app/routes/admin.games.tsx49
-rw-r--r--frontend/app/routes/admin.games_.$gameId.tsx127
-rw-r--r--frontend/app/routes/admin.users.tsx2
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx2
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx2
-rw-r--r--openapi.yaml176
15 files changed, 1197 insertions, 46 deletions
diff --git a/Makefile b/Makefile
index ba2e10c..7adf808 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,9 @@ initdb:
make psql-query < ./backend/schema.sql
make psql-query < ./backend/fixtures/dev.sql
+.PHONY: openapi
+openapi: oapi-codegen openapi-typescript
+
.PHONY: oapi-codegen
oapi-codegen:
cd backend; make oapi-codegen
diff --git a/backend/api/generated.go b/backend/api/generated.go
index abcdf12..a2fa329 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -24,13 +24,13 @@ import (
// Defines values for GameState.
const (
- Closed GameState = "closed"
- Finished GameState = "finished"
- Gaming GameState = "gaming"
- Prepare GameState = "prepare"
- Starting GameState = "starting"
- WaitingEntries GameState = "waiting_entries"
- WaitingStart GameState = "waiting_start"
+ GameStateClosed GameState = "closed"
+ GameStateFinished GameState = "finished"
+ GameStateGaming GameState = "gaming"
+ GameStatePrepare GameState = "prepare"
+ GameStateStarting GameState = "starting"
+ GameStateWaitingEntries GameState = "waiting_entries"
+ GameStateWaitingStart GameState = "waiting_start"
)
// Defines values for GamePlayerMessageS2CExecResultPayloadStatus.
@@ -43,6 +43,17 @@ const (
GameWatcherMessageS2CExecResultPayloadStatusSuccess GameWatcherMessageS2CExecResultPayloadStatus = "success"
)
+// Defines values for PutAdminGamesGameIdJSONBodyState.
+const (
+ PutAdminGamesGameIdJSONBodyStateClosed PutAdminGamesGameIdJSONBodyState = "closed"
+ PutAdminGamesGameIdJSONBodyStateFinished PutAdminGamesGameIdJSONBodyState = "finished"
+ PutAdminGamesGameIdJSONBodyStateGaming PutAdminGamesGameIdJSONBodyState = "gaming"
+ PutAdminGamesGameIdJSONBodyStatePrepare PutAdminGamesGameIdJSONBodyState = "prepare"
+ PutAdminGamesGameIdJSONBodyStateStarting PutAdminGamesGameIdJSONBodyState = "starting"
+ PutAdminGamesGameIdJSONBodyStateWaitingEntries PutAdminGamesGameIdJSONBodyState = "waiting_entries"
+ PutAdminGamesGameIdJSONBodyStateWaitingStart PutAdminGamesGameIdJSONBodyState = "waiting_start"
+)
+
// Game defines model for Game.
type Game struct {
DisplayName string `json:"display_name"`
@@ -196,6 +207,33 @@ type User struct {
Username string `json:"username"`
}
+// GetAdminGamesParams defines parameters for GetAdminGames.
+type GetAdminGamesParams struct {
+ Authorization string `json:"Authorization"`
+}
+
+// GetAdminGamesGameIdParams defines parameters for GetAdminGamesGameId.
+type GetAdminGamesGameIdParams struct {
+ Authorization string `json:"Authorization"`
+}
+
+// PutAdminGamesGameIdJSONBody defines parameters for PutAdminGamesGameId.
+type PutAdminGamesGameIdJSONBody struct {
+ DisplayName *string `json:"display_name,omitempty"`
+ DurationSeconds *int `json:"duration_seconds,omitempty"`
+ ProblemId nullable.Nullable[int] `json:"problem_id,omitempty"`
+ StartedAt nullable.Nullable[int] `json:"started_at,omitempty"`
+ State *PutAdminGamesGameIdJSONBodyState `json:"state,omitempty"`
+}
+
+// PutAdminGamesGameIdParams defines parameters for PutAdminGamesGameId.
+type PutAdminGamesGameIdParams struct {
+ Authorization string `json:"Authorization"`
+}
+
+// PutAdminGamesGameIdJSONBodyState defines parameters for PutAdminGamesGameId.
+type PutAdminGamesGameIdJSONBodyState string
+
// GetAdminUsersParams defines parameters for GetAdminUsers.
type GetAdminUsersParams struct {
Authorization string `json:"Authorization"`
@@ -223,6 +261,9 @@ type GetTokenParams struct {
Authorization string `json:"Authorization"`
}
+// PutAdminGamesGameIdJSONRequestBody defines body for PutAdminGamesGameId for application/json ContentType.
+type PutAdminGamesGameIdJSONRequestBody PutAdminGamesGameIdJSONBody
+
// PostLoginJSONRequestBody defines body for PostLogin for application/json ContentType.
type PostLoginJSONRequestBody PostLoginJSONBody
@@ -590,6 +631,15 @@ func (t *GameWatcherMessageS2C) UnmarshalJSON(b []byte) error {
// ServerInterface represents all server handlers.
type ServerInterface interface {
+ // List games
+ // (GET /admin/games)
+ GetAdminGames(ctx echo.Context, params GetAdminGamesParams) error
+ // Get a game
+ // (GET /admin/games/{game_id})
+ GetAdminGamesGameId(ctx echo.Context, gameId int, params GetAdminGamesGameIdParams) error
+ // Update a game
+ // (PUT /admin/games/{game_id})
+ PutAdminGamesGameId(ctx echo.Context, gameId int, params PutAdminGamesGameIdParams) error
// List all users
// (GET /admin/users)
GetAdminUsers(ctx echo.Context, params GetAdminUsersParams) error
@@ -612,6 +662,113 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface
}
+// GetAdminGames converts echo context to params.
+func (w *ServerInterfaceWrapper) GetAdminGames(ctx echo.Context) error {
+ var err error
+
+ // Parameter object where we will unmarshal all parameters from the context
+ var params GetAdminGamesParams
+
+ headers := ctx.Request().Header
+ // ------------- Required header parameter "Authorization" -------------
+ if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found {
+ var Authorization string
+ n := len(valueList)
+ if n != 1 {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n))
+ }
+
+ err = runtime.BindStyledParameterWithOptions("simple", "Authorization", valueList[0], &Authorization, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true})
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter Authorization: %s", err))
+ }
+
+ params.Authorization = Authorization
+ } else {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found"))
+ }
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.GetAdminGames(ctx, params)
+ return err
+}
+
+// GetAdminGamesGameId converts echo context to params.
+func (w *ServerInterfaceWrapper) GetAdminGamesGameId(ctx echo.Context) error {
+ var err error
+ // ------------- Path parameter "game_id" -------------
+ var gameId int
+
+ err = runtime.BindStyledParameterWithOptions("simple", "game_id", ctx.Param("game_id"), &gameId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
+ if err != nil {
+ 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 GetAdminGamesGameIdParams
+
+ headers := ctx.Request().Header
+ // ------------- Required header parameter "Authorization" -------------
+ if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found {
+ var Authorization string
+ n := len(valueList)
+ if n != 1 {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n))
+ }
+
+ err = runtime.BindStyledParameterWithOptions("simple", "Authorization", valueList[0], &Authorization, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true})
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter Authorization: %s", err))
+ }
+
+ params.Authorization = Authorization
+ } else {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found"))
+ }
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.GetAdminGamesGameId(ctx, gameId, params)
+ return err
+}
+
+// PutAdminGamesGameId converts echo context to params.
+func (w *ServerInterfaceWrapper) PutAdminGamesGameId(ctx echo.Context) error {
+ var err error
+ // ------------- Path parameter "game_id" -------------
+ var gameId int
+
+ err = runtime.BindStyledParameterWithOptions("simple", "game_id", ctx.Param("game_id"), &gameId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
+ if err != nil {
+ 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 PutAdminGamesGameIdParams
+
+ headers := ctx.Request().Header
+ // ------------- Required header parameter "Authorization" -------------
+ if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found {
+ var Authorization string
+ n := len(valueList)
+ if n != 1 {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n))
+ }
+
+ err = runtime.BindStyledParameterWithOptions("simple", "Authorization", valueList[0], &Authorization, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true})
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter Authorization: %s", err))
+ }
+
+ params.Authorization = Authorization
+ } else {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found"))
+ }
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.PutAdminGamesGameId(ctx, gameId, params)
+ return err
+}
+
// GetAdminUsers converts echo context to params.
func (w *ServerInterfaceWrapper) GetAdminUsers(ctx echo.Context) error {
var err error
@@ -786,6 +943,9 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
Handler: si,
}
+ router.GET(baseURL+"/admin/games", wrapper.GetAdminGames)
+ router.GET(baseURL+"/admin/games/:game_id", wrapper.GetAdminGamesGameId)
+ router.PUT(baseURL+"/admin/games/:game_id", wrapper.PutAdminGamesGameId)
router.GET(baseURL+"/admin/users", wrapper.GetAdminUsers)
router.GET(baseURL+"/games", wrapper.GetGames)
router.GET(baseURL+"/games/:game_id", wrapper.GetGamesGameId)
@@ -794,6 +954,129 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
}
+type GetAdminGamesRequestObject struct {
+ Params GetAdminGamesParams
+}
+
+type GetAdminGamesResponseObject interface {
+ VisitGetAdminGamesResponse(w http.ResponseWriter) error
+}
+
+type GetAdminGames200JSONResponse struct {
+ Games []Game `json:"games"`
+}
+
+func (response GetAdminGames200JSONResponse) VisitGetAdminGamesResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetAdminGames403JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response GetAdminGames403JSONResponse) VisitGetAdminGamesResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(403)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetAdminGamesGameIdRequestObject struct {
+ GameId int `json:"game_id"`
+ Params GetAdminGamesGameIdParams
+}
+
+type GetAdminGamesGameIdResponseObject interface {
+ VisitGetAdminGamesGameIdResponse(w http.ResponseWriter) error
+}
+
+type GetAdminGamesGameId200JSONResponse struct {
+ Game Game `json:"game"`
+}
+
+func (response GetAdminGamesGameId200JSONResponse) VisitGetAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetAdminGamesGameId403JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response GetAdminGamesGameId403JSONResponse) VisitGetAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(403)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetAdminGamesGameId404JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response GetAdminGamesGameId404JSONResponse) VisitGetAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(404)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type PutAdminGamesGameIdRequestObject struct {
+ GameId int `json:"game_id"`
+ Params PutAdminGamesGameIdParams
+ Body *PutAdminGamesGameIdJSONRequestBody
+}
+
+type PutAdminGamesGameIdResponseObject interface {
+ VisitPutAdminGamesGameIdResponse(w http.ResponseWriter) error
+}
+
+type PutAdminGamesGameId204Response struct {
+}
+
+func (response PutAdminGamesGameId204Response) VisitPutAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.WriteHeader(204)
+ return nil
+}
+
+type PutAdminGamesGameId400JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response PutAdminGamesGameId400JSONResponse) VisitPutAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(400)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type PutAdminGamesGameId403JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response PutAdminGamesGameId403JSONResponse) VisitPutAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(403)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type PutAdminGamesGameId404JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response PutAdminGamesGameId404JSONResponse) VisitPutAdminGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(404)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
type GetAdminUsersRequestObject struct {
Params GetAdminUsersParams
}
@@ -863,7 +1146,9 @@ type GetGamesGameIdResponseObject interface {
VisitGetGamesGameIdResponse(w http.ResponseWriter) error
}
-type GetGamesGameId200JSONResponse Game
+type GetGamesGameId200JSONResponse struct {
+ Game Game `json:"game"`
+}
func (response GetGamesGameId200JSONResponse) VisitGetGamesGameIdResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
@@ -956,6 +1241,15 @@ func (response GetToken403JSONResponse) VisitGetTokenResponse(w http.ResponseWri
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
+ // List games
+ // (GET /admin/games)
+ GetAdminGames(ctx context.Context, request GetAdminGamesRequestObject) (GetAdminGamesResponseObject, error)
+ // Get a game
+ // (GET /admin/games/{game_id})
+ GetAdminGamesGameId(ctx context.Context, request GetAdminGamesGameIdRequestObject) (GetAdminGamesGameIdResponseObject, error)
+ // Update a game
+ // (PUT /admin/games/{game_id})
+ PutAdminGamesGameId(ctx context.Context, request PutAdminGamesGameIdRequestObject) (PutAdminGamesGameIdResponseObject, error)
// List all users
// (GET /admin/users)
GetAdminUsers(ctx context.Context, request GetAdminUsersRequestObject) (GetAdminUsersResponseObject, error)
@@ -985,6 +1279,89 @@ type strictHandler struct {
middlewares []StrictMiddlewareFunc
}
+// GetAdminGames operation middleware
+func (sh *strictHandler) GetAdminGames(ctx echo.Context, params GetAdminGamesParams) error {
+ var request GetAdminGamesRequestObject
+
+ request.Params = params
+
+ handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
+ return sh.ssi.GetAdminGames(ctx.Request().Context(), request.(GetAdminGamesRequestObject))
+ }
+ for _, middleware := range sh.middlewares {
+ handler = middleware(handler, "GetAdminGames")
+ }
+
+ response, err := handler(ctx, request)
+
+ if err != nil {
+ return err
+ } else if validResponse, ok := response.(GetAdminGamesResponseObject); ok {
+ return validResponse.VisitGetAdminGamesResponse(ctx.Response())
+ } else if response != nil {
+ return fmt.Errorf("unexpected response type: %T", response)
+ }
+ return nil
+}
+
+// GetAdminGamesGameId operation middleware
+func (sh *strictHandler) GetAdminGamesGameId(ctx echo.Context, gameId int, params GetAdminGamesGameIdParams) error {
+ var request GetAdminGamesGameIdRequestObject
+
+ request.GameId = gameId
+ request.Params = params
+
+ handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
+ return sh.ssi.GetAdminGamesGameId(ctx.Request().Context(), request.(GetAdminGamesGameIdRequestObject))
+ }
+ for _, middleware := range sh.middlewares {
+ handler = middleware(handler, "GetAdminGamesGameId")
+ }
+
+ response, err := handler(ctx, request)
+
+ if err != nil {
+ return err
+ } else if validResponse, ok := response.(GetAdminGamesGameIdResponseObject); ok {
+ return validResponse.VisitGetAdminGamesGameIdResponse(ctx.Response())
+ } else if response != nil {
+ return fmt.Errorf("unexpected response type: %T", response)
+ }
+ return nil
+}
+
+// PutAdminGamesGameId operation middleware
+func (sh *strictHandler) PutAdminGamesGameId(ctx echo.Context, gameId int, params PutAdminGamesGameIdParams) error {
+ var request PutAdminGamesGameIdRequestObject
+
+ request.GameId = gameId
+ request.Params = params
+
+ var body PutAdminGamesGameIdJSONRequestBody
+ if err := ctx.Bind(&body); err != nil {
+ return err
+ }
+ request.Body = &body
+
+ handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
+ return sh.ssi.PutAdminGamesGameId(ctx.Request().Context(), request.(PutAdminGamesGameIdRequestObject))
+ }
+ for _, middleware := range sh.middlewares {
+ handler = middleware(handler, "PutAdminGamesGameId")
+ }
+
+ response, err := handler(ctx, request)
+
+ if err != nil {
+ return err
+ } else if validResponse, ok := response.(PutAdminGamesGameIdResponseObject); ok {
+ return validResponse.VisitPutAdminGamesGameIdResponse(ctx.Response())
+ } else if response != nil {
+ return fmt.Errorf("unexpected response type: %T", response)
+ }
+ return nil
+}
+
// GetAdminUsers operation middleware
func (sh *strictHandler) GetAdminUsers(ctx echo.Context, params GetAdminUsersParams) error {
var request GetAdminUsersRequestObject
@@ -1118,30 +1495,32 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
+ "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",
+ "LMuy/wIAAP//vgYmOHMsAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/backend/api/handlers.go b/backend/api/handlers.go
index 519695f..558949b 100644
--- a/backend/api/handlers.go
+++ b/backend/api/handlers.go
@@ -5,8 +5,10 @@ import (
"errors"
"net/http"
"strings"
+ "time"
"github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
"github.com/nsfisis/iosdc-2024-albatross/backend/auth"
@@ -25,6 +27,179 @@ func NewHandler(queries *db.Queries) *ApiHandler {
}
}
+func (h *ApiHandler) GetAdminGames(ctx context.Context, request GetAdminGamesRequestObject) (GetAdminGamesResponseObject, error) {
+ user := ctx.Value("user").(*auth.JWTClaims)
+ if !user.IsAdmin {
+ return GetAdminGames403JSONResponse{
+ Message: "Forbidden",
+ }, nil
+ }
+ gameRows, err := h.q.ListGames(ctx)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ games := make([]Game, len(gameRows))
+ for i, row := range gameRows {
+ var startedAt *int
+ if row.StartedAt.Valid {
+ startedAtTimestamp := int(row.StartedAt.Time.Unix())
+ startedAt = &startedAtTimestamp
+ }
+ var problem *Problem
+ if row.ProblemID != nil {
+ if row.Title == nil || row.Description == nil {
+ panic("inconsistent data")
+ }
+ problem = &Problem{
+ ProblemId: int(*row.ProblemID),
+ Title: *row.Title,
+ Description: *row.Description,
+ }
+ }
+ games[i] = Game{
+ GameId: int(row.GameID),
+ State: GameState(row.State),
+ DisplayName: row.DisplayName,
+ DurationSeconds: int(row.DurationSeconds),
+ StartedAt: startedAt,
+ Problem: problem,
+ }
+ }
+ return GetAdminGames200JSONResponse{
+ Games: games,
+ }, nil
+}
+
+func (h *ApiHandler) GetAdminGamesGameId(ctx context.Context, request GetAdminGamesGameIdRequestObject) (GetAdminGamesGameIdResponseObject, error) {
+ user := ctx.Value("user").(*auth.JWTClaims)
+ if !user.IsAdmin {
+ return GetAdminGamesGameId403JSONResponse{
+ Message: "Forbidden",
+ }, nil
+ }
+ gameId := request.GameId
+ row, err := h.q.GetGameById(ctx, int32(gameId))
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return GetAdminGamesGameId404JSONResponse{
+ Message: "Game not found",
+ }, nil
+ } else {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ }
+ var startedAt *int
+ if row.StartedAt.Valid {
+ startedAtTimestamp := int(row.StartedAt.Time.Unix())
+ startedAt = &startedAtTimestamp
+ }
+ var problem *Problem
+ if row.ProblemID != nil {
+ if row.Title == nil || row.Description == nil {
+ panic("inconsistent data")
+ }
+ problem = &Problem{
+ ProblemId: int(*row.ProblemID),
+ Title: *row.Title,
+ Description: *row.Description,
+ }
+ }
+ game := Game{
+ GameId: int(row.GameID),
+ State: GameState(row.State),
+ DisplayName: row.DisplayName,
+ DurationSeconds: int(row.DurationSeconds),
+ StartedAt: startedAt,
+ Problem: problem,
+ }
+ return GetAdminGamesGameId200JSONResponse{
+ Game: game,
+ }, nil
+}
+
+func (h *ApiHandler) PutAdminGamesGameId(ctx context.Context, request PutAdminGamesGameIdRequestObject) (PutAdminGamesGameIdResponseObject, error) {
+ user := ctx.Value("user").(*auth.JWTClaims)
+ if !user.IsAdmin {
+ return PutAdminGamesGameId403JSONResponse{
+ Message: "Forbidden",
+ }, nil
+ }
+ gameID := request.GameId
+ displayName := request.Body.DisplayName
+ durationSeconds := request.Body.DurationSeconds
+ problemID := request.Body.ProblemId
+ startedAt := request.Body.StartedAt
+ state := request.Body.State
+
+ game, err := h.q.GetGameById(ctx, int32(gameID))
+ if err != nil {
+ if err == pgx.ErrNoRows {
+ return PutAdminGamesGameId404JSONResponse{
+ Message: "Game not found",
+ }, nil
+ } else {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ }
+
+ var changedState string
+ if state != nil {
+ changedState = string(*state)
+ } else {
+ changedState = game.State
+ }
+ var changedDisplayName string
+ if displayName != nil {
+ changedDisplayName = *displayName
+ } else {
+ changedDisplayName = game.DisplayName
+ }
+ var changedDurationSeconds int32
+ if durationSeconds != nil {
+ changedDurationSeconds = int32(*durationSeconds)
+ } else {
+ changedDurationSeconds = game.DurationSeconds
+ }
+ var changedStartedAt pgtype.Timestamp
+ if startedAt != nil {
+ startedAtValue, err := startedAt.Get()
+ if err == nil {
+ changedStartedAt = pgtype.Timestamp{
+ Time: time.Unix(int64(startedAtValue), 0),
+ Valid: true,
+ }
+ }
+ } else {
+ changedStartedAt = game.StartedAt
+ }
+ var changedProblemID *int32
+ if problemID != nil {
+ problemIDValue, err := problemID.Get()
+ if err == nil {
+ changedProblemID = new(int32)
+ *changedProblemID = int32(problemIDValue)
+ }
+ } else {
+ changedProblemID = game.ProblemID
+ }
+
+ err = h.q.UpdateGame(ctx, db.UpdateGameParams{
+ GameID: int32(gameID),
+ State: changedState,
+ DisplayName: changedDisplayName,
+ DurationSeconds: changedDurationSeconds,
+ StartedAt: changedStartedAt,
+ ProblemID: changedProblemID,
+ })
+ if err != nil {
+ return PutAdminGamesGameId400JSONResponse{
+ Message: err.Error(),
+ }, nil
+ }
+
+ return PutAdminGamesGameId204Response{}, nil
+}
+
func (h *ApiHandler) GetAdminUsers(ctx context.Context, request GetAdminUsersRequestObject) (GetAdminUsersResponseObject, error) {
user := ctx.Value("user").(*auth.JWTClaims)
if !user.IsAdmin {
@@ -196,7 +371,7 @@ func (h *ApiHandler) GetGamesGameId(ctx context.Context, request GetGamesGameIdR
if row.Title == nil || row.Description == nil {
panic("inconsistent data")
}
- if user.IsAdmin || (GameState(row.State) != Closed && GameState(row.State) != WaitingEntries) {
+ if user.IsAdmin || (GameState(row.State) != GameStateClosed && GameState(row.State) != GameStateWaitingEntries) {
problem = &Problem{
ProblemId: int(*row.ProblemID),
Title: *row.Title,
@@ -212,7 +387,9 @@ func (h *ApiHandler) GetGamesGameId(ctx context.Context, request GetGamesGameIdR
StartedAt: startedAt,
Problem: problem,
}
- return GetGamesGameId200JSONResponse(game), nil
+ return GetGamesGameId200JSONResponse{
+ Game: game,
+ }, nil
}
func _assertUserResponseIsCompatibleWithJWTClaims() {
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index 404f5d9..b5fef29 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -242,6 +242,38 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
return items, nil
}
+const updateGame = `-- name: UpdateGame :exec
+UPDATE games
+SET
+ state = $2,
+ display_name = $3,
+ duration_seconds = $4,
+ started_at = $5,
+ problem_id = $6
+WHERE game_id = $1
+`
+
+type UpdateGameParams struct {
+ GameID int32
+ State string
+ DisplayName string
+ DurationSeconds int32
+ StartedAt pgtype.Timestamp
+ ProblemID *int32
+}
+
+func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) error {
+ _, err := q.db.Exec(ctx, updateGame,
+ arg.GameID,
+ arg.State,
+ arg.DisplayName,
+ arg.DurationSeconds,
+ arg.StartedAt,
+ arg.ProblemID,
+ )
+ return err
+}
+
const updateGameStartedAt = `-- name: UpdateGameStartedAt :exec
UPDATE games
SET started_at = $2
diff --git a/backend/game/models.go b/backend/game/models.go
index 4e9ee87..8f3bc80 100644
--- a/backend/game/models.go
+++ b/backend/game/models.go
@@ -9,13 +9,13 @@ import (
type gameState = api.GameState
const (
- gameStateClosed gameState = api.Closed
- gameStateWaitingEntries gameState = api.WaitingEntries
- gameStateWaitingStart gameState = api.WaitingStart
- gameStatePrepare gameState = api.Prepare
- gameStateStarting gameState = api.Starting
- gameStateGaming gameState = api.Gaming
- gameStateFinished gameState = api.Finished
+ gameStateClosed gameState = api.GameStateClosed
+ gameStateWaitingEntries gameState = api.GameStateWaitingEntries
+ gameStateWaitingStart gameState = api.GameStateWaitingStart
+ gameStatePrepare gameState = api.GameStatePrepare
+ gameStateStarting gameState = api.GameStateStarting
+ gameStateGaming gameState = api.GameStateGaming
+ gameStateFinished gameState = api.GameStateFinished
)
type game struct {
diff --git a/backend/query.sql b/backend/query.sql
index ca2095c..ed8c7bc 100644
--- a/backend/query.sql
+++ b/backend/query.sql
@@ -37,3 +37,13 @@ SELECT * FROM games
LEFT JOIN problems ON games.problem_id = problems.problem_id
WHERE games.game_id = $1
LIMIT 1;
+
+-- name: UpdateGame :exec
+UPDATE games
+SET
+ state = $2,
+ display_name = $3,
+ duration_seconds = $4,
+ started_at = $5,
+ problem_id = $6
+WHERE game_id = $1;
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
index a6c2198..167860c 100644
--- a/frontend/.prettierignore
+++ b/frontend/.prettierignore
@@ -1,2 +1,3 @@
/.cache
/build
+/app/.server/api/schema.d.ts
diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts
index d12600c..000c876 100644
--- a/frontend/app/.server/api/schema.d.ts
+++ b/frontend/app/.server/api/schema.d.ts
@@ -198,7 +198,9 @@ export interface paths {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Game"];
+ "application/json": {
+ game: components["schemas"]["Game"];
+ };
};
};
/** @description Forbidden */
@@ -287,6 +289,200 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/admin/games": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List games */
+ get: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description List of games */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ games: components["schemas"]["Game"][];
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ [path: `/admin/games/${integer}`]: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get a game */
+ get: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path: {
+ game_id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A game */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ game: components["schemas"]["Game"];
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ /** @description Not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Not found */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ /** Update a game */
+ put: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path: {
+ game_id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /**
+ * @example closed
+ * @enum {string}
+ */
+ state?: "closed" | "waiting_entries" | "waiting_start" | "prepare" | "starting" | "gaming" | "finished";
+ /** @example Game 1 */
+ display_name?: string;
+ /** @example 360 */
+ duration_seconds?: number;
+ /** @example 946684800 */
+ started_at?: number | null;
+ /** @example 1 */
+ problem_id?: number | null;
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully updated */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Invalid request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Invalid request */
+ message: string;
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ /** @description Not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Not found */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record<string, never>;
export interface components {
diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx
index d5f3809..af82731 100644
--- a/frontend/app/routes/admin.dashboard.tsx
+++ b/frontend/app/routes/admin.dashboard.tsx
@@ -23,6 +23,9 @@ export default function AdminDashboard() {
<p>
<Link to="/admin/users">Users</Link>
</p>
+ <p>
+ <Link to="/admin/games">Games</Link>
+ </p>
</div>
);
}
diff --git a/frontend/app/routes/admin.games.tsx b/frontend/app/routes/admin.games.tsx
new file mode 100644
index 0000000..8362c6c
--- /dev/null
+++ b/frontend/app/routes/admin.games.tsx
@@ -0,0 +1,49 @@
+import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { useLoaderData, Link } from "@remix-run/react";
+import { isAuthenticated } from "../.server/auth";
+import { apiClient } from "../.server/api/client";
+
+export const meta: MetaFunction = () => {
+ return [{ title: "[Admin] Games | iOSDC 2024 Albatross.swift" }];
+};
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ const { user, token } = await isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+ if (!user.is_admin) {
+ throw new Error("Unauthorized");
+ }
+ const { data, error } = await apiClient.GET("/admin/games", {
+ params: {
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+ return { games: data.games };
+}
+
+export default function AdminGames() {
+ const { games } = useLoaderData<typeof loader>()!;
+
+ return (
+ <div>
+ <div>
+ <h1>[Admin] Games</h1>
+ <ul>
+ {games.map((game) => (
+ <li key={game.game_id}>
+ <Link to={`/admin/games/${game.game_id}`}>
+ {game.display_name} (id={game.game_id})
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx
new file mode 100644
index 0000000..5d83fb4
--- /dev/null
+++ b/frontend/app/routes/admin.games_.$gameId.tsx
@@ -0,0 +1,127 @@
+import type {
+ LoaderFunctionArgs,
+ MetaFunction,
+ ActionFunctionArgs,
+} from "@remix-run/node";
+import { useLoaderData, Form } from "@remix-run/react";
+import { isAuthenticated } from "../.server/auth";
+import { apiClient } from "../.server/api/client";
+
+export const meta: MetaFunction<typeof loader> = ({ data }) => {
+ return [
+ {
+ title: data
+ ? `[Admin] Game Edit ${data.game.display_name} | iOSDC 2024 Albatross.swift`
+ : "[Admin] Game Edit | iOSDC 2024 Albatross.swift",
+ },
+ ];
+};
+
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ const { user, token } = await isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+ if (!user.is_admin) {
+ throw new Error("Unauthorized");
+ }
+ const { gameId } = params;
+ const { data, error } = await apiClient.GET("/admin/games/{game_id}", {
+ params: {
+ path: {
+ game_id: Number(gameId),
+ },
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+ return { game: data.game };
+}
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ const { user, token } = await isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+ if (!user.is_admin) {
+ throw new Error("Unauthorized");
+ }
+ const { gameId } = params;
+
+ const formData = await request.formData();
+ const action = formData.get("action");
+
+ const nextState =
+ action === "open"
+ ? "waiting_entries"
+ : action === "start"
+ ? "waiting_start"
+ : null;
+ if (!nextState) {
+ throw new Error("Invalid action");
+ }
+
+ const { error } = await apiClient.PUT("/admin/games/{game_id}", {
+ params: {
+ path: {
+ game_id: Number(gameId),
+ },
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ body: {
+ state: nextState,
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+}
+
+export default function AdminGameEdit() {
+ const { game } = useLoaderData<typeof loader>()!;
+
+ return (
+ <div>
+ <div>
+ <h1>[Admin] Game Edit {game.display_name}</h1>
+ <ul>
+ <li>ID: {game.game_id}</li>
+ <li>State: {game.state}</li>
+ <li>Display Name: {game.display_name}</li>
+ <li>Duration Seconds: {game.duration_seconds}</li>
+ <li>
+ Started At:{" "}
+ {game.started_at
+ ? new Date(game.started_at * 1000).toString()
+ : "-"}
+ </li>
+ <li>Problem ID: {game.problem ? game.problem.problem_id : "-"}</li>
+ </ul>
+ <div>
+ <Form method="post">
+ <button
+ type="submit"
+ name="action"
+ value="open"
+ disabled={game.state !== "closed"}
+ >
+ Open
+ </button>
+ <button
+ type="submit"
+ name="action"
+ value="start"
+ disabled={game.state !== "waiting_start"}
+ >
+ Start
+ </button>
+ </Form>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/routes/admin.users.tsx b/frontend/app/routes/admin.users.tsx
index d9901a2..61a25bf 100644
--- a/frontend/app/routes/admin.users.tsx
+++ b/frontend/app/routes/admin.users.tsx
@@ -37,7 +37,7 @@ export default function AdminUsers() {
<ul>
{users.map((user) => (
<li key={user.user_id}>
- {user.display_name} (uid={user.user_id} username={user.username})
+ {user.display_name} (id={user.user_id} username={user.username})
{user.is_admin && <span> admin</span>}
</li>
))}
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx
index 3932b4b..2a3a68b 100644
--- a/frontend/app/routes/golf.$gameId.play.tsx
+++ b/frontend/app/routes/golf.$gameId.play.tsx
@@ -35,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
if (error) {
throw new Error(error.message);
}
- return data;
+ return data.game;
};
const fetchSockToken = async () => {
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
index eeb6fb9..da7baf1 100644
--- a/frontend/app/routes/golf.$gameId.watch.tsx
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -35,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
if (error) {
throw new Error(error.message);
}
- return data;
+ return data.game;
};
const fetchSockToken = async () => {
diff --git a/openapi.yaml b/openapi.yaml
index 050bae7..f28452b 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -141,7 +141,12 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/Game'
+ type: object
+ properties:
+ game:
+ $ref: '#/components/schemas/Game'
+ required:
+ - game
'403':
description: Forbidden
content:
@@ -201,6 +206,175 @@ paths:
example: "Forbidden operation"
required:
- message
+ /admin/games:
+ get:
+ summary: List games
+ parameters:
+ - in: header
+ name: Authorization
+ schema:
+ type: string
+ required: true
+ responses:
+ '200':
+ description: List of games
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ games:
+ type: array
+ items:
+ $ref: '#/components/schemas/Game'
+ required:
+ - games
+ '403':
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Forbidden operation"
+ required:
+ - message
+ /admin/games/{game_id}:
+ get:
+ summary: Get a game
+ parameters:
+ - in: path
+ name: game_id
+ schema:
+ type: integer
+ required: true
+ - in: header
+ name: Authorization
+ schema:
+ type: string
+ required: true
+ responses:
+ '200':
+ description: A game
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ game:
+ $ref: '#/components/schemas/Game'
+ required:
+ - game
+ '403':
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Forbidden operation"
+ required:
+ - message
+ '404':
+ description: Not found
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Not found"
+ required:
+ - message
+ put:
+ summary: Update a game
+ parameters:
+ - in: path
+ name: game_id
+ schema:
+ type: integer
+ required: true
+ - in: header
+ name: Authorization
+ schema:
+ type: string
+ required: true
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ state:
+ type: string
+ example: "closed"
+ enum:
+ - closed
+ - waiting_entries
+ - waiting_start
+ - prepare
+ - starting
+ - gaming
+ - finished
+ display_name:
+ type: string
+ example: "Game 1"
+ duration_seconds:
+ type: integer
+ example: 360
+ started_at:
+ nullable: true
+ type: integer
+ example: 946684800
+ problem_id:
+ nullable: true
+ type: integer
+ example: 1
+ responses:
+ '204':
+ description: Successfully updated
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Invalid request"
+ required:
+ - message
+ '403':
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Forbidden operation"
+ required:
+ - message
+ '404':
+ description: Not found
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: "Not found"
+ required:
+ - message
components:
schemas:
User: