mirror of
https://github.com/usatiuk/y.git
synced 2025-10-28 10:37:47 +01:00
frontend auth
This commit is contained in:
@@ -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
227
client/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
6
client/src/App.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
#appRoot {
|
||||
font-family: sans-serif;
|
||||
max-width: 50rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -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
4
client/src/Auth.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
.authForm {
|
||||
|
||||
}
|
||||
21
client/src/Home.tsx
Normal file
21
client/src/Home.tsx
Normal 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
41
client/src/Login.tsx
Normal 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
43
client/src/Signup.tsx
Normal 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
29
client/src/actions.ts
Normal 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
18
client/src/api/Person.ts
Normal 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
12
client/src/api/Token.ts
Normal 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
46
client/src/api/dto.ts
Normal 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
68
client/src/api/utils.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
5
client/src/loaders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { getSelf } from "./api/Person";
|
||||
|
||||
export async function homeLoader() {
|
||||
return await getSelf();
|
||||
}
|
||||
Reference in New Issue
Block a user