frontend auth

This commit is contained in:
Stepan Usatiuk
2023-12-16 16:19:05 +01:00
parent fc396f9ac6
commit ab566ebf24
37 changed files with 781 additions and 79 deletions

View File

@@ -1,21 +1,28 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/jsx-runtime",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"react"
"root": true,
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/jsx-runtime",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"react"
],
"rules": {
"@typescript-eslint/no-floating-promises": [
"error"
]
}
}

227
client/package-lock.json generated
View File

@@ -9,11 +9,16 @@
"version": "0.0.1",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@parcel/transformer-sass": "^2.10.3",
"@parcel/transformer-typescript-tsc": "^2.10.3",
"@parcel/validator-typescript": "^2.10.3",
"@types/eslint": "^8.44.9",
"@types/eslint-config-prettier": "^6.11.3",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
@@ -1638,6 +1643,25 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/transformer-sass": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@parcel/transformer-sass/-/transformer-sass-2.10.3.tgz",
"integrity": "sha512-Q8pTsMO+YuczBjmW8NdFcUjYpCdvY6EzYhPYw4eTyvldalGkaUVs1mJoKmWDHIDsIpdiaARxTpXP946B9cgE3w==",
"dev": true,
"dependencies": {
"@parcel/plugin": "2.10.3",
"@parcel/source-map": "^2.1.1",
"sass": "^1.38.0"
},
"engines": {
"node": ">= 12.0.0",
"parcel": "^2.10.3"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/transformer-svg": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.10.3.tgz",
@@ -2063,6 +2087,14 @@
"@parcel/core": "^2.10.3"
}
},
"node_modules/@remix-run/router": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.0.tgz",
"integrity": "sha512-WOHih+ClN7N8oHk9N4JUiMxQJmRVaOxcg8w7F/oHUXzJt920ekASLI/7cYX8XkntDWRhLZtsk6LbGrkgOAvi5A==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@swc/core": {
"version": "1.3.100",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.100.tgz",
@@ -2274,6 +2306,28 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/eslint": {
"version": "8.44.9",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.9.tgz",
"integrity": "sha512-6yBxcvwnnYoYT1Uk2d+jvIfsuP4mb2EdIxFnrPABj5a/838qe5bGkNLFOiipX4ULQ7XVQvTxOh7jO+BTAiqsEw==",
"dev": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-config-prettier": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz",
"integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2580,6 +2634,19 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2733,6 +2800,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -2852,6 +2928,45 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3772,6 +3887,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4104,6 +4233,12 @@
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
"integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==",
"dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -4206,6 +4341,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -5020,6 +5167,15 @@
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -5500,6 +5656,48 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.0.tgz",
"integrity": "sha512-hGZ0HXbwz3zw52pLZV3j3+ec+m/PQ9cTpBvqjFQmy2XVUWGn5MD+31oXHb6dVTxYzmAeaiUBYjkoNz66n3RGCg==",
"dependencies": {
"@remix-run/router": "1.14.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.0.tgz",
"integrity": "sha512-1dUdVj3cwc1npzJaf23gulB562ESNvxf7E4x8upNJycqyUm5BRRZ6dd3LrlzhtLaMrwOCO8R0zoiYxdaJx4LlQ==",
"dependencies": {
"@remix-run/router": "1.14.0",
"react-router": "6.21.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -5669,6 +5867,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sass": {
"version": "1.69.5",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
"integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
@@ -5779,8 +5994,6 @@
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -6283,6 +6496,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -11,11 +11,16 @@
"browserslist": "> 0.5%, last 2 versions, not dead",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@parcel/transformer-sass": "^2.10.3",
"@parcel/transformer-typescript-tsc": "^2.10.3",
"@parcel/validator-typescript": "^2.10.3",
"@types/eslint": "^8.44.9",
"@types/eslint-config-prettier": "^6.11.3",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",

6
client/src/App.scss Normal file
View File

@@ -0,0 +1,6 @@
#appRoot {
font-family: sans-serif;
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}

View File

@@ -1,3 +1,75 @@
import React from "react";
import {
createBrowserRouter,
redirect,
RouterProvider,
} from "react-router-dom";
import "./App.scss";
import { deleteToken, getToken } from "./api/utils";
import { Login } from "./Login";
import { Signup } from "./Signup";
import { Home } from "./Home";
import { loginAction, signupAction } from "./actions";
import { homeLoader } from "./loaders";
import { isError } from "./api/dto";
const router = createBrowserRouter([
{
path: "/",
loader: async () => {
if (getToken() == null) {
return redirect("/login");
} else {
return redirect("/home");
}
},
},
{
path: "/home",
loader: async () => {
if (getToken() == null) {
return redirect("/login");
}
const ret = await homeLoader();
if (isError(ret)) {
deleteToken();
return redirect("/");
}
return ret;
},
element: <Home />,
},
{
path: "/login",
element: <Login />,
loader: async () => {
if (getToken()) {
return redirect("/");
}
return null;
},
action: loginAction,
},
{
path: "/signup",
element: <Signup />,
loader: async () => {
if (getToken()) {
return redirect("/");
}
return null;
},
action: signupAction,
},
]);
export function App() {
return <h1>Hello worlffffd!</h1>;
return (
<React.StrictMode>
<div id={"appRoot"}>
<RouterProvider router={router} />
</div>
</React.StrictMode>
);
}

4
client/src/Auth.scss Normal file
View File

@@ -0,0 +1,4 @@
.authForm {
}

21
client/src/Home.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { Outlet, useLoaderData } from "react-router-dom";
import { homeLoader } from "./loaders";
import { isError } from "./api/dto";
export function Home() {
const loaderData = useLoaderData() as
| Awaited<ReturnType<typeof homeLoader>>
| undefined;
if (!loaderData || isError(loaderData)) {
return <div>Error</div>;
}
return (
<>
<a>username: {loaderData.username}</a>
<a>name: {loaderData.fullName}</a>
<Outlet />
</>
);
}

41
client/src/Login.tsx Normal file
View File

@@ -0,0 +1,41 @@
import "./Auth.scss";
import { Form, Link, useActionData, useNavigation } from "react-router-dom";
import { loginAction } from "./actions";
import { isError } from "./api/dto";
export function Login() {
const data = useActionData() as
| Awaited<ReturnType<typeof loginAction>>
| undefined;
if (data && !isError(data)) {
return <div className="authForm">Success, now log in!;</div>;
}
let errors: JSX.Element[] = [];
if (data) {
errors = data.errors.map((e) => {
return <a>{e}</a>;
});
}
const navigation = useNavigation();
const busy = navigation.state === "submitting";
return (
<div className="authForm">
{errors}
<Form method="post">
<label htmlFor="fname">Username:</label>
<input type="text" name="username" />
<label htmlFor="password">Password:</label>
<input type="password" name="password" />
<button type="submit" disabled={busy}>
Login
</button>
<Link to="/signup">Signup</Link>
</Form>
</div>
);
}

43
client/src/Signup.tsx Normal file
View File

@@ -0,0 +1,43 @@
import "./Auth.scss";
import { Form, Link, useActionData, useNavigation } from "react-router-dom";
import { signupAction } from "./actions";
import { isError } from "./api/dto";
export function Signup() {
const data = useActionData() as
| Awaited<ReturnType<typeof signupAction>>
| undefined;
if (data && !isError(data)) {
return <div className="authForm">Success, now log in!;</div>;
}
let errors: JSX.Element[] = [];
if (data) {
errors = data.errors.map((e) => {
return <a>{e}</a>;
});
}
const navigation = useNavigation();
const busy = navigation.state === "submitting";
return (
<div className="authForm">
{errors}
<Form method="post">
<label htmlFor="fname">Username:</label>
<input type="text" name="username" />
<label htmlFor="fname">Full name:</label>
<input type="text" name="fullName" />
<label htmlFor="password">Password:</label>
<input type="password" name="password" />
<button type="submit" disabled={busy}>
Signup
</button>
<Link to="/login">Login</Link>
</Form>
</div>
);
}

29
client/src/actions.ts Normal file
View File

@@ -0,0 +1,29 @@
import { signup } from "./api/Person";
import { ActionFunctionArgs, redirect } from "react-router-dom";
import { login } from "./api/Token";
import { isError } from "./api/dto";
import { setToken } from "./api/utils";
export async function loginAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const ret = await login(
formData.get("username")!.toString(),
formData.get("password")!.toString(),
);
if (!isError(ret)) {
setToken(ret.token);
return redirect("/home");
}
return ret;
}
export async function signupAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
return await signup(
formData.get("username")!.toString(),
formData.get("fullName")!.toString(),
formData.get("password")!.toString(),
);
}

18
client/src/api/Person.ts Normal file
View File

@@ -0,0 +1,18 @@
import { fetchJSON, fetchJSONAuth } from "./utils";
import { PersonToResp, TPersonToResp } from "./dto";
export async function signup(
username: string,
fullName: string,
password: string,
): Promise<TPersonToResp> {
return fetchJSON("/person", "POST", PersonToResp, {
username,
fullName,
password,
});
}
export async function getSelf(): Promise<TPersonToResp> {
return fetchJSONAuth("/person", "GET", PersonToResp);
}

12
client/src/api/Token.ts Normal file
View File

@@ -0,0 +1,12 @@
import { TokenToResp, TTokenToResp } from "./dto";
import { fetchJSON } from "./utils";
export async function login(
username: string,
password: string,
): Promise<TTokenToResp> {
return fetchJSON("/token", "POST", TokenToResp, {
username,
password,
});
}

46
client/src/api/dto.ts Normal file
View File

@@ -0,0 +1,46 @@
import { z } from "zod";
export const ErrorTo = z.object({
errors: z.array(z.string()),
code: z.number(),
});
export type TErrorTo = z.infer<typeof ErrorTo>;
function CreateAPIResponse<T extends z.ZodTypeAny>(obj: T) {
return z.union([ErrorTo, obj]);
}
export const PersonSignupTo = z.object({
username: z.string(),
fullName: z.string(),
password: z.string(),
});
export type TPersonSignupTo = z.infer<typeof PersonSignupTo>;
export const PersonTo = z.object({
uuid: z.string(),
username: z.string(),
fullName: z.string(),
});
export type TPersonTo = z.infer<typeof PersonTo>;
export const PersonToResp = CreateAPIResponse(PersonTo);
export type TPersonToResp = z.infer<typeof PersonToResp>;
export const TokenRequestTo = z.object({
username: z.string(),
password: z.string(),
});
export type TTokenRequestTo = z.infer<typeof TokenRequestTo>;
export const TokenTo = z.object({
token: z.string(),
});
export type TTokenTo = z.infer<typeof TokenTo>;
export const TokenToResp = CreateAPIResponse(TokenTo);
export type TTokenToResp = z.infer<typeof TokenToResp>;
export function isError(value: unknown): value is TErrorTo {
return ErrorTo.safeParse(value).success;
}

68
client/src/api/utils.ts Normal file
View File

@@ -0,0 +1,68 @@
// import { apiRoot } from "~src/env";
const apiRoot: string = "http://localhost:8080";
let token: string | null;
export function setToken(_token: string): void {
token = _token;
localStorage.setItem("jwt_token", token);
}
export function getToken(): string | null {
if (!token && localStorage.getItem("jwt_token") != null) {
token = localStorage.getItem("jwt_token");
}
return token;
}
export function deleteToken(): void {
token = null;
localStorage.removeItem("jwt_token");
}
export async function fetchJSON<T, P extends { parse: (arg: string) => T }>(
path: string,
method: string,
parser: P,
body?: string | Record<string, unknown> | File,
headers?: Record<string, string>,
): Promise<T> {
const reqBody = () =>
body instanceof File
? (() => {
const fd = new FormData();
fd.append("file", body);
return fd;
})()
: JSON.stringify(body);
const reqHeaders = () =>
body instanceof File
? headers
: { ...headers, "Content-Type": "application/json" };
const response = await fetch(apiRoot + path, {
method,
headers: reqHeaders(),
body: reqBody(),
});
return parser.parse(await response.json());
}
export async function fetchJSONAuth<T, P extends { parse: (arg: string) => T }>(
path: string,
method: string,
parser: P,
body?: string | Record<string, unknown> | File,
headers?: Record<string, unknown>,
): Promise<T> {
if (token) {
return fetchJSON(path, method, parser, body, {
...headers,
Authorization: `Bearer ${token}`,
});
} else {
throw new Error("Not logged in");
}
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello world</title>
<title>Y</title>
</head>
<body>
<div id="app"></div>

5
client/src/loaders.ts Normal file
View File

@@ -0,0 +1,5 @@
import { getSelf } from "./api/Person";
export async function homeLoader() {
return await getSelf();
}