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
This commit is contained in:
30
bruno/VoD-System/Auth/Get Google Token.bru
Normal file
30
bruno/VoD-System/Auth/Get Google Token.bru
Normal file
@@ -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
|
||||||
|
}
|
||||||
11
bruno/VoD-System/Auth/Get User.bru
Normal file
11
bruno/VoD-System/Auth/Get User.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Get User
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/auth/user
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
21
bruno/VoD-System/Auth/Login.bru
Normal file
21
bruno/VoD-System/Auth/Login.bru
Normal file
@@ -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);
|
||||||
|
}
|
||||||
11
bruno/VoD-System/Clips/Get all clips.bru
Normal file
11
bruno/VoD-System/Clips/Get all clips.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Get all clips
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/clips
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
startPoint: 130
|
startPoint: 10
|
||||||
endPoint: 140
|
endPoint: 40
|
||||||
|
title: best possible title
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
vars {
|
vars {
|
||||||
base_url: http://localhost:8080/api/v1
|
base_url: http://localhost:8080/api/v1
|
||||||
uuid:
|
uuid: OswQZ9CRRRasMCMEWZ5uqw
|
||||||
}
|
}
|
||||||
|
vars:secret [
|
||||||
|
google_client_id,
|
||||||
|
google_secret,
|
||||||
|
token
|
||||||
|
]
|
||||||
|
|||||||
361
frontend/package-lock.json
generated
361
frontend/package-lock.json
generated
@@ -9,7 +9,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-oauth/google": "^0.12.2",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"flowbite-react": "^0.11.8",
|
"flowbite-react": "^0.11.8",
|
||||||
@@ -18,7 +20,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-range-slider-input": "^3.2.1",
|
"react-range-slider-input": "^3.2.1",
|
||||||
"react-router-dom": "^7.6.1",
|
"react-router-dom": "^7.8.0",
|
||||||
"tailwindcss": "^4.1.7"
|
"tailwindcss": "^4.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -32,7 +34,8 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-mkcert": "^1.17.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@@ -871,19 +874,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.15.2",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.6.9",
|
"version": "1.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||||
@@ -1113,6 +1129,16 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.9",
|
"version": "1.0.0-beta.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
|
"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": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2089,6 +2115,23 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -2096,9 +2139,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2151,6 +2194,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"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": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -2258,6 +2314,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/comment-json": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
|
||||||
@@ -2381,6 +2449,15 @@
|
|||||||
"node": ">=16.0.0"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@@ -2402,6 +2479,20 @@
|
|||||||
"url": "https://dotenvx.com"
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.159",
|
"version": "1.5.159",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz",
|
||||||
@@ -2422,6 +2513,51 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||||
@@ -2918,6 +3054,42 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"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": "^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": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -2942,6 +3123,43 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -2968,6 +3186,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -3000,6 +3230,45 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -3468,6 +3737,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@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": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -3490,6 +3768,27 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -3741,6 +4040,12 @@
|
|||||||
"node": ">= 0.6.0"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3822,9 +4127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.6.1",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
|
||||||
"integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==",
|
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -3844,12 +4149,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.6.1",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
|
||||||
"integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==",
|
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.6.1"
|
"react-router": "7.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"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": {
|
"node_modules/vite/node_modules/fdir": {
|
||||||
"version": "6.4.5",
|
"version": "6.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
"postinstall": "flowbite-react patch"
|
"postinstall": "flowbite-react patch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-oauth/google": "^0.12.2",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"flowbite-react": "^0.11.8",
|
"flowbite-react": "^0.11.8",
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-range-slider-input": "^3.2.1",
|
"react-range-slider-input": "^3.2.1",
|
||||||
"react-router-dom": "^7.6.1",
|
"react-router-dom": "^7.8.0",
|
||||||
"tailwindcss": "^4.1.7"
|
"tailwindcss": "^4.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-mkcert": "^1.17.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { Menu, X } from 'lucide-react';
|
|||||||
import MenuButton from "./buttons/MenuButton.tsx";
|
import MenuButton from "./buttons/MenuButton.tsx";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type {User} from "../utils/types.ts";
|
import type {User} from "../utils/types.ts";
|
||||||
|
import { login } from "../utils/endpoints.ts";
|
||||||
import { Dropdown, DropdownItem } from "./Dropdown.tsx";
|
import { Dropdown, DropdownItem } from "./Dropdown.tsx";
|
||||||
|
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
type props = {
|
type props = {
|
||||||
sidebarToggled: boolean;
|
sidebarToggled: boolean;
|
||||||
@@ -12,9 +15,12 @@ type props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => {
|
const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => {
|
||||||
const apiUrl = import.meta.env.VITE_API_URL;
|
const navigate = useNavigate();
|
||||||
const loginUrl = `${apiUrl}/oauth2/authorization/google`;
|
|
||||||
const logoutUrl = `${apiUrl}/api/v1/auth/logout`;
|
const handleLogout = () => {
|
||||||
|
// delete token cookie
|
||||||
|
document.cookie = "token=; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(className, "flex justify-between")}>
|
<div className={clsx(className, "flex justify-between")}>
|
||||||
@@ -26,22 +32,30 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
|
|||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
className={"w-8 h-8 rounded-full inline-block"}
|
className={"w-8 h-8 rounded-full inline-block"}
|
||||||
src={user.profilePicture}
|
src={user.profilePictureUrl}
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown label={user.name}>
|
<Dropdown label={user.name}>
|
||||||
<DropdownItem item="Logout"
|
<DropdownItem item="Logout"
|
||||||
onClick={() => globalThis.location.href = logoutUrl}
|
onClick={() => handleLogout()}
|
||||||
className={"text-red-500 font-medium"} />
|
className={"text-red-500 font-medium"} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
) :
|
) :
|
||||||
(
|
(
|
||||||
<MenuButton className={"w-20 text-gray-600"}
|
<GoogleOAuthProvider
|
||||||
onClick={() => globalThis.location.href = loginUrl}>
|
clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
|
||||||
Login
|
<GoogleLogin
|
||||||
</MenuButton>
|
onSuccess={(credentialResponse) => {
|
||||||
|
if (!credentialResponse.credential) {
|
||||||
|
console.error("No credential received from Google Login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
login(credentialResponse.credential).then(() => {navigate(0)});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GoogleOAuthProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const Home = () => {
|
const Home = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
|
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts";
|
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<string> => {
|
||||||
|
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.
|
* Uploads a file to the backend.
|
||||||
* @param file - The file to upload.
|
* @param file - The file to upload.
|
||||||
@@ -8,7 +38,7 @@ const uploadFile = async (file: File): Promise<string> => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const response = await fetch('/api/v1/upload', {
|
const response = await fetch(API_URL + '/api/v1/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'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.
|
* @param uuid - The UUID of the video file to process.
|
||||||
*/
|
*/
|
||||||
const processFile = async (uuid: string) => {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to process file: ${response.status}`);
|
throw new Error(`Failed to process file: ${response.status}`);
|
||||||
@@ -75,7 +105,7 @@ const processFile = async (uuid: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const convertFile = 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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to convert file: ${response.status}`);
|
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.
|
* @param uuid - The UUID of the video file.
|
||||||
*/
|
*/
|
||||||
const getProgress = async (uuid: string): Promise<ProgressResult> => {
|
const getProgress = async (uuid: string): Promise<ProgressResult> => {
|
||||||
const response = await fetch(`/api/v1/progress/${uuid}`);
|
const response = await fetch(API_URL + `/api/v1/progress/${uuid}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch progress: ${response.status}`);
|
throw new Error(`Failed to fetch progress: ${response.status}`);
|
||||||
@@ -117,7 +147,7 @@ const getProgress = async (uuid: string): Promise<ProgressResult> => {
|
|||||||
* @param uuid - The UUID of the video file.
|
* @param uuid - The UUID of the video file.
|
||||||
*/
|
*/
|
||||||
const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
|
const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
|
||||||
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch metadata: ${response.status}`);
|
throw new Error(`Failed to fetch metadata: ${response.status}`);
|
||||||
@@ -136,7 +166,7 @@ const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
|
|||||||
* Fetches the current user information. Returns null if not authenticated.
|
* Fetches the current user information. Returns null if not authenticated.
|
||||||
*/
|
*/
|
||||||
const getUser = async (): Promise<null | User > => {
|
const getUser = async (): Promise<null | User > => {
|
||||||
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) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
@@ -148,6 +178,8 @@ const getUser = async (): Promise<null | User > => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(result.data);
|
||||||
|
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +187,7 @@ const getUser = async (): Promise<null | User > => {
|
|||||||
* Fetches all clips for the current user.
|
* Fetches all clips for the current user.
|
||||||
*/
|
*/
|
||||||
const getClips = async (): Promise<Clip[]> => {
|
const getClips = async (): Promise<Clip[]> => {
|
||||||
const response = await fetch('/api/v1/clips/', { credentials: 'include' });
|
const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include'});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorResult: APIResponse = await response.json();
|
const errorResult: APIResponse = await response.json();
|
||||||
@@ -175,7 +207,7 @@ const getClips = async (): Promise<Clip[]> => {
|
|||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
const getClipById = async (id: string): Promise<Clip | null> => {
|
const getClipById = async (id: string): Promise<Clip | null> => {
|
||||||
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch clip: ${response.status}`);
|
throw new Error(`Failed to fetch clip: ${response.status}`);
|
||||||
@@ -191,7 +223,7 @@ const getClipById = async (id: string): Promise<Clip | null> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
||||||
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) {
|
if (!response.ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -200,6 +232,7 @@ const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
login,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
editFile,
|
editFile,
|
||||||
processFile,
|
processFile,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type APIResponse = {
|
|||||||
type User = {
|
type User = {
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
profilePicture: string
|
profilePictureUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Clip = {
|
type Clip = {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import flowbiteReact from "flowbite-react/plugin/vite";
|
import flowbiteReact from "flowbite-react/plugin/vite";
|
||||||
|
import mkcert from 'vite-plugin-mkcert'
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss(), flowbiteReact()],
|
plugins: [react(), tailwindcss(), flowbiteReact(), mkcert()],
|
||||||
preview: {
|
preview: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|||||||
10
pom.xml
10
pom.xml
@@ -71,6 +71,16 @@
|
|||||||
<artifactId>mapstruct</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
<version>1.5.5.Final</version>
|
<version>1.5.5.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
<version>4.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.api-client</groupId>
|
||||||
|
<artifactId>google-api-client</artifactId>
|
||||||
|
<version>2.8.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +1,49 @@
|
|||||||
package com.ddf.vodsystem.configuration;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
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.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final CustomOAuth2UserService customOAuth2UserService;
|
|
||||||
|
|
||||||
@Value("${frontend.url}")
|
@Value("${frontend.url}")
|
||||||
private String frontendUrl;
|
private String frontendUrl;
|
||||||
|
|
||||||
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
|
private final JwtFilter jwtFilter;
|
||||||
this.customOAuth2UserService = customOAuth2UserService;
|
|
||||||
|
public SecurityConfig(JwtFilter jwtFilter) {
|
||||||
|
this.jwtFilter = jwtFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.cors(Customizer.withDefaults())
|
||||||
|
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.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/auth/login", "/api/v1/auth/user").permitAll()
|
||||||
.requestMatchers("/api/v1/upload", "/api/v1/download/**").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/edit/**", "/api/v1/process/**", "/api/v1/progress/**", "/api/v1/convert/**").permitAll()
|
||||||
.requestMatchers("/api/v1/metadata/**").permitAll()
|
.requestMatchers("/api/v1/metadata/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.oauth2Login(oauth2 -> oauth2
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
.userInfoEndpoint(userInfo -> userInfo
|
|
||||||
.userService(customOAuth2UserService)
|
|
||||||
)
|
|
||||||
.successHandler(successHandler()))
|
|
||||||
.logout(logout -> logout
|
|
||||||
.logoutUrl("/api/v1/auth/logout")
|
|
||||||
.logoutSuccessHandler(logoutSuccessHandler())
|
|
||||||
.invalidateHttpSession(true)
|
|
||||||
.deleteCookies("JSESSIONID")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
|
|||||||
@@ -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<APIResponse<Map<String, Object>>> 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"))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,11 @@ import com.ddf.vodsystem.services.ClipService;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.web.bind.annotation.*;
|
||||||
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 java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Controller
|
@RestController
|
||||||
@RequestMapping("/api/v1/clips")
|
@RequestMapping("/api/v1/clips")
|
||||||
public class ClipController {
|
public class ClipController {
|
||||||
private final ClipService clipService;
|
private final ClipService clipService;
|
||||||
@@ -25,12 +21,8 @@ public class ClipController {
|
|||||||
this.clipService = clipService;
|
this.clipService = clipService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("")
|
||||||
public ResponseEntity<APIResponse<List<ClipDTO>>> getClips(@AuthenticationPrincipal OAuth2User principal) {
|
public ResponseEntity<APIResponse<List<ClipDTO>>> getClips() {
|
||||||
if (principal == null) {
|
|
||||||
throw new NotAuthenticated("User is not authenticated");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Clip> clips = clipService.getClipsByUser();
|
List<Clip> clips = clipService.getClipsByUser();
|
||||||
List<ClipDTO> clipDTOs = clips.stream()
|
List<ClipDTO> clipDTOs = clips.stream()
|
||||||
.map(this::convertToDTO)
|
.map(this::convertToDTO)
|
||||||
@@ -42,11 +34,7 @@ public class ClipController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) {
|
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@PathVariable Long id) {
|
||||||
if (principal == null) {
|
|
||||||
throw new NotAuthenticated("User is not authenticated");
|
|
||||||
}
|
|
||||||
|
|
||||||
Clip clip = clipService.getClipById(id);
|
Clip clip = clipService.getClipById(id);
|
||||||
if (clip == null) {
|
if (clip == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(NotAuthenticated.class)
|
@ExceptionHandler(NotAuthenticated.class)
|
||||||
public ResponseEntity<APIResponse<Void>> handleNotAuthenticated(NotAuthenticated ex) {
|
public ResponseEntity<APIResponse<Void>> handleNotAuthenticated(NotAuthenticated ex) {
|
||||||
logger.error("NotAuthenticated: {}", ex.getMessage());
|
logger.error("NotAuthenticated: {}", ex.getMessage());
|
||||||
APIResponse<Void> response = new APIResponse<>(ERROR, "User is not authenticated", null);
|
APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<APIResponse<User>> 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<APIResponse<TokenDTO>> 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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.ddf.vodsystem.dto;
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -16,13 +13,13 @@ public class Job {
|
|||||||
private VideoMetadata inputVideoMetadata;
|
private VideoMetadata inputVideoMetadata;
|
||||||
private VideoMetadata outputVideoMetadata = new VideoMetadata();
|
private VideoMetadata outputVideoMetadata = new VideoMetadata();
|
||||||
|
|
||||||
// security
|
|
||||||
private SecurityContext securityContext;
|
|
||||||
|
|
||||||
// job status
|
// job status
|
||||||
private JobStatus status = new JobStatus();
|
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.uuid = uuid;
|
||||||
this.inputFile = inputFile;
|
this.inputFile = inputFile;
|
||||||
this.outputFile = outputFile;
|
this.outputFile = outputFile;
|
||||||
|
|||||||
10
src/main/java/com/ddf/vodsystem/dto/TokenDTO.java
Normal file
10
src/main/java/com/ddf/vodsystem/dto/TokenDTO.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TokenDTO {
|
||||||
|
private String token;
|
||||||
|
}
|
||||||
@@ -26,6 +26,9 @@ public class User {
|
|||||||
@Column(name = "name", nullable = false)
|
@Column(name = "name", nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "profile_picture_url")
|
||||||
|
private String profilePictureUrl;
|
||||||
|
|
||||||
@Column(name = "role", nullable = false)
|
@Column(name = "role", nullable = false)
|
||||||
private Integer role; // 0: user, 1: admin
|
private Integer role; // 0: user, 1: admin
|
||||||
|
|
||||||
|
|||||||
@@ -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<String, Object> getAttributes() {
|
|
||||||
return oauth2User.getAttributes();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
|
||||||
return oauth2User.getAuthorities();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return oauth2User.getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<OAuth2UserRequest, OAuth2User> {
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
90
src/main/java/com/ddf/vodsystem/security/JwtFilter.java
Normal file
90
src/main/java/com/ddf/vodsystem/security/JwtFilter.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/main/java/com/ddf/vodsystem/security/JwtService.java
Normal file
54
src/main/java/com/ddf/vodsystem/security/JwtService.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
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.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.repositories.ClipRepository;
|
import com.ddf.vodsystem.repositories.ClipRepository;
|
||||||
import com.ddf.vodsystem.services.media.CompressionService;
|
import com.ddf.vodsystem.services.media.CompressionService;
|
||||||
@@ -64,7 +66,7 @@ public class ClipService {
|
|||||||
ProgressTracker progress)
|
ProgressTracker progress)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
User user = userService.getUser();
|
User user = userService.getLoggedInUser();
|
||||||
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
|
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
|
||||||
compressionService.compress(inputFile, outputFile, outputMetadata, progress)
|
compressionService.compress(inputFile, outputFile, outputMetadata, progress)
|
||||||
.thenRun(() -> {
|
.thenRun(() -> {
|
||||||
@@ -75,7 +77,7 @@ public class ClipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Clip> getClipsByUser() {
|
public List<Clip> getClipsByUser() {
|
||||||
User user = userService.getUser();
|
User user = userService.getLoggedInUser();
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
logger.warn("No authenticated user found");
|
logger.warn("No authenticated user found");
|
||||||
@@ -124,7 +126,7 @@ public class ClipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAuthenticatedForClip(Clip clip) {
|
public boolean isAuthenticatedForClip(Clip clip) {
|
||||||
User user = userService.getUser();
|
User user = userService.getLoggedInUser();
|
||||||
if (user == null || clip == null) {
|
if (user == null || clip == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -132,14 +134,22 @@ public class ClipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void persistClip(VideoMetadata videoMetadata,
|
private void persistClip(VideoMetadata videoMetadata,
|
||||||
User user,
|
User user,
|
||||||
File tempFile,
|
File tempFile,
|
||||||
String fileName) {
|
String fileName) {
|
||||||
// Move clip from temp to output directory
|
// Move clip from temp to output directory
|
||||||
File clipFile = directoryService.getUserClipsFile(user.getId(), fileName);
|
File clipFile = directoryService.getUserClipsFile(user.getId(), fileName);
|
||||||
File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png");
|
File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png");
|
||||||
directoryService.cutFile(tempFile, clipFile);
|
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 {
|
try {
|
||||||
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
|
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
|
||||||
@@ -152,15 +162,17 @@ public class ClipService {
|
|||||||
Clip clip = new Clip();
|
Clip clip = new Clip();
|
||||||
clip.setUser(user);
|
clip.setUser(user);
|
||||||
clip.setTitle(videoMetadata.getTitle() != null ? videoMetadata.getTitle() : "Untitled Clip");
|
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.setCreatedAt(LocalDateTime.now());
|
||||||
clip.setWidth(videoMetadata.getWidth());
|
clip.setWidth(clipMetadata.getWidth());
|
||||||
clip.setHeight(videoMetadata.getHeight());
|
clip.setHeight(clipMetadata.getHeight());
|
||||||
clip.setFps(videoMetadata.getFps());
|
clip.setFps(clipMetadata.getFps());
|
||||||
clip.setDuration(videoMetadata.getEndPoint() - videoMetadata.getStartPoint());
|
clip.setDuration(clipMetadata.getEndPoint() - clipMetadata.getStartPoint());
|
||||||
clip.setFileSize(videoMetadata.getFileSize());
|
clip.setFileSize(clipMetadata.getFileSize());
|
||||||
clip.setVideoPath(clipFile.getPath());
|
clip.setVideoPath(clipFile.getPath());
|
||||||
clip.setThumbnailPath(thumbnailFile.getPath());
|
clip.setThumbnailPath(thumbnailFile.getPath());
|
||||||
clipRepository.save(clip);
|
clipRepository.save(clip);
|
||||||
|
|
||||||
|
logger.info("Clip created successfully with ID: {}", clip.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ public class DownloadService {
|
|||||||
public Resource downloadInput(String uuid) {
|
public Resource downloadInput(String uuid) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
|
|
||||||
if (job == null) {
|
|
||||||
throw new JobNotFound("Job doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
File file = job.getInputFile();
|
File file = job.getInputFile();
|
||||||
return new FileSystemResource(file);
|
return new FileSystemResource(file);
|
||||||
}
|
}
|
||||||
@@ -41,10 +37,6 @@ public class DownloadService {
|
|||||||
public Resource downloadOutput(String uuid) {
|
public Resource downloadOutput(String uuid) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
|
|
||||||
if (job == null) {
|
|
||||||
throw new JobNotFound("Job doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!job.getStatus().getProcess().isComplete()) {
|
if (!job.getStatus().getProcess().isComplete()) {
|
||||||
throw new JobNotFinished("Job is not finished");
|
throw new JobNotFinished("Job is not finished");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,13 +46,7 @@ public class UploadService {
|
|||||||
// add job
|
// add job
|
||||||
logger.info("Uploaded file and creating job with UUID: {}", uuid);
|
logger.info("Uploaded file and creating job with UUID: {}", uuid);
|
||||||
|
|
||||||
VideoMetadata videoMetadata;
|
VideoMetadata videoMetadata = getMetadataWithTimeout(inputFile);
|
||||||
try {
|
|
||||||
videoMetadata = metadataService.getVideoMetadata(inputFile).get(5, TimeUnit.SECONDS);
|
|
||||||
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new FFMPEGException(e.getMessage());
|
|
||||||
}
|
|
||||||
Job job = new Job(uuid, inputFile, outputFile, videoMetadata);
|
Job job = new Job(uuid, inputFile, outputFile, videoMetadata);
|
||||||
jobService.add(job);
|
jobService.add(job);
|
||||||
|
|
||||||
@@ -67,4 +61,13 @@ public class UploadService {
|
|||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bb.array());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,111 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.User;
|
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.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||||
import org.springframework.stereotype.Service;
|
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
|
@Service
|
||||||
public class UserService {
|
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();
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomOAuth2User oAuth2user) {
|
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof User) {
|
||||||
return oAuth2user.getUser();
|
return (User) auth.getPrincipal();
|
||||||
}
|
}
|
||||||
return null;
|
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<User> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private VideoMetadata parseVideoMetadata(JsonNode node) {
|
private VideoMetadata parseVideoMetadata(JsonNode node) {
|
||||||
VideoMetadata metadata = new VideoMetadata();
|
VideoMetadata metadata = new VideoMetadata();
|
||||||
metadata.setStartPoint(0f);
|
metadata.setStartPoint(0f);
|
||||||
|
|||||||
@@ -9,10 +9,5 @@ spring.sql.init.mode=always
|
|||||||
spring.sql.init.schema-locations=classpath:db/schema.sql
|
spring.sql.init.schema-locations=classpath:db/schema.sql
|
||||||
#spring.sql.init.data-locations=classpath:db/data.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
|
||||||
frontend.url=http://localhost:5173
|
frontend.url=https://localhost:5173
|
||||||
|
|||||||
@@ -8,5 +8,13 @@ storage.temp.inputs=videos/inputs/
|
|||||||
storage.temp.outputs=videos/outputs/
|
storage.temp.outputs=videos/outputs/
|
||||||
storage.outputs=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 Configuration
|
||||||
server.servlet.session.timeout=30m
|
server.servlet.session.timeout=30m
|
||||||
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
username VARCHAR(50) NOT NULL UNIQUE,
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
email VARCHAR(100) NOT NULL UNIQUE,
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
|
profile_picture_url VARCHAR(255),
|
||||||
role INTEGER DEFAULT 0,
|
role INTEGER DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user