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();
|
||||
}
|
||||
@@ -27,6 +27,7 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.security:spring-security-test'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
@@ -34,6 +35,8 @@ dependencies {
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
// For TestRestTemplate as default client can't handle UNAUTHORIZED response
|
||||
testImplementation 'org.apache.httpcomponents.client5:httpclient5:5.3'
|
||||
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.ErrorTo;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
@ControllerAdvice
|
||||
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
@ExceptionHandler(value = {ConstraintViolationException.class})
|
||||
protected ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
|
||||
return handleExceptionInternal(ex,
|
||||
new ErrorTo(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage), HttpStatus.BAD_REQUEST.value()),
|
||||
new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.PersonSignupTo;
|
||||
import com.usatiuk.tjv.y.server.dto.PersonTo;
|
||||
import com.usatiuk.tjv.y.server.dto.PersonSignupRequest;
|
||||
import com.usatiuk.tjv.y.server.dto.converters.PersonMapper;
|
||||
import com.usatiuk.tjv.y.server.entity.Person;
|
||||
import com.usatiuk.tjv.y.server.service.PersonService;
|
||||
@@ -24,7 +24,7 @@ public class PersonController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public PersonTo signup(@RequestBody PersonSignupRequest signupRequest) throws UserAlreadyExistsException {
|
||||
public PersonTo signup(@RequestBody PersonSignupTo signupRequest) throws UserAlreadyExistsException {
|
||||
Person toCreate = new Person();
|
||||
toCreate.setUsername(signupRequest.username())
|
||||
.setPassword(signupRequest.password())
|
||||
@@ -44,6 +44,15 @@ public class PersonController {
|
||||
return PersonMapper.makeDto(found.get());
|
||||
}
|
||||
|
||||
@GetMapping(path = "")
|
||||
public PersonTo getSelf(Principal principal) throws UserNotFoundException {
|
||||
Optional<Person> found = personService.readById(principal.getName());
|
||||
|
||||
if (found.isEmpty()) throw new UserNotFoundException();
|
||||
|
||||
return PersonMapper.makeDto(found.get());
|
||||
}
|
||||
|
||||
@GetMapping(path = "/followers")
|
||||
public Stream<PersonTo> getFollowers(Principal principal) throws UserNotFoundException {
|
||||
return personService.getFollowers(principal.getName()).stream().map(PersonMapper::makeDto);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.PostCreate;
|
||||
import com.usatiuk.tjv.y.server.dto.PostCreateTo;
|
||||
import com.usatiuk.tjv.y.server.dto.PostTo;
|
||||
import com.usatiuk.tjv.y.server.dto.converters.PostMapper;
|
||||
import com.usatiuk.tjv.y.server.entity.Person;
|
||||
@@ -28,10 +28,10 @@ public class PostController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public PostTo createPost(Principal principal, @RequestBody PostCreate postCreate) {
|
||||
public PostTo createPost(Principal principal, @RequestBody PostCreateTo postCreateTo) {
|
||||
Post post = new Post();
|
||||
post.setAuthor(entityManager.getReference(Person.class, principal.getName()));
|
||||
post.setText(postCreate.text());
|
||||
post.setText(postCreateTo.text());
|
||||
return PostMapper.makeDto(postService.create(post));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.TokenRequest;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponse;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenRequestTo;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponseTo;
|
||||
import com.usatiuk.tjv.y.server.entity.Person;
|
||||
import com.usatiuk.tjv.y.server.service.PersonService;
|
||||
import com.usatiuk.tjv.y.server.service.TokenService;
|
||||
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -28,12 +25,12 @@ public class TokenController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public TokenResponse request(@RequestBody TokenRequest tokenRequest) throws UserNotFoundException {
|
||||
Optional<Person> found = personService.login(tokenRequest.username(), tokenRequest.password());
|
||||
public TokenResponseTo request(@RequestBody TokenRequestTo tokenRequestTo) throws UserNotFoundException {
|
||||
Optional<Person> found = personService.login(tokenRequestTo.username(), tokenRequestTo.password());
|
||||
|
||||
if (found.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
|
||||
return new TokenResponse(tokenService.generateToken(found.get().getId()));
|
||||
return new TokenResponseTo(tokenService.generateToken(found.get().getId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public record ErrorTo(String[] errors, Integer code) {
|
||||
public ErrorTo(Collection<String> errors, Integer code) {
|
||||
this(errors.toArray(String[]::new), code);
|
||||
}
|
||||
|
||||
public ErrorTo(Stream<String> errors, Integer code) {
|
||||
this(errors.toArray(String[]::new), code);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record PersonSignupRequest(String username, String fullName, String password) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record PersonSignupTo(String username, String fullName, String password) {
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record PostCreate(String text) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record PostCreateTo(String text) {
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record TokenRequest(String username, String password) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record TokenRequestTo(String username, String password) {
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record TokenResponse(String token) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.usatiuk.tjv.y.server.dto;
|
||||
|
||||
public record TokenResponseTo(String token) {
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.usatiuk.tjv.y.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
@@ -21,10 +23,15 @@ public class Person implements EntityWithId<String> {
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private String uuid;
|
||||
|
||||
@Size(max = 100, message = "Username can't be longer than 100")
|
||||
@NotBlank(message = "Username can't be empty")
|
||||
@Column(unique = true)
|
||||
private String username;
|
||||
|
||||
@Size(max = 100, message = "Name can't be longer than 100")
|
||||
@NotBlank(message = "Name can't be empty")
|
||||
private String fullName;
|
||||
@NotBlank(message = "Password can't be empty")
|
||||
private String password;
|
||||
|
||||
@OneToMany(mappedBy = "author")
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
package com.usatiuk.tjv.y.server.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.usatiuk.tjv.y.server.dto.ErrorTo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -23,14 +38,26 @@ public class WebSecurityConfig {
|
||||
this.jwtRequestFilter = jwtRequestFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
|
||||
return new MvcRequestMatcher.Builder(introspector);
|
||||
@Component
|
||||
class ErrorAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
|
||||
throws IOException {
|
||||
|
||||
var err = new ErrorTo(List.of("Authentication failed"), HttpStatus.UNAUTHORIZED.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
OutputStream responseStream = response.getOutputStream();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.writeValue(responseStream, err);
|
||||
responseStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain configure(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
|
||||
return http.cors(withDefaults())
|
||||
public SecurityFilterChain configure(HttpSecurity http, HandlerMappingIntrospector introspector, AuthenticationEntryPoint authenticationEntryPoint) throws Exception {
|
||||
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector);
|
||||
return http.cors(Customizer.withDefaults())
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers(mvc.pattern(HttpMethod.GET, "/post/*")).permitAll()
|
||||
@@ -41,7 +68,15 @@ public class WebSecurityConfig {
|
||||
.anyRequest().hasAuthority(UserRoles.ROLE_USER.name()))
|
||||
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
jwt.secret=JKLASJKLASJKLJHKLDFAHJKFDSHJKFJHKDSHJKFHJKSDFJHKSDJHKFJHKS98346783467899782345jkhgsdoigh938g
|
||||
logging.level.root=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponse;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponseTo;
|
||||
import com.usatiuk.tjv.y.server.entity.Person;
|
||||
import com.usatiuk.tjv.y.server.entity.Post;
|
||||
import com.usatiuk.tjv.y.server.repository.PersonRepository;
|
||||
@@ -41,18 +41,18 @@ public abstract class DemoDataDbTest {
|
||||
|
||||
protected static final String person1Password = "p1p";
|
||||
protected Person person1;
|
||||
protected TokenResponse person1Auth;
|
||||
protected TokenResponseTo person1Auth;
|
||||
protected static final String person2Password = "p2p";
|
||||
protected Person person2;
|
||||
protected TokenResponse person2Auth;
|
||||
protected TokenResponseTo person2Auth;
|
||||
protected static final String person3Password = "p3p";
|
||||
protected Person person3;
|
||||
protected TokenResponse person3Auth;
|
||||
protected TokenResponseTo person3Auth;
|
||||
|
||||
protected Post post1;
|
||||
protected Post post2;
|
||||
|
||||
protected HttpHeaders createAuthHeaders(TokenResponse personAuth) {
|
||||
protected HttpHeaders createAuthHeaders(TokenResponseTo personAuth) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + personAuth.token());
|
||||
@@ -67,20 +67,20 @@ public abstract class DemoDataDbTest {
|
||||
.setUsername("person1")
|
||||
.setFullName("Person 1")
|
||||
.setPassword(passwordEncoder.encode(person1Password)));
|
||||
person1Auth = new TokenResponse(tokenService.generateToken(person1.getUuid()));
|
||||
person1Auth = new TokenResponseTo(tokenService.generateToken(person1.getUuid()));
|
||||
person2 = personRepository.save(
|
||||
new Person()
|
||||
.setUsername("person2")
|
||||
.setFullName("Person 2")
|
||||
.setPassword(passwordEncoder.encode(person2Password)).setFollowing(List.of(person1)));
|
||||
person2Auth = new TokenResponse(tokenService.generateToken(person2.getUuid()));
|
||||
person2Auth = new TokenResponseTo(tokenService.generateToken(person2.getUuid()));
|
||||
person3 = personRepository.save(
|
||||
new Person()
|
||||
.setUsername("person3")
|
||||
.setFullName("Person 3")
|
||||
.setPassword(passwordEncoder.encode(person3Password))
|
||||
.setFollowing(List.of(person2, person1)));
|
||||
person3Auth = new TokenResponse(tokenService.generateToken(person3.getUuid()));
|
||||
person3Auth = new TokenResponseTo(tokenService.generateToken(person3.getUuid()));
|
||||
|
||||
post1 = postRepository.save(new Post().setAuthor(person1).setText("post 1"));
|
||||
post2 = postRepository.save(new Post().setAuthor(person2).setText("post 2"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.PersonSignupRequest;
|
||||
import com.usatiuk.tjv.y.server.dto.PersonSignupTo;
|
||||
import com.usatiuk.tjv.y.server.dto.PersonTo;
|
||||
import com.usatiuk.tjv.y.server.dto.converters.PersonMapper;
|
||||
import com.usatiuk.tjv.y.server.repository.PersonRepository;
|
||||
@@ -21,7 +21,7 @@ public class PersonControllerTest extends DemoDataDbTest {
|
||||
@Test
|
||||
void shouldSignUp() {
|
||||
var response = restTemplate.exchange(addr + "/person", HttpMethod.POST,
|
||||
new HttpEntity<>(new PersonSignupRequest("usernew", "full name", "pass")),
|
||||
new HttpEntity<>(new PersonSignupTo("usernew", "full name", "pass")),
|
||||
PersonTo.class);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
@@ -50,6 +50,21 @@ public class PersonControllerTest extends DemoDataDbTest {
|
||||
Assertions.assertEquals(personToResponse.fullName(), person1.getFullName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetSelf() {
|
||||
var response = restTemplate.exchange(addr + "/person",
|
||||
HttpMethod.GET, new HttpEntity<>(createAuthHeaders(person1Auth)), PersonTo.class);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
|
||||
PersonTo personToResponse = response.getBody();
|
||||
Assertions.assertNotNull(personToResponse);
|
||||
|
||||
Assertions.assertEquals(personToResponse.username(), person1.getUsername());
|
||||
Assertions.assertEquals(personToResponse.fullName(), person1.getFullName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetFollowers() {
|
||||
var response = restTemplate.exchange(addr + "/person/followers",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.PostCreate;
|
||||
import com.usatiuk.tjv.y.server.dto.PostCreateTo;
|
||||
import com.usatiuk.tjv.y.server.dto.PostTo;
|
||||
import com.usatiuk.tjv.y.server.dto.converters.PostMapper;
|
||||
import com.usatiuk.tjv.y.server.repository.PostRepository;
|
||||
@@ -22,16 +22,16 @@ public class PostControllerTest extends DemoDataDbTest {
|
||||
void shouldNotCreatePostWithoutAuth() {
|
||||
Long postsBefore = postRepository.count();
|
||||
var response = restTemplate.exchange(addr + "/post", HttpMethod.POST,
|
||||
new HttpEntity<>(new PostCreate("test text")), PostTo.class);
|
||||
new HttpEntity<>(new PostCreateTo("test text")), PostTo.class);
|
||||
|
||||
Assertions.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
|
||||
Assertions.assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
|
||||
|
||||
Assertions.assertEquals(postRepository.count(), postsBefore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreatePost() {
|
||||
var entity = new HttpEntity<>(new PostCreate("test text"), createAuthHeaders(person1Auth));
|
||||
var entity = new HttpEntity<>(new PostCreateTo("test text"), createAuthHeaders(person1Auth));
|
||||
|
||||
var response = restTemplate.exchange(addr + "/post", HttpMethod.POST,
|
||||
entity, PostTo.class);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.usatiuk.tjv.y.server.controller;
|
||||
|
||||
import com.usatiuk.tjv.y.server.dto.TokenRequest;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponse;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenRequestTo;
|
||||
import com.usatiuk.tjv.y.server.dto.TokenResponseTo;
|
||||
import com.usatiuk.tjv.y.server.service.TokenService;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -18,12 +18,12 @@ public class TokenControllerTest extends DemoDataDbTest {
|
||||
@Test
|
||||
void shouldLogin() {
|
||||
var response = restTemplate.exchange(addr + "/token", HttpMethod.POST,
|
||||
new HttpEntity<>(new TokenRequest(person1.getUsername(), person1Password)), TokenResponse.class);
|
||||
new HttpEntity<>(new TokenRequestTo(person1.getUsername(), person1Password)), TokenResponseTo.class);
|
||||
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
|
||||
TokenResponse parsedResponse = response.getBody();
|
||||
TokenResponseTo parsedResponse = response.getBody();
|
||||
Assertions.assertNotNull(parsedResponse);
|
||||
|
||||
Assertions.assertTrue(tokenService.parseToken(parsedResponse.token()).isPresent());
|
||||
|
||||
Reference in New Issue
Block a user