From 1b794787a5194405cd99d80981008e20aa0e953d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 14:18:30 +0900 Subject: feat(proxy): proxy /admin/* to api-server --- nginx.conf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nginx.conf b/nginx.conf index cee5b5f..92c21f3 100644 --- a/nginx.conf +++ b/nginx.conf @@ -20,6 +20,14 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location /admin/ { + proxy_pass http://api-server; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /sock/ { proxy_pass http://api-server; proxy_http_version 1.1; -- cgit v1.2.3-70-g09d2 From 264fa15fb2ba5f0b9636cda44b64deb3c56aa99d Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 14:50:30 +0900 Subject: feat(backend): serve /admin/* pages from api-server --- README.md | 7 +- backend/admin/assets/css/LICENSE.normalize.css.md | 21 + backend/admin/assets/css/LICENSE.sakura.css.txt | 21 + backend/admin/assets/css/normalize.css | 461 ++++++++++++++++++++++ backend/admin/assets/css/sakura.css | 226 +++++++++++ backend/admin/handlers.go | 240 +++++++++++ backend/admin/renderer.go | 49 +++ backend/admin/templates/base.html | 25 ++ backend/admin/templates/dashboard.html | 10 + backend/admin/templates/game_edit.html | 45 +++ backend/admin/templates/games.html | 17 + backend/admin/templates/user_edit.html | 33 ++ backend/admin/templates/users.html | 17 + backend/main.go | 6 + 14 files changed, 1177 insertions(+), 1 deletion(-) create mode 100644 backend/admin/assets/css/LICENSE.normalize.css.md create mode 100644 backend/admin/assets/css/LICENSE.sakura.css.txt create mode 100644 backend/admin/assets/css/normalize.css create mode 100644 backend/admin/assets/css/sakura.css create mode 100644 backend/admin/handlers.go create mode 100644 backend/admin/renderer.go create mode 100644 backend/admin/templates/base.html create mode 100644 backend/admin/templates/dashboard.html create mode 100644 backend/admin/templates/game_edit.html create mode 100644 backend/admin/templates/games.html create mode 100644 backend/admin/templates/user_edit.html create mode 100644 backend/admin/templates/users.html diff --git a/README.md b/README.md index 04b1fbc..5c50c0f 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,10 @@ # License -The contents of the repository is licensed under The MIT License, except for [the site favicon](frontend/public/favicon.svg). +The contents of the repository is licensed under The MIT License, except for + +* [backend/admin/assets/css/normalize.css](backend/admin/assets/normalize.css), +* [backend/admin/assets/css/sakura.css](backend/admin/assets/sakura.css) and +* [frontend/public/favicon.svg](frontend/public/favicon.svg). + See [LICENSE](./LICENSE) for copylight notice. diff --git a/backend/admin/assets/css/LICENSE.normalize.css.md b/backend/admin/assets/css/LICENSE.normalize.css.md new file mode 100644 index 0000000..43b5ddc --- /dev/null +++ b/backend/admin/assets/css/LICENSE.normalize.css.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright © Nicolas Gallagher and Jonathan Neal + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/admin/assets/css/LICENSE.sakura.css.txt b/backend/admin/assets/css/LICENSE.sakura.css.txt new file mode 100644 index 0000000..5c6f3e4 --- /dev/null +++ b/backend/admin/assets/css/LICENSE.sakura.css.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Mitesh Shah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/admin/assets/css/normalize.css b/backend/admin/assets/css/normalize.css new file mode 100644 index 0000000..9b77e0e --- /dev/null +++ b/backend/admin/assets/css/normalize.css @@ -0,0 +1,461 @@ +/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ + +/** + * 1. Change the default font family in all browsers (opinionated). + * 2. Correct the line height in all browsers. + * 3. Prevent adjustments of font size after orientation changes in + * IE on Windows Phone and in iOS. + */ + +/* Document + ========================================================================== */ + +html { + font-family: sans-serif; /* 1 */ + line-height: 1.15; /* 2 */ + -ms-text-size-adjust: 100%; /* 3 */ + -webkit-text-size-adjust: 100%; /* 3 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers (opinionated). + */ + +body { + margin: 0; +} + +/** + * Add the correct display in IE 9-. + */ + +article, +aside, +footer, +header, +nav, +section { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + * 1. Add the correct display in IE. + */ + +figcaption, +figure, +main { /* 1 */ + display: block; +} + +/** + * Add the correct margin in IE 8. + */ + +figure { + margin: 1em 40px; +} + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * 1. Remove the gray background on active links in IE 10. + * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + background-color: transparent; /* 1 */ + -webkit-text-decoration-skip: objects; /* 2 */ +} + +/** + * Remove the outline on focused links when they are also active or hovered + * in all browsers (opinionated). + */ + +a:active, +a:hover { + outline-width: 0; +} + +/** + * 1. Remove the bottom border in Firefox 39-. + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Prevent the duplicate application of `bolder` by the next rule in Safari 6. + */ + +b, +strong { + font-weight: inherit; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font style in Android 4.3-. + */ + +dfn { + font-style: italic; +} + +/** + * Add the correct background and color in IE 9-. + */ + +mark { + background-color: #ff0; + color: #000; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +audio, +video { + display: inline-block; +} + +/** + * Add the correct display in iOS 4-7. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Remove the border on images inside links in IE 10-. + */ + +img { + border-style: none; +} + +/** + * Hide the overflow in IE. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers (opinionated). + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + * controls in Android 4. + * 2. Correct the inability to style clickable types in iOS and Safari. + */ + +button, +html [type="button"], /* 1 */ +[type="reset"], +[type="submit"] { + -webkit-appearance: button; /* 2 */ +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Change the border, margin, and padding in all browsers (opinionated). + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * 1. Add the correct display in IE 9-. + * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Remove the default vertical scrollbar in IE. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10-. + * 2. Remove the padding in IE 10-. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in IE 9-. + * 1. Add the correct display in Edge, IE, and Firefox. + */ + +details, /* 1 */ +menu { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Scripting + ========================================================================== */ + +/** + * Add the correct display in IE 9-. + */ + +canvas { + display: inline-block; +} + +/** + * Add the correct display in IE. + */ + +template { + display: none; +} + +/* Hidden + ========================================================================== */ + +/** + * Add the correct display in IE 10-. + */ + +[hidden] { + display: none; +} diff --git a/backend/admin/assets/css/sakura.css b/backend/admin/assets/css/sakura.css new file mode 100644 index 0000000..3992573 --- /dev/null +++ b/backend/admin/assets/css/sakura.css @@ -0,0 +1,226 @@ +/* Sakura.css v1.5.0 + * ================ + * Minimal css theme. + * Project: https://github.com/oxalorg/sakura/ + */ +/* Body */ +html { + font-size: 62.5%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; +} + +body { + font-size: 1.8rem; + line-height: 1.618; + max-width: 38em; + margin: auto; + color: #4a4a4a; + background-color: #f9f9f9; + padding: 13px; +} + +@media (max-width: 684px) { + body { + font-size: 1.53rem; + } +} +@media (max-width: 382px) { + body { + font-size: 1.35rem; + } +} +h1, h2, h3, h4, h5, h6 { + line-height: 1.1; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; +} + +h1 { + font-size: 2.35em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.75em; +} + +h4 { + font-size: 1.5em; +} + +h5 { + font-size: 1.25em; +} + +h6 { + font-size: 1em; +} + +p { + margin-top: 0px; + margin-bottom: 2.5rem; +} + +small, sub, sup { + font-size: 75%; +} + +hr { + border-color: #1d7484; +} + +a { + text-decoration: none; + color: #1d7484; +} +a:visited { + color: #144f5a; +} +a:hover { + color: #982c61; + border-bottom: 2px solid #4a4a4a; +} + +ul { + padding-left: 1.4em; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +li { + margin-bottom: 0.4em; +} + +blockquote { + margin-left: 0px; + margin-right: 0px; + padding-left: 1em; + padding-top: 0.8em; + padding-bottom: 0.8em; + padding-right: 0.8em; + border-left: 5px solid #1d7484; + margin-bottom: 2.5rem; + background-color: #f1f1f1; +} + +blockquote p { + margin-bottom: 0; +} + +img, video { + height: auto; + max-width: 100%; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +/* Pre and Code */ +pre { + background-color: #f1f1f1; + display: block; + padding: 1em; + overflow-x: auto; + margin-top: 0px; + margin-bottom: 2.5rem; + font-size: 0.9em; +} + +code, kbd, samp { + font-size: 0.9em; + padding: 0 0.5em; + background-color: #f1f1f1; + white-space: pre-wrap; +} + +pre > code { + padding: 0; + background-color: transparent; + white-space: pre; + font-size: 1em; +} + +/* Tables */ +table { + text-align: justify; + width: 100%; + border-collapse: collapse; + margin-bottom: 2rem; +} + +td, th { + padding: 0.5em; + border-bottom: 1px solid #f1f1f1; +} + +/* Buttons, forms and input */ +input, textarea { + border: 1px solid #4a4a4a; +} +input:focus, textarea:focus { + border: 1px solid #1d7484; +} + +textarea { + width: 100%; +} + +.button, button, input[type=submit], input[type=reset], input[type=button], input[type=file]::file-selector-button { + display: inline-block; + padding: 5px 10px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background-color: #1d7484; + color: #f9f9f9; + border-radius: 1px; + border: 1px solid #1d7484; + cursor: pointer; + box-sizing: border-box; +} +.button[disabled], button[disabled], input[type=submit][disabled], input[type=reset][disabled], input[type=button][disabled], input[type=file]::file-selector-button[disabled] { + cursor: default; + opacity: 0.5; +} +.button:hover, button:hover, input[type=submit]:hover, input[type=reset]:hover, input[type=button]:hover, input[type=file]::file-selector-button:hover { + background-color: #982c61; + color: #f9f9f9; + outline: 0; +} +.button:focus-visible, button:focus-visible, input[type=submit]:focus-visible, input[type=reset]:focus-visible, input[type=button]:focus-visible, input[type=file]::file-selector-button:focus-visible { + outline-style: solid; + outline-width: 2px; +} + +textarea, select, input { + color: #4a4a4a; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + margin-bottom: 10px; + background-color: #f1f1f1; + border: 1px solid #f1f1f1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; +} +textarea:focus, select:focus, input:focus { + border: 1px solid #1d7484; + outline: 0; +} + +input[type=checkbox]:focus { + outline: 1px dotted #1d7484; +} + +label, legend, fieldset { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} diff --git a/backend/admin/handlers.go b/backend/admin/handlers.go new file mode 100644 index 0000000..1c7995d --- /dev/null +++ b/backend/admin/handlers.go @@ -0,0 +1,240 @@ +package admin + +import ( + "errors" + "net/http" + "strconv" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" + + "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" +) + +var jst = time.FixedZone("Asia/Tokyo", 9*60*60) + +type AdminHandler struct { + q *db.Queries + hubs GameHubsInterface +} + +type GameHubsInterface interface { + StartGame(gameID int) error +} + +func NewAdminHandler(q *db.Queries, hubs GameHubsInterface) *AdminHandler { + return &AdminHandler{ + q: q, + hubs: hubs, + } +} + +func (h *AdminHandler) RegisterHandlers(g *echo.Group) { + g.Use(newAssetsMiddleware()) + + g.GET("/dashboard", h.getDashboard) + g.GET("/users", h.getUsers) + g.GET("/users/:userID", h.getUserEdit) + g.GET("/games", h.getGames) + g.GET("/games/:gameID", h.getGameEdit) + g.POST("/games/:gameID", h.postGameEdit) +} + +func (h *AdminHandler) getDashboard(c echo.Context) error { + return c.Render(http.StatusOK, "dashboard", echo.Map{ + "Title": "Dashboard", + }) +} + +func (h *AdminHandler) getUsers(c echo.Context) error { + rows, err := h.q.ListUsers(c.Request().Context()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + users := make([]echo.Map, len(rows)) + for i, u := range rows { + users[i] = echo.Map{ + "UserID": u.UserID, + "Username": u.Username, + "DisplayName": u.DisplayName, + "IconPath": u.IconPath, + "IsAdmin": u.IsAdmin, + } + } + + return c.Render(http.StatusOK, "users", echo.Map{ + "Title": "Users", + "Users": users, + }) +} + +func (h *AdminHandler) getUserEdit(c echo.Context) error { + userID, err := strconv.Atoi(c.Param("userID")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid user id") + } + row, err := h.q.GetUserByID(c.Request().Context(), int32(userID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound) + } else { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + + return c.Render(http.StatusOK, "user_edit", echo.Map{ + "Title": "User Edit", + "User": echo.Map{ + "UserID": row.UserID, + "Username": row.Username, + "DisplayName": row.DisplayName, + "IconPath": row.IconPath, + "IsAdmin": row.IsAdmin, + }, + }) +} + +func (h *AdminHandler) getGames(c echo.Context) error { + rows, err := h.q.ListGames(c.Request().Context()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + games := make([]echo.Map, len(rows)) + for i, g := range rows { + var startedAt string + if !g.StartedAt.Valid { + startedAt = g.StartedAt.Time.In(jst).Format("2006-01-02T15:04:05") + } + games[i] = echo.Map{ + "GameID": g.GameID, + "State": g.State, + "DisplayName": g.DisplayName, + "DurationSeconds": g.DurationSeconds, + "StartedAt": startedAt, + "ProblemID": g.ProblemID, + } + } + + return c.Render(http.StatusOK, "games", echo.Map{ + "Title": "Games", + "Games": games, + }) +} + +func (h *AdminHandler) getGameEdit(c echo.Context) error { + gameID, err := strconv.Atoi(c.Param("gameID")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") + } + row, err := h.q.GetGameByID(c.Request().Context(), int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound) + } else { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + + var startedAt string + if !row.StartedAt.Valid { + startedAt = row.StartedAt.Time.In(jst).Format("2006-01-02T15:04:05") + } + + return c.Render(http.StatusOK, "game_edit", echo.Map{ + "Title": "Game Edit", + "Game": echo.Map{ + "GameID": row.GameID, + "State": row.State, + "DisplayName": row.DisplayName, + "DurationSeconds": row.DurationSeconds, + "StartedAt": startedAt, + "ProblemID": row.ProblemID, + }, + }) +} + +func (h *AdminHandler) postGameEdit(c echo.Context) error { + gameID, err := strconv.Atoi(c.Param("gameID")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") + } + row, err := h.q.GetGameByID(c.Request().Context(), int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return echo.NewHTTPError(http.StatusNotFound) + } else { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + + state := c.FormValue("state") + displayName := c.FormValue("display_name") + durationSeconds, err := strconv.Atoi(c.FormValue("duration_seconds")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid duration_seconds") + } + var problemID *int + { + problemIDRaw := c.FormValue("problem_id") + if problemIDRaw != "" { + problemIDInt, err := strconv.Atoi(problemIDRaw) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid problem_id") + } + problemID = &problemIDInt + } + } + var startedAt *time.Time + { + startedAtRaw := c.FormValue("started_at") + if startedAtRaw != "" { + startedAtTime, err := time.Parse("2006-01-02T15:04:05", startedAtRaw) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid started_at") + } + startedAt = &startedAtTime + } + } + + var changedStartedAt pgtype.Timestamp + if startedAt == nil { + changedStartedAt = pgtype.Timestamp{ + Valid: false, + } + } else { + changedStartedAt = pgtype.Timestamp{ + Time: *startedAt, + Valid: true, + } + } + var changedProblemID *int32 + if problemID == nil { + changedProblemID = nil + } else { + changedProblemID = new(int32) + *changedProblemID = int32(*problemID) + } + + { + // TODO: + if state != row.State && state == "prepare" { + h.hubs.StartGame(int(gameID)) + } + } + + err = h.q.UpdateGame(c.Request().Context(), db.UpdateGameParams{ + GameID: int32(gameID), + State: state, + DisplayName: displayName, + DurationSeconds: int32(durationSeconds), + StartedAt: changedStartedAt, + ProblemID: changedProblemID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.String(http.StatusNoContent, "") +} diff --git a/backend/admin/renderer.go b/backend/admin/renderer.go new file mode 100644 index 0000000..468677f --- /dev/null +++ b/backend/admin/renderer.go @@ -0,0 +1,49 @@ +package admin + +import ( + "embed" + "html/template" + "io" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +var ( + //go:embed templates + templatesFS embed.FS + //go:embed assets + assetsFS embed.FS +) + +type Renderer struct { + templates map[string]*template.Template +} + +func NewRenderer() *Renderer { + return &Renderer{ + templates: make(map[string]*template.Template), + } +} + +func (r *Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + tmpl, ok := r.templates[name] + if !ok { + t, err := template.ParseFS(templatesFS, "templates/base.html", "templates/"+name+".html") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + r.templates[name] = t + tmpl = t + } + return tmpl.ExecuteTemplate(w, name+".html", data) +} + +func newAssetsMiddleware() echo.MiddlewareFunc { + return middleware.StaticWithConfig(middleware.StaticConfig{ + Root: "/assets", + Filesystem: http.FS(assetsFS), + IgnoreBase: true, + }) +} diff --git a/backend/admin/templates/base.html b/backend/admin/templates/base.html new file mode 100644 index 0000000..4bcdbdd --- /dev/null +++ b/backend/admin/templates/base.html @@ -0,0 +1,25 @@ + + + + ADMIN {{ .Title }} | iOSDC Japan 2024 Albatross.swift + + + + + +
+

ADMIN {{ .Title }}

+

+ This is an admin page. +

+
+ +
+
+ {{ block "content" . }}{{ end }} +
+
+ + diff --git a/backend/admin/templates/dashboard.html b/backend/admin/templates/dashboard.html new file mode 100644 index 0000000..2d7e8ad --- /dev/null +++ b/backend/admin/templates/dashboard.html @@ -0,0 +1,10 @@ +{{ template "base.html" . }} + +{{ define "content" }} +

+ Users +

+

+ Games +

+{{ end }} diff --git a/backend/admin/templates/game_edit.html b/backend/admin/templates/game_edit.html new file mode 100644 index 0000000..8bc5410 --- /dev/null +++ b/backend/admin/templates/game_edit.html @@ -0,0 +1,45 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +Dashboard | Games +{{ end }} + +{{ define "content" }} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+{{ end }} diff --git a/backend/admin/templates/games.html b/backend/admin/templates/games.html new file mode 100644 index 0000000..244fc94 --- /dev/null +++ b/backend/admin/templates/games.html @@ -0,0 +1,17 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +Dashboard +{{ end }} + +{{ define "content" }} + +{{ end }} diff --git a/backend/admin/templates/user_edit.html b/backend/admin/templates/user_edit.html new file mode 100644 index 0000000..9089b1e --- /dev/null +++ b/backend/admin/templates/user_edit.html @@ -0,0 +1,33 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +Dashboard | Users +{{ end }} + +{{ define "content" }} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+{{ end }} diff --git a/backend/admin/templates/users.html b/backend/admin/templates/users.html new file mode 100644 index 0000000..656ad53 --- /dev/null +++ b/backend/admin/templates/users.html @@ -0,0 +1,17 @@ +{{ template "base.html" . }} + +{{ define "breadcrumb" }} +Dashboard +{{ end }} + +{{ define "content" }} + +{{ end }} diff --git a/backend/main.go b/backend/main.go index 939df03..7330109 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,6 +11,7 @@ import ( "github.com/labstack/echo/v4/middleware" oapimiddleware "github.com/oapi-codegen/echo-middleware" + "github.com/nsfisis/iosdc-japan-2024-albatross/backend/admin" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/api" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/game" @@ -53,6 +54,7 @@ func main() { queries := db.New(connPool) e := echo.New() + e.Renderer = admin.NewRenderer() e.Use(middleware.Logger()) e.Use(middleware.Recover()) @@ -77,6 +79,10 @@ func main() { apiHandler := api.NewHandler(queries, gameHubs) api.RegisterHandlers(apiGroup, api.NewStrictHandler(apiHandler, nil)) + adminHandler := admin.NewAdminHandler(queries, gameHubs) + adminGroup := e.Group("/admin") + adminHandler.RegisterHandlers(adminGroup) + gameHubs.Run() if err := e.Start(":80"); err != http.ErrServerClosed { -- cgit v1.2.3-70-g09d2 From dd3440009ae75ecc00e10e48b014ef23bb446964 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 17:06:23 +0900 Subject: feat(frontend): remove admin pages --- frontend/app/.server/auth.ts | 10 --- frontend/app/routes/admin.dashboard.tsx | 29 --------- frontend/app/routes/admin.games.tsx | 35 ---------- frontend/app/routes/admin.games_.$gameId.tsx | 95 ---------------------------- frontend/app/routes/admin.tsx | 8 --- frontend/app/routes/admin.users.tsx | 34 ---------- 6 files changed, 211 deletions(-) delete mode 100644 frontend/app/routes/admin.dashboard.tsx delete mode 100644 frontend/app/routes/admin.games.tsx delete mode 100644 frontend/app/routes/admin.games_.$gameId.tsx delete mode 100644 frontend/app/routes/admin.tsx delete mode 100644 frontend/app/routes/admin.users.tsx diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index a4811e2..d5ffe0f 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -40,16 +40,6 @@ export async function ensureUserLoggedIn( return { user, token }; } -export async function ensureAdminUserLoggedIn( - request: Request | Session, -): Promise<{ user: User; token: string }> { - const { user, token } = await ensureUserLoggedIn(request); - if (!user.is_admin) { - throw new Error("Forbidden"); - } - return { user, token }; -} - export async function ensureUserNotLoggedIn( request: Request | Session, ): Promise { diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx deleted file mode 100644 index 8a0c9a8..0000000 --- a/frontend/app/routes/admin.dashboard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Form, Link } from "@remix-run/react"; -import { ensureAdminUserLoggedIn } from "../.server/auth"; - -export const meta: MetaFunction = () => [ - { title: "[Admin] Dashboard | iOSDC Japan 2024 Albatross.swift" }, -]; - -export async function loader({ request }: LoaderFunctionArgs) { - await ensureAdminUserLoggedIn(request); - return null; -} - -export default function AdminDashboard() { - return ( -
-

[Admin] Dashboard

-

- Users -

-

- Games -

-
- -
-
- ); -} diff --git a/frontend/app/routes/admin.games.tsx b/frontend/app/routes/admin.games.tsx deleted file mode 100644 index f9d15f7..0000000 --- a/frontend/app/routes/admin.games.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, useLoaderData } from "@remix-run/react"; -import { adminApiGetGames } from "../.server/api/client"; -import { ensureAdminUserLoggedIn } from "../.server/auth"; - -export const meta: MetaFunction = () => [ - { title: "[Admin] Games | iOSDC Japan 2024 Albatross.swift" }, -]; - -export async function loader({ request }: LoaderFunctionArgs) { - const { token } = await ensureAdminUserLoggedIn(request); - const { games } = await adminApiGetGames(token); - return { games }; -} - -export default function AdminGames() { - const { games } = useLoaderData()!; - - return ( -
-
-

[Admin] Games

-
    - {games.map((game) => ( -
  • - - {game.display_name} (id={game.game_id}) - -
  • - ))} -
-
-
- ); -} diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx deleted file mode 100644 index c4d75c1..0000000 --- a/frontend/app/routes/admin.games_.$gameId.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type { - ActionFunctionArgs, - LoaderFunctionArgs, - MetaFunction, -} from "@remix-run/node"; -import { Form, useLoaderData } from "@remix-run/react"; -import { adminApiGetGame, adminApiPutGame } from "../.server/api/client"; -import { ensureAdminUserLoggedIn } from "../.server/auth"; - -export const meta: MetaFunction = ({ data }) => [ - { - title: data - ? `[Admin] Game Edit ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift` - : "[Admin] Game Edit | iOSDC Japan 2024 Albatross.swift", - }, -]; - -export async function loader({ request, params }: LoaderFunctionArgs) { - const { token } = await ensureAdminUserLoggedIn(request); - const { gameId } = params; - const { game } = await adminApiGetGame(token, Number(gameId)); - return { game }; -} - -export async function action({ request, params }: ActionFunctionArgs) { - const { token } = await ensureAdminUserLoggedIn(request); - const { gameId } = params; - - const formData = await request.formData(); - const action = formData.get("action"); - - const nextState = - action === "open" - ? "waiting_entries" - : action === "start" - ? "prepare" - : null; - if (!nextState) { - throw new Error("Invalid action"); - } - - await adminApiPutGame(token, Number(gameId), { - state: nextState, - }); - return null; -} - -export default function AdminGameEdit() { - const { game } = useLoaderData()!; - - return ( -
-
-

[Admin] Game Edit {game.display_name}

-
    -
  • ID: {game.game_id}
  • -
  • State: {game.state}
  • -
  • Display Name: {game.display_name}
  • -
  • Duration Seconds: {game.duration_seconds}
  • -
  • - Started At:{" "} - {game.started_at - ? new Date(game.started_at * 1000).toString() - : "-"} -
  • -
  • Problem ID: {game.problem ? game.problem.problem_id : "-"}
  • -
-
-
-
- -
-
- -
-
-
-
-
- ); -} diff --git a/frontend/app/routes/admin.tsx b/frontend/app/routes/admin.tsx deleted file mode 100644 index ceef37e..0000000 --- a/frontend/app/routes/admin.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { LinksFunction } from "@remix-run/node"; -import normalizeCss from "sakura.css/css/normalize.css?url"; -import sakuraCss from "sakura.css/css/sakura.css?url"; - -export const links: LinksFunction = () => [ - { rel: "stylesheet", href: normalizeCss }, - { rel: "stylesheet", href: sakuraCss }, -]; diff --git a/frontend/app/routes/admin.users.tsx b/frontend/app/routes/admin.users.tsx deleted file mode 100644 index c403285..0000000 --- a/frontend/app/routes/admin.users.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import { adminApiGetUsers } from "../.server/api/client"; -import { ensureAdminUserLoggedIn } from "../.server/auth"; - -export const meta: MetaFunction = () => [ - { title: "[Admin] Users | iOSDC Japan 2024 Albatross.swift" }, -]; - -export async function loader({ request }: LoaderFunctionArgs) { - const { token } = await ensureAdminUserLoggedIn(request); - const { users } = await adminApiGetUsers(token); - return { users }; -} - -export default function AdminUsers() { - const { users } = useLoaderData()!; - - return ( -
-
-

[Admin] Users

-
    - {users.map((user) => ( -
  • - {user.display_name} (id={user.user_id} username={user.username}) - {user.is_admin && admin} -
  • - ))} -
-
-
- ); -} -- cgit v1.2.3-70-g09d2 From 2034d5efe124ffa8a9bb56821a9dfcfea27425ff Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 17:09:39 +0900 Subject: feat(frontend): redirect to /admin/dashboard if logged-in user is admin --- frontend/app/routes/dashboard.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index 229375c..e23d7aa 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -11,7 +11,11 @@ export const meta: MetaFunction = () => [ export async function loader({ request }: LoaderFunctionArgs) { const { user, token } = await ensureUserLoggedIn(request); if (user.is_admin) { - return redirect("/admin/dashboard"); + return redirect( + process.env.NODE_ENV === "development" + ? "http://localhost:8002/admin/dashboard" + : "/admin/dashboard", + ); } const { games } = await apiGetGames(token); return { @@ -26,10 +30,7 @@ export default function Dashboard() { return (
-

- {user.username}{" "} - {user.is_admin && admin} -

+

{user.username}

User

    -- cgit v1.2.3-70-g09d2 From d87507918f33b289ac4fc4dece8a54fa3aa34923 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 17:24:36 +0900 Subject: feat(backend): add /logout to /admin/dashboard --- backend/admin/handlers.go | 2 +- backend/admin/templates/dashboard.html | 3 +++ backend/main.go | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/admin/handlers.go b/backend/admin/handlers.go index 1c7995d..f81856c 100644 --- a/backend/admin/handlers.go +++ b/backend/admin/handlers.go @@ -236,5 +236,5 @@ func (h *AdminHandler) postGameEdit(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - return c.String(http.StatusNoContent, "") + return c.NoContent(http.StatusNoContent) } diff --git a/backend/admin/templates/dashboard.html b/backend/admin/templates/dashboard.html index 2d7e8ad..cdb8ba1 100644 --- a/backend/admin/templates/dashboard.html +++ b/backend/admin/templates/dashboard.html @@ -7,4 +7,7 @@

    Games

    +
    + +
    {{ end }} diff --git a/backend/main.go b/backend/main.go index 7330109..2d38ee5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -83,6 +83,13 @@ func main() { adminGroup := e.Group("/admin") adminHandler.RegisterHandlers(adminGroup) + // For local dev: + // This is never used in production because the reverse proxy sends /logout + // to the app server. + e.POST("/logout", func(c echo.Context) error { + return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/logout") + }) + gameHubs.Run() if err := e.Start(":80"); err != http.ErrServerClosed { -- cgit v1.2.3-70-g09d2 From 0f0324b396f3eab53606c8f770d26337dd0e291a Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 20:33:37 +0900 Subject: feat: authenticate users in admin pages --- backend/admin/handlers.go | 21 +++++++++++++++++++++ backend/main.go | 8 +++++--- frontend/app/.server/auth.ts | 35 +++++++++++++++++++++++++++++++---- frontend/app/.server/cookie.ts | 41 +++++++++++++++++++++++++++++++++++++++++ frontend/app/.server/session.ts | 16 ++++++++++------ frontend/package-lock.json | 1 + frontend/package.json | 1 + 7 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 frontend/app/.server/cookie.ts diff --git a/backend/admin/handlers.go b/backend/admin/handlers.go index f81856c..14523e6 100644 --- a/backend/admin/handlers.go +++ b/backend/admin/handlers.go @@ -10,6 +10,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" + "github.com/nsfisis/iosdc-japan-2024-albatross/backend/auth" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" ) @@ -31,8 +32,28 @@ func NewAdminHandler(q *db.Queries, hubs GameHubsInterface) *AdminHandler { } } +func newAdminMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + jwt, err := c.Cookie("albatross_token") + if err != nil { + return c.Redirect(http.StatusSeeOther, "/login") + } + claims, err := auth.ParseJWT(jwt.Value) + if err != nil { + return c.Redirect(http.StatusSeeOther, "/login") + } + if !claims.IsAdmin { + return echo.NewHTTPError(http.StatusForbidden) + } + return next(c) + } + } +} + func (h *AdminHandler) RegisterHandlers(g *echo.Group) { g.Use(newAssetsMiddleware()) + g.Use(newAdminMiddleware()) g.GET("/dashboard", h.getDashboard) g.GET("/users", h.getUsers) diff --git a/backend/main.go b/backend/main.go index 2d38ee5..e2e4bbd 100644 --- a/backend/main.go +++ b/backend/main.go @@ -83,9 +83,11 @@ func main() { adminGroup := e.Group("/admin") adminHandler.RegisterHandlers(adminGroup) - // For local dev: - // This is never used in production because the reverse proxy sends /logout - // to the app server. + // For local dev: This is never used in production because the reverse + // proxy sends /login and /logout to the app server. + e.GET("/login", func(c echo.Context) error { + return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/login") + }) e.POST("/logout", func(c echo.Context) error { return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/logout") }) diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index d5ffe0f..2c9d23c 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -1,10 +1,12 @@ +import { redirect } from "@remix-run/node"; import type { Session } from "@remix-run/server-runtime"; import { jwtDecode } from "jwt-decode"; import { Authenticator } from "remix-auth"; import { FormStrategy } from "remix-auth-form"; import { apiPostLogin } from "./api/client"; import { components } from "./api/schema"; -import { sessionStorage } from "./session"; +import { createUnstructuredCookie } from "./cookie"; +import { cookieOptions, sessionStorage } from "./session"; const authenticator = new Authenticator(sessionStorage); @@ -19,15 +21,40 @@ authenticator.use( export type User = components["schemas"]["User"]; +// This cookie is used to directly store the JWT for the API server. +// Remix's createCookie() returns "structured" cookies, which cannot be reused directly by non-Remix servers. +const tokenCookie = createUnstructuredCookie("albatross_token", cookieOptions); + export async function login(request: Request): Promise { - return await authenticator.authenticate("default", request, { - successRedirect: "/dashboard", + const jwt = await authenticator.authenticate("default", request, { failureRedirect: "/login", }); + + const session = await sessionStorage.getSession( + request.headers.get("cookie"), + ); + session.set(authenticator.sessionKey, jwt); + + throw redirect("/dashboard", { + headers: [ + ["Set-Cookie", await sessionStorage.commitSession(session)], + ["Set-Cookie", await tokenCookie.serialize(jwt)], + ], + }); } export async function logout(request: Request | Session): Promise { - return await authenticator.logout(request, { redirectTo: "/" }); + try { + return await authenticator.logout(request, { redirectTo: "/" }); + } catch (response) { + if (response instanceof Response) { + response.headers.append( + "Set-Cookie", + await tokenCookie.serialize("", { maxAge: 0, expires: new Date(0) }), + ); + } + throw response; + } } export async function ensureUserLoggedIn( diff --git a/frontend/app/.server/cookie.ts b/frontend/app/.server/cookie.ts new file mode 100644 index 0000000..cccbe78 --- /dev/null +++ b/frontend/app/.server/cookie.ts @@ -0,0 +1,41 @@ +import { Cookie, CookieOptions } from "@remix-run/server-runtime"; +import { parse, serialize } from "cookie"; + +// Remix's createCookie() returns "structured" cookies, which are cookies that hold a JSON-encoded object. +// This is not suitable for interoperation with other systems that expect a simple string value. +// This function creates an "unstructured" cookie, a simple plain text. +export function createUnstructuredCookie( + name: string, + cookieOptions?: CookieOptions, +): Cookie { + const { secrets = [], ...options } = { + path: "/", + sameSite: "lax" as const, + ...cookieOptions, + }; + + return { + get name() { + return name; + }, + get isSigned() { + return secrets.length > 0; + }, + get expires() { + return typeof options.maxAge !== "undefined" + ? new Date(Date.now() + options.maxAge * 1000) + : options.expires; + }, + async parse(cookieHeader, parseOptions) { + if (!cookieHeader) return null; + const cookies = parse(cookieHeader, { ...options, ...parseOptions }); + return name in cookies ? cookies[name] : null; + }, + async serialize(value, serializeOptions) { + return serialize(name, value, { + ...options, + ...serializeOptions, + }); + }, + }; +} diff --git a/frontend/app/.server/session.ts b/frontend/app/.server/session.ts index 79810f4..102bcd2 100644 --- a/frontend/app/.server/session.ts +++ b/frontend/app/.server/session.ts @@ -1,13 +1,17 @@ import { createCookieSessionStorage } from "@remix-run/node"; +export const cookieOptions = { + sameSite: "lax" as const, + path: "/", + httpOnly: true, + // secure: process.env.NODE_ENV === "production", + secure: false, // TODO + secrets: ["TODO"], +}; + export const sessionStorage = createCookieSessionStorage({ cookie: { name: "albatross_session", - sameSite: "lax", - path: "/", - httpOnly: true, - secrets: ["TODO"], - // secure: process.env.NODE_ENV === "production", - secure: false, // TODO + ...cookieOptions, }, }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a49235c..0e6f7bf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "@remix-run/node": "^2.10.3", "@remix-run/react": "^2.10.3", "@remix-run/serve": "^2.10.3", + "cookie": "^0.6.0", "isbot": "^5.1.13", "jwt-decode": "^4.0.0", "openapi-fetch": "^0.10.2", diff --git a/frontend/package.json b/frontend/package.json index 30d385a..e4eefac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@remix-run/node": "^2.10.3", "@remix-run/react": "^2.10.3", "@remix-run/serve": "^2.10.3", + "cookie": "^0.6.0", "isbot": "^5.1.13", "jwt-decode": "^4.0.0", "openapi-fetch": "^0.10.2", -- cgit v1.2.3-70-g09d2 From 3963dedada8fdc43b18dfec1313c184d2f4fdc47 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 20:34:54 +0900 Subject: chore(frontend): remove disused dependency --- frontend/package-lock.json | 6 ------ frontend/package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0e6f7bf..d5c12fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,7 +19,6 @@ "remix-auth": "^3.7.0", "remix-auth-form": "^1.5.0", "remix-utils": "^7.6.0", - "sakura.css": "^1.5.0", "use-debounce": "^10.0.1" }, "devDependencies": { @@ -9674,11 +9673,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sakura.css": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sakura.css/-/sakura.css-1.5.0.tgz", - "integrity": "sha512-AcAZa9F4SCs2xaKLWcXQxJxKfeod2PN3sR31+R22MKuyoJxNChH1wBG4mQaY9gVpJ3VpNA1XHPOrOM9hFo9cSw==" - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index e4eefac..44af089 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,6 @@ "remix-auth": "^3.7.0", "remix-auth-form": "^1.5.0", "remix-utils": "^7.6.0", - "sakura.css": "^1.5.0", "use-debounce": "^10.0.1" }, "devDependencies": { -- cgit v1.2.3-70-g09d2 From f4bae7f755ca25b2547dc98b2db2fdb255948bc5 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 4 Aug 2024 20:48:50 +0900 Subject: chore: remove admin APIs to communicate between app-server and api-server --- backend/api/generated.go | 561 ++--------------------------------- backend/api/handler_wrapper.go | 76 ----- backend/api/handlers.go | 189 +----------- backend/game/models.go | 14 +- frontend/app/.server/api/client.ts | 49 +-- frontend/app/.server/api/schema.d.ts | 177 ----------- openapi.yaml | 122 -------- 7 files changed, 41 insertions(+), 1147 deletions(-) diff --git a/backend/api/generated.go b/backend/api/generated.go index f7da9ee..ea1c315 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -24,13 +24,13 @@ import ( // Defines values for GameState. const ( - GameStateClosed GameState = "closed" - GameStateFinished GameState = "finished" - GameStateGaming GameState = "gaming" - GameStatePrepare GameState = "prepare" - GameStateStarting GameState = "starting" - GameStateWaitingEntries GameState = "waiting_entries" - GameStateWaitingStart GameState = "waiting_start" + Closed GameState = "closed" + Finished GameState = "finished" + Gaming GameState = "gaming" + Prepare GameState = "prepare" + Starting GameState = "starting" + WaitingEntries GameState = "waiting_entries" + WaitingStart GameState = "waiting_start" ) // Defines values for GamePlayerMessageS2CExecResultPayloadStatus. @@ -43,17 +43,6 @@ const ( GameWatcherMessageS2CExecResultPayloadStatusSuccess GameWatcherMessageS2CExecResultPayloadStatus = "success" ) -// Defines values for AdminPutGameJSONBodyState. -const ( - AdminPutGameJSONBodyStateClosed AdminPutGameJSONBodyState = "closed" - AdminPutGameJSONBodyStateFinished AdminPutGameJSONBodyState = "finished" - AdminPutGameJSONBodyStateGaming AdminPutGameJSONBodyState = "gaming" - AdminPutGameJSONBodyStatePrepare AdminPutGameJSONBodyState = "prepare" - AdminPutGameJSONBodyStateStarting AdminPutGameJSONBodyState = "starting" - AdminPutGameJSONBodyStateWaitingEntries AdminPutGameJSONBodyState = "waiting_entries" - AdminPutGameJSONBodyStateWaitingStart AdminPutGameJSONBodyState = "waiting_start" -) - // Error defines model for Error. type Error struct { Message string `json:"message"` @@ -241,38 +230,6 @@ type NotFound = Error // Unauthorized defines model for Unauthorized. type Unauthorized = Error -// AdminGetGamesParams defines parameters for AdminGetGames. -type AdminGetGamesParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// AdminGetGameParams defines parameters for AdminGetGame. -type AdminGetGameParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// AdminPutGameJSONBody defines parameters for AdminPutGame. -type AdminPutGameJSONBody 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 *AdminPutGameJSONBodyState `json:"state,omitempty"` -} - -// AdminPutGameParams defines parameters for AdminPutGame. -type AdminPutGameParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - -// AdminPutGameJSONBodyState defines parameters for AdminPutGame. -type AdminPutGameJSONBodyState string - -// AdminGetUsersParams defines parameters for AdminGetUsers. -type AdminGetUsersParams struct { - Authorization HeaderAuthorization `json:"Authorization"` -} - // GetGamesParams defines parameters for GetGames. type GetGamesParams struct { Authorization HeaderAuthorization `json:"Authorization"` @@ -294,9 +251,6 @@ type GetTokenParams struct { Authorization HeaderAuthorization `json:"Authorization"` } -// AdminPutGameJSONRequestBody defines body for AdminPutGame for application/json ContentType. -type AdminPutGameJSONRequestBody AdminPutGameJSONBody - // PostLoginJSONRequestBody defines body for PostLogin for application/json ContentType. type PostLoginJSONRequestBody PostLoginJSONBody @@ -690,18 +644,6 @@ func (t *GameWatcherMessageS2C) UnmarshalJSON(b []byte) error { // ServerInterface represents all server handlers. type ServerInterface interface { - // List games - // (GET /admin/games) - AdminGetGames(ctx echo.Context, params AdminGetGamesParams) error - // Get a game - // (GET /admin/games/{game_id}) - AdminGetGame(ctx echo.Context, gameID PathGameID, params AdminGetGameParams) error - // Update a game - // (PUT /admin/games/{game_id}) - AdminPutGame(ctx echo.Context, gameID PathGameID, params AdminPutGameParams) error - // List all users - // (GET /admin/users) - AdminGetUsers(ctx echo.Context, params AdminGetUsersParams) error // List games // (GET /games) GetGames(ctx echo.Context, params GetGamesParams) error @@ -721,144 +663,6 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } -// AdminGetGames converts echo context to params. -func (w *ServerInterfaceWrapper) AdminGetGames(ctx echo.Context) error { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params AdminGetGamesParams - - 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.AdminGetGames(ctx, params) - return err -} - -// AdminGetGame converts echo context to params. -func (w *ServerInterfaceWrapper) AdminGetGame(ctx echo.Context) error { - var err error - // ------------- Path parameter "game_id" ------------- - var gameID PathGameID - - 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 AdminGetGameParams - - 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.AdminGetGame(ctx, gameID, params) - return err -} - -// AdminPutGame converts echo context to params. -func (w *ServerInterfaceWrapper) AdminPutGame(ctx echo.Context) error { - var err error - // ------------- Path parameter "game_id" ------------- - var gameID PathGameID - - 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 AdminPutGameParams - - 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.AdminPutGame(ctx, gameID, params) - return err -} - -// AdminGetUsers converts echo context to params. -func (w *ServerInterfaceWrapper) AdminGetUsers(ctx echo.Context) error { - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params AdminGetUsersParams - - 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.AdminGetUsers(ctx, params) - return err -} - // GetGames converts echo context to params. func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error { var err error @@ -996,10 +800,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } - router.GET(baseURL+"/admin/games", wrapper.AdminGetGames) - router.GET(baseURL+"/admin/games/:game_id", wrapper.AdminGetGame) - router.PUT(baseURL+"/admin/games/:game_id", wrapper.AdminPutGame) - router.GET(baseURL+"/admin/users", wrapper.AdminGetUsers) router.GET(baseURL+"/games", wrapper.GetGames) router.GET(baseURL+"/games/:game_id", wrapper.GetGame) router.POST(baseURL+"/login", wrapper.PostLogin) @@ -1015,181 +815,6 @@ type NotFoundJSONResponse Error type UnauthorizedJSONResponse Error -type AdminGetGamesRequestObject struct { - Params AdminGetGamesParams -} - -type AdminGetGamesResponseObject interface { - VisitAdminGetGamesResponse(w http.ResponseWriter) error -} - -type AdminGetGames200JSONResponse struct { - Games []Game `json:"games"` -} - -func (response AdminGetGames200JSONResponse) VisitAdminGetGamesResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGames401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response AdminGetGames401JSONResponse) VisitAdminGetGamesResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGames403JSONResponse struct{ ForbiddenJSONResponse } - -func (response AdminGetGames403JSONResponse) VisitAdminGetGamesResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGameRequestObject struct { - GameID PathGameID `json:"game_id"` - Params AdminGetGameParams -} - -type AdminGetGameResponseObject interface { - VisitAdminGetGameResponse(w http.ResponseWriter) error -} - -type AdminGetGame200JSONResponse struct { - Game Game `json:"game"` -} - -func (response AdminGetGame200JSONResponse) VisitAdminGetGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGame401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response AdminGetGame401JSONResponse) VisitAdminGetGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGame403JSONResponse struct{ ForbiddenJSONResponse } - -func (response AdminGetGame403JSONResponse) VisitAdminGetGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetGame404JSONResponse struct{ NotFoundJSONResponse } - -func (response AdminGetGame404JSONResponse) VisitAdminGetGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - - return json.NewEncoder(w).Encode(response) -} - -type AdminPutGameRequestObject struct { - GameID PathGameID `json:"game_id"` - Params AdminPutGameParams - Body *AdminPutGameJSONRequestBody -} - -type AdminPutGameResponseObject interface { - VisitAdminPutGameResponse(w http.ResponseWriter) error -} - -type AdminPutGame204Response struct { -} - -func (response AdminPutGame204Response) VisitAdminPutGameResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type AdminPutGame400JSONResponse struct{ BadRequestJSONResponse } - -func (response AdminPutGame400JSONResponse) VisitAdminPutGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type AdminPutGame401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response AdminPutGame401JSONResponse) VisitAdminPutGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) -} - -type AdminPutGame403JSONResponse struct{ ForbiddenJSONResponse } - -func (response AdminPutGame403JSONResponse) VisitAdminPutGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - - return json.NewEncoder(w).Encode(response) -} - -type AdminPutGame404JSONResponse struct{ NotFoundJSONResponse } - -func (response AdminPutGame404JSONResponse) VisitAdminPutGameResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetUsersRequestObject struct { - Params AdminGetUsersParams -} - -type AdminGetUsersResponseObject interface { - VisitAdminGetUsersResponse(w http.ResponseWriter) error -} - -type AdminGetUsers200JSONResponse struct { - Users []User `json:"users"` -} - -func (response AdminGetUsers200JSONResponse) VisitAdminGetUsersResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetUsers401JSONResponse struct{ UnauthorizedJSONResponse } - -func (response AdminGetUsers401JSONResponse) VisitAdminGetUsersResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(401) - - return json.NewEncoder(w).Encode(response) -} - -type AdminGetUsers403JSONResponse struct{ ForbiddenJSONResponse } - -func (response AdminGetUsers403JSONResponse) VisitAdminGetUsersResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(403) - - return json.NewEncoder(w).Encode(response) -} - type GetGamesRequestObject struct { Params GetGamesParams } @@ -1332,18 +957,6 @@ func (response GetToken401JSONResponse) VisitGetTokenResponse(w http.ResponseWri // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // List games - // (GET /admin/games) - AdminGetGames(ctx context.Context, request AdminGetGamesRequestObject) (AdminGetGamesResponseObject, error) - // Get a game - // (GET /admin/games/{game_id}) - AdminGetGame(ctx context.Context, request AdminGetGameRequestObject) (AdminGetGameResponseObject, error) - // Update a game - // (PUT /admin/games/{game_id}) - AdminPutGame(ctx context.Context, request AdminPutGameRequestObject) (AdminPutGameResponseObject, error) - // List all users - // (GET /admin/users) - AdminGetUsers(ctx context.Context, request AdminGetUsersRequestObject) (AdminGetUsersResponseObject, error) // List games // (GET /games) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) @@ -1370,114 +983,6 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// AdminGetGames operation middleware -func (sh *strictHandler) AdminGetGames(ctx echo.Context, params AdminGetGamesParams) error { - var request AdminGetGamesRequestObject - - request.Params = params - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.AdminGetGames(ctx.Request().Context(), request.(AdminGetGamesRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AdminGetGames") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(AdminGetGamesResponseObject); ok { - return validResponse.VisitAdminGetGamesResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// AdminGetGame operation middleware -func (sh *strictHandler) AdminGetGame(ctx echo.Context, gameID PathGameID, params AdminGetGameParams) error { - var request AdminGetGameRequestObject - - request.GameID = gameID - request.Params = params - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.AdminGetGame(ctx.Request().Context(), request.(AdminGetGameRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AdminGetGame") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(AdminGetGameResponseObject); ok { - return validResponse.VisitAdminGetGameResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// AdminPutGame operation middleware -func (sh *strictHandler) AdminPutGame(ctx echo.Context, gameID PathGameID, params AdminPutGameParams) error { - var request AdminPutGameRequestObject - - request.GameID = gameID - request.Params = params - - var body AdminPutGameJSONRequestBody - if err := ctx.Bind(&body); err != nil { - return err - } - request.Body = &body - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.AdminPutGame(ctx.Request().Context(), request.(AdminPutGameRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AdminPutGame") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(AdminPutGameResponseObject); ok { - return validResponse.VisitAdminPutGameResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// AdminGetUsers operation middleware -func (sh *strictHandler) AdminGetUsers(ctx echo.Context, params AdminGetUsersParams) error { - var request AdminGetUsersRequestObject - - request.Params = params - - handler := func(ctx echo.Context, request interface{}) (interface{}, error) { - return sh.ssi.AdminGetUsers(ctx.Request().Context(), request.(AdminGetUsersRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AdminGetUsers") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(AdminGetUsersResponseObject); ok { - return validResponse.VisitAdminGetUsersResponse(ctx.Response()) - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // GetGames operation middleware func (sh *strictHandler) GetGames(ctx echo.Context, params GetGamesParams) error { var request GetGamesRequestObject @@ -1586,33 +1091,31 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "wKIoin8DAAD//5oSt8+jKgAA", + "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", + "//9iv6ZqPCEAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 939e37a..fdee581 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -39,82 +39,6 @@ func parseJWTClaimsFromAuthorizationHeader(authorization string) (*auth.JWTClaim return claims, nil } -func (h *ApiHandlerWrapper) AdminGetGame(ctx context.Context, request AdminGetGameRequestObject) (AdminGetGameResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { - return AdminGetGame401JSONResponse{ - UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Unauthorized", - }, - }, nil - } - if !user.IsAdmin { - return AdminGetGame403JSONResponse{ - ForbiddenJSONResponse: ForbiddenJSONResponse{ - Message: "Forbidden", - }, - }, nil - } - return h.innerHandler.AdminGetGame(ctx, request, user) -} - -func (h *ApiHandlerWrapper) AdminGetGames(ctx context.Context, request AdminGetGamesRequestObject) (AdminGetGamesResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { - return AdminGetGames401JSONResponse{ - UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Unauthorized", - }, - }, nil - } - if !user.IsAdmin { - return AdminGetGames403JSONResponse{ - ForbiddenJSONResponse: ForbiddenJSONResponse{ - Message: "Forbidden", - }, - }, nil - } - return h.innerHandler.AdminGetGames(ctx, request, user) -} - -func (h *ApiHandlerWrapper) AdminGetUsers(ctx context.Context, request AdminGetUsersRequestObject) (AdminGetUsersResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { - return AdminGetUsers401JSONResponse{ - UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Unauthorized", - }, - }, nil - } - if !user.IsAdmin { - return AdminGetUsers403JSONResponse{ - ForbiddenJSONResponse: ForbiddenJSONResponse{ - Message: "Forbidden", - }, - }, nil - } - return h.innerHandler.AdminGetUsers(ctx, request, user) -} - -func (h *ApiHandlerWrapper) AdminPutGame(ctx context.Context, request AdminPutGameRequestObject) (AdminPutGameResponseObject, error) { - user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) - if err != nil { - return AdminPutGame401JSONResponse{ - UnauthorizedJSONResponse: UnauthorizedJSONResponse{ - Message: "Unauthorized", - }, - }, nil - } - if !user.IsAdmin { - return AdminPutGame403JSONResponse{ - ForbiddenJSONResponse: ForbiddenJSONResponse{ - Message: "Forbidden", - }, - }, nil - } - return h.innerHandler.AdminPutGame(ctx, request, user) -} - func (h *ApiHandlerWrapper) GetGame(ctx context.Context, request GetGameRequestObject) (GetGameResponseObject, error) { user, err := parseJWTClaimsFromAuthorizationHeader(request.Params.Authorization) if err != nil { diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 8dfc11c..a824f17 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -4,10 +4,8 @@ import ( "context" "errors" "net/http" - "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/auth" @@ -23,191 +21,6 @@ type GameHubsInterface interface { StartGame(gameID int) error } -func (h *ApiHandler) AdminGetGames(ctx context.Context, request AdminGetGamesRequestObject, user *auth.JWTClaims) (AdminGetGamesResponseObject, error) { - 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 AdminGetGames200JSONResponse{ - Games: games, - }, nil -} - -func (h *ApiHandler) AdminGetGame(ctx context.Context, request AdminGetGameRequestObject, user *auth.JWTClaims) (AdminGetGameResponseObject, error) { - gameID := request.GameID - row, err := h.q.GetGameByID(ctx, int32(gameID)) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return AdminGetGame404JSONResponse{ - NotFoundJSONResponse: NotFoundJSONResponse{ - 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 AdminGetGame200JSONResponse{ - Game: game, - }, nil -} - -func (h *ApiHandler) AdminPutGame(ctx context.Context, request AdminPutGameRequestObject, user *auth.JWTClaims) (AdminPutGameResponseObject, error) { - 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 AdminPutGame404JSONResponse{ - NotFoundJSONResponse: NotFoundJSONResponse{ - Message: "Game not found", - }, - }, nil - } else { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } - - var changedState string - if state != nil { - changedState = string(*state) - // TODO: - if changedState != game.State && changedState == "prepare" { - h.hubs.StartGame(int(gameID)) - } - } 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 AdminPutGame400JSONResponse{ - BadRequestJSONResponse: BadRequestJSONResponse{ - Message: err.Error(), - }, - }, nil - } - - return AdminPutGame204Response{}, nil -} - -func (h *ApiHandler) AdminGetUsers(ctx context.Context, request AdminGetUsersRequestObject, user *auth.JWTClaims) (AdminGetUsersResponseObject, error) { - users, err := h.q.ListUsers(ctx) - if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - responseUsers := make([]User, len(users)) - for i, u := range users { - responseUsers[i] = User{ - UserID: int(u.UserID), - Username: u.Username, - DisplayName: u.DisplayName, - IconPath: u.IconPath, - IsAdmin: u.IsAdmin, - } - } - return AdminGetUsers200JSONResponse{ - Users: responseUsers, - }, nil -} - func (h *ApiHandler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password @@ -311,7 +124,7 @@ func (h *ApiHandler) GetGame(ctx context.Context, request GetGameRequestObject, if row.Title == nil || row.Description == nil { panic("inconsistent data") } - if user.IsAdmin || (GameState(row.State) != GameStateClosed && GameState(row.State) != GameStateWaitingEntries) { + if user.IsAdmin || (GameState(row.State) != Closed && GameState(row.State) != WaitingEntries) { problem = &Problem{ ProblemID: int(*row.ProblemID), Title: *row.Title, diff --git a/backend/game/models.go b/backend/game/models.go index 13e3d0f..6c299d6 100644 --- a/backend/game/models.go +++ b/backend/game/models.go @@ -9,13 +9,13 @@ import ( type gameState = api.GameState const ( - 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 + 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 ) type game struct { diff --git a/frontend/app/.server/api/client.ts b/frontend/app/.server/api/client.ts index a78180b..0db4c14 100644 --- a/frontend/app/.server/api/client.ts +++ b/frontend/app/.server/api/client.ts @@ -1,5 +1,5 @@ import createClient from "openapi-fetch"; -import type { operations, paths } from "./schema"; +import type { paths } from "./schema"; const apiClient = createClient({ baseUrl: @@ -46,50 +46,3 @@ export async function apiGetToken(token: string) { if (error) throw new Error(error.message); return data; } - -export async function adminApiGetUsers(token: string) { - const { data, error } = await apiClient.GET("/admin/users", { - params: { - header: { Authorization: `Bearer ${token}` }, - }, - }); - if (error) throw new Error(error.message); - return data; -} - -export async function adminApiGetGames(token: string) { - const { data, error } = await apiClient.GET("/admin/games", { - params: { - header: { Authorization: `Bearer ${token}` }, - }, - }); - if (error) throw new Error(error.message); - return data; -} - -export async function adminApiGetGame(token: string, gameId: number) { - const { data, error } = await apiClient.GET("/admin/games/{game_id}", { - params: { - header: { Authorization: `Bearer ${token}` }, - path: { game_id: gameId }, - }, - }); - if (error) throw new Error(error.message); - return data; -} - -export async function adminApiPutGame( - token: string, - gameId: number, - body: operations["adminPutGame"]["requestBody"]["content"]["application/json"], -) { - const { data, error } = await apiClient.PUT("/admin/games/{game_id}", { - params: { - header: { Authorization: `Bearer ${token}` }, - path: { game_id: gameId }, - }, - body, - }); - if (error) throw new Error(error.message); - return data; -} diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 1c8cead..88067a8 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -72,58 +72,6 @@ export interface paths { patch?: never; trace?: never; }; - "/admin/users": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all users */ - get: operations["adminGetUsers"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/games": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List games */ - get: operations["adminGetGames"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/admin/games/{game_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a game */ - get: operations["adminGetGame"]; - /** Update a game */ - put: operations["adminPutGame"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { @@ -433,129 +381,4 @@ export interface operations { 404: components["responses"]["NotFound"]; }; }; - adminGetUsers: { - parameters: { - query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of users */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - users: components["schemas"]["User"][]; - }; - }; - }; - 401: components["responses"]["Unauthorized"]; - 403: components["responses"]["Forbidden"]; - }; - }; - adminGetGames: { - parameters: { - query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of games */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - games: components["schemas"]["Game"][]; - }; - }; - }; - 401: components["responses"]["Unauthorized"]; - 403: components["responses"]["Forbidden"]; - }; - }; - adminGetGame: { - parameters: { - query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; - path: { - game_id: components["parameters"]["path_game_id"]; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description A game */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - game: components["schemas"]["Game"]; - }; - }; - }; - 401: components["responses"]["Unauthorized"]; - 403: components["responses"]["Forbidden"]; - 404: components["responses"]["NotFound"]; - }; - }; - adminPutGame: { - parameters: { - query?: never; - header: { - Authorization: components["parameters"]["header_authorization"]; - }; - path: { - game_id: components["parameters"]["path_game_id"]; - }; - 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; - }; - 400: components["responses"]["BadRequest"]; - 401: components["responses"]["Unauthorized"]; - 403: components["responses"]["Forbidden"]; - 404: components["responses"]["NotFound"]; - }; - }; } diff --git a/openapi.yaml b/openapi.yaml index d04951d..2c91ad1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -108,128 +108,6 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' - /admin/users: - get: - operationId: adminGetUsers - summary: List all users - parameters: - - $ref: '#/components/parameters/header_authorization' - responses: - '200': - description: List of users - content: - application/json: - schema: - type: object - properties: - users: - type: array - items: - $ref: '#/components/schemas/User' - required: - - users - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - /admin/games: - get: - operationId: adminGetGames - summary: List games - parameters: - - $ref: '#/components/parameters/header_authorization' - responses: - '200': - description: List of games - content: - application/json: - schema: - type: object - properties: - games: - type: array - items: - $ref: '#/components/schemas/Game' - required: - - games - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - /admin/games/{game_id}: - get: - operationId: adminGetGame - summary: Get a game - parameters: - - $ref: '#/components/parameters/header_authorization' - - $ref: '#/components/parameters/path_game_id' - responses: - '200': - description: A game - content: - application/json: - schema: - type: object - properties: - game: - $ref: '#/components/schemas/Game' - required: - - game - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' - put: - operationId: adminPutGame - summary: Update a game - parameters: - - $ref: '#/components/parameters/header_authorization' - - $ref: '#/components/parameters/path_game_id' - 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': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' - '404': - $ref: '#/components/responses/NotFound' components: parameters: header_authorization: -- cgit v1.2.3-70-g09d2