aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
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 /backend
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'
Diffstat (limited to 'backend')
-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
17 files changed, 1244 insertions, 800 deletions
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 {