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