aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/.server/auth.ts124
-rw-r--r--frontend/app/routes/admin.dashboard.tsx14
-rw-r--r--frontend/app/routes/admin.games.tsx9
-rw-r--r--frontend/app/routes/admin.games_.$gameId.tsx16
-rw-r--r--frontend/app/routes/admin.tsx10
-rw-r--r--frontend/app/routes/admin.users.tsx9
-rw-r--r--frontend/app/routes/dashboard.tsx6
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx6
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx6
-rw-r--r--frontend/app/routes/login.tsx12
-rw-r--r--frontend/app/routes/logout.tsx4
-rw-r--r--frontend/package.json9
12 files changed, 73 insertions, 152 deletions
diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts
index 0c7742a..a4811e2 100644
--- a/frontend/app/.server/auth.ts
+++ b/frontend/app/.server/auth.ts
@@ -6,108 +6,54 @@ import { apiPostLogin } from "./api/client";
import { components } from "./api/schema";
import { sessionStorage } from "./session";
-export const authenticator = new Authenticator<string>(sessionStorage);
-
-async function login(username: string, password: string): Promise<string> {
- return (await apiPostLogin(username, password)).token;
-}
+const authenticator = new Authenticator<string>(sessionStorage);
authenticator.use(
new FormStrategy(async ({ form }) => {
const username = String(form.get("username"));
const password = String(form.get("password"));
- return await login(username, password);
+ return (await apiPostLogin(username, password)).token;
}),
"default",
);
export type User = components["schemas"]["User"];
-export async function isAuthenticated(
- request: Request | Session,
- options?: {
- successRedirect?: never;
- failureRedirect?: never;
- headers?: never;
- },
-): Promise<{ user: User; token: string } | null>;
-export async function isAuthenticated(
- request: Request | Session,
- options: {
- successRedirect: string;
- failureRedirect?: never;
- headers?: HeadersInit;
- },
-): Promise<null>;
-export async function isAuthenticated(
- request: Request | Session,
- options: {
- successRedirect?: never;
- failureRedirect: string;
- headers?: HeadersInit;
- },
-): Promise<{ user: User; token: string }>;
-export async function isAuthenticated(
+export async function login(request: Request): Promise<never> {
+ return await authenticator.authenticate("default", request, {
+ successRedirect: "/dashboard",
+ failureRedirect: "/login",
+ });
+}
+
+export async function logout(request: Request | Session): Promise<never> {
+ return await authenticator.logout(request, { redirectTo: "/" });
+}
+
+export async function ensureUserLoggedIn(
request: Request | Session,
- options: {
- successRedirect: string;
- failureRedirect: string;
- headers?: HeadersInit;
- },
-): Promise<null>;
-export async function isAuthenticated(
+): Promise<{ user: User; token: string }> {
+ const token = await authenticator.isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+ const user = jwtDecode<User>(token);
+ return { user, token };
+}
+
+export async function ensureAdminUserLoggedIn(
request: Request | Session,
- options:
- | {
- successRedirect?: never;
- failureRedirect?: never;
- headers?: never;
- }
- | {
- successRedirect: string;
- failureRedirect?: never;
- headers?: HeadersInit;
- }
- | {
- successRedirect?: never;
- failureRedirect: string;
- headers?: HeadersInit;
- }
- | {
- successRedirect: string;
- failureRedirect: string;
- headers?: HeadersInit;
- } = {},
-): Promise<{ user: User; token: string } | null> {
- // This function's signature should be compatible with `authenticator.isAuthenticated` but TypeScript does not infer it correctly.
- let jwt;
- const { successRedirect, failureRedirect, headers } = options;
- if (successRedirect && failureRedirect) {
- jwt = await authenticator.isAuthenticated(request, {
- successRedirect,
- failureRedirect,
- headers,
- });
- } else if (!successRedirect && failureRedirect) {
- jwt = await authenticator.isAuthenticated(request, {
- failureRedirect,
- headers,
- });
- } else if (successRedirect && !failureRedirect) {
- jwt = await authenticator.isAuthenticated(request, {
- successRedirect,
- headers,
- });
- } else {
- jwt = await authenticator.isAuthenticated(request);
+): Promise<{ user: User; token: string }> {
+ const { user, token } = await ensureUserLoggedIn(request);
+ if (!user.is_admin) {
+ throw new Error("Forbidden");
}
+ return { user, token };
+}
- if (!jwt) {
- return null;
- }
- const user = jwtDecode<User>(jwt);
- return {
- user,
- token: jwt,
- };
+export async function ensureUserNotLoggedIn(
+ request: Request | Session,
+): Promise<null> {
+ return await authenticator.isAuthenticated(request, {
+ successRedirect: "/dashboard",
+ });
}
diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx
index ce3e910..f91406f 100644
--- a/frontend/app/routes/admin.dashboard.tsx
+++ b/frontend/app/routes/admin.dashboard.tsx
@@ -1,18 +1,13 @@
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
-import { Link } from "@remix-run/react";
-import { isAuthenticated } from "../.server/auth";
+import { Form, Link } from "@remix-run/react";
+import { ensureAdminUserLoggedIn } from "../.server/auth";
export const meta: MetaFunction = () => {
return [{ title: "[Admin] Dashboard | iOSDC Japan 2024 Albatross.swift" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
- const { user } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
- if (!user.is_admin) {
- throw new Error("Unauthorized");
- }
+ await ensureAdminUserLoggedIn(request);
return null;
}
@@ -26,6 +21,9 @@ export default function AdminDashboard() {
<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
index af3554e..23e45c5 100644
--- a/frontend/app/routes/admin.games.tsx
+++ b/frontend/app/routes/admin.games.tsx
@@ -1,19 +1,14 @@
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { adminApiGetGames } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureAdminUserLoggedIn } from "../.server/auth";
export const meta: MetaFunction = () => {
return [{ title: "[Admin] Games | iOSDC Japan 2024 Albatross.swift" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
- const { user, token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
- if (!user.is_admin) {
- throw new Error("Unauthorized");
- }
+ const { token } = await ensureAdminUserLoggedIn(request);
const { games } = await adminApiGetGames(token);
return { games };
}
diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx
index 34860ab..0d2cac6 100644
--- a/frontend/app/routes/admin.games_.$gameId.tsx
+++ b/frontend/app/routes/admin.games_.$gameId.tsx
@@ -5,7 +5,7 @@ import type {
} from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { adminApiGetGame, adminApiPutGame } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureAdminUserLoggedIn } from "../.server/auth";
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
@@ -18,24 +18,14 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
};
export async function loader({ request, params }: LoaderFunctionArgs) {
- const { user, token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
- if (!user.is_admin) {
- throw new Error("Unauthorized");
- }
+ 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 { user, token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
- if (!user.is_admin) {
- throw new Error("Unauthorized");
- }
+ const { token } = await ensureAdminUserLoggedIn(request);
const { gameId } = params;
const formData = await request.formData();
diff --git a/frontend/app/routes/admin.tsx b/frontend/app/routes/admin.tsx
index 2b05356..ceef37e 100644
--- a/frontend/app/routes/admin.tsx
+++ b/frontend/app/routes/admin.tsx
@@ -1,2 +1,8 @@
-import "sakura.css/css/normalize.css";
-import "sakura.css/css/sakura.css";
+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
index 9eed263..219175e 100644
--- a/frontend/app/routes/admin.users.tsx
+++ b/frontend/app/routes/admin.users.tsx
@@ -1,19 +1,14 @@
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { adminApiGetUsers } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureAdminUserLoggedIn } from "../.server/auth";
export const meta: MetaFunction = () => {
return [{ title: "[Admin] Users | iOSDC Japan 2024 Albatross.swift" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
- const { user, token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
- if (!user.is_admin) {
- throw new Error("Unauthorized");
- }
+ const { token } = await ensureAdminUserLoggedIn(request);
const { users } = await adminApiGetUsers(token);
return { users };
}
diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx
index 1c2137d..45381e1 100644
--- a/frontend/app/routes/dashboard.tsx
+++ b/frontend/app/routes/dashboard.tsx
@@ -2,16 +2,14 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, Link, useLoaderData } from "@remix-run/react";
import { apiGetGames } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureUserLoggedIn } from "../.server/auth";
export const meta: MetaFunction = () => {
return [{ title: "Dashboard | iOSDC Japan 2024 Albatross.swift" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
- const { user, token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
+ const { user, token } = await ensureUserLoggedIn(request);
if (user.is_admin) {
return redirect("/admin/dashboard");
}
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx
index d498200..72f66bc 100644
--- a/frontend/app/routes/golf.$gameId.play.tsx
+++ b/frontend/app/routes/golf.$gameId.play.tsx
@@ -2,7 +2,7 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ClientOnly } from "remix-utils/client-only";
import { apiGetGame, apiGetToken } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureUserLoggedIn } from "../.server/auth";
import GolfPlayApp from "../components/GolfPlayApp.client";
import GolfPlayAppConnecting from "../components/GolfPlayApps/GolfPlayAppConnecting";
@@ -17,9 +17,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
};
export async function loader({ params, request }: LoaderFunctionArgs) {
- const { token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
+ const { token } = await ensureUserLoggedIn(request);
const fetchGame = async () => {
return (await apiGetGame(token, Number(params.gameId))).game;
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
index 0203e27..7c623e1 100644
--- a/frontend/app/routes/golf.$gameId.watch.tsx
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -2,7 +2,7 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ClientOnly } from "remix-utils/client-only";
import { apiGetGame, apiGetToken } from "../.server/api/client";
-import { isAuthenticated } from "../.server/auth";
+import { ensureUserLoggedIn } from "../.server/auth";
import GolfWatchApp from "../components/GolfWatchApp.client";
import GolfWatchAppConnecting from "../components/GolfWatchApps/GolfWatchAppConnecting";
@@ -17,9 +17,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
};
export async function loader({ params, request }: LoaderFunctionArgs) {
- const { token } = await isAuthenticated(request, {
- failureRedirect: "/login",
- });
+ const { token } = await ensureUserLoggedIn(request);
const fetchGame = async () => {
return (await apiGetGame(token, Number(params.gameId))).game;
diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx
index 95effaa..1c861fc 100644
--- a/frontend/app/routes/login.tsx
+++ b/frontend/app/routes/login.tsx
@@ -4,23 +4,19 @@ import type {
MetaFunction,
} from "@remix-run/node";
import { Form } from "@remix-run/react";
-import { authenticator } from "../.server/auth";
+import { ensureUserNotLoggedIn, login } from "../.server/auth";
export const meta: MetaFunction = () => {
return [{ title: "Login | iOSDC Japan 2024 Albatross.swift" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
- return await authenticator.isAuthenticated(request, {
- successRedirect: "/dashboard",
- });
+ return await ensureUserNotLoggedIn(request);
}
export async function action({ request }: ActionFunctionArgs) {
- return await authenticator.authenticate("default", request, {
- successRedirect: "/dashboard",
- failureRedirect: "/login",
- });
+ await login(request);
+ return null;
}
export default function Login() {
diff --git a/frontend/app/routes/logout.tsx b/frontend/app/routes/logout.tsx
index 012d9e9..d697be2 100644
--- a/frontend/app/routes/logout.tsx
+++ b/frontend/app/routes/logout.tsx
@@ -1,7 +1,7 @@
import type { ActionFunctionArgs } from "@remix-run/node";
-import { authenticator } from "../.server/auth";
+import { logout } from "../.server/auth";
export async function action({ request }: ActionFunctionArgs) {
- await authenticator.logout(request, { redirectTo: "/" });
+ await logout(request);
return null;
}
diff --git a/frontend/package.json b/frontend/package.json
index d54b3e0..30d385a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,12 +5,13 @@
"type": "module",
"scripts": {
"build": "remix vite:build",
- "check": "biome check --write",
+ "check": "npm run check:biome && npm run check:ts && npm run check:eslint",
+ "check:biome": "biome check --write",
+ "check:eslint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+ "check:ts": "tsc",
"dev": "remix vite:dev",
- "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"openapi-typescript": "openapi-typescript --output ./app/.server/api/schema.d.ts ../openapi.yaml",
- "start": "remix-serve ./build/server/index.js",
- "typecheck": "tsc"
+ "start": "remix-serve ./build/server/index.js"
},
"dependencies": {
"@remix-run/node": "^2.10.3",