From 16e177e220432e215e680ffb0b8623c7670298ac Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 30 Nov 2023 14:21:02 +0000 Subject: [PATCH 1/4] WIP - Auth0 implementation --- package-lock.json | 199 ++++++++++++++++-- packages/bridge-server/.env | 2 + packages/bridge-server/.vscode/launch.json | 21 ++ packages/bridge-server/package.json | 5 + packages/bridge-server/src/app.module.ts | 2 + .../src/authz/authz.controller.ts | 22 ++ .../bridge-server/src/authz/authz.guard.ts | 26 +++ .../bridge-server/src/authz/authz.module.ts | 12 ++ .../bridge-server/src/authz/jwt.strategy.ts | 33 +++ packages/bridge-ui/package.json | 1 + packages/bridge-ui/src/app/app.config.ts | 27 ++- .../login-dialog/login-dialog.component.ts | 2 + .../components/top-bar/top-bar.component.html | 9 +- .../components/top-bar/top-bar.component.ts | 11 +- .../src/app/services/auth.service.ts | 40 +++- 15 files changed, 382 insertions(+), 30 deletions(-) create mode 100644 packages/bridge-server/.env create mode 100644 packages/bridge-server/.vscode/launch.json create mode 100644 packages/bridge-server/src/authz/authz.controller.ts create mode 100644 packages/bridge-server/src/authz/authz.guard.ts create mode 100644 packages/bridge-server/src/authz/authz.module.ts create mode 100644 packages/bridge-server/src/authz/jwt.strategy.ts diff --git a/package-lock.json b/package-lock.json index d9761e2..1ceb16b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1589,6 +1589,25 @@ "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", "dev": true }, + "node_modules/@auth0/auth0-angular": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@auth0/auth0-angular/-/auth0-angular-2.2.1.tgz", + "integrity": "sha512-ie9CMNOdQ3mkhhe09SEporCayDuuPYHNmctyHeV6caVgZVSSu9FlFt2ec6CkX27ufDq7yCdyJGnDixWxNtUG+Q==", + "dependencies": { + "@auth0/auth0-spa-js": "^2.0.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13", + "@angular/core": ">=13", + "@angular/router": ">=13" + } + }, + "node_modules/@auth0/auth0-spa-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.2.tgz", + "integrity": "sha512-xdA65Z/U7++Y7L9Uwh8Q8OVOs6qgFz+fb7GAzHFjpr1icO37B//xdzLXm7ZRgA19RWrsNe1nme3h896igJSvvw==" + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -5881,6 +5900,15 @@ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/@nestjs/passport": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.2.tgz", + "integrity": "sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.2.9", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.9.tgz", @@ -7250,7 +7278,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -7269,7 +7296,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -7352,7 +7378,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -7364,7 +7389,6 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -7384,8 +7408,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/http-proxy": { "version": "1.17.14", @@ -7459,8 +7482,7 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/minimatch": { "version": "3.0.5", @@ -7511,17 +7533,45 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.13.tgz", + "integrity": "sha512-fjHaC6Bv8EpMMqzTnHP32SXlZGaNfBPC/Po5dmRGYi2Ky7ljXPbGnOy+SxZqa6iZvFgVhoJ1915Re3m93zmcfA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", - "dev": true + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/retry": { "version": "0.12.0", @@ -7539,7 +7589,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -7570,7 +7619,6 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -14775,6 +14823,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-beautify": { "version": "1.14.11", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", @@ -15018,6 +15074,22 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -15669,8 +15741,7 @@ "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "dev": true + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/lines-and-columns": { "version": "2.0.4", @@ -15830,6 +15901,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -15938,6 +16014,29 @@ "node": ">=10" } }, + "node_modules/lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, "node_modules/lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -18235,6 +18334,41 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18313,6 +18447,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg-connection-string": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", @@ -18719,6 +18858,11 @@ "dev": true, "optional": true }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -23764,9 +23908,13 @@ "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^2.0.3", "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", "@nestjs/sequelize": "^10.0.0", "bridge-shared": "^1.0.0", + "jwks-rsa": "^3.1.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sequelize": "^6.35.1", @@ -23783,6 +23931,7 @@ "@types/jest": "^29.5.2", "@types/multer": "^1.4.11", "@types/node": "^20.3.1", + "@types/passport-jwt": "^3.0.13", "@types/sequelize": "^4.28.18", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -23801,6 +23950,23 @@ "typescript": "^5.1.3" } }, + "packages/bridge-server/node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "packages/bridge-shared": { "version": "1.0.0", "license": "ISC", @@ -23836,6 +24002,7 @@ "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", + "@auth0/auth0-angular": "^2.2.1", "bridge-shared": "^1.0.0", "eventemitter3": "^5.0.1", "jwt-decode": "^4.0.0", diff --git a/packages/bridge-server/.env b/packages/bridge-server/.env new file mode 100644 index 0000000..1949dce --- /dev/null +++ b/packages/bridge-server/.env @@ -0,0 +1,2 @@ +AUTH0_ISSUER_URL=https://dev-0pjms7kv5foqe0ex.uk.auth0.com/ +AUTH0_AUDIENCE=https://ponyta.pkmn.cloud \ No newline at end of file diff --git a/packages/bridge-server/.vscode/launch.json b/packages/bridge-server/.vscode/launch.json new file mode 100644 index 0000000..814dd95 --- /dev/null +++ b/packages/bridge-server/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\src\\authz\\authz.guard.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/packages/bridge-server/package.json b/packages/bridge-server/package.json index 2c92998..33fdb0e 100644 --- a/packages/bridge-server/package.json +++ b/packages/bridge-server/package.json @@ -24,9 +24,13 @@ "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^2.0.3", "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", "@nestjs/sequelize": "^10.0.0", "bridge-shared": "^1.0.0", + "jwks-rsa": "^3.1.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "sequelize": "^6.35.1", @@ -43,6 +47,7 @@ "@types/jest": "^29.5.2", "@types/multer": "^1.4.11", "@types/node": "^20.3.1", + "@types/passport-jwt": "^3.0.13", "@types/sequelize": "^4.28.18", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/packages/bridge-server/src/app.module.ts b/packages/bridge-server/src/app.module.ts index 0b625f4..bdaa869 100644 --- a/packages/bridge-server/src/app.module.ts +++ b/packages/bridge-server/src/app.module.ts @@ -25,6 +25,7 @@ import { SeasonStandingsModule } from './season-standings/season-standings.modul import { UploadModule } from './upload/upload.module'; import { SseService } from './sse/sse.service'; import { SseModule } from './sse/sse.module'; +import { AuthzModule } from './authz/authz.module'; @Module({ imports: [ @@ -62,6 +63,7 @@ import { SseModule } from './sse/sse.module'; SeasonStandingsModule, UploadModule, SseModule, + AuthzModule, ], controllers: [AppController, SeasonsController, UploadController], providers: [AppService, UsersService, RacersService, RacesService, SeasonStandingsService, RaceResultsService, SseService], diff --git a/packages/bridge-server/src/authz/authz.controller.ts b/packages/bridge-server/src/authz/authz.controller.ts new file mode 100644 index 0000000..434c988 --- /dev/null +++ b/packages/bridge-server/src/authz/authz.controller.ts @@ -0,0 +1,22 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Post, + Request, + UseGuards + } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + + @Controller('authz') + export class AuthzController { + constructor() {} + + @UseGuards(AuthGuard('jwt')) + @Get('test') + getProfile(@Request() req) { + return "Hello"; + } + } \ No newline at end of file diff --git a/packages/bridge-server/src/authz/authz.guard.ts b/packages/bridge-server/src/authz/authz.guard.ts new file mode 100644 index 0000000..053c56e --- /dev/null +++ b/packages/bridge-server/src/authz/authz.guard.ts @@ -0,0 +1,26 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, + } from '@nestjs/common'; + +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + canActivate(context: ExecutionContext) { + console.log("runnign") + // Add your custom authentication logic here + // for example, call super.logIn(request) to establish a session. + return super.canActivate(context); + } + + handleRequest(err, user, info) { + console.log("runnign!!") + // You can throw an exception based on either "info" or "err" arguments + if (err || !user) { + throw err || new UnauthorizedException(); + } + return user; + } + } \ No newline at end of file diff --git a/packages/bridge-server/src/authz/authz.module.ts b/packages/bridge-server/src/authz/authz.module.ts new file mode 100644 index 0000000..e135cd6 --- /dev/null +++ b/packages/bridge-server/src/authz/authz.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from './jwt.strategy'; +import { AuthzController } from './authz.controller'; + +@Module({ + imports: [PassportModule.register({ defaultStrategy: 'jwt' })], + providers: [JwtStrategy], + controllers: [AuthzController], + exports: [PassportModule], +}) +export class AuthzModule {} diff --git a/packages/bridge-server/src/authz/jwt.strategy.ts b/packages/bridge-server/src/authz/jwt.strategy.ts new file mode 100644 index 0000000..4a3d970 --- /dev/null +++ b/packages/bridge-server/src/authz/jwt.strategy.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { passportJwtSecret } from 'jwks-rsa'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor() { + super({ + secretOrKeyProvider: passportJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `${process.env.AUTH0_ISSUER_URL}.well-known/jwks.json`, + }), + + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + audience: process.env.AUTH0_AUDIENCE, + issuer: `${process.env.AUTH0_ISSUER_URL}`, + algorithms: ['RS256'], + }); + } + + validate(payload: unknown): unknown { + console.log(payload) + return payload; + } + + +} \ No newline at end of file diff --git a/packages/bridge-ui/package.json b/packages/bridge-ui/package.json index d40522e..76260f8 100644 --- a/packages/bridge-ui/package.json +++ b/packages/bridge-ui/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", + "@auth0/auth0-angular": "^2.2.1", "bridge-shared": "^1.0.0", "eventemitter3": "^5.0.1", "jwt-decode": "^4.0.0", diff --git a/packages/bridge-ui/src/app/app.config.ts b/packages/bridge-ui/src/app/app.config.ts index 35c1101..09fc0cd 100644 --- a/packages/bridge-ui/src/app/app.config.ts +++ b/packages/bridge-ui/src/app/app.config.ts @@ -7,6 +7,7 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { routes } from './app.routes'; import { AuthInterceptor } from './interceptors/auth.interceptor'; import { SnackbarInterceptor } from './interceptors/snackbar.interceptor'; +import { AuthHttpInterceptor, AuthModule } from '@auth0/auth0-angular'; export const appConfig: ApplicationConfig = { providers: [ @@ -16,7 +17,31 @@ export const appConfig: ApplicationConfig = { withInterceptorsFromDi() ), importProvidersFrom(MatNativeDateModule), - {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: SnackbarInterceptor, multi: true}, + importProvidersFrom(AuthModule.forRoot({ + domain: 'dev-0pjms7kv5foqe0ex.uk.auth0.com', + clientId: 'GZFNgt0sFgBZFbj5uBK5s1cRZELYUstm', + + authorizationParams: { + redirect_uri: window.location.origin, + audience: 'https://ponyta.pkmn.cloud' + }, + httpInterceptor: { + allowedList: [ + { + // Match any request that starts 'https://{yourDomain}/api/v2/' (note the asterisk) + //uri: 'https://ponyta.pkmn.cloud/*', + uri: 'http://localhost:3000/*', + tokenOptions: { + authorizationParams: { + // The attached token should target this audience + audience: 'https://ponyta.pkmn.cloud' + } + } + } + ] + } + })) ] }; diff --git a/packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts b/packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts index fe7a2cb..71c182e 100644 --- a/packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts +++ b/packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts @@ -49,6 +49,7 @@ export class LoginDialogComponent { let username = this.username.value != undefined ? this.username.value : ""; let password = this.password.value != undefined ? this.password.value : ""; + /* this.authService.login(username, password).subscribe(data => { console.log(data) if (data.access_token) { @@ -56,5 +57,6 @@ export class LoginDialogComponent { window.location.reload(); } }) + */ } } diff --git a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html index 8322a66..2005461 100644 --- a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html +++ b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html @@ -12,13 +12,16 @@ [options] = "options" (themeChange)="themeChangeHandler($event)"> - @if(isAuthed() == false) { + + @if(isAuthenticated == false) { } - @if(isAuthed()) { - } diff --git a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts index 450f1b0..8392843 100644 --- a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts +++ b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts @@ -16,6 +16,8 @@ import { ThemeOption } from '../../models/theme-option.model'; import { LoginDialogComponent } from '../login-dialog/login-dialog.component'; import { AuthService } from '../../services/auth.service'; +//import { AuthService } from '@auth0/auth0-angular'; + @Component({ selector: 'app-top-bar', standalone: true, @@ -34,11 +36,12 @@ import { AuthService } from '../../services/auth.service'; export class TopBarComponent { constructor( private readonly themeService: ThemeService, - private authService: AuthService, + public authService: AuthService, public dialog: MatDialog ) {} options: Array = []; + isAuthenticated: boolean = false; async ngOnInit() { this.themeService.getThemeOptions().subscribe(data => { @@ -51,7 +54,11 @@ export class TopBarComponent { } onLoginClick() { - this.dialog.open(LoginDialogComponent); + this.authService.login(); + } + + onLogoutClick() { + this.authService.logout(); } isAuthed() { diff --git a/packages/bridge-ui/src/app/services/auth.service.ts b/packages/bridge-ui/src/app/services/auth.service.ts index 1bcf69e..7bf4f65 100644 --- a/packages/bridge-ui/src/app/services/auth.service.ts +++ b/packages/bridge-ui/src/app/services/auth.service.ts @@ -4,33 +4,57 @@ import { Observable } from 'rxjs'; import { jwtDecode } from "jwt-decode"; import { ServerEndpointService } from './server-endpoint.service'; +import { AuthService as Auth0Service } from '@auth0/auth0-angular'; + @Injectable({ providedIn: 'root' }) export class AuthService { + _isAuthenticated:boolean = false; + constructor( private httpClient: HttpClient, private serverEndpointService: ServerEndpointService, - ) { } + private auth0: Auth0Service + ) + { + localStorage.removeItem('token'); + + this.auth0.isAuthenticated$.subscribe(authed => { + this._isAuthenticated = authed; + }); - login(username: string, password: string): Observable { - const headers = new HttpHeaders(); - headers.append('Content-Type', 'application/json'); - return this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"auth/login", { username, password }, { headers }) + this.auth0.user$.subscribe(user => { + console.log(user); + }) + + this.auth0.idTokenClaims$.subscribe(data => { + console.log(data) + if (data && data.__raw) { + localStorage.setItem('token', data.__raw); + } + }) + } + + login() { + this.auth0.loginWithRedirect(); } testProfile(): Observable { - console.log("SendTestProfile") - return this.httpClient.get(this.serverEndpointService.getCurrentEndpoint()+"auth/profile") + return this.httpClient.get(this.serverEndpointService.getCurrentEndpoint()+"authz/test") } logout(): void { - // Clear the token from local storage + this.auth0.logout(); localStorage.removeItem('token'); } isAuthenticated(): boolean { + if(!this._isAuthenticated) { + return false; + } + const token = localStorage.getItem('token'); if (!token) { From ce4196a099ea0bb394eb731611875745d0bc9dec Mon Sep 17 00:00:00 2001 From: Quildra Date: Wed, 20 Dec 2023 11:26:03 +0000 Subject: [PATCH 2/4] More work on auth0 integration --- .../src/seasons/seasons.controller.ts | 7 +- packages/bridge-ui/src/app/app.config.ts | 30 +++++++- .../components/top-bar/top-bar.component.html | 13 ++-- .../components/top-bar/top-bar.component.ts | 26 +++---- .../season-details.component.html | 8 +- .../season-details.component.ts | 9 +-- .../app/pages/seasons/seasons.component.html | 2 +- .../app/pages/seasons/seasons.component.ts | 7 +- .../src/app/services/auth.service.ts | 76 ------------------- .../src/app/services/seasons.service.ts | 2 +- ....service.spec.ts => users.service.spec.ts} | 8 +- .../src/app/services/users.service.ts | 67 ++++++++++++++++ 12 files changed, 128 insertions(+), 127 deletions(-) delete mode 100644 packages/bridge-ui/src/app/services/auth.service.ts rename packages/bridge-ui/src/app/services/{auth.service.spec.ts => users.service.spec.ts} (56%) create mode 100644 packages/bridge-ui/src/app/services/users.service.ts diff --git a/packages/bridge-server/src/seasons/seasons.controller.ts b/packages/bridge-server/src/seasons/seasons.controller.ts index 202fdbd..29279ed 100644 --- a/packages/bridge-server/src/seasons/seasons.controller.ts +++ b/packages/bridge-server/src/seasons/seasons.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { SeasonsService } from './seasons.service'; import { SeasonStandingsService } from 'src/season-standings/season-standings.service'; -import { AuthGuard } from 'src/auth/auth.guard'; +import { AuthGuard } from '@nestjs/passport'; @Controller('seasons') export class SeasonsController { @@ -25,10 +25,9 @@ export class SeasonsController { return this.seasonStandingsService.updateStandings(params.id); } - @UseGuards(AuthGuard) - @Post() + @UseGuards(AuthGuard('jwt')) + @Post('create') create(@Body() body: any) { return this.seasonsService.create(body.title, body.subTitle, body.startingDate); } - } diff --git a/packages/bridge-ui/src/app/app.config.ts b/packages/bridge-ui/src/app/app.config.ts index 09fc0cd..b42aa88 100644 --- a/packages/bridge-ui/src/app/app.config.ts +++ b/packages/bridge-ui/src/app/app.config.ts @@ -30,12 +30,34 @@ export const appConfig: ApplicationConfig = { httpInterceptor: { allowedList: [ { - // Match any request that starts 'https://{yourDomain}/api/v2/' (note the asterisk) - //uri: 'https://ponyta.pkmn.cloud/*', - uri: 'http://localhost:3000/*', + uriMatcher: (uri) => { + let is_create = uri.match('.+\/create'); + return is_create != null; + }, + tokenOptions: { + authorizationParams: { + audience: 'https://ponyta.pkmn.cloud' + } + } + }, + { + uriMatcher: (uri) => { + let is_update = uri.match('.+\/update'); + return is_update != null; + }, + tokenOptions: { + authorizationParams: { + audience: 'https://ponyta.pkmn.cloud' + } + } + }, + { + uriMatcher: (uri) => { + let is_delete = uri.match('.+\/delete'); + return is_delete != null; + }, tokenOptions: { authorizationParams: { - // The attached token should target this audience audience: 'https://ponyta.pkmn.cloud' } } diff --git a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html index 2005461..4546a3c 100644 --- a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html +++ b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.html @@ -12,17 +12,18 @@ [options] = "options" (themeChange)="themeChangeHandler($event)"> - - @if(isAuthenticated == false) { - } @else { - + } \ No newline at end of file diff --git a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts index 8392843..3227762 100644 --- a/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts +++ b/packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts @@ -13,10 +13,9 @@ import { ThemeMenuComponent } from '../theme-menu/theme-menu.component'; import { ThemeService } from '../../services/theme.service'; import { ThemeOption } from '../../models/theme-option.model'; -import { LoginDialogComponent } from '../login-dialog/login-dialog.component'; -import { AuthService } from '../../services/auth.service'; - -//import { AuthService } from '@auth0/auth0-angular'; +import { AuthService } from '@auth0/auth0-angular'; +import { UsersService } from '../../services/users.service'; +import { MatTooltipModule } from '@angular/material/tooltip'; @Component({ selector: 'app-top-bar', @@ -27,6 +26,7 @@ import { AuthService } from '../../services/auth.service'; MatToolbarModule, MatButtonModule, MatIconModule, + MatTooltipModule, ThemeMenuComponent ], templateUrl: './top-bar.component.html', @@ -37,16 +37,18 @@ export class TopBarComponent { constructor( private readonly themeService: ThemeService, public authService: AuthService, + public usersService: UsersService, public dialog: MatDialog ) {} options: Array = []; - isAuthenticated: boolean = false; - async ngOnInit() { + ngOnInit() { this.themeService.getThemeOptions().subscribe(data => { this.options = data; }); + + this.usersService.refreshUserDetails(); } themeChangeHandler(seletedTheme: string) { @@ -54,20 +56,10 @@ export class TopBarComponent { } onLoginClick() { - this.authService.login(); + this.authService.loginWithRedirect(); } onLogoutClick() { this.authService.logout(); } - - isAuthed() { - return this.authService.isAuthenticated(); - } - - onProfile() { - this.authService.testProfile().subscribe(data => { - console.log(data) - }); - } } diff --git a/packages/bridge-ui/src/app/pages/season-details/season-details.component.html b/packages/bridge-ui/src/app/pages/season-details/season-details.component.html index 6eaf301..01e7a5f 100644 --- a/packages/bridge-ui/src/app/pages/season-details/season-details.component.html +++ b/packages/bridge-ui/src/app/pages/season-details/season-details.component.html @@ -3,9 +3,11 @@

{{season.title}}

{{season.subTitle}}

- @if(isAuthed()) { - - + @if(usersService.isAuthenticated) { + @if(usersService.canCreateRaces()) { + + } +
} diff --git a/packages/bridge-ui/src/app/pages/season-details/season-details.component.ts b/packages/bridge-ui/src/app/pages/season-details/season-details.component.ts index 30253da..efa4bb8 100644 --- a/packages/bridge-ui/src/app/pages/season-details/season-details.component.ts +++ b/packages/bridge-ui/src/app/pages/season-details/season-details.component.ts @@ -14,10 +14,11 @@ import { UploadReplayDialogComponent } from '../../components/upload-replay-dial import { SeasonsService } from '../../services/seasons.service'; import { RacesService } from '../../services/races.service'; -import { AuthService } from '../../services/auth.service'; +import { AuthService } from '@auth0/auth0-angular'; import { Season } from '../../models/season.model'; import { Race } from '../../models/race.model'; import { NewRaceDialogComponent } from '../../components/new-race-dialog/new-race-dialog.component'; +import { UsersService } from '../../services/users.service'; @Component({ selector: 'app-season-details', @@ -46,7 +47,7 @@ export class SeasonDetailsComponent { private seasonsService: SeasonsService, private racesService: RacesService, private dialog: MatDialog, - private authService: AuthService, + public usersService: UsersService, ) {} ngOnInit() { @@ -60,10 +61,6 @@ export class SeasonDetailsComponent { }); } - isAuthed() { - return this.authService.isAuthenticated(); - } - openUploadReplayDialog(id: string) { this.dialog.open(UploadReplayDialogComponent, { diff --git a/packages/bridge-ui/src/app/pages/seasons/seasons.component.html b/packages/bridge-ui/src/app/pages/seasons/seasons.component.html index ab95b4b..4220b7c 100644 --- a/packages/bridge-ui/src/app/pages/seasons/seasons.component.html +++ b/packages/bridge-ui/src/app/pages/seasons/seasons.component.html @@ -1,5 +1,5 @@
- @if(isAuthed()){ + @if(userService.canCreateSeasons()){ } @for (season of seasons; track season) { diff --git a/packages/bridge-ui/src/app/pages/seasons/seasons.component.ts b/packages/bridge-ui/src/app/pages/seasons/seasons.component.ts index 9588ba2..8d99e68 100644 --- a/packages/bridge-ui/src/app/pages/seasons/seasons.component.ts +++ b/packages/bridge-ui/src/app/pages/seasons/seasons.component.ts @@ -2,12 +2,12 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SeasonsService } from '../../services/seasons.service'; -import { AuthService } from '../../services/auth.service'; import { SeasonCardComponent } from '../../components/season-card/season-card.component'; import { SeasonCardNewComponent } from '../../components/season-card-new/season-card-new.component'; import { Season } from '../../models/season.model'; +import { UsersService } from '../../services/users.service'; @Component({ selector: 'app-seasons', @@ -26,7 +26,7 @@ export class SeasonsComponent { constructor( private seasonService: SeasonsService, - private authService: AuthService + public userService: UsersService ) {} ngOnInit() { @@ -35,7 +35,4 @@ export class SeasonsComponent { }); } - isAuthed() { - return this.authService.isAuthenticated(); - } } diff --git a/packages/bridge-ui/src/app/services/auth.service.ts b/packages/bridge-ui/src/app/services/auth.service.ts deleted file mode 100644 index 7bf4f65..0000000 --- a/packages/bridge-ui/src/app/services/auth.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Observable } from 'rxjs'; -import { jwtDecode } from "jwt-decode"; -import { ServerEndpointService } from './server-endpoint.service'; - -import { AuthService as Auth0Service } from '@auth0/auth0-angular'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - - _isAuthenticated:boolean = false; - - constructor( - private httpClient: HttpClient, - private serverEndpointService: ServerEndpointService, - private auth0: Auth0Service - ) - { - localStorage.removeItem('token'); - - this.auth0.isAuthenticated$.subscribe(authed => { - this._isAuthenticated = authed; - }); - - this.auth0.user$.subscribe(user => { - console.log(user); - }) - - this.auth0.idTokenClaims$.subscribe(data => { - console.log(data) - if (data && data.__raw) { - localStorage.setItem('token', data.__raw); - } - }) - } - - login() { - this.auth0.loginWithRedirect(); - } - - testProfile(): Observable { - return this.httpClient.get(this.serverEndpointService.getCurrentEndpoint()+"authz/test") - } - - logout(): void { - this.auth0.logout(); - localStorage.removeItem('token'); - } - - isAuthenticated(): boolean { - if(!this._isAuthenticated) { - return false; - } - - const token = localStorage.getItem('token'); - - if (!token) { - return false; - } - - try { - const decoded: any = jwtDecode(token); - - // Check if the token is expired - const isTokenExpired = decoded.exp < Date.now() / 1000; - - return !isTokenExpired; - } catch (error) { - console.error('Error decoding JWT:', error); - return false; - } - } -} diff --git a/packages/bridge-ui/src/app/services/seasons.service.ts b/packages/bridge-ui/src/app/services/seasons.service.ts index b379eed..c18939a 100644 --- a/packages/bridge-ui/src/app/services/seasons.service.ts +++ b/packages/bridge-ui/src/app/services/seasons.service.ts @@ -81,7 +81,7 @@ export class SeasonsService { } create(title: string, subTitle: string, startingDate: Date) { - return this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"seasons/", {title, subTitle, startingDate}); + return this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"seasons/create", {title, subTitle, startingDate}); } updateSeason(id: string) { diff --git a/packages/bridge-ui/src/app/services/auth.service.spec.ts b/packages/bridge-ui/src/app/services/users.service.spec.ts similarity index 56% rename from packages/bridge-ui/src/app/services/auth.service.spec.ts rename to packages/bridge-ui/src/app/services/users.service.spec.ts index f1251ca..f81244a 100644 --- a/packages/bridge-ui/src/app/services/auth.service.spec.ts +++ b/packages/bridge-ui/src/app/services/users.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { AuthService } from './auth.service'; +import { UsersService } from './users.service'; -describe('AuthService', () => { - let service: AuthService; +describe('UsersService', () => { + let service: UsersService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(AuthService); + service = TestBed.inject(UsersService); }); it('should be created', () => { diff --git a/packages/bridge-ui/src/app/services/users.service.ts b/packages/bridge-ui/src/app/services/users.service.ts new file mode 100644 index 0000000..1d02d85 --- /dev/null +++ b/packages/bridge-ui/src/app/services/users.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { AuthService, User } from '@auth0/auth0-angular'; + +import * as jwt_decode from 'jwt-decode'; + +@Injectable({ + providedIn: 'root' +}) +export class UsersService { + + isAuthenticated = false; + permissions: string[] = []; + user: User | null | undefined = null; + + constructor( + public authService: AuthService, + ) { } + + private hasPermission(permission: string) { + return this.permissions.includes(permission); + } + + refreshUserDetails() { + this.authService.isAuthenticated$.subscribe(isAuthed => { + this.isAuthenticated = isAuthed; + console.log(this.isAuthenticated); + + if(!this.isAuthenticated) { return } + + this.authService.user$.subscribe(data => { + this.user = data; + }) + + this.authService.getAccessTokenSilently().subscribe(data => { + try { + let decoded = jwt_decode.jwtDecode(data) as any; + this.permissions = decoded['permissions']; + } + catch (error) { + + } + }) + }) + } + + getUserName() :string { + if(!this.isAuthenticated || !this.user) {return ""} + + return this.user.nickname || ""; + } + + canCreateSeasons() : boolean { + return this.hasPermission("create:seasons") + } + + canEditSeasons() : boolean { + return this.hasPermission("edit:seasons") + } + + canDeleteSeasons() : boolean { + return this.hasPermission("delete:seasons") + } + + canCreateRaces(): boolean { + return this.hasPermission("create:races") + } +} From 76b9815570ab13501a186d9326f670b593ea6c4f Mon Sep 17 00:00:00 2001 From: Quildra Date: Wed, 20 Dec 2023 12:42:09 +0000 Subject: [PATCH 3/4] Add the beginngins of a role guard --- .../src/authorization/roles.decorator.ts | 3 +++ .../src/authorization/roles.guard.ts | 26 +++++++++++++++++++ .../bridge-server/src/authz/authz.guard.ts | 2 -- .../src/seasons/seasons.controller.ts | 6 ++++- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 packages/bridge-server/src/authorization/roles.decorator.ts create mode 100644 packages/bridge-server/src/authorization/roles.guard.ts diff --git a/packages/bridge-server/src/authorization/roles.decorator.ts b/packages/bridge-server/src/authorization/roles.decorator.ts new file mode 100644 index 0000000..12f0336 --- /dev/null +++ b/packages/bridge-server/src/authorization/roles.decorator.ts @@ -0,0 +1,3 @@ +import { Reflector } from '@nestjs/core'; + +export const Roles = Reflector.createDecorator(); \ No newline at end of file diff --git a/packages/bridge-server/src/authorization/roles.guard.ts b/packages/bridge-server/src/authorization/roles.guard.ts new file mode 100644 index 0000000..ab26f93 --- /dev/null +++ b/packages/bridge-server/src/authorization/roles.guard.ts @@ -0,0 +1,26 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Roles } from './roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const roles = this.reflector.get(Roles, context.getHandler()); + if (!roles) { + return true; + } + const request = context.switchToHttp().getRequest(); + const user = request.user; + + roles.forEach(role => { + if(user.permissions.includes(role) == false) { + console.log("Missing " + role + " permission") + return false; + } + }); + + return true; + } +} diff --git a/packages/bridge-server/src/authz/authz.guard.ts b/packages/bridge-server/src/authz/authz.guard.ts index 053c56e..d2c4833 100644 --- a/packages/bridge-server/src/authz/authz.guard.ts +++ b/packages/bridge-server/src/authz/authz.guard.ts @@ -9,14 +9,12 @@ import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { - console.log("runnign") // Add your custom authentication logic here // for example, call super.logIn(request) to establish a session. return super.canActivate(context); } handleRequest(err, user, info) { - console.log("runnign!!") // You can throw an exception based on either "info" or "err" arguments if (err || !user) { throw err || new UnauthorizedException(); diff --git a/packages/bridge-server/src/seasons/seasons.controller.ts b/packages/bridge-server/src/seasons/seasons.controller.ts index 29279ed..16aedd4 100644 --- a/packages/bridge-server/src/seasons/seasons.controller.ts +++ b/packages/bridge-server/src/seasons/seasons.controller.ts @@ -2,6 +2,9 @@ import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { SeasonsService } from './seasons.service'; import { SeasonStandingsService } from 'src/season-standings/season-standings.service'; import { AuthGuard } from '@nestjs/passport'; +import { JwtAuthGuard } from 'src/authz/authz.guard'; +import { RolesGuard } from 'src/authorization/roles.guard'; +import { Roles } from 'src/authorization/roles.decorator'; @Controller('seasons') export class SeasonsController { @@ -25,8 +28,9 @@ export class SeasonsController { return this.seasonStandingsService.updateStandings(params.id); } - @UseGuards(AuthGuard('jwt')) + @UseGuards(JwtAuthGuard, RolesGuard) @Post('create') + @Roles(['create:seasons']) create(@Body() body: any) { return this.seasonsService.create(body.title, body.subTitle, body.startingDate); } From 517e85c83da17bb77b008b5c8cc6f4fe319ebb84 Mon Sep 17 00:00:00 2001 From: Quildra Date: Wed, 20 Dec 2023 13:08:00 +0000 Subject: [PATCH 4/4] Added users to the backend --- .../migrations/20231220114130-create-user.js | 36 +++++++++++++++++++ packages/bridge-server/models/user.js | 27 ++++++++++++++ .../bridge-server/src/auth/auth.service.ts | 9 +---- .../src/users/users.controller.ts | 21 +++++++++++ .../bridge-server/src/users/users.model.ts | 20 +++++++++++ .../bridge-server/src/users/users.module.ts | 9 ++++- .../bridge-server/src/users/users.service.ts | 29 +++++++-------- packages/bridge-ui/src/app/app.config.ts | 11 ++++++ .../src/app/services/users.service.ts | 17 +++++++++ 9 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 packages/bridge-server/migrations/20231220114130-create-user.js create mode 100644 packages/bridge-server/models/user.js create mode 100644 packages/bridge-server/src/users/users.controller.ts create mode 100644 packages/bridge-server/src/users/users.model.ts diff --git a/packages/bridge-server/migrations/20231220114130-create-user.js b/packages/bridge-server/migrations/20231220114130-create-user.js new file mode 100644 index 0000000..3c2d71b --- /dev/null +++ b/packages/bridge-server/migrations/20231220114130-create-user.js @@ -0,0 +1,36 @@ +'use strict'; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('Users', { + auth0id: { + allowNull: false, + primaryKey: true, + type: Sequelize.STRING + }, + nickname: { + type: Sequelize.STRING + }, + picture: { + type: Sequelize.STRING + }, + realName:{ + type: Sequelize.STRING + }, + lastLogin: { + type: Sequelize.DATE + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('Users'); + } +}; \ No newline at end of file diff --git a/packages/bridge-server/models/user.js b/packages/bridge-server/models/user.js new file mode 100644 index 0000000..7fc115a --- /dev/null +++ b/packages/bridge-server/models/user.js @@ -0,0 +1,27 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // define association here + } + } + User.init({ + nickname: DataTypes.STRING, + auth0id: DataTypes.STRING, + picture: DataTypes.STRING, + realName: DataTypes.STRING, + lastLogin: DataTypes.DATE + }, { + sequelize, + modelName: 'User', + }); + return User; +}; \ No newline at end of file diff --git a/packages/bridge-server/src/auth/auth.service.ts b/packages/bridge-server/src/auth/auth.service.ts index 0c8cb97..7b359fb 100644 --- a/packages/bridge-server/src/auth/auth.service.ts +++ b/packages/bridge-server/src/auth/auth.service.ts @@ -12,13 +12,6 @@ export class AuthService { ) {} async signIn(username: string, pass: string): Promise { - const user = await this.usersService.findOne(username); - if (user?.password !== pass) { - throw new UnauthorizedException(); - } - const payload = { sub: user.userId, username: user.username }; - return { - access_token: await this.jwtService.signAsync(payload), - }; + } } \ No newline at end of file diff --git a/packages/bridge-server/src/users/users.controller.ts b/packages/bridge-server/src/users/users.controller.ts new file mode 100644 index 0000000..ed0a350 --- /dev/null +++ b/packages/bridge-server/src/users/users.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from 'src/authz/authz.guard'; +import { UsersService } from './users.service'; + +@Controller('users') +export class UsersController { + constructor( + private usersService: UsersService + ) {} + + @Get(':id') + findOne(@Param() params: any) { + //return this.seasonsService.findOne(params.id); + } + + @UseGuards(JwtAuthGuard) + @Post('login') + updateLastLogin(@Body() body: any) { + return this.usersService.updateLastLogin(body.id, body.nickname, body.picture, body.time); + } +} diff --git a/packages/bridge-server/src/users/users.model.ts b/packages/bridge-server/src/users/users.model.ts new file mode 100644 index 0000000..16dc9aa --- /dev/null +++ b/packages/bridge-server/src/users/users.model.ts @@ -0,0 +1,20 @@ +import { Column, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript"; + +@Table +export class User extends Model { + @PrimaryKey + @Column + auth0id: string; + + @Column + nickname: string; + + @Column + picture: string; + + @Column + realName: string; + + @Column + lastLogin: Date; +} \ No newline at end of file diff --git a/packages/bridge-server/src/users/users.module.ts b/packages/bridge-server/src/users/users.module.ts index 8fa904f..8e9efb6 100644 --- a/packages/bridge-server/src/users/users.module.ts +++ b/packages/bridge-server/src/users/users.module.ts @@ -1,8 +1,15 @@ import { Module } from '@nestjs/common'; +import { SequelizeModule } from '@nestjs/sequelize'; import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; +import { User } from './users.model'; @Module({ + imports: [ + SequelizeModule.forFeature([User]), + ], providers: [UsersService], - exports: [UsersService], + controllers: [UsersController], + exports: [SequelizeModule, UsersService], }) export class UsersModule {} diff --git a/packages/bridge-server/src/users/users.service.ts b/packages/bridge-server/src/users/users.service.ts index eb6ff83..76bd512 100644 --- a/packages/bridge-server/src/users/users.service.ts +++ b/packages/bridge-server/src/users/users.service.ts @@ -1,24 +1,25 @@ import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/sequelize'; +import { Sequelize } from 'sequelize-typescript'; +import { User } from './users.model'; // This should be a real class/interface representing a user entity -export type User = any; +//export type User = any; @Injectable() export class UsersService { - private readonly users = [ - { - userId: 1, - username: 'john', - password: 'changeme', - }, - { - userId: 2, - username: 'maria', - password: 'guess', - }, - ]; async findOne(username: string): Promise { - return this.users.find(user => user.username === username); + return undefined; + } + + constructor( + @InjectModel(User) private userModel: typeof User, + private sequelize: Sequelize + ) + {} + + updateLastLogin(id, nickname, picture, time) { + this.userModel.upsert({auth0id: id, nickname: nickname, picture: picture, lastLogin: time}); } } \ No newline at end of file diff --git a/packages/bridge-ui/src/app/app.config.ts b/packages/bridge-ui/src/app/app.config.ts index b42aa88..1011b5f 100644 --- a/packages/bridge-ui/src/app/app.config.ts +++ b/packages/bridge-ui/src/app/app.config.ts @@ -61,6 +61,17 @@ export const appConfig: ApplicationConfig = { audience: 'https://ponyta.pkmn.cloud' } } + }, + { + uriMatcher: (uri) => { + let is_login = uri.match('.+\/login'); + return is_login != null; + }, + tokenOptions: { + authorizationParams: { + audience: 'https://ponyta.pkmn.cloud' + } + } } ] } diff --git a/packages/bridge-ui/src/app/services/users.service.ts b/packages/bridge-ui/src/app/services/users.service.ts index 1d02d85..dca4343 100644 --- a/packages/bridge-ui/src/app/services/users.service.ts +++ b/packages/bridge-ui/src/app/services/users.service.ts @@ -1,7 +1,9 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AuthService, User } from '@auth0/auth0-angular'; import * as jwt_decode from 'jwt-decode'; +import { ServerEndpointService } from './server-endpoint.service'; @Injectable({ providedIn: 'root' @@ -14,6 +16,8 @@ export class UsersService { constructor( public authService: AuthService, + private httpClient: HttpClient, + private serverEndpointService: ServerEndpointService, ) { } private hasPermission(permission: string) { @@ -29,6 +33,8 @@ export class UsersService { this.authService.user$.subscribe(data => { this.user = data; + console.log(this.user); + this.reportLogin(); }) this.authService.getAccessTokenSilently().subscribe(data => { @@ -43,6 +49,15 @@ export class UsersService { }) } + reportLogin() { + if(!this.user) { return } + let id = this.user.sub; + let nickname = this.user.nickname; + let picture = this.user.picture; + let time = Date.now(); + this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"users/login", {id, nickname, picture, time}).subscribe(); + } + getUserName() :string { if(!this.isAuthenticated || !this.user) {return ""} @@ -64,4 +79,6 @@ export class UsersService { canCreateRaces(): boolean { return this.hasPermission("create:races") } + + }