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 {
|
||||
startPoint: 130
|
||||
endPoint: 140
|
||||
startPoint: 10
|
||||
endPoint: 40
|
||||
title: best possible title
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
vars {
|
||||
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",
|
||||
"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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={clsx(className, "flex justify-between")}>
|
||||
@@ -26,22 +32,30 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
|
||||
<div>
|
||||
<img
|
||||
className={"w-8 h-8 rounded-full inline-block"}
|
||||
src={user.profilePicture}
|
||||
src={user.profilePictureUrl}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
|
||||
<Dropdown label={user.name}>
|
||||
<DropdownItem item="Logout"
|
||||
onClick={() => globalThis.location.href = logoutUrl}
|
||||
onClick={() => handleLogout()}
|
||||
className={"text-red-500 font-medium"} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
) :
|
||||
(
|
||||
<MenuButton className={"w-20 text-gray-600"}
|
||||
onClick={() => globalThis.location.href = loginUrl}>
|
||||
Login
|
||||
</MenuButton>
|
||||
<GoogleOAuthProvider
|
||||
clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
|
||||
<GoogleLogin
|
||||
onSuccess={(credentialResponse) => {
|
||||
if (!credentialResponse.credential) {
|
||||
console.error("No credential received from Google Login");
|
||||
return;
|
||||
}
|
||||
login(credentialResponse.credential).then(() => {navigate(0)});
|
||||
}}
|
||||
/>
|
||||
</GoogleOAuthProvider>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Home = () => {
|
||||
|
||||
return (
|
||||
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
|
||||
{/* Logo */}
|
||||
|
||||
@@ -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<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.
|
||||
* @param file - The file to upload.
|
||||
@@ -8,7 +38,7 @@ const uploadFile = async (file: File): Promise<string> => {
|
||||
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<ProgressResult> => {
|
||||
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<ProgressResult> => {
|
||||
* @param uuid - The UUID of the video file.
|
||||
*/
|
||||
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) {
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
return null;
|
||||
@@ -148,6 +178,8 @@ const getUser = async (): Promise<null | User > => {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(result.data);
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@@ -155,7 +187,7 @@ const getUser = async (): Promise<null | User > => {
|
||||
* Fetches all clips for the current user.
|
||||
*/
|
||||
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) {
|
||||
const errorResult: APIResponse = await response.json();
|
||||
@@ -175,7 +207,7 @@ const getClips = async (): Promise<Clip[]> => {
|
||||
* @param id
|
||||
*/
|
||||
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) {
|
||||
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 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<boolean> => {
|
||||
}
|
||||
|
||||
export {
|
||||
login,
|
||||
uploadFile,
|
||||
editFile,
|
||||
processFile,
|
||||
|
||||
@@ -18,7 +18,7 @@ type APIResponse = {
|
||||
type User = {
|
||||
name: string,
|
||||
email: string,
|
||||
profilePicture: string
|
||||
profilePictureUrl: string
|
||||
}
|
||||
|
||||
type Clip = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
pom.xml
10
pom.xml
@@ -71,6 +71,16 @@
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>1.5.5.Final</version>
|
||||
</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>
|
||||
|
||||
<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;
|
||||
|
||||
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();
|
||||
|
||||
@@ -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.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<APIResponse<List<ClipDTO>>> getClips(@AuthenticationPrincipal OAuth2User principal) {
|
||||
if (principal == null) {
|
||||
throw new NotAuthenticated("User is not authenticated");
|
||||
}
|
||||
|
||||
@GetMapping("")
|
||||
public ResponseEntity<APIResponse<List<ClipDTO>>> getClips() {
|
||||
List<Clip> clips = clipService.getClipsByUser();
|
||||
List<ClipDTO> clipDTOs = clips.stream()
|
||||
.map(this::convertToDTO)
|
||||
@@ -42,11 +34,7 @@ public class ClipController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) {
|
||||
if (principal == null) {
|
||||
throw new NotAuthenticated("User is not authenticated");
|
||||
}
|
||||
|
||||
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@PathVariable Long id) {
|
||||
Clip clip = clipService.getClipById(id);
|
||||
if (clip == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
|
||||
@@ -73,7 +73,7 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(NotAuthenticated.class)
|
||||
public ResponseEntity<APIResponse<Void>> handleNotAuthenticated(NotAuthenticated ex) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
|
||||
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)
|
||||
private String name;
|
||||
|
||||
@Column(name = "profile_picture_url")
|
||||
private String profilePictureUrl;
|
||||
|
||||
@Column(name = "role", nullable = false)
|
||||
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.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<Clip> 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;
|
||||
}
|
||||
@@ -140,6 +142,14 @@ public class ClipService {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<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) {
|
||||
VideoMetadata metadata = new VideoMetadata();
|
||||
metadata.setStartPoint(0f);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user