From 13e9a10c1964bc40c698efa7bc8e47acc5cb25b4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 1 Aug 2024 22:46:56 +0900 Subject: chore(frontend): add `npm run check` --- frontend/package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'frontend') 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", -- cgit v1.2.3-70-g09d2 From 3d566086003efd952cd1535ee9d971dab5d58a15 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 1 Aug 2024 22:43:27 +0900 Subject: refactor(frontend): simplify login check --- frontend/app/.server/auth.ts | 124 ++++++++------------------- frontend/app/routes/admin.dashboard.tsx | 9 +- frontend/app/routes/admin.games.tsx | 9 +- frontend/app/routes/admin.games_.$gameId.tsx | 16 +--- frontend/app/routes/admin.users.tsx | 9 +- frontend/app/routes/dashboard.tsx | 6 +- frontend/app/routes/golf.$gameId.play.tsx | 6 +- frontend/app/routes/golf.$gameId.watch.tsx | 6 +- frontend/app/routes/login.tsx | 12 +-- frontend/app/routes/logout.tsx | 4 +- 10 files changed, 56 insertions(+), 145 deletions(-) (limited to 'frontend') 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(sessionStorage); - -async function login(username: string, password: string): Promise { - return (await apiPostLogin(username, password)).token; -} +const authenticator = new Authenticator(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; -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 { + return await authenticator.authenticate("default", request, { + successRedirect: "/dashboard", + failureRedirect: "/login", + }); +} + +export async function logout(request: Request | Session): Promise { + return await authenticator.logout(request, { redirectTo: "/" }); +} + +export async function ensureUserLoggedIn( request: Request | Session, - options: { - successRedirect: string; - failureRedirect: string; - headers?: HeadersInit; - }, -): Promise; -export async function isAuthenticated( +): Promise<{ user: User; token: string }> { + const token = await authenticator.isAuthenticated(request, { + failureRedirect: "/login", + }); + const user = jwtDecode(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(jwt); - return { - user, - token: jwt, - }; +export async function ensureUserNotLoggedIn( + request: Request | Session, +): Promise { + 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..4eb90b5 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 { 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; } 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 = ({ data }) => { return [ @@ -18,24 +18,14 @@ export const meta: MetaFunction = ({ 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.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 = ({ 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 = ({ 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; } -- cgit v1.2.3-70-g09d2 From 16a2909bc0670226fb639d03edd0e9b7f20b54c6 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 1 Aug 2024 23:33:42 +0900 Subject: feat(frontend): add logout button to /admin/dashboard --- frontend/app/routes/admin.dashboard.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'frontend') diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx index 4eb90b5..f91406f 100644 --- a/frontend/app/routes/admin.dashboard.tsx +++ b/frontend/app/routes/admin.dashboard.tsx @@ -1,5 +1,5 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link } from "@remix-run/react"; +import { Form, Link } from "@remix-run/react"; import { ensureAdminUserLoggedIn } from "../.server/auth"; export const meta: MetaFunction = () => { @@ -21,6 +21,9 @@ export default function AdminDashboard() {

Games

+
+ +
); } -- cgit v1.2.3-70-g09d2 From ca4e86c99a935c41b319efea43365221569c7d62 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 1 Aug 2024 23:34:25 +0900 Subject: fix(frontend): fix an issue where stylesheets for admin pages are not unloaded when you logout --- frontend/app/routes/admin.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'frontend') 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 }, +]; -- cgit v1.2.3-70-g09d2