From 662966f138de047a06e1cb16b1b22698ccff2acd Mon Sep 17 00:00:00 2001 From: Dylan De Faoite <98231127+ThisBirchWood@users.noreply.github.com> Date: Sun, 10 Aug 2025 22:41:37 +0200 Subject: [PATCH] 14 standardize and clean api and fix bruno configuration (#25) * ADD JWT authentication support with token generation and validation * ADD JWT handling after successful login * ADD user authentication and standardize user retrieval * COMBINE token dtos * ADD JWT authentication filter * IMPROVE token handling * STANDARDIZE API endpoints and improve JWT handling * REMOVE extra logging * REMOVE redundant job existence checks * UPDATE Bruno Google token * REFACTOR some classes * ADD JWT cookie check * ADD AuthProvider and CORS configuration; UPDATE API endpoints for consistency * ADD JWT validation check; * ADD profile picture to database * ADD reload after login to update page * PATCH login issue * REMOVE unused classes * ADJUST logging in JwtFilter * REMOVE unused React component --- bruno/VoD-System/Auth/Get Google Token.bru | 30 ++ bruno/VoD-System/Auth/Get User.bru | 11 + bruno/VoD-System/Auth/Login.bru | 21 + bruno/VoD-System/Clips/Get all clips.bru | 11 + bruno/VoD-System/Edit/Edit.bru | 5 +- bruno/VoD-System/environments/local.bru | 7 +- frontend/package-lock.json | 361 +++++++++++++++++- frontend/package.json | 7 +- frontend/src/components/Topbar.tsx | 32 +- frontend/src/pages/Home.tsx | 1 + frontend/src/utils/endpoints.ts | 53 ++- frontend/src/utils/types.ts | 2 +- frontend/vite.config.ts | 4 +- pom.xml | 10 + .../vodsystem/configuration/CorsConfig.java | 28 ++ .../configuration/SecurityConfig.java | 30 +- .../vodsystem/controllers/AuthController.java | 45 --- .../vodsystem/controllers/ClipController.java | 22 +- .../controllers/GlobalExceptionHandler.java | 2 +- .../vodsystem/controllers/UserController.java | 54 +++ src/main/java/com/ddf/vodsystem/dto/Job.java | 11 +- .../java/com/ddf/vodsystem/dto/TokenDTO.java | 10 + .../java/com/ddf/vodsystem/entities/User.java | 3 + .../vodsystem/security/CustomOAuth2User.java | 36 -- .../security/CustomOAuth2UserService.java | 47 --- .../com/ddf/vodsystem/security/JwtFilter.java | 90 +++++ .../ddf/vodsystem/security/JwtService.java | 54 +++ .../ddf/vodsystem/services/ClipService.java | 36 +- .../vodsystem/services/DownloadService.java | 8 - .../ddf/vodsystem/services/UploadService.java | 17 +- .../ddf/vodsystem/services/UserService.java | 101 ++++- .../services/media/MetadataService.java | 1 + .../resources/application-local.properties | 7 +- src/main/resources/application.properties | 10 +- src/main/resources/db/schema.sql | 1 + 35 files changed, 916 insertions(+), 252 deletions(-) create mode 100644 bruno/VoD-System/Auth/Get Google Token.bru create mode 100644 bruno/VoD-System/Auth/Get User.bru create mode 100644 bruno/VoD-System/Auth/Login.bru create mode 100644 bruno/VoD-System/Clips/Get all clips.bru create mode 100644 src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java delete mode 100644 src/main/java/com/ddf/vodsystem/controllers/AuthController.java create mode 100644 src/main/java/com/ddf/vodsystem/controllers/UserController.java create mode 100644 src/main/java/com/ddf/vodsystem/dto/TokenDTO.java delete mode 100644 src/main/java/com/ddf/vodsystem/security/CustomOAuth2User.java delete mode 100644 src/main/java/com/ddf/vodsystem/security/CustomOAuth2UserService.java create mode 100644 src/main/java/com/ddf/vodsystem/security/JwtFilter.java create mode 100644 src/main/java/com/ddf/vodsystem/security/JwtService.java diff --git a/bruno/VoD-System/Auth/Get Google Token.bru b/bruno/VoD-System/Auth/Get Google Token.bru new file mode 100644 index 0000000..0012a20 --- /dev/null +++ b/bruno/VoD-System/Auth/Get Google Token.bru @@ -0,0 +1,30 @@ +meta { + name: Get Google Token + type: http + seq: 3 +} + +get { + url: + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: http://localhost:8080 + authorization_url: https://accounts.google.com/o/oauth2/v2/auth + access_token_url: https://oauth2.googleapis.com/token + refresh_token_url: + client_id: {{google_client_id}} + client_secret: {{google_secret}} + scope: profile email + state: + pkce: false + credentials_placement: body + credentials_id: credentials + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false +} diff --git a/bruno/VoD-System/Auth/Get User.bru b/bruno/VoD-System/Auth/Get User.bru new file mode 100644 index 0000000..28036c9 --- /dev/null +++ b/bruno/VoD-System/Auth/Get User.bru @@ -0,0 +1,11 @@ +meta { + name: Get User + type: http + seq: 1 +} + +get { + url: {{base_url}}/auth/user + body: none + auth: inherit +} diff --git a/bruno/VoD-System/Auth/Login.bru b/bruno/VoD-System/Auth/Login.bru new file mode 100644 index 0000000..a317e24 --- /dev/null +++ b/bruno/VoD-System/Auth/Login.bru @@ -0,0 +1,21 @@ +meta { + name: Login + type: http + seq: 2 +} + +post { + url: {{base_url}}/auth/login + body: json + auth: none +} + +body:json { + { + "token": "{{$oauth2.credentials.id_token}}" + } +} + +script:post-response { + bru.setEnvVar("token", res.getBody().data.token); +} diff --git a/bruno/VoD-System/Clips/Get all clips.bru b/bruno/VoD-System/Clips/Get all clips.bru new file mode 100644 index 0000000..d78388d --- /dev/null +++ b/bruno/VoD-System/Clips/Get all clips.bru @@ -0,0 +1,11 @@ +meta { + name: Get all clips + type: http + seq: 1 +} + +get { + url: {{base_url}}/clips + body: none + auth: inherit +} diff --git a/bruno/VoD-System/Edit/Edit.bru b/bruno/VoD-System/Edit/Edit.bru index 4836047..e8a9e73 100644 --- a/bruno/VoD-System/Edit/Edit.bru +++ b/bruno/VoD-System/Edit/Edit.bru @@ -11,6 +11,7 @@ post { } body:form-urlencoded { - startPoint: 130 - endPoint: 140 + startPoint: 10 + endPoint: 40 + title: best possible title } diff --git a/bruno/VoD-System/environments/local.bru b/bruno/VoD-System/environments/local.bru index 594e52d..9792964 100644 --- a/bruno/VoD-System/environments/local.bru +++ b/bruno/VoD-System/environments/local.bru @@ -1,4 +1,9 @@ vars { base_url: http://localhost:8080/api/v1 - uuid: + uuid: OswQZ9CRRRasMCMEWZ5uqw } +vars:secret [ + google_client_id, + google_secret, + token +] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2a1c6b0..c811e8e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { + "@react-oauth/google": "^0.12.2", "@tailwindcss/vite": "^4.1.7", + "axios": "^1.11.0", "clsx": "^2.1.1", "dotenv": "^16.5.0", "flowbite-react": "^0.11.8", @@ -18,7 +20,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-range-slider-input": "^3.2.1", - "react-router-dom": "^7.6.1", + "react-router-dom": "^7.8.0", "tailwindcss": "^4.1.7" }, "devDependencies": { @@ -32,7 +34,8 @@ "globals": "^16.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vite-plugin-mkcert": "^1.17.8" } }, "node_modules/@ampproject/remapping": { @@ -871,19 +874,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -1113,6 +1129,16 @@ "node": ">= 8" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", @@ -1907,9 +1933,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2089,6 +2115,23 @@ "node": ">=4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2096,9 +2139,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2151,6 +2194,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2258,6 +2314,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comment-json": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", @@ -2381,6 +2449,15 @@ "node": ">=16.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2402,6 +2479,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.159", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz", @@ -2422,6 +2513,51 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -2918,6 +3054,42 @@ "node": ">=10" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2932,6 +3104,15 @@ "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2942,6 +3123,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2968,6 +3186,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3000,6 +3230,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3468,6 +3737,15 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3490,6 +3768,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3741,6 +4040,12 @@ "node": ">= 0.6.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3822,9 +4127,9 @@ } }, "node_modules/react-router": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz", - "integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3844,12 +4149,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz", - "integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", + "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", "license": "MIT", "dependencies": { - "react-router": "7.6.1" + "react-router": "7.8.0" }, "engines": { "node": ">=20.0.0" @@ -4388,6 +4693,24 @@ } } }, + "node_modules/vite-plugin-mkcert": { + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.8.tgz", + "integrity": "sha512-S+4tNEyGqdZQ3RLAG54ETeO2qyURHWrVjUWKYikLAbmhh/iJ+36gDEja4OWwFyXNuvyXcZwNt5TZZR9itPeG5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.8.3", + "debug": "^4.4.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=v16.7.0" + }, + "peerDependencies": { + "vite": ">=3" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.4.5", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index d265c35..419829e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,9 @@ "postinstall": "flowbite-react patch" }, "dependencies": { + "@react-oauth/google": "^0.12.2", "@tailwindcss/vite": "^4.1.7", + "axios": "^1.11.0", "clsx": "^2.1.1", "dotenv": "^16.5.0", "flowbite-react": "^0.11.8", @@ -21,7 +23,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-range-slider-input": "^3.2.1", - "react-router-dom": "^7.6.1", + "react-router-dom": "^7.8.0", "tailwindcss": "^4.1.7" }, "devDependencies": { @@ -35,6 +37,7 @@ "globals": "^16.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vite-plugin-mkcert": "^1.17.8" } } diff --git a/frontend/src/components/Topbar.tsx b/frontend/src/components/Topbar.tsx index 3fdfbdb..d5352c1 100644 --- a/frontend/src/components/Topbar.tsx +++ b/frontend/src/components/Topbar.tsx @@ -2,7 +2,10 @@ import { Menu, X } from 'lucide-react'; import MenuButton from "./buttons/MenuButton.tsx"; import clsx from "clsx"; import type {User} from "../utils/types.ts"; +import { login } from "../utils/endpoints.ts"; import { Dropdown, DropdownItem } from "./Dropdown.tsx"; +import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google'; +import {useNavigate} from "react-router-dom"; type props = { sidebarToggled: boolean; @@ -12,9 +15,12 @@ type props = { } const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => { - const apiUrl = import.meta.env.VITE_API_URL; - const loginUrl = `${apiUrl}/oauth2/authorization/google`; - const logoutUrl = `${apiUrl}/api/v1/auth/logout`; + const navigate = useNavigate(); + + const handleLogout = () => { + // delete token cookie + document.cookie = "token=; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + } return (
@@ -26,22 +32,30 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
globalThis.location.href = logoutUrl} + onClick={() => handleLogout()} className={"text-red-500 font-medium"} />
) : ( - globalThis.location.href = loginUrl}> - Login - + + { + if (!credentialResponse.credential) { + console.error("No credential received from Google Login"); + return; + } + login(credentialResponse.credential).then(() => {navigate(0)}); + }} + /> + )}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 85e3807..1e2cf29 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,4 +1,5 @@ const Home = () => { + return (
{/* Logo */} diff --git a/frontend/src/utils/endpoints.ts b/frontend/src/utils/endpoints.ts index 5d42585..c76f966 100644 --- a/frontend/src/utils/endpoints.ts +++ b/frontend/src/utils/endpoints.ts @@ -1,5 +1,35 @@ import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts"; +const API_URL = import.meta.env.VITE_API_URL; + +/** + * Login function + * @param GoogleToken - The Google token received from the frontend. + * @return A promise that resolves to a JWT + */ +const login = async (GoogleToken: string): Promise => { + const response = await fetch(API_URL + '/api/v1/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token: GoogleToken }), + credentials: 'include' + }); + + if (!response.ok) { + throw new Error(`Login failed: ${response.status}`); + } + + const result: APIResponse = await response.json(); + + if (result.status === "error") { + throw new Error(`Login failed: ${result.message}`); + } + + return result.data.token; +} + /** * Uploads a file to the backend. * @param file - The file to upload. @@ -8,7 +38,7 @@ const uploadFile = async (file: File): Promise => { const formData = new FormData(); formData.append('file', file); - const response = await fetch('/api/v1/upload', { + const response = await fetch(API_URL + '/api/v1/upload', { method: 'POST', body: formData, }); @@ -36,7 +66,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => { } } - const response = await fetch(`/api/v1/edit/${uuid}`, { + const response = await fetch(API_URL + `/api/v1/edit/${uuid}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -61,7 +91,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => { * @param uuid - The UUID of the video file to process. */ const processFile = async (uuid: string) => { - const response = await fetch(`/api/v1/process/${uuid}`); + const response = await fetch(API_URL + `/api/v1/process/${uuid}`, {credentials: "include"}); if (!response.ok) { throw new Error(`Failed to process file: ${response.status}`); @@ -75,7 +105,7 @@ const processFile = async (uuid: string) => { }; const convertFile = async (uuid: string) => { - const response = await fetch(`/api/v1/convert/${uuid}`); + const response = await fetch(API_URL + `/api/v1/convert/${uuid}`); if (!response.ok) { throw new Error(`Failed to convert file: ${response.status}`); @@ -93,7 +123,7 @@ const convertFile = async (uuid: string) => { * @param uuid - The UUID of the video file. */ const getProgress = async (uuid: string): Promise => { - const response = await fetch(`/api/v1/progress/${uuid}`); + const response = await fetch(API_URL + `/api/v1/progress/${uuid}`); if (!response.ok) { throw new Error(`Failed to fetch progress: ${response.status}`); @@ -117,7 +147,7 @@ const getProgress = async (uuid: string): Promise => { * @param uuid - The UUID of the video file. */ const getMetadata = async (uuid: string): Promise => { - const response = await fetch(`/api/v1/metadata/original/${uuid}`); + const response = await fetch(API_URL + `/api/v1/metadata/original/${uuid}`, {credentials: "include"}); if (!response.ok) { throw new Error(`Failed to fetch metadata: ${response.status}`); @@ -136,7 +166,7 @@ const getMetadata = async (uuid: string): Promise => { * Fetches the current user information. Returns null if not authenticated. */ const getUser = async (): Promise => { - const response = await fetch('/api/v1/auth/user', {credentials: "include",}); + const response = await fetch(API_URL + '/api/v1/auth/user', {credentials: "include"}); if (!response.ok) { return null; @@ -148,6 +178,8 @@ const getUser = async (): Promise => { return null; } + console.log(result.data); + return result.data; } @@ -155,7 +187,7 @@ const getUser = async (): Promise => { * Fetches all clips for the current user. */ const getClips = async (): Promise => { - const response = await fetch('/api/v1/clips/', { credentials: 'include' }); + const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include'}); if (!response.ok) { const errorResult: APIResponse = await response.json(); @@ -175,7 +207,7 @@ const getClips = async (): Promise => { * @param id */ const getClipById = async (id: string): Promise => { - const response = await fetch(`/api/v1/clips/${id}`, {credentials: "include",}); + const response = await fetch(API_URL + `/api/v1/clips/${id}`, {credentials: "include",}); if (!response.ok) { throw new Error(`Failed to fetch clip: ${response.status}`); @@ -191,7 +223,7 @@ const getClipById = async (id: string): Promise => { }; const isThumbnailAvailable = async (id: number): Promise => { - const response = await fetch(`/api/v1/download/thumbnail/${id}`); + const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"}); if (!response.ok) { return false; } @@ -200,6 +232,7 @@ const isThumbnailAvailable = async (id: number): Promise => { } export { + login, uploadFile, editFile, processFile, diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts index 0809f6b..9607b69 100644 --- a/frontend/src/utils/types.ts +++ b/frontend/src/utils/types.ts @@ -18,7 +18,7 @@ type APIResponse = { type User = { name: string, email: string, - profilePicture: string + profilePictureUrl: string } type Clip = { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7559c01..3f67366 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,10 +2,12 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import flowbiteReact from "flowbite-react/plugin/vite"; +import mkcert from 'vite-plugin-mkcert' + // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss(), flowbiteReact()], + plugins: [react(), tailwindcss(), flowbiteReact(), mkcert()], preview: { port: 5173, strictPort: true, diff --git a/pom.xml b/pom.xml index 33fdb83..b992532 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,16 @@ mapstruct 1.5.5.Final + + com.auth0 + java-jwt + 4.5.0 + + + com.google.api-client + google-api-client + 2.8.0 + diff --git a/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java new file mode 100644 index 0000000..3812aed --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java @@ -0,0 +1,28 @@ +package com.ddf.vodsystem.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig { + + @Value("${frontend.url}") + private String frontendUrl; + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/v1/**") + .allowedOrigins(frontendUrl) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } +} diff --git a/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java b/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java index a0842dc..dc56705 100644 --- a/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java +++ b/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java @@ -1,51 +1,49 @@ package com.ddf.vodsystem.configuration; -import com.ddf.vodsystem.security.CustomOAuth2UserService; +import com.ddf.vodsystem.security.JwtFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; @Configuration @EnableWebSecurity public class SecurityConfig { - private final CustomOAuth2UserService customOAuth2UserService; - @Value("${frontend.url}") private String frontendUrl; - public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) { - this.customOAuth2UserService = customOAuth2UserService; + private final JwtFilter jwtFilter; + + public SecurityConfig(JwtFilter jwtFilter) { + this.jwtFilter = jwtFilter; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) + .cors(Customizer.withDefaults()) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/download/clip/**").authenticated() + .requestMatchers(HttpMethod.OPTIONS, "/api/v1/**").permitAll() + .requestMatchers("/api/v1/download/clip/**","/api/v1/clips/", "/api/v1/clips/**").authenticated() .requestMatchers("/api/v1/auth/login", "/api/v1/auth/user").permitAll() .requestMatchers("/api/v1/upload", "/api/v1/download/**").permitAll() .requestMatchers("/api/v1/edit/**", "/api/v1/process/**", "/api/v1/progress/**", "/api/v1/convert/**").permitAll() .requestMatchers("/api/v1/metadata/**").permitAll() .anyRequest().authenticated() ) - .oauth2Login(oauth2 -> oauth2 - .userInfoEndpoint(userInfo -> userInfo - .userService(customOAuth2UserService) - ) - .successHandler(successHandler())) - .logout(logout -> logout - .logoutUrl("/api/v1/auth/logout") - .logoutSuccessHandler(logoutSuccessHandler()) - .invalidateHttpSession(true) - .deleteCookies("JSESSIONID") + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); return http.build(); diff --git a/src/main/java/com/ddf/vodsystem/controllers/AuthController.java b/src/main/java/com/ddf/vodsystem/controllers/AuthController.java deleted file mode 100644 index 3f65892..0000000 --- a/src/main/java/com/ddf/vodsystem/controllers/AuthController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ddf.vodsystem.controllers; - -import com.ddf.vodsystem.dto.APIResponse; -import com.ddf.vodsystem.exceptions.NotAuthenticated; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/auth/") -public class AuthController { - @GetMapping("/user") - public ResponseEntity>> user(@AuthenticationPrincipal OAuth2User principal) { - if (principal == null) { - throw new NotAuthenticated("User is not authenticated"); - } - - if ( - principal.getAttribute("email") == null - || principal.getAttribute("name") == null - || principal.getAttribute("picture") == null) - { - return ResponseEntity.status(HttpStatus.BAD_REQUEST). - body(new APIResponse<>( - "error", - "Required user attributes are missing", - null - )); - } - - return ResponseEntity.ok( - new APIResponse<>("success", "User details retrieved successfully", Map.of( - "name", principal.getAttribute("name"), - "email", principal.getAttribute("email"), - "profilePicture", principal.getAttribute("picture")) - ) - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java index 03cf32a..e340c8d 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java @@ -8,15 +8,11 @@ import com.ddf.vodsystem.services.ClipService; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; import java.util.List; -@Controller +@RestController @RequestMapping("/api/v1/clips") public class ClipController { private final ClipService clipService; @@ -25,12 +21,8 @@ public class ClipController { this.clipService = clipService; } - @GetMapping("/") - public ResponseEntity>> getClips(@AuthenticationPrincipal OAuth2User principal) { - if (principal == null) { - throw new NotAuthenticated("User is not authenticated"); - } - + @GetMapping("") + public ResponseEntity>> getClips() { List clips = clipService.getClipsByUser(); List clipDTOs = clips.stream() .map(this::convertToDTO) @@ -42,11 +34,7 @@ public class ClipController { } @GetMapping("/{id}") - public ResponseEntity> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) { - if (principal == null) { - throw new NotAuthenticated("User is not authenticated"); - } - + public ResponseEntity> getClipById(@PathVariable Long id) { Clip clip = clipService.getClipById(id); if (clip == null) { return ResponseEntity.notFound().build(); diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index 106abf8..90f520f 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -73,7 +73,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(NotAuthenticated.class) public ResponseEntity> handleNotAuthenticated(NotAuthenticated ex) { logger.error("NotAuthenticated: {}", ex.getMessage()); - APIResponse response = new APIResponse<>(ERROR, "User is not authenticated", null); + APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); } } \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/controllers/UserController.java b/src/main/java/com/ddf/vodsystem/controllers/UserController.java new file mode 100644 index 0000000..cd46d35 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/controllers/UserController.java @@ -0,0 +1,54 @@ +package com.ddf.vodsystem.controllers; + +import com.ddf.vodsystem.dto.APIResponse; +import com.ddf.vodsystem.dto.TokenDTO; +import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.services.UserService; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth/") +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/user") + public ResponseEntity> user() { + User user = userService.getLoggedInUser(); + + if (user == null) { + throw new NotAuthenticated("User not authenticated"); + } + + return ResponseEntity.ok( + new APIResponse<>("success", "User retrieved successfully", user) + ); + } + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody TokenDTO tokenDTO, + HttpServletResponse response) { + String jwt = userService.login(tokenDTO.getToken()); + + ResponseCookie cookie = ResponseCookie.from("token", jwt) + .httpOnly(true) + .maxAge(60 * 60 * 24) + .sameSite("None") + .secure(true) + .path("/") + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); + + return ResponseEntity.ok( + new APIResponse<>("success", "Logged in successfully", new TokenDTO(jwt)) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/dto/Job.java b/src/main/java/com/ddf/vodsystem/dto/Job.java index 2a175ce..ef04080 100644 --- a/src/main/java/com/ddf/vodsystem/dto/Job.java +++ b/src/main/java/com/ddf/vodsystem/dto/Job.java @@ -1,9 +1,6 @@ package com.ddf.vodsystem.dto; import java.io.File; - -import org.springframework.security.core.context.SecurityContext; - import lombok.Data; @Data @@ -16,13 +13,13 @@ public class Job { private VideoMetadata inputVideoMetadata; private VideoMetadata outputVideoMetadata = new VideoMetadata(); - // security - private SecurityContext securityContext; - // job status private JobStatus status = new JobStatus(); - public Job(String uuid, File inputFile, File outputFile, VideoMetadata inputVideoMetadata) { + public Job(String uuid, + File inputFile, + File outputFile, + VideoMetadata inputVideoMetadata) { this.uuid = uuid; this.inputFile = inputFile; this.outputFile = outputFile; diff --git a/src/main/java/com/ddf/vodsystem/dto/TokenDTO.java b/src/main/java/com/ddf/vodsystem/dto/TokenDTO.java new file mode 100644 index 0000000..41a3f07 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/dto/TokenDTO.java @@ -0,0 +1,10 @@ +package com.ddf.vodsystem.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TokenDTO { + private String token; +} diff --git a/src/main/java/com/ddf/vodsystem/entities/User.java b/src/main/java/com/ddf/vodsystem/entities/User.java index 20e47a2..f8a5455 100644 --- a/src/main/java/com/ddf/vodsystem/entities/User.java +++ b/src/main/java/com/ddf/vodsystem/entities/User.java @@ -26,6 +26,9 @@ public class User { @Column(name = "name", nullable = false) private String name; + @Column(name = "profile_picture_url") + private String profilePictureUrl; + @Column(name = "role", nullable = false) private Integer role; // 0: user, 1: admin diff --git a/src/main/java/com/ddf/vodsystem/security/CustomOAuth2User.java b/src/main/java/com/ddf/vodsystem/security/CustomOAuth2User.java deleted file mode 100644 index 0c204c8..0000000 --- a/src/main/java/com/ddf/vodsystem/security/CustomOAuth2User.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ddf.vodsystem.security; - -import com.ddf.vodsystem.entities.User; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import java.util.Collection; -import java.util.Map; - -@Getter -public class CustomOAuth2User implements OAuth2User { - - private final OAuth2User oauth2User; - private final User user; - - public CustomOAuth2User(OAuth2User oauth2User, User user) { - this.oauth2User = oauth2User; - this.user = user; - } - - @Override - public Map getAttributes() { - return oauth2User.getAttributes(); - } - - @Override - public Collection getAuthorities() { - return oauth2User.getAuthorities(); - } - - @Override - public String getName() { - return oauth2User.getName(); - } -} diff --git a/src/main/java/com/ddf/vodsystem/security/CustomOAuth2UserService.java b/src/main/java/com/ddf/vodsystem/security/CustomOAuth2UserService.java deleted file mode 100644 index a611b0a..0000000 --- a/src/main/java/com/ddf/vodsystem/security/CustomOAuth2UserService.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.ddf.vodsystem.security; -import com.ddf.vodsystem.entities.User; -import com.ddf.vodsystem.repositories.UserRepository; - -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - - -@Service -public class CustomOAuth2UserService implements OAuth2UserService { - - private final UserRepository userRepository; - - public CustomOAuth2UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Override - public CustomOAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - var delegate = new DefaultOAuth2UserService(); - var oAuth2User = delegate.loadUser(userRequest); - - String email = oAuth2User.getAttribute("email"); - String name = oAuth2User.getAttribute("name"); - String googleId = oAuth2User.getAttribute("sub"); - - User user = userRepository.findByGoogleId(googleId) - .orElseGet(() -> { - User newUser = new User(); - newUser.setEmail(email); - newUser.setName(name); - newUser.setGoogleId(googleId); - newUser.setUsername(email); - newUser.setRole(0); - newUser.setCreatedAt(LocalDateTime.now()); - return userRepository.save(newUser); - }); - - return new CustomOAuth2User(oAuth2User, user); - } -} diff --git a/src/main/java/com/ddf/vodsystem/security/JwtFilter.java b/src/main/java/com/ddf/vodsystem/security/JwtFilter.java new file mode 100644 index 0000000..6c4e72f --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/security/JwtFilter.java @@ -0,0 +1,90 @@ +package com.ddf.vodsystem.security; + +import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.services.UserService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@Component +public class JwtFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final UserService userService; + private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class); + + public JwtFilter(JwtService jwtService, + UserService userService) { + this.jwtService = jwtService; + this.userService = userService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String jwt = null; + + // 1. Try to get the JWT from the Authorization header + String authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + logger.debug("JWT found in Authorization header"); + jwt = authorizationHeader.substring(7); + } + + // 2. If no JWT was found in the header, try to get it from a cookie + if (jwt == null) { + logger.debug("JWT not found in Authorization header, checking cookies"); + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + jwt = Arrays.stream(cookies) + .filter(cookie -> "token".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + } + } + + if (jwt == null) { + logger.debug("No JWT found in request"); + filterChain.doFilter(request, response); + return; + } + + logger.debug("JWT found in request"); + Long userId = jwtService.validateTokenAndGetUserId(jwt); + + if (userId == null) { + logger.warn("Invalid JWT: {}", jwt); + filterChain.doFilter(request, response); + return; + } + + User user; + try { + user = userService.getUserById(userId); + } catch (NotAuthenticated e) { + filterChain.doFilter(request, response); + return; + } + + Authentication authentication = new UsernamePasswordAuthenticationToken(user, jwt, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/ddf/vodsystem/security/JwtService.java b/src/main/java/com/ddf/vodsystem/security/JwtService.java new file mode 100644 index 0000000..a9c26e8 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/security/JwtService.java @@ -0,0 +1,54 @@ +package com.ddf.vodsystem.security; + +import java.util.Date; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.stereotype.Service; + +@Service +public class JwtService { + + private final long jwtExpiration; + private final Algorithm algorithm; // Algorithm is now initialized after secret key is available + + private static final String USER_ID_CLAIM = "userId"; + private static final String ISSUER = "vodsystem"; + + public JwtService(@Value("${jwt.secret.key}") String jwtSecretKey, + @Value("${jwt.expiration}") long jwtExpiration) { + this.jwtExpiration = jwtExpiration; + this.algorithm = Algorithm.HMAC256(jwtSecretKey); + } + + public String generateToken(Long userId) { + return JWT.create() + .withClaim(USER_ID_CLAIM, userId) + .withIssuer(ISSUER) + .withExpiresAt(new Date(System.currentTimeMillis() + jwtExpiration)) + .sign(algorithm); + } + + public Long validateTokenAndGetUserId(String token) { + try { + JWTVerifier verifier = JWT.require(algorithm) + .withClaimPresence(USER_ID_CLAIM) + .withIssuer(ISSUER) + .build(); + + DecodedJWT jwt = verifier.verify(token); + + if (jwt.getExpiresAt() == null || jwt.getExpiresAt().before(new Date())) { + return null; + } + + return jwt.getClaim(USER_ID_CLAIM).asLong(); + } catch (JwtException | IllegalArgumentException | TokenExpiredException ignored) { + return null; + } + } +} diff --git a/src/main/java/com/ddf/vodsystem/services/ClipService.java b/src/main/java/com/ddf/vodsystem/services/ClipService.java index 4b3af35..d49ea2b 100644 --- a/src/main/java/com/ddf/vodsystem/services/ClipService.java +++ b/src/main/java/com/ddf/vodsystem/services/ClipService.java @@ -8,7 +8,9 @@ import java.io.File; import java.io.IOException; import java.time.LocalDateTime; import java.util.List; +import java.util.concurrent.ExecutionException; +import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.repositories.ClipRepository; import com.ddf.vodsystem.services.media.CompressionService; @@ -64,7 +66,7 @@ public class ClipService { ProgressTracker progress) throws IOException, InterruptedException { - User user = userService.getUser(); + User user = userService.getLoggedInUser(); metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata); compressionService.compress(inputFile, outputFile, outputMetadata, progress) .thenRun(() -> { @@ -75,7 +77,7 @@ public class ClipService { } public List getClipsByUser() { - User user = userService.getUser(); + User user = userService.getLoggedInUser(); if (user == null) { logger.warn("No authenticated user found"); @@ -124,7 +126,7 @@ public class ClipService { } public boolean isAuthenticatedForClip(Clip clip) { - User user = userService.getUser(); + User user = userService.getLoggedInUser(); if (user == null || clip == null) { return false; } @@ -132,14 +134,22 @@ public class ClipService { } private void persistClip(VideoMetadata videoMetadata, - User user, - File tempFile, - String fileName) { + User user, + File tempFile, + String fileName) { // Move clip from temp to output directory File clipFile = directoryService.getUserClipsFile(user.getId(), fileName); File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png"); directoryService.cutFile(tempFile, clipFile); + VideoMetadata clipMetadata; + try { + clipMetadata = metadataService.getVideoMetadata(clipFile).get(); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new FFMPEGException("Error retrieving video metadata for clip: " + e.getMessage()); + } + try { thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f); @@ -152,15 +162,17 @@ public class ClipService { Clip clip = new Clip(); clip.setUser(user); clip.setTitle(videoMetadata.getTitle() != null ? videoMetadata.getTitle() : "Untitled Clip"); - clip.setDescription(videoMetadata.getDescription()); + clip.setDescription(videoMetadata.getDescription() != null ? videoMetadata.getDescription() : ""); clip.setCreatedAt(LocalDateTime.now()); - clip.setWidth(videoMetadata.getWidth()); - clip.setHeight(videoMetadata.getHeight()); - clip.setFps(videoMetadata.getFps()); - clip.setDuration(videoMetadata.getEndPoint() - videoMetadata.getStartPoint()); - clip.setFileSize(videoMetadata.getFileSize()); + clip.setWidth(clipMetadata.getWidth()); + clip.setHeight(clipMetadata.getHeight()); + clip.setFps(clipMetadata.getFps()); + clip.setDuration(clipMetadata.getEndPoint() - clipMetadata.getStartPoint()); + clip.setFileSize(clipMetadata.getFileSize()); clip.setVideoPath(clipFile.getPath()); clip.setThumbnailPath(thumbnailFile.getPath()); clipRepository.save(clip); + + logger.info("Clip created successfully with ID: {}", clip.getId()); } } diff --git a/src/main/java/com/ddf/vodsystem/services/DownloadService.java b/src/main/java/com/ddf/vodsystem/services/DownloadService.java index 89f050c..022ed96 100644 --- a/src/main/java/com/ddf/vodsystem/services/DownloadService.java +++ b/src/main/java/com/ddf/vodsystem/services/DownloadService.java @@ -30,10 +30,6 @@ public class DownloadService { public Resource downloadInput(String uuid) { Job job = jobService.getJob(uuid); - if (job == null) { - throw new JobNotFound("Job doesn't exist"); - } - File file = job.getInputFile(); return new FileSystemResource(file); } @@ -41,10 +37,6 @@ public class DownloadService { public Resource downloadOutput(String uuid) { Job job = jobService.getJob(uuid); - if (job == null) { - throw new JobNotFound("Job doesn't exist"); - } - if (!job.getStatus().getProcess().isComplete()) { throw new JobNotFinished("Job is not finished"); } diff --git a/src/main/java/com/ddf/vodsystem/services/UploadService.java b/src/main/java/com/ddf/vodsystem/services/UploadService.java index 2083dae..5c97c35 100644 --- a/src/main/java/com/ddf/vodsystem/services/UploadService.java +++ b/src/main/java/com/ddf/vodsystem/services/UploadService.java @@ -46,13 +46,7 @@ public class UploadService { // add job logger.info("Uploaded file and creating job with UUID: {}", uuid); - VideoMetadata videoMetadata; - try { - videoMetadata = metadataService.getVideoMetadata(inputFile).get(5, TimeUnit.SECONDS); - } catch (ExecutionException | TimeoutException | InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FFMPEGException(e.getMessage()); - } + VideoMetadata videoMetadata = getMetadataWithTimeout(inputFile); Job job = new Job(uuid, inputFile, outputFile, videoMetadata); jobService.add(job); @@ -67,4 +61,13 @@ public class UploadService { return Base64.getUrlEncoder().withoutPadding().encodeToString(bb.array()); } + private VideoMetadata getMetadataWithTimeout(File file) { + try { + return metadataService.getVideoMetadata(file).get(5, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException | InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FFMPEGException(e.getMessage()); + } + } + } diff --git a/src/main/java/com/ddf/vodsystem/services/UserService.java b/src/main/java/com/ddf/vodsystem/services/UserService.java index 946dbfe..7dc2032 100644 --- a/src/main/java/com/ddf/vodsystem/services/UserService.java +++ b/src/main/java/com/ddf/vodsystem/services/UserService.java @@ -1,18 +1,111 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.entities.User; -import com.ddf.vodsystem.security.CustomOAuth2User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.repositories.UserRepository; +import com.ddf.vodsystem.security.JwtService; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Optional; + @Service public class UserService { - public User getUser() { + private final GoogleIdTokenVerifier verifier; + private final UserRepository userRepository; + private final JwtService jwtService; + + + public UserService(UserRepository userRepository, + JwtService jwtService, + @Value("${google.client.id}") String googleClientId) { + this.userRepository = userRepository; + + NetHttpTransport transport = new NetHttpTransport(); + JsonFactory jsonFactory = new GsonFactory(); + + this.verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) + .setAudience(Collections.singletonList(googleClientId)) + .build(); + this.jwtService = jwtService; + } + + public User getUserById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotAuthenticated("User not found")); + } + + public User getLoggedInUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomOAuth2User oAuth2user) { - return oAuth2user.getUser(); + if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof User) { + return (User) auth.getPrincipal(); } return null; } + + public String login(String idToken) { + GoogleIdToken googleIdToken = getGoogleIdToken(idToken); + String googleId = googleIdToken.getPayload().getSubject(); + + if (googleId == null) { + throw new NotAuthenticated("Invalid ID token"); + } + + User googleUser = getGoogleUser(googleIdToken); + User user = createOrUpdateUser(googleUser); + + return jwtService.generateToken(user.getId()); + } + + private User createOrUpdateUser(User user) { + Optional existingUser = userRepository.findByGoogleId(user.getGoogleId()); + + if (existingUser.isEmpty()) { + user.setRole(0); + user.setCreatedAt(LocalDateTime.now()); + return userRepository.saveAndFlush(user); + } + + User existing = existingUser.get(); + existing.setEmail(user.getEmail()); + existing.setName(user.getName()); + existing.setProfilePictureUrl(user.getProfilePictureUrl()); + existing.setUsername(user.getUsername()); + return userRepository.saveAndFlush(existing); + } + + private User getGoogleUser(GoogleIdToken idToken) { + String googleId = idToken.getPayload().getSubject(); + String email = idToken.getPayload().getEmail(); + String name = (String) idToken.getPayload().get("name"); + String profilePictureUrl = (String) idToken.getPayload().get("picture"); + + User user = new User(); + user.setGoogleId(googleId); + user.setEmail(email); + user.setName(name); + user.setUsername(email); + user.setProfilePictureUrl(profilePictureUrl); + + return user; + } + + private GoogleIdToken getGoogleIdToken(String idToken) { + try { + return verifier.verify(idToken); + } catch (GeneralSecurityException | IOException e) { + throw new NotAuthenticated("Invalid ID token: " + e.getMessage()); + } + } } diff --git a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java index 318485e..e647c13 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java @@ -61,6 +61,7 @@ public class MetadataService { } } + private VideoMetadata parseVideoMetadata(JsonNode node) { VideoMetadata metadata = new VideoMetadata(); metadata.setStartPoint(0f); diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index d3d61e3..24ff790 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -9,10 +9,5 @@ spring.sql.init.mode=always spring.sql.init.schema-locations=classpath:db/schema.sql #spring.sql.init.data-locations=classpath:db/data.sql -# Security -spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} -spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} -spring.security.oauth2.client.registration.google.scope=profile,email - # Frontend -frontend.url=http://localhost:5173 +frontend.url=https://localhost:5173 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e2605ea..b3ac3a0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,5 +8,13 @@ storage.temp.inputs=videos/inputs/ storage.temp.outputs=videos/outputs/ storage.outputs=outputs/ +## Security +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.scope=profile,email + +jwt.secret.key=${JWT_SECRET_KEY} +jwt.expiration=3600000 + ## Server Configuration -server.servlet.session.timeout=30m +server.servlet.session.timeout=30m \ No newline at end of file diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index c8bcb1d..d8b1362 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, + profile_picture_url VARCHAR(255), role INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );