aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/api
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-07-28 17:13:01 +0900
committernsfisis <nsfisis@gmail.com>2024-07-28 17:13:01 +0900
commitaadc8cf037855b99cb82798c7b0ebaafc5bb025b (patch)
tree4ea9f9db9dbe7cf1b7720205ae281a6b8bcca8e9 /backend/api
parent90741e8336b4ffba090bf08c3b899992860e2d98 (diff)
parent2d5f913a431c4223a16c88551ffff4100ac483c4 (diff)
downloadiosdc-japan-2024-albatross-aadc8cf037855b99cb82798c7b0ebaafc5bb025b.tar.gz
iosdc-japan-2024-albatross-aadc8cf037855b99cb82798c7b0ebaafc5bb025b.tar.zst
iosdc-japan-2024-albatross-aadc8cf037855b99cb82798c7b0ebaafc5bb025b.zip
Merge branch 'game-entry'
Diffstat (limited to 'backend/api')
-rw-r--r--backend/api/generated.go220
-rw-r--r--backend/api/handlers.go128
-rw-r--r--backend/api/workaround.go22
3 files changed, 321 insertions, 49 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go
index 1abdf73..921f4b3 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -17,32 +17,73 @@ import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
+ "github.com/oapi-codegen/runtime"
strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo"
)
+// 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"
+)
+
+// Game defines model for Game.
+type Game struct {
+ DisplayName string `json:"display_name"`
+ DurationSeconds int `json:"duration_seconds"`
+ GameId int `json:"game_id"`
+ Problem *Problem `json:"problem,omitempty"`
+ StartedAt *int `json:"started_at,omitempty"`
+ State GameState `json:"state"`
+}
+
+// GameState defines model for Game.State.
+type GameState string
+
// JwtPayload defines model for JwtPayload.
type JwtPayload struct {
DisplayName string `json:"display_name"`
- IconPath *string `json:"icon_path"`
+ IconPath *string `json:"icon_path,omitempty"`
IsAdmin bool `json:"is_admin"`
- UserId float32 `json:"user_id"`
+ UserId int `json:"user_id"`
Username string `json:"username"`
}
-// PostApiLoginJSONBody defines parameters for PostApiLogin.
-type PostApiLoginJSONBody struct {
+// Problem defines model for Problem.
+type Problem struct {
+ Description string `json:"description"`
+ ProblemId int `json:"problem_id"`
+ Title string `json:"title"`
+}
+
+// GetGamesParams defines parameters for GetGames.
+type GetGamesParams struct {
+ PlayerId *int `form:"player_id,omitempty" json:"player_id,omitempty"`
+ Authorization string `json:"Authorization"`
+}
+
+// PostLoginJSONBody defines parameters for PostLogin.
+type PostLoginJSONBody struct {
Password string `json:"password"`
Username string `json:"username"`
}
-// PostApiLoginJSONRequestBody defines body for PostApiLogin for application/json ContentType.
-type PostApiLoginJSONRequestBody PostApiLoginJSONBody
+// PostLoginJSONRequestBody defines body for PostLogin for application/json ContentType.
+type PostLoginJSONRequestBody PostLoginJSONBody
// ServerInterface represents all server handlers.
type ServerInterface interface {
+ // List games
+ // (GET /games)
+ GetGames(ctx echo.Context, params GetGamesParams) error
// User login
- // (POST /api/login)
- PostApiLogin(ctx echo.Context) error
+ // (POST /login)
+ PostLogin(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
@@ -50,12 +91,49 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface
}
-// PostApiLogin converts echo context to params.
-func (w *ServerInterfaceWrapper) PostApiLogin(ctx echo.Context) error {
+// GetGames converts echo context to params.
+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
+ // ------------- Optional query parameter "player_id" -------------
+
+ err = runtime.BindQueryParameter("form", true, false, "player_id", ctx.QueryParams(), &params.PlayerId)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter player_id: %s", err))
+ }
+
+ 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.PostApiLogin(ctx)
+ err = w.Handler.GetGames(ctx, params)
+ return err
+}
+
+// PostLogin converts echo context to params.
+func (w *ServerInterfaceWrapper) PostLogin(ctx echo.Context) error {
+ var err error
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.PostLogin(ctx)
return err
}
@@ -87,34 +165,65 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
Handler: si,
}
- router.POST(baseURL+"/api/login", wrapper.PostApiLogin)
+ router.GET(baseURL+"/games", wrapper.GetGames)
+ router.POST(baseURL+"/login", wrapper.PostLogin)
+
+}
+
+type GetGamesRequestObject struct {
+ Params GetGamesParams
+}
+
+type GetGamesResponseObject interface {
+ VisitGetGamesResponse(w http.ResponseWriter) error
+}
+
+type GetGames200JSONResponse struct {
+ Games []Game `json:"games"`
+}
+
+func (response GetGames200JSONResponse) VisitGetGamesResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetGames403JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response GetGames403JSONResponse) VisitGetGamesResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(403)
+ return json.NewEncoder(w).Encode(response)
}
-type PostApiLoginRequestObject struct {
- Body *PostApiLoginJSONRequestBody
+type PostLoginRequestObject struct {
+ Body *PostLoginJSONRequestBody
}
-type PostApiLoginResponseObject interface {
- VisitPostApiLoginResponse(w http.ResponseWriter) error
+type PostLoginResponseObject interface {
+ VisitPostLoginResponse(w http.ResponseWriter) error
}
-type PostApiLogin200JSONResponse struct {
+type PostLogin200JSONResponse struct {
Token string `json:"token"`
}
-func (response PostApiLogin200JSONResponse) VisitPostApiLoginResponse(w http.ResponseWriter) error {
+func (response PostLogin200JSONResponse) VisitPostLoginResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
-type PostApiLogin401JSONResponse struct {
+type PostLogin401JSONResponse struct {
Message string `json:"message"`
}
-func (response PostApiLogin401JSONResponse) VisitPostApiLoginResponse(w http.ResponseWriter) error {
+func (response PostLogin401JSONResponse) VisitPostLoginResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(401)
@@ -123,9 +232,12 @@ func (response PostApiLogin401JSONResponse) VisitPostApiLoginResponse(w http.Res
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
+ // List games
+ // (GET /games)
+ GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error)
// User login
- // (POST /api/login)
- PostApiLogin(ctx context.Context, request PostApiLoginRequestObject) (PostApiLoginResponseObject, error)
+ // (POST /login)
+ PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error)
}
type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc
@@ -140,29 +252,54 @@ type strictHandler struct {
middlewares []StrictMiddlewareFunc
}
-// PostApiLogin operation middleware
-func (sh *strictHandler) PostApiLogin(ctx echo.Context) error {
- var request PostApiLoginRequestObject
+// GetGames operation middleware
+func (sh *strictHandler) GetGames(ctx echo.Context, params GetGamesParams) 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))
+ }
+ for _, middleware := range sh.middlewares {
+ handler = middleware(handler, "GetGames")
+ }
+
+ response, err := handler(ctx, request)
+
+ if err != nil {
+ return err
+ } else if validResponse, ok := response.(GetGamesResponseObject); ok {
+ return validResponse.VisitGetGamesResponse(ctx.Response())
+ } else if response != nil {
+ return fmt.Errorf("unexpected response type: %T", response)
+ }
+ return nil
+}
+
+// PostLogin operation middleware
+func (sh *strictHandler) PostLogin(ctx echo.Context) error {
+ var request PostLoginRequestObject
- var body PostApiLoginJSONRequestBody
+ var body PostLoginJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
- return sh.ssi.PostApiLogin(ctx.Request().Context(), request.(PostApiLoginRequestObject))
+ return sh.ssi.PostLogin(ctx.Request().Context(), request.(PostLoginRequestObject))
}
for _, middleware := range sh.middlewares {
- handler = middleware(handler, "PostApiLogin")
+ handler = middleware(handler, "PostLogin")
}
response, err := handler(ctx, request)
if err != nil {
return err
- } else if validResponse, ok := response.(PostApiLoginResponseObject); ok {
- return validResponse.VisitPostApiLoginResponse(ctx.Response())
+ } else if validResponse, ok := response.(PostLoginResponseObject); ok {
+ return validResponse.VisitPostLoginResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("unexpected response type: %T", response)
}
@@ -172,15 +309,20 @@ func (sh *strictHandler) PostApiLogin(ctx echo.Context) error {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/6RSwY7aQAz9lcjnKITdnnKj6oVVD0hVT1WFnMTA0Ik9HU+WjVb592qSEkhB2kpwAGbm",
- "+dnv+b1DJY0TJg4KxTtodaAGh78vp7DBzgrW8eS8OPLB0PBWG3UWuy1jQ/FMb9g4S1DAixw4+SIEKYTO",
- "xRsN3vAe+hRMJbx1GA7zkoVpcE+6OMqBs6PbQwrcWotlfA2+pXtUusW6MTxj2qHVC7gUsYQc0a2S35p6",
- "Bl4+PU9QbpuS/Bl5KypOdiuoT8HT79Z4qqH4MTW5IknnRl2N/XNik/JIVYA+0hneSewcTBj6rmyJwYtq",
- "YjhETpucqExWmzWk8EpejTAUkGfLLI/TiyNGZ6CA5yzPckghuj1sbIHOLKzsR8ucaIi/cacYjPC6hgI2",
- "omHlzNcBNYojDZ+l7iK2Eg7EQxk6Z001FC6OKnxJzm1UHKqexM/Nn26XT8/3kvLgGv66PbW+7/alKoZs",
- "uFAnrOPcT3n+gOogv2geTniLn+zq+0MpI8n94WvSyhsXxgR8a6uKVHettV2CbTgQhzgq1dHNT/nyASkN",
- "qeL+n1Ws+RWtqZPKUx17odUP5ZyJ/kfQmf+8zUR8Mq0zwrVtGvQdFPBdySdjsvu+7/8EAAD//3hNTqrS",
- "BAAA",
+ "H4sIAAAAAAAC/6xUTW/jNhD9K8K0R8FW4iDY+pai6CKLPRhoe1oExlgcS0wlkssZJesG+u8FqQ9btoqk",
+ "SHKIZGo+3nvzOC+Q29pZQ0YY1i/AeUk1xtfPWFN4Om8dedEUT5VmV+Fha/qv9ANrVxGsY3xyBSnIwYXf",
+ "LF6bAtoUVONRtDVbptwaxZO81W02pmgjVJAPOQXWtNVqEno1F+i83VVUh8CfPe1hDT8tj5yWPaHlpg9r",
+ "U2BBL6S2KJPqv9zc3n66+ZTNwmFB6fiapob1N8gry6QghWfUok2xJSM+aHQ8iX0gICSHnqDvHESJ/LqX",
+ "vTaaS1LwkJ6IibnoJ7oUs03B0/dGe1IBxaDSADCdzmdG+oexpN09Ui6B3Jdn2eChsqj+z7y/2NIkv1ma",
+ "m7jOrdk6lHKastQ1FsTLR1uaxaMrZlN5i6rWZpK5x4ppDN5ZWxGaEN0w+QubXK/mRhhCL1kEKK/KPHQ5",
+ "KXKh9Ih7TuHN0aRn8hLnXrswoimuP0vNieYEk8HgM1r1n950T0RLdca9RzV3ac8EOGk0VEon2C9JhxLa",
+ "7G1o2feGu2qH4i1zEoB5g1XyTLvkbnMPKTyR5ygDZIurRRYwW0cGnYY1rBbZIgt3CaWMwi2D9eNbQfEe",
+ "B1Wj1e9VWEYkn2NASPFYk5BnWH97geAs+N6QP0AKnR8gzHGYcLcwIuozDdu0zy4JFflj+l0jpfX6n9ge",
+ "TpUT39BMyVHlhxDMzhruuFxnWXjk1giZSAudq3QeKy8fuXPJsd7UTKMkWqjm1zZi3O/tODf0Hg+zC4b/",
+ "Y7oT78JXzZLYfdJltCncZKt3cKmJGYszw/5u/U4rRSYZp/2qdYdCb+Ew1o9VuKlr9IeBW0+sTWFZ2aJb",
+ "UM7yjPk2luVrDOmgEMuvVh3eoYZD5mfrp9d8PL26Xs1th3cuvH6vja3nBZx6vf1QP4v9m87W4o/wtzj5",
+ "/yqVrshbpv9Hk+fEvG+q6pBgIyUZCVBJdXa++mg735snrLRKck8q9MKKP9TOQ/1hmon1yTjOqcP/YvJJ",
+ "Z+u2bdt/AwAA//+RpToKFwoAAA==",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/backend/api/handlers.go b/backend/api/handlers.go
index c435d72..9856ce9 100644
--- a/backend/api/handlers.go
+++ b/backend/api/handlers.go
@@ -3,6 +3,7 @@ package api
import (
"context"
"net/http"
+ "strings"
"github.com/labstack/echo/v4"
@@ -10,6 +11,8 @@ import (
"github.com/nsfisis/iosdc-2024-albatross-backend/db"
)
+var _ StrictServerInterface = (*ApiHandler)(nil)
+
type ApiHandler struct {
q *db.Queries
}
@@ -20,43 +23,148 @@ func NewHandler(queries *db.Queries) *ApiHandler {
}
}
-func (h *ApiHandler) PostApiLogin(ctx context.Context, request PostApiLoginRequestObject) (PostApiLoginResponseObject, error) {
+func (h *ApiHandler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) {
username := request.Body.Username
password := request.Body.Password
userId, err := auth.Login(ctx, h.q, username, password)
if err != nil {
- return PostApiLogin401JSONResponse{
+ return PostLogin401JSONResponse{
Message: "Invalid username or password",
- }, echo.NewHTTPError(http.StatusUnauthorized, "Invalid username or password")
+ }, nil
}
user, err := h.q.GetUserById(ctx, int32(userId))
if err != nil {
- return PostApiLogin401JSONResponse{
+ return PostLogin401JSONResponse{
Message: "Invalid username or password",
- }, echo.NewHTTPError(http.StatusUnauthorized, "Invalid username or password")
+ }, nil
}
jwt, err := auth.NewJWT(&user)
if err != nil {
// TODO
- return PostApiLogin401JSONResponse{
- Message: "Internal Server Error",
- }, echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
+ return nil, echo.NewHTTPError(http.StatusInternalServerError)
}
- return PostApiLogin200JSONResponse{
+ return PostLogin200JSONResponse{
Token: jwt,
}, nil
}
+func (h *ApiHandler) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) {
+ user := ctx.Value("user").(*auth.JWTClaims)
+ playerId := request.Params.PlayerId
+ if !user.IsAdmin {
+ if playerId == nil || *playerId != user.UserID {
+ return GetGames403JSONResponse{
+ Message: "Forbidden",
+ }, nil
+ }
+ }
+ if playerId == nil {
+ gameRows, err := h.q.ListGames(ctx)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError)
+ }
+ 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.Valid {
+ if !row.Title.Valid || !row.Description.Valid {
+ panic("inconsistent data")
+ }
+ problem = &Problem{
+ ProblemId: int(row.ProblemID.Int32),
+ Title: row.Title.String,
+ Description: row.Description.String,
+ }
+ }
+ games[i] = Game{
+ GameId: int(row.GameID),
+ State: GameState(row.State),
+ DisplayName: row.DisplayName,
+ DurationSeconds: int(row.DurationSeconds),
+ StartedAt: startedAt,
+ Problem: problem,
+ }
+ }
+ return GetGames200JSONResponse{
+ Games: games,
+ }, nil
+ } else {
+ gameRows, err := h.q.ListGamesForPlayer(ctx, int32(*playerId))
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError)
+ }
+ 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.Valid {
+ if !row.Title.Valid || !row.Description.Valid {
+ panic("inconsistent data")
+ }
+ problem = &Problem{
+ ProblemId: int(row.ProblemID.Int32),
+ Title: row.Title.String,
+ Description: row.Description.String,
+ }
+ }
+ games[i] = Game{
+ GameId: int(row.GameID),
+ State: GameState(row.State),
+ DisplayName: row.DisplayName,
+ DurationSeconds: int(row.DurationSeconds),
+ StartedAt: startedAt,
+ Problem: problem,
+ }
+ }
+ return GetGames200JSONResponse{
+ Games: games,
+ }, nil
+ }
+}
+
func _assertJwtPayloadIsCompatibleWithJWTClaims() {
var c auth.JWTClaims
var p JwtPayload
- p.UserId = float32(c.UserID)
+ p.UserId = c.UserID
p.Username = c.Username
p.DisplayName = c.DisplayName
p.IconPath = c.IconPath
p.IsAdmin = c.IsAdmin
_ = p
}
+
+func NewJWTMiddleware() StrictMiddlewareFunc {
+ return func(handler StrictHandlerFunc, operationID string) StrictHandlerFunc {
+ if operationID == "PostLogin" {
+ return handler
+ } else {
+ return func(c echo.Context, request interface{}) (response interface{}, err error) {
+ authorization := c.Request().Header.Get("Authorization")
+ const prefix = "Bearer "
+ if !strings.HasPrefix(authorization, prefix) {
+ return nil, echo.NewHTTPError(http.StatusUnauthorized)
+ }
+ token := authorization[len(prefix):]
+
+ claims, err := auth.ParseJWT(token)
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusUnauthorized)
+ }
+ c.SetRequest(c.Request().WithContext(context.WithValue(c.Request().Context(), "user", claims)))
+ return handler(c, request)
+ }
+ }
+ }
+}
diff --git a/backend/api/workaround.go b/backend/api/workaround.go
new file mode 100644
index 0000000..a3c47d7
--- /dev/null
+++ b/backend/api/workaround.go
@@ -0,0 +1,22 @@
+package api
+
+import (
+ "github.com/getkin/kin-openapi/openapi3"
+)
+
+// Work-around for this issue:
+// https://stackoverflow.com/questions/70087465/echo-groups-not-working-with-openapi-generated-code-using-oapi-codegen
+func GetSwaggerWithPrefix(prefix string) (*openapi3.T, error) {
+ spec, err := GetSwagger()
+ if err != nil {
+ return nil, err
+ }
+
+ var prefixedPaths openapi3.Paths = openapi3.Paths{}
+ for key, value := range spec.Paths.Map() {
+ prefixedPaths.Set(prefix+key, value)
+ }
+
+ spec.Paths = &prefixedPaths
+ return spec, nil
+}