Browse Source

Merge branch 'new_auth'

feature/mapper-integration
Quildra 2 years ago
parent
commit
6fffd5a69a
  1. 199
      package-lock.json
  2. 2
      packages/bridge-server/.env
  3. 21
      packages/bridge-server/.vscode/launch.json
  4. 36
      packages/bridge-server/migrations/20231220114130-create-user.js
  5. 27
      packages/bridge-server/models/user.js
  6. 5
      packages/bridge-server/package.json
  7. 2
      packages/bridge-server/src/app.module.ts
  8. 9
      packages/bridge-server/src/auth/auth.service.ts
  9. 3
      packages/bridge-server/src/authorization/roles.decorator.ts
  10. 26
      packages/bridge-server/src/authorization/roles.guard.ts
  11. 22
      packages/bridge-server/src/authz/authz.controller.ts
  12. 24
      packages/bridge-server/src/authz/authz.guard.ts
  13. 12
      packages/bridge-server/src/authz/authz.module.ts
  14. 33
      packages/bridge-server/src/authz/jwt.strategy.ts
  15. 11
      packages/bridge-server/src/seasons/seasons.controller.ts
  16. 21
      packages/bridge-server/src/users/users.controller.ts
  17. 20
      packages/bridge-server/src/users/users.model.ts
  18. 9
      packages/bridge-server/src/users/users.module.ts
  19. 29
      packages/bridge-server/src/users/users.service.ts
  20. 1
      packages/bridge-ui/package.json
  21. 60
      packages/bridge-ui/src/app/app.config.ts
  22. 2
      packages/bridge-ui/src/app/components/login-dialog/login-dialog.component.ts
  23. 12
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.html
  24. 25
      packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts
  25. 8
      packages/bridge-ui/src/app/pages/season-details/season-details.component.html
  26. 9
      packages/bridge-ui/src/app/pages/season-details/season-details.component.ts
  27. 2
      packages/bridge-ui/src/app/pages/seasons/seasons.component.html
  28. 7
      packages/bridge-ui/src/app/pages/seasons/seasons.component.ts
  29. 52
      packages/bridge-ui/src/app/services/auth.service.ts
  30. 2
      packages/bridge-ui/src/app/services/seasons.service.ts
  31. 8
      packages/bridge-ui/src/app/services/users.service.spec.ts
  32. 84
      packages/bridge-ui/src/app/services/users.service.ts

199
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",

2
packages/bridge-server/.env

@ -0,0 +1,2 @@
AUTH0_ISSUER_URL=https://dev-0pjms7kv5foqe0ex.uk.auth0.com/
AUTH0_AUDIENCE=https://ponyta.pkmn.cloud

21
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": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\src\\authz\\authz.guard.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

36
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');
}
};

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

5
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",

2
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],

9
packages/bridge-server/src/auth/auth.service.ts

@ -12,13 +12,6 @@ export class AuthService {
) {}
async signIn(username: string, pass: string): Promise<any> {
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),
};
}
}

3
packages/bridge-server/src/authorization/roles.decorator.ts

@ -0,0 +1,3 @@
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();

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

22
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";
}
}

24
packages/bridge-server/src/authz/authz.guard.ts

@ -0,0 +1,24 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

12
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 {}

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

11
packages/bridge-server/src/seasons/seasons.controller.ts

@ -1,7 +1,10 @@
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';
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,10 +28,10 @@ export class SeasonsController {
return this.seasonStandingsService.updateStandings(params.id);
}
@UseGuards(AuthGuard)
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Post('create')
@Roles(['create:seasons'])
create(@Body() body: any) {
return this.seasonsService.create(body.title, body.subTitle, body.startingDate);
}
}

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

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

9
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 {}

29
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<User | undefined> {
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});
}
}

1
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",

60
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,64 @@ 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: [
{
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: {
audience: 'https://ponyta.pkmn.cloud'
}
}
},
{
uriMatcher: (uri) => {
let is_login = uri.match('.+\/login');
return is_login != null;
},
tokenOptions: {
authorizationParams: {
audience: 'https://ponyta.pkmn.cloud'
}
}
}
]
}
}))
]
};

2
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();
}
})
*/
}
}

12
packages/bridge-ui/src/app/components/top-bar/top-bar.component.html

@ -12,14 +12,18 @@
[options] = "options"
(themeChange)="themeChangeHandler($event)">
</app-theme-menu>
@if(isAuthed() == false) {
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onLoginClick()">
@if(usersService.isAuthenticated == false) {
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onLoginClick()" matTooltip="Login">
<mat-icon>login</mat-icon>
</button>
}
@if(isAuthed()) {
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onProfile()">
@else {
{{usersService.getUserName()}}
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" matTooltip="Profile">
<mat-icon>person_pin</mat-icon>
</button>
<button mat-icon-button class="example-icon" aria-label="Example icon-button with share icon" (click)="onLogoutClick()" matTooltip="Logout">
<mat-icon>logout</mat-icon>
</button>
}
</mat-toolbar>

25
packages/bridge-ui/src/app/components/top-bar/top-bar.component.ts

@ -13,8 +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 { UsersService } from '../../services/users.service';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-top-bar',
@ -25,6 +26,7 @@ import { AuthService } from '../../services/auth.service';
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatTooltipModule,
ThemeMenuComponent
],
templateUrl: './top-bar.component.html',
@ -34,16 +36,19 @@ import { AuthService } from '../../services/auth.service';
export class TopBarComponent {
constructor(
private readonly themeService: ThemeService,
private authService: AuthService,
public authService: AuthService,
public usersService: UsersService,
public dialog: MatDialog
) {}
options: Array<ThemeOption> = [];
async ngOnInit() {
ngOnInit() {
this.themeService.getThemeOptions().subscribe(data => {
this.options = data;
});
this.usersService.refreshUserDetails();
}
themeChangeHandler(seletedTheme: string) {
@ -51,16 +56,10 @@ export class TopBarComponent {
}
onLoginClick() {
this.dialog.open(LoginDialogComponent);
}
isAuthed() {
return this.authService.isAuthenticated();
this.authService.loginWithRedirect();
}
onProfile() {
this.authService.testProfile().subscribe(data => {
console.log(data)
});
onLogoutClick() {
this.authService.logout();
}
}

8
packages/bridge-ui/src/app/pages/season-details/season-details.component.html

@ -3,9 +3,11 @@
<h1>{{season.title}}</h1>
<h3>{{season.subTitle}}</h3>
@if(isAuthed()) {
<button mat-raised-button color="warn" (click)="openNewRaceDialog(season.id)">New Race</button>
<button mat-raised-button color="warn" (click)="forceUpdate(season.id)">Update Season</button>
@if(usersService.isAuthenticated) {
@if(usersService.canCreateRaces()) {
<button mat-raised-button color="warn" (click)="openNewRaceDialog(season.id)">New Race</button>
}
<mat-divider></mat-divider>
<br/>
}

9
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,
{

2
packages/bridge-ui/src/app/pages/seasons/seasons.component.html

@ -1,5 +1,5 @@
<div class="season-grid-list docs-guide-list">
@if(isAuthed()){
@if(userService.canCreateSeasons()){
<app-season-card-new></app-season-card-new>
}
@for (season of seasons; track season) {

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

52
packages/bridge-ui/src/app/services/auth.service.ts

@ -1,52 +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';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(
private httpClient: HttpClient,
private serverEndpointService: ServerEndpointService,
) { }
login(username: string, password: string): Observable<any> {
const headers = new HttpHeaders();
headers.append('Content-Type', 'application/json');
return this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"auth/login", { username, password }, { headers })
}
testProfile(): Observable<any> {
console.log("SendTestProfile")
return this.httpClient.get(this.serverEndpointService.getCurrentEndpoint()+"auth/profile")
}
logout(): void {
// Clear the token from local storage
localStorage.removeItem('token');
}
isAuthenticated(): boolean {
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;
}
}
}

2
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) {

8
packages/bridge-ui/src/app/services/auth.service.spec.ts → 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', () => {

84
packages/bridge-ui/src/app/services/users.service.ts

@ -0,0 +1,84 @@
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'
})
export class UsersService {
isAuthenticated = false;
permissions: string[] = [];
user: User | null | undefined = null;
constructor(
public authService: AuthService,
private httpClient: HttpClient,
private serverEndpointService: ServerEndpointService,
) { }
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;
console.log(this.user);
this.reportLogin();
})
this.authService.getAccessTokenSilently().subscribe(data => {
try {
let decoded = jwt_decode.jwtDecode(data) as any;
this.permissions = decoded['permissions'];
}
catch (error) {
}
})
})
}
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 ""}
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")
}
}
Loading…
Cancel
Save