aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/.server/api/client.ts7
-rw-r--r--frontend/app/.server/api/schema.d.ts82
-rw-r--r--frontend/app/.server/auth.ts27
-rw-r--r--frontend/app/routes/dashboard.tsx48
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>