aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-04 20:49:12 +0900
committernsfisis <nsfisis@gmail.com>2024-08-04 20:49:12 +0900
commitff959dadb1f990173b9df3105ccfc96b1c6c092e (patch)
tree4690c2aabafaedb50f86ece4900c9616d9518947
parentfa7755592845a44928e88d2ab78cc04425aa9024 (diff)
parentf4bae7f755ca25b2547dc98b2db2fdb255948bc5 (diff)
downloadiosdc-japan-2024-albatross-ff959dadb1f990173b9df3105ccfc96b1c6c092e.tar.gz
iosdc-japan-2024-albatross-ff959dadb1f990173b9df3105ccfc96b1c6c092e.tar.zst
iosdc-japan-2024-albatross-ff959dadb1f990173b9df3105ccfc96b1c6c092e.zip
Merge branch 'feat/admin-pages'
-rw-r--r--README.md7
-rw-r--r--backend/admin/assets/css/LICENSE.normalize.css.md21
-rw-r--r--backend/admin/assets/css/LICENSE.sakura.css.txt21
-rw-r--r--backend/admin/assets/css/normalize.css461
-rw-r--r--backend/admin/assets/css/sakura.css226
-rw-r--r--backend/admin/handlers.go261
-rw-r--r--backend/admin/renderer.go49
-rw-r--r--backend/admin/templates/base.html25
-rw-r--r--backend/admin/templates/dashboard.html13
-rw-r--r--backend/admin/templates/game_edit.html45
-rw-r--r--backend/admin/templates/games.html17
-rw-r--r--backend/admin/templates/user_edit.html33
-rw-r--r--backend/admin/templates/users.html17
-rw-r--r--backend/api/generated.go561
-rw-r--r--backend/api/handler_wrapper.go76
-rw-r--r--backend/api/handlers.go189
-rw-r--r--backend/game/models.go14
-rw-r--r--backend/main.go15
-rw-r--r--frontend/app/.server/api/client.ts49
-rw-r--r--frontend/app/.server/api/schema.d.ts177
-rw-r--r--frontend/app/.server/auth.ts45
-rw-r--r--frontend/app/.server/cookie.ts41
-rw-r--r--frontend/app/.server/session.ts16
-rw-r--r--frontend/app/routes/admin.dashboard.tsx29
-rw-r--r--frontend/app/routes/admin.games.tsx35
-rw-r--r--frontend/app/routes/admin.games_.$gameId.tsx95
-rw-r--r--frontend/app/routes/admin.tsx8
-rw-r--r--frontend/app/routes/admin.users.tsx34
-rw-r--r--frontend/app/routes/dashboard.tsx11
-rw-r--r--frontend/package-lock.json7
-rw-r--r--frontend/package.json2
-rw-r--r--nginx.conf8
-rw-r--r--openapi.yaml122
33 files changed, 1349 insertions, 1381 deletions
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..14523e6
--- /dev/null
+++ b/backend/admin/handlers.go
@@ -0,0 +1,261 @@
+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/auth"
+ "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 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)
+ 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.NoContent(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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ADMIN {{ .Title }} | iOSDC Japan 2024 Albatross.swift</title>
+ <link rel="icon" href="/favicon.svg">
+ <link rel="stylesheet" href="/admin/css/normalize.css">
+ <link rel="stylesheet" href="/admin/css/sakura.css">
+</head>
+<body>
+ <section>
+ <h1>ADMIN {{ .Title }}</h1>
+ <p>
+ <em>This is an admin page.</em>
+ </p>
+ <section>
+ <nav>
+ {{ block "breadcrumb" . }}{{ end }}
+ </nav>
+ </section>
+ <section>
+ {{ block "content" . }}{{ end }}
+ </section>
+ </section>
+</body>
+</html>
diff --git a/backend/admin/templates/dashboard.html b/backend/admin/templates/dashboard.html
new file mode 100644
index 0000000..cdb8ba1
--- /dev/null
+++ b/backend/admin/templates/dashboard.html
@@ -0,0 +1,13 @@
+{{ template "base.html" . }}
+
+{{ define "content" }}
+<p>
+ <a href="/admin/users">Users</a>
+</p>
+<p>
+ <a href="/admin/games">Games</a>
+</p>
+<form method="post" action="/logout">
+ <button type="submit">Logout</button>
+</form>
+{{ 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" }}
+<a href="/admin/dashboard">Dashboard</a> | <a href="/admin/games">Games</a>
+{{ end }}
+
+{{ define "content" }}
+<form>
+ <div>
+ <label>Game ID</label>
+ <input type="text" name="game_id" value="{{ .Game.GameID }}" readonly required>
+ </div>
+ <div>
+ <label>Display Name</label>
+ <input type="text" name="display_name" value="{{ .Game.DisplayName }}" required>
+ </div>
+ <div>
+ <label>State</label>
+ <select>
+ <option value="closed"{{ if eq .Game.State "closed" }} selected{{ end }}>Closed</option>
+ <option value="waiting_entries"{{ if eq .Game.State "waiting_entries" }} selected{{ end }}>WaitingEntries</option>
+ <option value="waiting_start"{{ if eq .Game.State "waiting_start" }} selected{{ end }}>WaitingStart</option>
+ <option value="prepare"{{ if eq .Game.State "prepare" }} selected{{ end }}>Prepare</option>
+ <option value="starting"{{ if eq .Game.State "starting" }} selected{{ end }}>Starting</option>
+ <option value="gaming"{{ if eq .Game.State "gaming" }} selected{{ end }}>Gaming</option>
+ <option value="finished"{{ if eq .Game.State "finished" }} selected{{ end }}>Finished</option>
+ </select>
+ </div>
+ <div>
+ <label>Duration Seconds</label>
+ <input type="number" name="duration_seconds" value="{{ .Game.DurationSeconds }}" required>
+ </div>
+ <div>
+ <label>Started At</label>
+ <input type="datetime-local" name="started_at" value="{{ .Game.StartedAt }}">
+ </div>
+ <div>
+ <label>Problem ID</label>
+ <input type="text" name="problem_id" value="{{ .Game.ProblemID }}" disabled>
+ </div>
+ <div>
+ <button type="submit">Save</button>
+ </div>
+</form>
+{{ 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" }}
+<a href="/admin/dashboard">Dashboard</a>
+{{ end }}
+
+{{ define "content" }}
+<ul>
+ {{ range .Games }}
+ <li>
+ <a href="/admin/games/{{ .GameID }}">
+ {{ .DisplayName }} (id={{ .GameID }})
+ </a>
+ </li>
+ {{ end }}
+</ul>
+{{ 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" }}
+<a href="/admin/dashboard">Dashboard</a> | <a href="/admin/users">Users</a>
+{{ end }}
+
+{{ define "content" }}
+<form>
+ <div>
+ <label>User ID</label>
+ <input type="text" name="user_id" value="{{ .User.UserID }}" readonly required>
+ </div>
+ <div>
+ <label>Username</label>
+ <input type="text" name="username" value="{{ .User.Username }}" readonly required>
+ </div>
+ <div>
+ <label>Display Name</label>
+ <input type="text" name="display_name" value="{{ .User.DisplayName }}" required>
+ </div>
+ <div>
+ <label>Icon Path</label>
+ <input type="text" name="icon_path" value="{{ .User.IconPath }}">
+ </div>
+ <div>
+ <label>Is Admin</label>
+ <input type="checkbox" name="is_admin"{{ if .User.IsAdmin }} checked{{ end }}>
+ </div>
+ <div>
+ <button type="submit">Save</button>
+ </div>
+</form>
+{{ 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" }}
+<a href="/admin/dashboard">Dashboard</a>
+{{ end }}
+
+{{ define "content" }}
+<ul>
+ {{ range .Users }}
+ <li>
+ <a href="/admin/users/{{ .UserID }}">
+ {{ .DisplayName }} (id={{ .UserID }} username={{ .Username }}){{ if .IsAdmin }} <em>admin</em>{{ end }}
+ </a>
+ </li>
+ {{ end }}
+</ul>
+{{ end }}
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
@@ -691,18 +645,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
// Get a game
@@ -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
}
@@ -1333,18 +958,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)
// Get a game
@@ -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/backend/main.go b/backend/main.go
index 939df03..e2e4bbd 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,19 @@ 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)
+
+ // 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")
+ })
+
gameHubs.Run()
if err := e.Start(":80"); err != http.ErrServerClosed {
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<paths>({
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<string, never>;
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/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts
index a4811e2..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<string>(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<never> {
- 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<never> {
- 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(
@@ -40,16 +67,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<null> {
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/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 (
- <div>
- <h1>[Admin] Dashboard</h1>
- <p>
- <Link to="/admin/users">Users</Link>
- </p>
- <p>
- <Link to="/admin/games">Games</Link>
- </p>
- <Form method="post" action="/logout">
- <button type="submit">Logout</button>
- </Form>
- </div>
- );
-}
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<typeof loader>()!;
-
- return (
- <div>
- <div>
- <h1>[Admin] Games</h1>
- <ul>
- {games.map((game) => (
- <li key={game.game_id}>
- <Link to={`/admin/games/${game.game_id}`}>
- {game.display_name} (id={game.game_id})
- </Link>
- </li>
- ))}
- </ul>
- </div>
- </div>
- );
-}
diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx
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<typeof loader> = ({ 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<typeof loader>()!;
-
- return (
- <div>
- <div>
- <h1>[Admin] Game Edit {game.display_name}</h1>
- <ul>
- <li>ID: {game.game_id}</li>
- <li>State: {game.state}</li>
- <li>Display Name: {game.display_name}</li>
- <li>Duration Seconds: {game.duration_seconds}</li>
- <li>
- Started At:{" "}
- {game.started_at
- ? new Date(game.started_at * 1000).toString()
- : "-"}
- </li>
- <li>Problem ID: {game.problem ? game.problem.problem_id : "-"}</li>
- </ul>
- <div>
- <Form method="post">
- <div>
- <button
- type="submit"
- name="action"
- value="open"
- disabled={game.state !== "closed"}
- >
- Open
- </button>
- </div>
- <div>
- <button
- type="submit"
- name="action"
- value="start"
- disabled={game.state !== "waiting_start"}
- >
- Start
- </button>
- </div>
- </Form>
- </div>
- </div>
- </div>
- );
-}
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<typeof loader>()!;
-
- return (
- <div>
- <div>
- <h1>[Admin] Users</h1>
- <ul>
- {users.map((user) => (
- <li key={user.user_id}>
- {user.display_name} (id={user.user_id} username={user.username})
- {user.is_admin && <span> admin</span>}
- </li>
- ))}
- </ul>
- </div>
- </div>
- );
-}
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 (
<div className="min-h-screen p-8">
<div className="p-6 rounded shadow-md max-w-4xl mx-auto">
- <h1 className="text-3xl font-bold mb-4">
- {user.username}{" "}
- {user.is_admin && <span className="text-red-500 text-lg">admin</span>}
- </h1>
+ <h1 className="text-3xl font-bold mb-4">{user.username}</h1>
<h2 className="text-2xl font-semibold mb-2">User</h2>
<div className="mb-6">
<ul className="list-disc list-inside">
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a49235c..d5c12fe 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",
@@ -18,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": {
@@ -9673,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 30d385a..44af089 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",
@@ -26,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": {
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;
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: