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:
Dylan De Faoite
2025-08-10 22:41:37 +02:00
committed by GitHub
parent 20f7ec8db4
commit 662966f138
35 changed files with 916 additions and 252 deletions

View 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
}

View File

@@ -0,0 +1,11 @@
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{base_url}}/auth/user
body: none
auth: inherit
}

View 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);
}

View File

@@ -0,0 +1,11 @@
meta {
name: Get all clips
type: http
seq: 1
}
get {
url: {{base_url}}/clips
body: none
auth: inherit
}

View File

@@ -11,6 +11,7 @@ post {
} }
body:form-urlencoded { body:form-urlencoded {
startPoint: 130 startPoint: 10
endPoint: 140 endPoint: 40
title: best possible title
} }

View File

@@ -1,4 +1,9 @@
vars { vars {
base_url: http://localhost:8080/api/v1 base_url: http://localhost:8080/api/v1
uuid: uuid: OswQZ9CRRRasMCMEWZ5uqw
} }
vars:secret [
google_client_id,
google_secret,
token
]

View File

@@ -9,7 +9,9 @@
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@react-oauth/google": "^0.12.2",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"axios": "^1.11.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flowbite-react": "^0.11.8", "flowbite-react": "^0.11.8",
@@ -18,7 +20,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-range-slider-input": "^3.2.1", "react-range-slider-input": "^3.2.1",
"react-router-dom": "^7.6.1", "react-router-dom": "^7.8.0",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
@@ -32,7 +34,8 @@
"globals": "^16.0.0", "globals": "^16.0.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^6.3.5" "vite": "^6.3.5",
"vite-plugin-mkcert": "^1.17.8"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -871,19 +874,32 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.3.1", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.14.0", "@eslint/core": "^0.15.2",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.9", "version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@@ -1113,6 +1129,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@react-oauth/google": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz",
"integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.9", "version": "1.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
@@ -1907,9 +1933,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2089,6 +2115,23 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2096,9 +2139,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2151,6 +2194,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2258,6 +2314,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comment-json": { "node_modules/comment-json": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
@@ -2381,6 +2449,15 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -2402,6 +2479,20 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.159", "version": "1.5.159",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz",
@@ -2422,6 +2513,51 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@@ -2918,6 +3054,42 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2932,6 +3104,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2942,6 +3123,43 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2968,6 +3186,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3000,6 +3230,45 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3468,6 +3737,15 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3490,6 +3768,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3741,6 +4040,12 @@
"node": ">= 0.6.0" "node": ">= 0.6.0"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3822,9 +4127,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.6.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==", "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -3844,12 +4149,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.6.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
"integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==", "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.6.1" "react-router": "7.8.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@@ -4388,6 +4693,24 @@
} }
} }
}, },
"node_modules/vite-plugin-mkcert": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.8.tgz",
"integrity": "sha512-S+4tNEyGqdZQ3RLAG54ETeO2qyURHWrVjUWKYikLAbmhh/iJ+36gDEja4OWwFyXNuvyXcZwNt5TZZR9itPeG5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"axios": "^1.8.3",
"debug": "^4.4.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=v16.7.0"
},
"peerDependencies": {
"vite": ">=3"
}
},
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.5", "version": "6.4.5",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",

View File

@@ -12,7 +12,9 @@
"postinstall": "flowbite-react patch" "postinstall": "flowbite-react patch"
}, },
"dependencies": { "dependencies": {
"@react-oauth/google": "^0.12.2",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"axios": "^1.11.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flowbite-react": "^0.11.8", "flowbite-react": "^0.11.8",
@@ -21,7 +23,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-range-slider-input": "^3.2.1", "react-range-slider-input": "^3.2.1",
"react-router-dom": "^7.6.1", "react-router-dom": "^7.8.0",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
@@ -35,6 +37,7 @@
"globals": "^16.0.0", "globals": "^16.0.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.30.1", "typescript-eslint": "^8.30.1",
"vite": "^6.3.5" "vite": "^6.3.5",
"vite-plugin-mkcert": "^1.17.8"
} }
} }

View File

@@ -2,7 +2,10 @@ import { Menu, X } from 'lucide-react';
import MenuButton from "./buttons/MenuButton.tsx"; import MenuButton from "./buttons/MenuButton.tsx";
import clsx from "clsx"; import clsx from "clsx";
import type {User} from "../utils/types.ts"; import type {User} from "../utils/types.ts";
import { login } from "../utils/endpoints.ts";
import { Dropdown, DropdownItem } from "./Dropdown.tsx"; import { Dropdown, DropdownItem } from "./Dropdown.tsx";
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
import {useNavigate} from "react-router-dom";
type props = { type props = {
sidebarToggled: boolean; sidebarToggled: boolean;
@@ -12,9 +15,12 @@ type props = {
} }
const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => { const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) => {
const apiUrl = import.meta.env.VITE_API_URL; const navigate = useNavigate();
const loginUrl = `${apiUrl}/oauth2/authorization/google`;
const logoutUrl = `${apiUrl}/api/v1/auth/logout`; const handleLogout = () => {
// delete token cookie
document.cookie = "token=; Secure; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
return ( return (
<div className={clsx(className, "flex justify-between")}> <div className={clsx(className, "flex justify-between")}>
@@ -26,22 +32,30 @@ const Topbar = ({sidebarToggled, setSidebarToggled, user, className}: props) =>
<div> <div>
<img <img
className={"w-8 h-8 rounded-full inline-block"} className={"w-8 h-8 rounded-full inline-block"}
src={user.profilePicture} src={user.profilePictureUrl}
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
/> />
<Dropdown label={user.name}> <Dropdown label={user.name}>
<DropdownItem item="Logout" <DropdownItem item="Logout"
onClick={() => globalThis.location.href = logoutUrl} onClick={() => handleLogout()}
className={"text-red-500 font-medium"} /> className={"text-red-500 font-medium"} />
</Dropdown> </Dropdown>
</div> </div>
) : ) :
( (
<MenuButton className={"w-20 text-gray-600"} <GoogleOAuthProvider
onClick={() => globalThis.location.href = loginUrl}> clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
Login <GoogleLogin
</MenuButton> onSuccess={(credentialResponse) => {
if (!credentialResponse.credential) {
console.error("No credential received from Google Login");
return;
}
login(credentialResponse.credential).then(() => {navigate(0)});
}}
/>
</GoogleOAuthProvider>
)} )}
</div> </div>

View File

@@ -1,4 +1,5 @@
const Home = () => { const Home = () => {
return ( return (
<div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}> <div className={"max-h-screen flex flex-col justify-center items-center px-6 py-12 text-gray-900"}>
{/* Logo */} {/* Logo */}

View File

@@ -1,5 +1,35 @@
import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts"; import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts";
const API_URL = import.meta.env.VITE_API_URL;
/**
* Login function
* @param GoogleToken - The Google token received from the frontend.
* @return A promise that resolves to a JWT
*/
const login = async (GoogleToken: string): Promise<string> => {
const response = await fetch(API_URL + '/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: GoogleToken }),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`);
}
const result: APIResponse = await response.json();
if (result.status === "error") {
throw new Error(`Login failed: ${result.message}`);
}
return result.data.token;
}
/** /**
* Uploads a file to the backend. * Uploads a file to the backend.
* @param file - The file to upload. * @param file - The file to upload.
@@ -8,7 +38,7 @@ const uploadFile = async (file: File): Promise<string> => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const response = await fetch('/api/v1/upload', { const response = await fetch(API_URL + '/api/v1/upload', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -36,7 +66,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => {
} }
} }
const response = await fetch(`/api/v1/edit/${uuid}`, { const response = await fetch(API_URL + `/api/v1/edit/${uuid}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -61,7 +91,7 @@ const editFile = async (uuid: string, videoMetadata: VideoMetadata) => {
* @param uuid - The UUID of the video file to process. * @param uuid - The UUID of the video file to process.
*/ */
const processFile = async (uuid: string) => { const processFile = async (uuid: string) => {
const response = await fetch(`/api/v1/process/${uuid}`); const response = await fetch(API_URL + `/api/v1/process/${uuid}`, {credentials: "include"});
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to process file: ${response.status}`); throw new Error(`Failed to process file: ${response.status}`);
@@ -75,7 +105,7 @@ const processFile = async (uuid: string) => {
}; };
const convertFile = async (uuid: string) => { const convertFile = async (uuid: string) => {
const response = await fetch(`/api/v1/convert/${uuid}`); const response = await fetch(API_URL + `/api/v1/convert/${uuid}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to convert file: ${response.status}`); throw new Error(`Failed to convert file: ${response.status}`);
@@ -93,7 +123,7 @@ const convertFile = async (uuid: string) => {
* @param uuid - The UUID of the video file. * @param uuid - The UUID of the video file.
*/ */
const getProgress = async (uuid: string): Promise<ProgressResult> => { const getProgress = async (uuid: string): Promise<ProgressResult> => {
const response = await fetch(`/api/v1/progress/${uuid}`); const response = await fetch(API_URL + `/api/v1/progress/${uuid}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch progress: ${response.status}`); throw new Error(`Failed to fetch progress: ${response.status}`);
@@ -117,7 +147,7 @@ const getProgress = async (uuid: string): Promise<ProgressResult> => {
* @param uuid - The UUID of the video file. * @param uuid - The UUID of the video file.
*/ */
const getMetadata = async (uuid: string): Promise<VideoMetadata> => { const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
const response = await fetch(`/api/v1/metadata/original/${uuid}`); const response = await fetch(API_URL + `/api/v1/metadata/original/${uuid}`, {credentials: "include"});
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch metadata: ${response.status}`); throw new Error(`Failed to fetch metadata: ${response.status}`);
@@ -136,7 +166,7 @@ const getMetadata = async (uuid: string): Promise<VideoMetadata> => {
* Fetches the current user information. Returns null if not authenticated. * Fetches the current user information. Returns null if not authenticated.
*/ */
const getUser = async (): Promise<null | User > => { const getUser = async (): Promise<null | User > => {
const response = await fetch('/api/v1/auth/user', {credentials: "include",}); const response = await fetch(API_URL + '/api/v1/auth/user', {credentials: "include"});
if (!response.ok) { if (!response.ok) {
return null; return null;
@@ -148,6 +178,8 @@ const getUser = async (): Promise<null | User > => {
return null; return null;
} }
console.log(result.data);
return result.data; return result.data;
} }
@@ -155,7 +187,7 @@ const getUser = async (): Promise<null | User > => {
* Fetches all clips for the current user. * Fetches all clips for the current user.
*/ */
const getClips = async (): Promise<Clip[]> => { const getClips = async (): Promise<Clip[]> => {
const response = await fetch('/api/v1/clips/', { credentials: 'include' }); const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include'});
if (!response.ok) { if (!response.ok) {
const errorResult: APIResponse = await response.json(); const errorResult: APIResponse = await response.json();
@@ -175,7 +207,7 @@ const getClips = async (): Promise<Clip[]> => {
* @param id * @param id
*/ */
const getClipById = async (id: string): Promise<Clip | null> => { const getClipById = async (id: string): Promise<Clip | null> => {
const response = await fetch(`/api/v1/clips/${id}`, {credentials: "include",}); const response = await fetch(API_URL + `/api/v1/clips/${id}`, {credentials: "include",});
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch clip: ${response.status}`); throw new Error(`Failed to fetch clip: ${response.status}`);
@@ -191,7 +223,7 @@ const getClipById = async (id: string): Promise<Clip | null> => {
}; };
const isThumbnailAvailable = async (id: number): Promise<boolean> => { const isThumbnailAvailable = async (id: number): Promise<boolean> => {
const response = await fetch(`/api/v1/download/thumbnail/${id}`); const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
if (!response.ok) { if (!response.ok) {
return false; return false;
} }
@@ -200,6 +232,7 @@ const isThumbnailAvailable = async (id: number): Promise<boolean> => {
} }
export { export {
login,
uploadFile, uploadFile,
editFile, editFile,
processFile, processFile,

View File

@@ -18,7 +18,7 @@ type APIResponse = {
type User = { type User = {
name: string, name: string,
email: string, email: string,
profilePicture: string profilePictureUrl: string
} }
type Clip = { type Clip = {

View File

@@ -2,10 +2,12 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import flowbiteReact from "flowbite-react/plugin/vite"; import flowbiteReact from "flowbite-react/plugin/vite";
import mkcert from 'vite-plugin-mkcert'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss(), flowbiteReact()], plugins: [react(), tailwindcss(), flowbiteReact(), mkcert()],
preview: { preview: {
port: 5173, port: 5173,
strictPort: true, strictPort: true,

10
pom.xml
View File

@@ -71,6 +71,16 @@
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version> <version>1.5.5.Final</version>
</dependency> </dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>2.8.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -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);
}
};
}
}

View File

@@ -1,51 +1,49 @@
package com.ddf.vodsystem.configuration; package com.ddf.vodsystem.configuration;
import com.ddf.vodsystem.security.CustomOAuth2UserService; import com.ddf.vodsystem.security.JwtFilter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class SecurityConfig { public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Value("${frontend.url}") @Value("${frontend.url}")
private String frontendUrl; private String frontendUrl;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) { private final JwtFilter jwtFilter;
this.customOAuth2UserService = customOAuth2UserService;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/download/clip/**").authenticated() .requestMatchers(HttpMethod.OPTIONS, "/api/v1/**").permitAll()
.requestMatchers("/api/v1/download/clip/**","/api/v1/clips/", "/api/v1/clips/**").authenticated()
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/user").permitAll() .requestMatchers("/api/v1/auth/login", "/api/v1/auth/user").permitAll()
.requestMatchers("/api/v1/upload", "/api/v1/download/**").permitAll() .requestMatchers("/api/v1/upload", "/api/v1/download/**").permitAll()
.requestMatchers("/api/v1/edit/**", "/api/v1/process/**", "/api/v1/progress/**", "/api/v1/convert/**").permitAll() .requestMatchers("/api/v1/edit/**", "/api/v1/process/**", "/api/v1/progress/**", "/api/v1/convert/**").permitAll()
.requestMatchers("/api/v1/metadata/**").permitAll() .requestMatchers("/api/v1/metadata/**").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oauth2Login(oauth2 -> oauth2 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(successHandler()))
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
); );
return http.build(); return http.build();

View File

@@ -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"))
)
);
}
}

View File

@@ -8,15 +8,11 @@ import com.ddf.vodsystem.services.ClipService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List; import java.util.List;
@Controller @RestController
@RequestMapping("/api/v1/clips") @RequestMapping("/api/v1/clips")
public class ClipController { public class ClipController {
private final ClipService clipService; private final ClipService clipService;
@@ -25,12 +21,8 @@ public class ClipController {
this.clipService = clipService; this.clipService = clipService;
} }
@GetMapping("/") @GetMapping("")
public ResponseEntity<APIResponse<List<ClipDTO>>> getClips(@AuthenticationPrincipal OAuth2User principal) { public ResponseEntity<APIResponse<List<ClipDTO>>> getClips() {
if (principal == null) {
throw new NotAuthenticated("User is not authenticated");
}
List<Clip> clips = clipService.getClipsByUser(); List<Clip> clips = clipService.getClipsByUser();
List<ClipDTO> clipDTOs = clips.stream() List<ClipDTO> clipDTOs = clips.stream()
.map(this::convertToDTO) .map(this::convertToDTO)
@@ -42,11 +34,7 @@ public class ClipController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<APIResponse<ClipDTO>> getClipById(@AuthenticationPrincipal OAuth2User principal, @PathVariable Long id) { public ResponseEntity<APIResponse<ClipDTO>> getClipById(@PathVariable Long id) {
if (principal == null) {
throw new NotAuthenticated("User is not authenticated");
}
Clip clip = clipService.getClipById(id); Clip clip = clipService.getClipById(id);
if (clip == null) { if (clip == null) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();

View File

@@ -73,7 +73,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(NotAuthenticated.class) @ExceptionHandler(NotAuthenticated.class)
public ResponseEntity<APIResponse<Void>> handleNotAuthenticated(NotAuthenticated ex) { public ResponseEntity<APIResponse<Void>> handleNotAuthenticated(NotAuthenticated ex) {
logger.error("NotAuthenticated: {}", ex.getMessage()); logger.error("NotAuthenticated: {}", ex.getMessage());
APIResponse<Void> response = new APIResponse<>(ERROR, "User is not authenticated", null); APIResponse<Void> response = new APIResponse<>(ERROR, ex.getMessage(), null);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
} }
} }

View File

@@ -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))
);
}
}

View File

@@ -1,9 +1,6 @@
package com.ddf.vodsystem.dto; package com.ddf.vodsystem.dto;
import java.io.File; import java.io.File;
import org.springframework.security.core.context.SecurityContext;
import lombok.Data; import lombok.Data;
@Data @Data
@@ -16,13 +13,13 @@ public class Job {
private VideoMetadata inputVideoMetadata; private VideoMetadata inputVideoMetadata;
private VideoMetadata outputVideoMetadata = new VideoMetadata(); private VideoMetadata outputVideoMetadata = new VideoMetadata();
// security
private SecurityContext securityContext;
// job status // job status
private JobStatus status = new JobStatus(); private JobStatus status = new JobStatus();
public Job(String uuid, File inputFile, File outputFile, VideoMetadata inputVideoMetadata) { public Job(String uuid,
File inputFile,
File outputFile,
VideoMetadata inputVideoMetadata) {
this.uuid = uuid; this.uuid = uuid;
this.inputFile = inputFile; this.inputFile = inputFile;
this.outputFile = outputFile; this.outputFile = outputFile;

View File

@@ -0,0 +1,10 @@
package com.ddf.vodsystem.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class TokenDTO {
private String token;
}

View File

@@ -26,6 +26,9 @@ public class User {
@Column(name = "name", nullable = false) @Column(name = "name", nullable = false)
private String name; private String name;
@Column(name = "profile_picture_url")
private String profilePictureUrl;
@Column(name = "role", nullable = false) @Column(name = "role", nullable = false)
private Integer role; // 0: user, 1: admin private Integer role; // 0: user, 1: admin

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View 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);
}
}

View 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;
}
}
}

View File

@@ -8,7 +8,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import com.ddf.vodsystem.exceptions.FFMPEGException;
import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.exceptions.NotAuthenticated;
import com.ddf.vodsystem.repositories.ClipRepository; import com.ddf.vodsystem.repositories.ClipRepository;
import com.ddf.vodsystem.services.media.CompressionService; import com.ddf.vodsystem.services.media.CompressionService;
@@ -64,7 +66,7 @@ public class ClipService {
ProgressTracker progress) ProgressTracker progress)
throws IOException, InterruptedException { throws IOException, InterruptedException {
User user = userService.getUser(); User user = userService.getLoggedInUser();
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata); metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
compressionService.compress(inputFile, outputFile, outputMetadata, progress) compressionService.compress(inputFile, outputFile, outputMetadata, progress)
.thenRun(() -> { .thenRun(() -> {
@@ -75,7 +77,7 @@ public class ClipService {
} }
public List<Clip> getClipsByUser() { public List<Clip> getClipsByUser() {
User user = userService.getUser(); User user = userService.getLoggedInUser();
if (user == null) { if (user == null) {
logger.warn("No authenticated user found"); logger.warn("No authenticated user found");
@@ -124,7 +126,7 @@ public class ClipService {
} }
public boolean isAuthenticatedForClip(Clip clip) { public boolean isAuthenticatedForClip(Clip clip) {
User user = userService.getUser(); User user = userService.getLoggedInUser();
if (user == null || clip == null) { if (user == null || clip == null) {
return false; return false;
} }
@@ -140,6 +142,14 @@ public class ClipService {
File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png"); File thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png");
directoryService.cutFile(tempFile, clipFile); directoryService.cutFile(tempFile, clipFile);
VideoMetadata clipMetadata;
try {
clipMetadata = metadataService.getVideoMetadata(clipFile).get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new FFMPEGException("Error retrieving video metadata for clip: " + e.getMessage());
}
try { try {
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f); thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
@@ -152,15 +162,17 @@ public class ClipService {
Clip clip = new Clip(); Clip clip = new Clip();
clip.setUser(user); clip.setUser(user);
clip.setTitle(videoMetadata.getTitle() != null ? videoMetadata.getTitle() : "Untitled Clip"); clip.setTitle(videoMetadata.getTitle() != null ? videoMetadata.getTitle() : "Untitled Clip");
clip.setDescription(videoMetadata.getDescription()); clip.setDescription(videoMetadata.getDescription() != null ? videoMetadata.getDescription() : "");
clip.setCreatedAt(LocalDateTime.now()); clip.setCreatedAt(LocalDateTime.now());
clip.setWidth(videoMetadata.getWidth()); clip.setWidth(clipMetadata.getWidth());
clip.setHeight(videoMetadata.getHeight()); clip.setHeight(clipMetadata.getHeight());
clip.setFps(videoMetadata.getFps()); clip.setFps(clipMetadata.getFps());
clip.setDuration(videoMetadata.getEndPoint() - videoMetadata.getStartPoint()); clip.setDuration(clipMetadata.getEndPoint() - clipMetadata.getStartPoint());
clip.setFileSize(videoMetadata.getFileSize()); clip.setFileSize(clipMetadata.getFileSize());
clip.setVideoPath(clipFile.getPath()); clip.setVideoPath(clipFile.getPath());
clip.setThumbnailPath(thumbnailFile.getPath()); clip.setThumbnailPath(thumbnailFile.getPath());
clipRepository.save(clip); clipRepository.save(clip);
logger.info("Clip created successfully with ID: {}", clip.getId());
} }
} }

View File

@@ -30,10 +30,6 @@ public class DownloadService {
public Resource downloadInput(String uuid) { public Resource downloadInput(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobService.getJob(uuid);
if (job == null) {
throw new JobNotFound("Job doesn't exist");
}
File file = job.getInputFile(); File file = job.getInputFile();
return new FileSystemResource(file); return new FileSystemResource(file);
} }
@@ -41,10 +37,6 @@ public class DownloadService {
public Resource downloadOutput(String uuid) { public Resource downloadOutput(String uuid) {
Job job = jobService.getJob(uuid); Job job = jobService.getJob(uuid);
if (job == null) {
throw new JobNotFound("Job doesn't exist");
}
if (!job.getStatus().getProcess().isComplete()) { if (!job.getStatus().getProcess().isComplete()) {
throw new JobNotFinished("Job is not finished"); throw new JobNotFinished("Job is not finished");
} }

View File

@@ -46,13 +46,7 @@ public class UploadService {
// add job // add job
logger.info("Uploaded file and creating job with UUID: {}", uuid); logger.info("Uploaded file and creating job with UUID: {}", uuid);
VideoMetadata videoMetadata; VideoMetadata videoMetadata = getMetadataWithTimeout(inputFile);
try {
videoMetadata = metadataService.getVideoMetadata(inputFile).get(5, TimeUnit.SECONDS);
} catch (ExecutionException | TimeoutException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new FFMPEGException(e.getMessage());
}
Job job = new Job(uuid, inputFile, outputFile, videoMetadata); Job job = new Job(uuid, inputFile, outputFile, videoMetadata);
jobService.add(job); jobService.add(job);
@@ -67,4 +61,13 @@ public class UploadService {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bb.array()); return Base64.getUrlEncoder().withoutPadding().encodeToString(bb.array());
} }
private VideoMetadata getMetadataWithTimeout(File file) {
try {
return metadataService.getVideoMetadata(file).get(5, TimeUnit.SECONDS);
} catch (ExecutionException | TimeoutException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new FFMPEGException(e.getMessage());
}
}
} }

View File

@@ -1,18 +1,111 @@
package com.ddf.vodsystem.services; package com.ddf.vodsystem.services;
import com.ddf.vodsystem.entities.User; import com.ddf.vodsystem.entities.User;
import com.ddf.vodsystem.security.CustomOAuth2User; import com.ddf.vodsystem.exceptions.NotAuthenticated;
import com.ddf.vodsystem.repositories.UserRepository;
import com.ddf.vodsystem.security.JwtService;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Optional;
@Service @Service
public class UserService { public class UserService {
public User getUser() { private final GoogleIdTokenVerifier verifier;
private final UserRepository userRepository;
private final JwtService jwtService;
public UserService(UserRepository userRepository,
JwtService jwtService,
@Value("${google.client.id}") String googleClientId) {
this.userRepository = userRepository;
NetHttpTransport transport = new NetHttpTransport();
JsonFactory jsonFactory = new GsonFactory();
this.verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
.setAudience(Collections.singletonList(googleClientId))
.build();
this.jwtService = jwtService;
}
public User getUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new NotAuthenticated("User not found"));
}
public User getLoggedInUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomOAuth2User oAuth2user) { if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof User) {
return oAuth2user.getUser(); return (User) auth.getPrincipal();
} }
return null; return null;
} }
public String login(String idToken) {
GoogleIdToken googleIdToken = getGoogleIdToken(idToken);
String googleId = googleIdToken.getPayload().getSubject();
if (googleId == null) {
throw new NotAuthenticated("Invalid ID token");
}
User googleUser = getGoogleUser(googleIdToken);
User user = createOrUpdateUser(googleUser);
return jwtService.generateToken(user.getId());
}
private User createOrUpdateUser(User user) {
Optional<User> existingUser = userRepository.findByGoogleId(user.getGoogleId());
if (existingUser.isEmpty()) {
user.setRole(0);
user.setCreatedAt(LocalDateTime.now());
return userRepository.saveAndFlush(user);
}
User existing = existingUser.get();
existing.setEmail(user.getEmail());
existing.setName(user.getName());
existing.setProfilePictureUrl(user.getProfilePictureUrl());
existing.setUsername(user.getUsername());
return userRepository.saveAndFlush(existing);
}
private User getGoogleUser(GoogleIdToken idToken) {
String googleId = idToken.getPayload().getSubject();
String email = idToken.getPayload().getEmail();
String name = (String) idToken.getPayload().get("name");
String profilePictureUrl = (String) idToken.getPayload().get("picture");
User user = new User();
user.setGoogleId(googleId);
user.setEmail(email);
user.setName(name);
user.setUsername(email);
user.setProfilePictureUrl(profilePictureUrl);
return user;
}
private GoogleIdToken getGoogleIdToken(String idToken) {
try {
return verifier.verify(idToken);
} catch (GeneralSecurityException | IOException e) {
throw new NotAuthenticated("Invalid ID token: " + e.getMessage());
}
}
} }

View File

@@ -61,6 +61,7 @@ public class MetadataService {
} }
} }
private VideoMetadata parseVideoMetadata(JsonNode node) { private VideoMetadata parseVideoMetadata(JsonNode node) {
VideoMetadata metadata = new VideoMetadata(); VideoMetadata metadata = new VideoMetadata();
metadata.setStartPoint(0f); metadata.setStartPoint(0f);

View File

@@ -9,10 +9,5 @@ spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:db/schema.sql spring.sql.init.schema-locations=classpath:db/schema.sql
#spring.sql.init.data-locations=classpath:db/data.sql #spring.sql.init.data-locations=classpath:db/data.sql
# Security
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile,email
# Frontend # Frontend
frontend.url=http://localhost:5173 frontend.url=https://localhost:5173

View File

@@ -8,5 +8,13 @@ storage.temp.inputs=videos/inputs/
storage.temp.outputs=videos/outputs/ storage.temp.outputs=videos/outputs/
storage.outputs=outputs/ storage.outputs=outputs/
## Security
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile,email
jwt.secret.key=${JWT_SECRET_KEY}
jwt.expiration=3600000
## Server Configuration ## Server Configuration
server.servlet.session.timeout=30m server.servlet.session.timeout=30m

View File

@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users (
username VARCHAR(50) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
profile_picture_url VARCHAR(255),
role INTEGER DEFAULT 0, role INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );