From 519008c4bae3db046004e4bc2aaa23a2e66311c7 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 28 Jul 2024 00:08:12 +0900 Subject: frontend: jwt --- frontend/app/services/auth.server.ts | 141 +++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 frontend/app/services/auth.server.ts (limited to 'frontend/app/services/auth.server.ts') diff --git a/frontend/app/services/auth.server.ts b/frontend/app/services/auth.server.ts new file mode 100644 index 0000000..144a7cd --- /dev/null +++ b/frontend/app/services/auth.server.ts @@ -0,0 +1,141 @@ +import { Authenticator } from "remix-auth"; +import { FormStrategy } from "remix-auth-form"; +import { sessionStorage } from "./session.server"; +import { jwtDecode } from "jwt-decode"; +import type { Session } from "@remix-run/server-runtime"; + +export const authenticator = new Authenticator(sessionStorage); + +async function login(username: string, password: string): Promise { + const res = await fetch(`http://api-server/api/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + }); + if (!res.ok) { + throw new Error("Invalid username or password"); + } + const user = await res.json(); + return user.token; +} + +authenticator.use( + new FormStrategy(async ({ form }) => { + const username = String(form.get("username")); + const password = String(form.get("password")); + return await login(username, password); + }), + "default", +); + +type JwtPayload = { + user_id: number; + username: string; + display_username: string; + icon_path: string | null; + is_admin: boolean; +}; + +export type User = { + userId: number; + username: string; + displayUsername: string; + iconPath: string | null; + isAdmin: boolean; +}; + +export async function isAuthenticated( + request: Request | Session, + options?: { + successRedirect?: never; + failureRedirect?: never; + headers?: never; + }, +): Promise; +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; +export async function isAuthenticated( + request: Request | Session, + options: { + successRedirect: string; + failureRedirect: string; + headers?: HeadersInit; + }, +): Promise; +export async function isAuthenticated( + 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 { + let jwt; + + // This function's signature should be compatible with `authenticator.isAuthenticated` but TypeScript does not infer it correctly. + 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); + } + + if (!jwt) { + return null; + } + // TODO: runtime type check + const payload = jwtDecode(jwt); + return { + userId: payload.user_id, + username: payload.username, + displayUsername: payload.display_username, + iconPath: payload.icon_path, + isAdmin: payload.is_admin, + }; +} -- cgit v1.2.3-70-g09d2