diff options
Diffstat (limited to 'frontend/app')
| -rw-r--r-- | frontend/app/.server/api/client.ts | 7 | ||||
| -rw-r--r-- | frontend/app/.server/api/schema.d.ts | 82 | ||||
| -rw-r--r-- | frontend/app/.server/auth.ts | 27 | ||||
| -rw-r--r-- | frontend/app/routes/dashboard.tsx | 48 |
4 files changed, 135 insertions, 29 deletions
diff --git a/frontend/app/.server/api/client.ts b/frontend/app/.server/api/client.ts index 12f2fc6..8e50b7e 100644 --- a/frontend/app/.server/api/client.ts +++ b/frontend/app/.server/api/client.ts @@ -1,4 +1,9 @@ import createClient from "openapi-fetch"; import type { paths } from "./schema"; -export const apiClient = createClient<paths>({ baseUrl: "http://api-server/" }); +export const apiClient = createClient<paths>({ + baseUrl: + process.env.NODE_ENV === "development" + ? "http://localhost:8002/api/" + : "http://api-server/api/", +}); diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 5219ac8..cd87705 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -4,7 +4,7 @@ */ export interface paths { - "/api/login": { + "/login": { parameters: { query?: never; header?: never; @@ -64,6 +64,60 @@ export interface paths { patch?: never; trace?: never; }; + "/games": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List games */ + get: { + parameters: { + query?: { + player_id?: number; + }; + header: { + Authorization: string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of games */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + games: components["schemas"]["Game"][]; + }; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example Forbidden operation */ + message: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record<string, never>; export interface components { @@ -76,10 +130,34 @@ export interface components { /** @example John Doe */ display_name: string; /** @example /images/john.jpg */ - icon_path?: string | null; + icon_path?: string; /** @example false */ is_admin: boolean; }; + Game: { + /** @example 1 */ + game_id: number; + /** + * @example active + * @enum {string} + */ + state: "closed" | "waiting_entries" | "waiting_start" | "prepare" | "starting" | "gaming" | "finished"; + /** @example Game 1 */ + display_name: string; + /** @example 360 */ + duration_seconds: number; + /** @example 946684800 */ + started_at?: number; + problem?: components["schemas"]["Problem"]; + }; + Problem: { + /** @example 1 */ + problem_id: number; + /** @example Problem 1 */ + title: string; + /** @example This is a problem */ + description: string; + }; }; responses: never; parameters: never; diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index a3496af..988b30c 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -9,7 +9,7 @@ import { components } from "./api/schema"; export const authenticator = new Authenticator<string>(sessionStorage); async function login(username: string, password: string): Promise<string> { - const { data, error } = await apiClient.POST("/api/login", { + const { data, error } = await apiClient.POST("/login", { body: { username, password, @@ -30,15 +30,7 @@ authenticator.use( "default", ); -type JwtPayload = components["schemas"]["JwtPayload"]; - -export type User = { - userId: number; - username: string; - displayName: string; - iconPath: string | null; - isAdmin: boolean; -}; +export type User = components["schemas"]["JwtPayload"]; export async function isAuthenticated( request: Request | Session, @@ -47,7 +39,7 @@ export async function isAuthenticated( failureRedirect?: never; headers?: never; }, -): Promise<User | null>; +): Promise<{ user: User; token: string } | null>; export async function isAuthenticated( request: Request | Session, options: { @@ -63,7 +55,7 @@ export async function isAuthenticated( failureRedirect: string; headers?: HeadersInit; }, -): Promise<User>; +): Promise<{ user: User; token: string }>; export async function isAuthenticated( request: Request | Session, options: { @@ -95,7 +87,7 @@ export async function isAuthenticated( failureRedirect: string; headers?: HeadersInit; } = {}, -): Promise<User | null> { +): 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; @@ -122,12 +114,9 @@ export async function isAuthenticated( if (!jwt) { return null; } - const payload = jwtDecode<JwtPayload>(jwt); + const user = jwtDecode<User>(jwt); return { - userId: payload.user_id, - username: payload.username, - displayName: payload.display_name, - iconPath: payload.icon_path ?? null, - isAdmin: payload.is_admin, + user, + token: jwt, }; } diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index 407dda6..9836d1b 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -1,33 +1,67 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; +import { Link, useLoaderData } from "@remix-run/react"; import { isAuthenticated } from "../.server/auth"; -import { useLoaderData } from "@remix-run/react"; +import { apiClient } from "../.server/api/client"; export async function loader({ request }: LoaderFunctionArgs) { - return await isAuthenticated(request, { + const { user, token } = await isAuthenticated(request, { failureRedirect: "/login", }); + const { data, error } = await apiClient.GET("/games", { + params: { + query: { + player_id: user.user_id, + }, + header: { + Authorization: `Bearer ${token}`, + }, + }, + }); + if (error) { + throw new Error(error.message); + } + return { + user, + games: data.games, + }; } export default function Dashboard() { - const user = useLoaderData<typeof loader>()!; + const { user, games } = useLoaderData<typeof loader>()!; return ( <div className="min-h-screen p-8"> <div className="p-6 rounded shadow-md max-w-4xl mx-auto"> <h1 className="text-3xl font-bold mb-4"> {user.username}{" "} - {user.isAdmin && <span className="text-red-500 text-lg">admin</span>} + {user.is_admin && <span className="text-red-500 text-lg">admin</span>} </h1> <h2 className="text-2xl font-semibold mb-2">User</h2> <div className="mb-6"> <ul className="list-disc list-inside"> - <li>Name: {user.displayName}</li> + <li>Name: {user.display_name}</li> </ul> </div> - <h2 className="text-2xl font-semibold mb-2">Game</h2> + <h2 className="text-2xl font-semibold mb-2">Games</h2> <div> <ul className="list-disc list-inside"> - <li>TODO</li> + {games.map((game) => ( + <li key={game.game_id}> + {game.display_name}{" "} + {game.state === "closed" || game.state === "finished" ? ( + <span className="inline-block px-6 py-2 text-gray-400 bg-gray-200 cursor-not-allowed rounded"> + Entry + </span> + ) : ( + <Link + to={`/game/${game.game_id}/play`} + className="inline-block px-6 py-2 text-white bg-blue-500 hover:bg-blue-700 rounded" + > + Entry + </Link> + )} + </li> + ))} </ul> </div> </div> |
