aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backend/admin/handlers.go21
-rw-r--r--backend/main.go8
-rw-r--r--frontend/app/.server/auth.ts35
-rw-r--r--frontend/app/.server/cookie.ts41
-rw-r--r--frontend/app/.server/session.ts16
-rw-r--r--frontend/package-lock.json1
-rw-r--r--frontend/package.json1
7 files changed, 110 insertions, 13 deletions
diff --git a/backend/admin/handlers.go b/backend/admin/handlers.go
index f81856c..14523e6 100644
--- a/backend/admin/handlers.go
+++ b/backend/admin/handlers.go
@@ -10,6 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
+ "github.com/nsfisis/iosdc-japan-2024-albatross/backend/auth"
"github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
)
@@ -31,8 +32,28 @@ func NewAdminHandler(q *db.Queries, hubs GameHubsInterface) *AdminHandler {
}
}
+func newAdminMiddleware() echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ jwt, err := c.Cookie("albatross_token")
+ if err != nil {
+ return c.Redirect(http.StatusSeeOther, "/login")
+ }
+ claims, err := auth.ParseJWT(jwt.Value)
+ if err != nil {
+ return c.Redirect(http.StatusSeeOther, "/login")
+ }
+ if !claims.IsAdmin {
+ return echo.NewHTTPError(http.StatusForbidden)
+ }
+ return next(c)
+ }
+ }
+}
+
func (h *AdminHandler) RegisterHandlers(g *echo.Group) {
g.Use(newAssetsMiddleware())
+ g.Use(newAdminMiddleware())
g.GET("/dashboard", h.getDashboard)
g.GET("/users", h.getUsers)
diff --git a/backend/main.go b/backend/main.go
index 2d38ee5..e2e4bbd 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -83,9 +83,11 @@ func main() {
adminGroup := e.Group("/admin")
adminHandler.RegisterHandlers(adminGroup)
- // For local dev:
- // This is never used in production because the reverse proxy sends /logout
- // to the app server.
+ // For local dev: This is never used in production because the reverse
+ // proxy sends /login and /logout to the app server.
+ e.GET("/login", func(c echo.Context) error {
+ return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/login")
+ })
e.POST("/logout", func(c echo.Context) error {
return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/logout")
})
diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts
index d5ffe0f..2c9d23c 100644
--- a/frontend/app/.server/auth.ts
+++ b/frontend/app/.server/auth.ts
@@ -1,10 +1,12 @@
+import { redirect } from "@remix-run/node";
import type { Session } from "@remix-run/server-runtime";
import { jwtDecode } from "jwt-decode";
import { Authenticator } from "remix-auth";
import { FormStrategy } from "remix-auth-form";
import { apiPostLogin } from "./api/client";
import { components } from "./api/schema";
-import { sessionStorage } from "./session";
+import { createUnstructuredCookie } from "./cookie";
+import { cookieOptions, sessionStorage } from "./session";
const authenticator = new Authenticator<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(
diff --git a/frontend/app/.server/cookie.ts b/frontend/app/.server/cookie.ts
new file mode 100644
index 0000000..cccbe78
--- /dev/null
+++ b/frontend/app/.server/cookie.ts
@@ -0,0 +1,41 @@
+import { Cookie, CookieOptions } from "@remix-run/server-runtime";
+import { parse, serialize } from "cookie";
+
+// Remix's createCookie() returns "structured" cookies, which are cookies that hold a JSON-encoded object.
+// This is not suitable for interoperation with other systems that expect a simple string value.
+// This function creates an "unstructured" cookie, a simple plain text.
+export function createUnstructuredCookie(
+ name: string,
+ cookieOptions?: CookieOptions,
+): Cookie {
+ const { secrets = [], ...options } = {
+ path: "/",
+ sameSite: "lax" as const,
+ ...cookieOptions,
+ };
+
+ return {
+ get name() {
+ return name;
+ },
+ get isSigned() {
+ return secrets.length > 0;
+ },
+ get expires() {
+ return typeof options.maxAge !== "undefined"
+ ? new Date(Date.now() + options.maxAge * 1000)
+ : options.expires;
+ },
+ async parse(cookieHeader, parseOptions) {
+ if (!cookieHeader) return null;
+ const cookies = parse(cookieHeader, { ...options, ...parseOptions });
+ return name in cookies ? cookies[name] : null;
+ },
+ async serialize(value, serializeOptions) {
+ return serialize(name, value, {
+ ...options,
+ ...serializeOptions,
+ });
+ },
+ };
+}
diff --git a/frontend/app/.server/session.ts b/frontend/app/.server/session.ts
index 79810f4..102bcd2 100644
--- a/frontend/app/.server/session.ts
+++ b/frontend/app/.server/session.ts
@@ -1,13 +1,17 @@
import { createCookieSessionStorage } from "@remix-run/node";
+export const cookieOptions = {
+ sameSite: "lax" as const,
+ path: "/",
+ httpOnly: true,
+ // secure: process.env.NODE_ENV === "production",
+ secure: false, // TODO
+ secrets: ["TODO"],
+};
+
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "albatross_session",
- sameSite: "lax",
- path: "/",
- httpOnly: true,
- secrets: ["TODO"],
- // secure: process.env.NODE_ENV === "production",
- secure: false, // TODO
+ ...cookieOptions,
},
});
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a49235c..0e6f7bf 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
"@remix-run/node": "^2.10.3",
"@remix-run/react": "^2.10.3",
"@remix-run/serve": "^2.10.3",
+ "cookie": "^0.6.0",
"isbot": "^5.1.13",
"jwt-decode": "^4.0.0",
"openapi-fetch": "^0.10.2",
diff --git a/frontend/package.json b/frontend/package.json
index 30d385a..e4eefac 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"@remix-run/node": "^2.10.3",
"@remix-run/react": "^2.10.3",
"@remix-run/serve": "^2.10.3",
+ "cookie": "^0.6.0",
"isbot": "^5.1.13",
"jwt-decode": "^4.0.0",
"openapi-fetch": "^0.10.2",