From ab566ebf2476e718976db32219deb40cd2cfe309 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sat, 16 Dec 2023 16:19:05 +0100 Subject: [PATCH] frontend auth --- client/.eslintrc.json | 43 ++-- client/package-lock.json | 227 +++++++++++++++++- client/package.json | 7 +- client/src/App.scss | 6 + client/src/App.tsx | 74 +++++- client/src/Auth.scss | 4 + client/src/Home.tsx | 21 ++ client/src/Login.tsx | 41 ++++ client/src/Signup.tsx | 43 ++++ client/src/actions.ts | 29 +++ client/src/api/Person.ts | 18 ++ client/src/api/Token.ts | 12 + client/src/api/dto.ts | 46 ++++ client/src/api/utils.ts | 68 ++++++ client/src/index.html | 2 +- client/src/loaders.ts | 5 + server/build.gradle | 3 + .../controller/ApiExceptionHandler.java | 22 ++ .../y/server/controller/PersonController.java | 13 +- .../y/server/controller/PostController.java | 6 +- .../y/server/controller/TokenController.java | 15 +- .../com/usatiuk/tjv/y/server/dto/ErrorTo.java | 14 ++ .../tjv/y/server/dto/PersonSignupRequest.java | 4 - .../tjv/y/server/dto/PersonSignupTo.java | 4 + .../usatiuk/tjv/y/server/dto/PostCreate.java | 4 - .../tjv/y/server/dto/PostCreateTo.java | 4 + .../tjv/y/server/dto/TokenRequest.java | 4 - .../tjv/y/server/dto/TokenRequestTo.java | 4 + .../tjv/y/server/dto/TokenResponse.java | 4 - .../tjv/y/server/dto/TokenResponseTo.java | 4 + .../usatiuk/tjv/y/server/entity/Person.java | 7 + .../y/server/security/WebSecurityConfig.java | 47 +++- .../src/main/resources/application.properties | 4 +- .../y/server/controller/DemoDataDbTest.java | 16 +- .../controller/PersonControllerTest.java | 19 +- .../server/controller/PostControllerTest.java | 8 +- .../controller/TokenControllerTest.java | 8 +- 37 files changed, 781 insertions(+), 79 deletions(-) create mode 100644 client/src/App.scss create mode 100644 client/src/Auth.scss create mode 100644 client/src/Home.tsx create mode 100644 client/src/Login.tsx create mode 100644 client/src/Signup.tsx create mode 100644 client/src/actions.ts create mode 100644 client/src/api/Person.ts create mode 100644 client/src/api/Token.ts create mode 100644 client/src/api/dto.ts create mode 100644 client/src/api/utils.ts create mode 100644 client/src/loaders.ts create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/ErrorTo.java delete mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupRequest.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupTo.java delete mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreate.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java delete mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequest.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java delete mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponseTo.java diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 194146f..0417aa6 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -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" ] + } } diff --git a/client/package-lock.json b/client/package-lock.json index fffb7e4..19a6901 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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" + } } } } diff --git a/client/package.json b/client/package.json index 4369082..6659bca 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/App.scss b/client/src/App.scss new file mode 100644 index 0000000..6779299 --- /dev/null +++ b/client/src/App.scss @@ -0,0 +1,6 @@ +#appRoot { + font-family: sans-serif; + max-width: 50rem; + margin-left: auto; + margin-right: auto; +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 1b317bf..f1b35da 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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: , + }, + { + path: "/login", + element: , + loader: async () => { + if (getToken()) { + return redirect("/"); + } + return null; + }, + action: loginAction, + }, + { + path: "/signup", + element: , + loader: async () => { + if (getToken()) { + return redirect("/"); + } + return null; + }, + action: signupAction, + }, +]); + export function App() { - return

Hello worlffffd!

; + return ( + +
+ +
+
+ ); } diff --git a/client/src/Auth.scss b/client/src/Auth.scss new file mode 100644 index 0000000..33f288f --- /dev/null +++ b/client/src/Auth.scss @@ -0,0 +1,4 @@ + +.authForm { + +} \ No newline at end of file diff --git a/client/src/Home.tsx b/client/src/Home.tsx new file mode 100644 index 0000000..f76fe5d --- /dev/null +++ b/client/src/Home.tsx @@ -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> + | undefined; + + if (!loaderData || isError(loaderData)) { + return
Error
; + } + + return ( + <> + username: {loaderData.username} + name: {loaderData.fullName} + + + ); +} diff --git a/client/src/Login.tsx b/client/src/Login.tsx new file mode 100644 index 0000000..89dae46 --- /dev/null +++ b/client/src/Login.tsx @@ -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> + | undefined; + + if (data && !isError(data)) { + return
Success, now log in!;
; + } + + let errors: JSX.Element[] = []; + + if (data) { + errors = data.errors.map((e) => { + return {e}; + }); + } + + const navigation = useNavigation(); + const busy = navigation.state === "submitting"; + + return ( +
+ {errors} +
+ + + + + + Signup +
+
+ ); +} diff --git a/client/src/Signup.tsx b/client/src/Signup.tsx new file mode 100644 index 0000000..b88497b --- /dev/null +++ b/client/src/Signup.tsx @@ -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> + | undefined; + + if (data && !isError(data)) { + return
Success, now log in!;
; + } + + let errors: JSX.Element[] = []; + + if (data) { + errors = data.errors.map((e) => { + return {e}; + }); + } + + const navigation = useNavigation(); + const busy = navigation.state === "submitting"; + + return ( +
+ {errors} +
+ + + + + + + + Login +
+
+ ); +} diff --git a/client/src/actions.ts b/client/src/actions.ts new file mode 100644 index 0000000..ece4b3c --- /dev/null +++ b/client/src/actions.ts @@ -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(), + ); +} diff --git a/client/src/api/Person.ts b/client/src/api/Person.ts new file mode 100644 index 0000000..3135234 --- /dev/null +++ b/client/src/api/Person.ts @@ -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 { + return fetchJSON("/person", "POST", PersonToResp, { + username, + fullName, + password, + }); +} + +export async function getSelf(): Promise { + return fetchJSONAuth("/person", "GET", PersonToResp); +} diff --git a/client/src/api/Token.ts b/client/src/api/Token.ts new file mode 100644 index 0000000..a36336e --- /dev/null +++ b/client/src/api/Token.ts @@ -0,0 +1,12 @@ +import { TokenToResp, TTokenToResp } from "./dto"; +import { fetchJSON } from "./utils"; + +export async function login( + username: string, + password: string, +): Promise { + return fetchJSON("/token", "POST", TokenToResp, { + username, + password, + }); +} diff --git a/client/src/api/dto.ts b/client/src/api/dto.ts new file mode 100644 index 0000000..7a40e3b --- /dev/null +++ b/client/src/api/dto.ts @@ -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; + +function CreateAPIResponse(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; + +export const PersonTo = z.object({ + uuid: z.string(), + username: z.string(), + fullName: z.string(), +}); +export type TPersonTo = z.infer; + +export const PersonToResp = CreateAPIResponse(PersonTo); +export type TPersonToResp = z.infer; + +export const TokenRequestTo = z.object({ + username: z.string(), + password: z.string(), +}); +export type TTokenRequestTo = z.infer; + +export const TokenTo = z.object({ + token: z.string(), +}); +export type TTokenTo = z.infer; + +export const TokenToResp = CreateAPIResponse(TokenTo); +export type TTokenToResp = z.infer; + +export function isError(value: unknown): value is TErrorTo { + return ErrorTo.safeParse(value).success; +} diff --git a/client/src/api/utils.ts b/client/src/api/utils.ts new file mode 100644 index 0000000..0ad738f --- /dev/null +++ b/client/src/api/utils.ts @@ -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 }>( + path: string, + method: string, + parser: P, + body?: string | Record | File, + headers?: Record, +): Promise { + 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 }>( + path: string, + method: string, + parser: P, + body?: string | Record | File, + headers?: Record, +): Promise { + if (token) { + return fetchJSON(path, method, parser, body, { + ...headers, + Authorization: `Bearer ${token}`, + }); + } else { + throw new Error("Not logged in"); + } +} diff --git a/client/src/index.html b/client/src/index.html index ab51a94..b3b7946 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -2,7 +2,7 @@ - Hello world + Y
diff --git a/client/src/loaders.ts b/client/src/loaders.ts new file mode 100644 index 0000000..4c3645e --- /dev/null +++ b/client/src/loaders.ts @@ -0,0 +1,5 @@ +import { getSelf } from "./api/Person"; + +export async function homeLoader() { + return await getSelf(); +} diff --git a/server/build.gradle b/server/build.gradle index 110af72..3caf017 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -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' diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java new file mode 100644 index 0000000..0669041 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java @@ -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 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); + } +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java index 84f70ef..8894aff 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java @@ -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 found = personService.readById(principal.getName()); + + if (found.isEmpty()) throw new UserNotFoundException(); + + return PersonMapper.makeDto(found.get()); + } + @GetMapping(path = "/followers") public Stream getFollowers(Principal principal) throws UserNotFoundException { return personService.getFollowers(principal.getName()).stream().map(PersonMapper::makeDto); diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java index c27a591..a709efb 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java @@ -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)); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java index ee7819e..309ba9c 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java @@ -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 found = personService.login(tokenRequest.username(), tokenRequest.password()); + public TokenResponseTo request(@RequestBody TokenRequestTo tokenRequestTo) throws UserNotFoundException { + Optional 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())); } } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/ErrorTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/ErrorTo.java new file mode 100644 index 0000000..da857d0 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/ErrorTo.java @@ -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 errors, Integer code) { + this(errors.toArray(String[]::new), code); + } + + public ErrorTo(Stream errors, Integer code) { + this(errors.toArray(String[]::new), code); + } +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupRequest.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupRequest.java deleted file mode 100644 index 60192d2..0000000 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.usatiuk.tjv.y.server.dto; - -public record PersonSignupRequest(String username, String fullName, String password) { -} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupTo.java new file mode 100644 index 0000000..5b23bb2 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonSignupTo.java @@ -0,0 +1,4 @@ +package com.usatiuk.tjv.y.server.dto; + +public record PersonSignupTo(String username, String fullName, String password) { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreate.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreate.java deleted file mode 100644 index a862b73..0000000 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreate.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.usatiuk.tjv.y.server.dto; - -public record PostCreate(String text) { -} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java new file mode 100644 index 0000000..59702bb --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java @@ -0,0 +1,4 @@ +package com.usatiuk.tjv.y.server.dto; + +public record PostCreateTo(String text) { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequest.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequest.java deleted file mode 100644 index bd958f2..0000000 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.usatiuk.tjv.y.server.dto; - -public record TokenRequest(String username, String password) { -} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java new file mode 100644 index 0000000..a0699ee --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java @@ -0,0 +1,4 @@ +package com.usatiuk.tjv.y.server.dto; + +public record TokenRequestTo(String username, String password) { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponse.java deleted file mode 100644 index 86edf1a..0000000 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.usatiuk.tjv.y.server.dto; - -public record TokenResponse(String token) { -} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponseTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponseTo.java new file mode 100644 index 0000000..172e012 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenResponseTo.java @@ -0,0 +1,4 @@ +package com.usatiuk.tjv.y.server.dto; + +public record TokenResponseTo(String token) { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java index 3ac57f5..c97cb82 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java @@ -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 { @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") diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java b/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java index 663ba6b..16b8d04 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java @@ -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; + } } diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 9f3f1b6..9948c7c 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -1 +1,3 @@ -jwt.secret=JKLASJKLASJKLJHKLDFAHJKFDSHJKFJHKDSHJKFHJKSDFJHKSDJHKFJHKS98346783467899782345jkhgsdoigh938g \ No newline at end of file +jwt.secret=JKLASJKLASJKLJHKLDFAHJKFDSHJKFJHKDSHJKFHJKSDFJHKSDJHKFJHKS98346783467899782345jkhgsdoigh938g +logging.level.root=DEBUG +logging.level.org.springframework.security=DEBUG \ No newline at end of file diff --git a/server/src/test/java/com/usatiuk/tjv/y/server/controller/DemoDataDbTest.java b/server/src/test/java/com/usatiuk/tjv/y/server/controller/DemoDataDbTest.java index 438cb2a..59a0c10 100644 --- a/server/src/test/java/com/usatiuk/tjv/y/server/controller/DemoDataDbTest.java +++ b/server/src/test/java/com/usatiuk/tjv/y/server/controller/DemoDataDbTest.java @@ -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")); diff --git a/server/src/test/java/com/usatiuk/tjv/y/server/controller/PersonControllerTest.java b/server/src/test/java/com/usatiuk/tjv/y/server/controller/PersonControllerTest.java index fc5a7f4..41c10ec 100644 --- a/server/src/test/java/com/usatiuk/tjv/y/server/controller/PersonControllerTest.java +++ b/server/src/test/java/com/usatiuk/tjv/y/server/controller/PersonControllerTest.java @@ -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", diff --git a/server/src/test/java/com/usatiuk/tjv/y/server/controller/PostControllerTest.java b/server/src/test/java/com/usatiuk/tjv/y/server/controller/PostControllerTest.java index 7eeae16..488824d 100644 --- a/server/src/test/java/com/usatiuk/tjv/y/server/controller/PostControllerTest.java +++ b/server/src/test/java/com/usatiuk/tjv/y/server/controller/PostControllerTest.java @@ -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); diff --git a/server/src/test/java/com/usatiuk/tjv/y/server/controller/TokenControllerTest.java b/server/src/test/java/com/usatiuk/tjv/y/server/controller/TokenControllerTest.java index a875233..c546f82 100644 --- a/server/src/test/java/com/usatiuk/tjv/y/server/controller/TokenControllerTest.java +++ b/server/src/test/java/com/usatiuk/tjv/y/server/controller/TokenControllerTest.java @@ -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());