WIP: Get tests passing for github oauth, almost fully implement it
This commit is contained in:
parent
b2a0cafe6e
commit
64edba3cf8
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tsed/ajv": "^7.83.0",
|
"@tsed/ajv": "^7.83.0",
|
||||||
"@tsed/common": "^7.83.0",
|
"@tsed/common": "^7.83.0",
|
||||||
|
"@tsed/components-scan": "^7.83.0",
|
||||||
"@tsed/core": "^7.83.0",
|
"@tsed/core": "^7.83.0",
|
||||||
"@tsed/di": "^7.83.0",
|
"@tsed/di": "^7.83.0",
|
||||||
"@tsed/engines": "^7.83.0",
|
"@tsed/engines": "^7.83.0",
|
||||||
|
@ -44,6 +45,8 @@
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"method-override": "^3.0.0",
|
"method-override": "^3.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
"passport-github": "^1.1.0",
|
||||||
|
"passport-oauth2": "^1.8.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
|
@ -62,6 +65,8 @@
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
|
"@types/passport-github": "^1.1.12",
|
||||||
|
"@types/passport-oauth2": "^1.4.17",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||||
|
@ -943,7 +948,6 @@
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
|
@ -957,7 +961,6 @@
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
|
@ -967,7 +970,6 @@
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
|
@ -1800,6 +1802,31 @@
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsed/components-scan": {
|
||||||
|
"version": "7.83.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsed/components-scan/-/components-scan-7.83.0.tgz",
|
||||||
|
"integrity": "sha512-VhUZ8PllelafXIaURWnY623SNTRyTjFBCOQvz5Sokw92Sj9bngE2rTKxrrAj+Rl0qXMrUMZIuEYX6zvhfkn7pA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tsed/normalize-path": "7.83.0",
|
||||||
|
"globby": "11.1.0",
|
||||||
|
"tslib": "2.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tsed/core": "7.83.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@tsed/core": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tsed/components-scan/node_modules/tslib": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/@tsed/core": {
|
"node_modules/@tsed/core": {
|
||||||
"version": "7.83.0",
|
"version": "7.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tsed/core/-/core-7.83.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tsed/core/-/core-7.83.0.tgz",
|
||||||
|
@ -2543,6 +2570,16 @@
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/oauth": {
|
||||||
|
"version": "0.9.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.5.tgz",
|
||||||
|
"integrity": "sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/passport": {
|
"node_modules/@types/passport": {
|
||||||
"version": "1.0.16",
|
"version": "1.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
|
||||||
|
@ -2553,6 +2590,30 @@
|
||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/passport-github": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-github/-/passport-github-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/passport": "*",
|
||||||
|
"@types/passport-oauth2": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/passport-oauth2": {
|
||||||
|
"version": "1.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz",
|
||||||
|
"integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/oauth": "*",
|
||||||
|
"@types/passport": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.15",
|
"version": "6.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||||
|
@ -3390,6 +3451,15 @@
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/array-union": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asap": {
|
"node_modules/asap": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
|
@ -3463,6 +3533,15 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64url": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
@ -4359,6 +4438,18 @@
|
||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dir-glob": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-type": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
@ -5185,7 +5276,6 @@
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
|
@ -5229,7 +5319,6 @@
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
|
@ -5631,7 +5720,6 @@
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
|
@ -5656,6 +5744,26 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/globby": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"array-union": "^2.1.0",
|
||||||
|
"dir-glob": "^3.0.1",
|
||||||
|
"fast-glob": "^3.2.9",
|
||||||
|
"ignore": "^5.2.0",
|
||||||
|
"merge2": "^1.4.1",
|
||||||
|
"slash": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||||
|
@ -5940,7 +6048,6 @@
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
|
@ -6076,7 +6183,6 @@
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -6095,7 +6201,6 @@
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
|
@ -6572,7 +6677,6 @@
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
|
@ -7067,6 +7171,12 @@
|
||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
@ -7305,6 +7415,38 @@
|
||||||
"url": "https://github.com/sponsors/jaredhanson"
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport-github": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-oauth2": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-oauth2": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64url": "3.x.x",
|
||||||
|
"oauth": "0.10.x",
|
||||||
|
"passport-strategy": "1.x.x",
|
||||||
|
"uid2": "0.0.x",
|
||||||
|
"utils-merge": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/passport-strategy": {
|
"node_modules/passport-strategy": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||||
|
@ -7391,6 +7533,15 @@
|
||||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-type": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
|
@ -7636,7 +7787,6 @@
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
@ -7795,7 +7945,6 @@
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
|
@ -7968,7 +8117,6 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
@ -8363,6 +8511,15 @@
|
||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/slash": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/smart-buffer": {
|
"node_modules/smart-buffer": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
@ -9572,6 +9729,12 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uid2": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tsed/ajv": "^7.83.0",
|
"@tsed/ajv": "^7.83.0",
|
||||||
"@tsed/common": "^7.83.0",
|
"@tsed/common": "^7.83.0",
|
||||||
|
"@tsed/components-scan": "^7.83.0",
|
||||||
"@tsed/core": "^7.83.0",
|
"@tsed/core": "^7.83.0",
|
||||||
"@tsed/di": "^7.83.0",
|
"@tsed/di": "^7.83.0",
|
||||||
"@tsed/engines": "^7.83.0",
|
"@tsed/engines": "^7.83.0",
|
||||||
|
@ -53,6 +54,8 @@
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"method-override": "^3.0.0",
|
"method-override": "^3.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
"passport-github": "^1.1.0",
|
||||||
|
"passport-oauth2": "^1.8.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
|
@ -71,6 +74,8 @@
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
|
"@types/passport-github": "^1.1.12",
|
||||||
|
"@types/passport-oauth2": "^1.4.17",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||||
|
|
|
@ -8,13 +8,14 @@ import "@tsed/passport";
|
||||||
import { config } from "./config/index";
|
import { config } from "./config/index";
|
||||||
import * as rest from "./controllers/rest/index";
|
import * as rest from "./controllers/rest/index";
|
||||||
import * as pages from "./controllers/pages/index";
|
import * as pages from "./controllers/pages/index";
|
||||||
|
import { User } from "./entities/User";
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
...config,
|
...config,
|
||||||
acceptMimes: ["application/json"],
|
acceptMimes: ["application/json"],
|
||||||
httpPort: process.env.PORT || 8083,
|
httpPort: process.env.PORT || 8083,
|
||||||
httpsPort: false, // CHANGE
|
httpsPort: false, // CHANGE
|
||||||
disableComponentsScan: true,
|
disableComponentsScan: false,
|
||||||
ajv: {
|
ajv: {
|
||||||
returnsCoercedValues: true
|
returnsCoercedValues: true
|
||||||
},
|
},
|
||||||
|
@ -28,6 +29,10 @@ import * as pages from "./controllers/pages/index";
|
||||||
specVersion: "3.0.1"
|
specVersion: "3.0.1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
componentsScan: [`./protocols/*.ts`, `./services/*.ts`],
|
||||||
|
passport: {
|
||||||
|
userInfoModel: User
|
||||||
|
},
|
||||||
middlewares: [
|
middlewares: [
|
||||||
"cors",
|
"cors",
|
||||||
"cookie-parser",
|
"cookie-parser",
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterAll, beforeAll, afterEach } from "vitest";
|
||||||
|
import { PlatformTest, Req } from "@tsed/common";
|
||||||
|
import { GithubProtocol } from "../../protocols/GthubProtocol";
|
||||||
|
import { UserService } from "../../services/UserService";
|
||||||
|
import { Server } from "../../Server";
|
||||||
|
|
||||||
|
describe("GithubProtocol", () => {
|
||||||
|
let protocol: GithubProtocol;
|
||||||
|
let userService: UserService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await PlatformTest.create({ platform: Server });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
return PlatformTest.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
userService = {
|
||||||
|
findOrCreate: vi.fn().mockResolvedValue({ id: "user123", username: "githubuser" })
|
||||||
|
} as unknown as UserService;
|
||||||
|
|
||||||
|
protocol = await PlatformTest.invoke<GithubProtocol>(GithubProtocol, [{ token: UserService, use: userService }]);
|
||||||
|
|
||||||
|
// Mock fetch for GitHub API call
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
json: vi.fn().mockResolvedValue([
|
||||||
|
{ value: "user@example.com", verified: true },
|
||||||
|
{ value: "user2@example.com", verified: false }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call $onVerify and return a user", async () => {
|
||||||
|
const mockReq = {
|
||||||
|
query: { state: "github-state" }
|
||||||
|
} as unknown as Req;
|
||||||
|
const mockAccessToken = "mock-access-token";
|
||||||
|
const mockProfile = { username: "githubuser" };
|
||||||
|
|
||||||
|
const result = await protocol.$onVerify(mockReq, mockAccessToken, "", mockProfile);
|
||||||
|
|
||||||
|
expect(userService.findOrCreate).toHaveBeenCalledWith({
|
||||||
|
service: "github",
|
||||||
|
serviceIdentifier: "github-state",
|
||||||
|
username: "githubuser",
|
||||||
|
emails: [{ value: "user@example.com", verified: true }],
|
||||||
|
accessToken: mockAccessToken
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ id: "user123", username: "githubuser" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if no verified emails are found", async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
json: vi.fn().mockResolvedValue([])
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockReq = { query: { state: "github-state" } } as unknown as Req;
|
||||||
|
const mockAccessToken = "mock-access-token";
|
||||||
|
const mockProfile = { username: "githubuser" };
|
||||||
|
|
||||||
|
await expect(protocol.$onVerify(mockReq, mockAccessToken, "", mockProfile)).rejects.toThrow("No verified email found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified emails from GitHub", async () => {
|
||||||
|
const emails = await protocol.fetchVerifiedEmails("mock-access-token");
|
||||||
|
|
||||||
|
expect(emails).toEqual([{ value: "user@example.com", verified: true }]);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith("https://api.github.com/user/emails", expect.anything());
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Controller, Get, Req, Res, Next, QueryParams } from "@tsed/common";
|
||||||
|
import { Authenticate } from "@tsed/passport";
|
||||||
|
import { Configuration } from "@tsed/common";
|
||||||
|
import { Response, Request, NextFunction } from "express";
|
||||||
|
|
||||||
|
@Controller("/auth")
|
||||||
|
export class AuthController {
|
||||||
|
@Configuration()
|
||||||
|
private config: Configuration;
|
||||||
|
|
||||||
|
@Get("/github")
|
||||||
|
async githubLogin(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Next() next: NextFunction,
|
||||||
|
@QueryParams("serviceIdentifier") serviceIdentifier: string
|
||||||
|
) {
|
||||||
|
if (!serviceIdentifier) {
|
||||||
|
res.status(400).send("serviceIdentifier is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate authentication with the 'state' parameter
|
||||||
|
return Authenticate("github", {
|
||||||
|
scope: ["user:email"],
|
||||||
|
state: serviceIdentifier
|
||||||
|
})(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/github/callback")
|
||||||
|
@Authenticate("github", { failureRedirect: "/login" })
|
||||||
|
async githubCallback(@Req() req: Request, @Res() res: Response) {
|
||||||
|
// Authentication was successful
|
||||||
|
// You can redirect the user to a specific page or return user info
|
||||||
|
|
||||||
|
// Example: Redirect to the home page
|
||||||
|
res.redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/logout")
|
||||||
|
logout(@Req() req: Request, @Res() res: Response) {
|
||||||
|
req.logout((err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).send("Error logging out");
|
||||||
|
}
|
||||||
|
res.redirect("/");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,22 +18,26 @@ describe("LinkController", () => {
|
||||||
it("should call POST /rest/links and GET /rest/links/:id", async () => {
|
it("should call POST /rest/links and GET /rest/links/:id", async () => {
|
||||||
const request = SuperTest(PlatformTest.callback());
|
const request = SuperTest(PlatformTest.callback());
|
||||||
const username = `silentsilas-${randomUUID()}`;
|
const username = `silentsilas-${randomUUID()}`;
|
||||||
const response = await request
|
const postResponse = await request
|
||||||
.post("/rest/links")
|
.post("/rest/links")
|
||||||
.send({
|
.send({
|
||||||
service: "github",
|
service: "github",
|
||||||
serviceUsername: username
|
serviceIdentifier: username
|
||||||
})
|
})
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
const response2 = await request.get(`/rest/users/${response.body.userId}`).expect(200);
|
const getOneResponse = await request.get(`/rest/users/${postResponse.body.userId}`).expect(200);
|
||||||
|
const getAllResponse = await request.get("/rest/users/").expect(200);
|
||||||
|
|
||||||
expect(response.body.id).toBeTruthy();
|
expect(postResponse.body.id).toBeTruthy();
|
||||||
expect(response.body.service).toEqual("github");
|
expect(postResponse.body.service).toEqual("github");
|
||||||
expect(response.body.serviceUsername).toEqual(username);
|
expect(postResponse.body.serviceIdentifier).toEqual(username);
|
||||||
|
|
||||||
expect(response2.body.id).toEqual(response.body.userId);
|
expect(getOneResponse.body.id).toEqual(postResponse.body.userId);
|
||||||
expect(response2.body.service).toEqual("github");
|
expect(getOneResponse.body.service).toEqual("github");
|
||||||
expect(response2.body.serviceUsername).toEqual(username);
|
expect(getOneResponse.body.serviceIdentifier).toEqual(username);
|
||||||
|
|
||||||
|
expect(getAllResponse.body).toBeInstanceOf(Array);
|
||||||
|
expect(getAllResponse.body.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,67 +1,53 @@
|
||||||
import { BodyParams, PathParams } from "@tsed/platform-params";
|
import { BodyParams, PathParams, Req, Controller } from "@tsed/common";
|
||||||
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||||
import { Controller, Inject } from "@tsed/di";
|
import { Authenticate } from "@tsed/passport";
|
||||||
import { Link } from "../../entities/link/Link";
|
import { Link } from "../../entities/link/Link";
|
||||||
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import { User } from "../../entities/User";
|
|
||||||
import { CreateLinkDto } from "../../entities/link/CreateLinkDTO";
|
import { CreateLinkDto } from "../../entities/link/CreateLinkDTO";
|
||||||
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
import { UserService } from "../../services/UserService";
|
||||||
|
import { LinkService } from "../../services/LinkService"; // Create a new service for Link operations
|
||||||
|
import { User } from "../../entities/User";
|
||||||
|
|
||||||
@Controller("/links")
|
@Controller("/links")
|
||||||
export class LinkController {
|
export class LinkController {
|
||||||
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
constructor(
|
||||||
|
private linkService: LinkService, // Inject LinkService
|
||||||
|
private userService: UserService // Inject UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@Summary("Create a new link")
|
@Summary("Create a new link")
|
||||||
@Description("Creates a new link and associates it with a user")
|
@Description("Creates a new link and associates it with a user")
|
||||||
@Returns(201, Link)
|
@Returns(201, Link)
|
||||||
async create(@BodyParams() linkData: CreateLinkDto): Promise<Link> {
|
async create(@BodyParams() linkData: CreateLinkDto): Promise<Link> {
|
||||||
return executeWithRetry(async (queryRunner) => {
|
// Delegate user creation logic to UserService
|
||||||
const userRepository = queryRunner.manager.getRepository(User);
|
const user = await this.userService.findOrCreate(linkData);
|
||||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
|
||||||
|
|
||||||
let user = await userRepository.findOne({
|
// Use LinkService to handle the link creation
|
||||||
where: {
|
return this.linkService.createLink(linkData, user);
|
||||||
service: linkData.service,
|
|
||||||
serviceUsername: linkData.serviceUsername
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = userRepository.create({
|
|
||||||
service: linkData.service,
|
|
||||||
serviceUsername: linkData.serviceUsername
|
|
||||||
});
|
|
||||||
user = await queryRunner.manager.save(User, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
const link = linkRepository.create({
|
|
||||||
...linkData,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
|
|
||||||
return queryRunner.manager.save(Link, link);
|
|
||||||
}, this.sqliteDataSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@Summary("Get all links")
|
@Authenticate("github")
|
||||||
|
@Summary("Get all links for the authenticated user")
|
||||||
@(Returns(200, Array).Of(Link))
|
@(Returns(200, Array).Of(Link))
|
||||||
async getList(): Promise<Link[]> {
|
async getList(@Req() req: Req): Promise<Link[]> {
|
||||||
return executeWithRetry(async (queryRunner) => {
|
const user = req.user as User;
|
||||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
return this.linkService.getLinksForUser(user);
|
||||||
return linkRepository.find({ relations: ["user"] });
|
|
||||||
}, this.sqliteDataSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/:id")
|
@Get("/:id")
|
||||||
@Summary("Get a link by ID")
|
@Summary("Get a link by ID without text content")
|
||||||
@Returns(200, Link)
|
@Returns(200, Link)
|
||||||
async getOne(@PathParams("id") id: string): Promise<Link | null> {
|
async getOne(@PathParams("id") id: string): Promise<Link | null> {
|
||||||
return executeWithRetry(async (queryRunner) => {
|
return this.linkService.getLinkById(id);
|
||||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
}
|
||||||
return linkRepository.findOne({ where: { id }, relations: ["user"] });
|
|
||||||
}, this.sqliteDataSource);
|
@Get("/:id/content")
|
||||||
|
@Authenticate("github")
|
||||||
|
@Summary("Get the content of a link if authorized")
|
||||||
|
@Returns(200, String)
|
||||||
|
async getLinkContent(@PathParams("id") id: string, @Req() req: Req): Promise<string> {
|
||||||
|
const user = req.user as User;
|
||||||
|
return this.linkService.getLinkContentById(id, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import { expect, describe, it, beforeEach, afterEach, beforeAll } from "vitest";
|
import { expect, describe, it, afterEach, beforeAll, beforeEach } from "vitest";
|
||||||
import { PlatformTest } from "@tsed/common";
|
import { PlatformTest } from "@tsed/common";
|
||||||
import { UserController } from "./UserController";
|
import { UserController } from "./UserController";
|
||||||
import { User } from "../../entities/User";
|
import { User } from "../../entities/User";
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { Server } from "../../Server";
|
import { Server } from "../../Server";
|
||||||
import { sqliteDatasource } from "src/datasources/SqliteDatasource";
|
import { SqliteDatasource, sqliteDatasource } from "src/datasources/SqliteDatasource";
|
||||||
|
|
||||||
describe("UserController", () => {
|
describe("UserController", () => {
|
||||||
let controller: UserController;
|
let controller: UserController;
|
||||||
|
|
||||||
beforeAll(PlatformTest.bootstrap(Server));
|
beforeAll(
|
||||||
|
PlatformTest.bootstrap(Server, {
|
||||||
|
imports: [sqliteDatasource]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
controller = await PlatformTest.invoke(UserController, [
|
controller = await PlatformTest.invoke(UserController, [
|
||||||
{
|
{
|
||||||
token: DataSource,
|
token: SqliteDatasource,
|
||||||
use: sqliteDatasource
|
use: sqliteDatasource
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -30,7 +33,8 @@ describe("UserController", () => {
|
||||||
const user = new User();
|
const user = new User();
|
||||||
user.id = userId;
|
user.id = userId;
|
||||||
user.service = "github";
|
user.service = "github";
|
||||||
user.serviceUsername = `silentsilas-${userId}`;
|
user.serviceIdentifier = `user-${userId}`;
|
||||||
|
user.links = [];
|
||||||
const repo = sqliteDatasource.getRepository(User);
|
const repo = sqliteDatasource.getRepository(User);
|
||||||
await repo.save(user);
|
await repo.save(user);
|
||||||
|
|
||||||
|
@ -39,6 +43,6 @@ describe("UserController", () => {
|
||||||
expect(result).toEqual(user);
|
expect(result).toEqual(user);
|
||||||
expect(result?.id).toEqual(userId);
|
expect(result?.id).toEqual(userId);
|
||||||
expect(result?.service).toEqual("github");
|
expect(result?.service).toEqual("github");
|
||||||
expect(result?.serviceUsername).toEqual(`silentsilas-${userId}`);
|
expect(result?.serviceIdentifier).toEqual(`user-${userId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,14 +2,13 @@ import { PathParams } from "@tsed/platform-params";
|
||||||
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||||
import { Controller, Inject } from "@tsed/di";
|
import { Controller, Inject } from "@tsed/di";
|
||||||
import { User } from "../../entities/User";
|
import { User } from "../../entities/User";
|
||||||
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
|
||||||
import { Forbidden } from "@tsed/exceptions";
|
import { Forbidden } from "@tsed/exceptions";
|
||||||
|
import { UserService } from "../../services/UserService";
|
||||||
|
|
||||||
@Controller("/users")
|
@Controller("/users")
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
@Inject()
|
||||||
|
service: UserService;
|
||||||
|
|
||||||
// disable the create method and endpoint
|
// disable the create method and endpoint
|
||||||
@Post("/")
|
@Post("/")
|
||||||
|
@ -24,21 +23,13 @@ export class UserController {
|
||||||
@Summary("Get all users")
|
@Summary("Get all users")
|
||||||
@(Returns(200, Array).Of(User))
|
@(Returns(200, Array).Of(User))
|
||||||
async getList(): Promise<User[]> {
|
async getList(): Promise<User[]> {
|
||||||
return executeWithRetry(async (queryRunner) => {
|
return this.service.getAllUsers();
|
||||||
return queryRunner.manager.find(User);
|
|
||||||
}, this.sqliteDataSource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("/:id")
|
@Get("/:id")
|
||||||
@Summary("Get a user by ID")
|
@Summary("Get a user by ID")
|
||||||
@Returns(200, User)
|
@Returns(200, User)
|
||||||
async getOne(@PathParams("id") id: string): Promise<User | null> {
|
async getOne(@PathParams("id") id: string): Promise<User | null> {
|
||||||
return executeWithRetry(async (queryRunner) => {
|
return this.service.getUserById(id);
|
||||||
return queryRunner.manager.findOne(User, {
|
|
||||||
where: {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, this.sqliteDataSource);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
* @file Automatically generated by barrelsby.
|
* @file Automatically generated by barrelsby.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./AuthController";
|
||||||
export * from "./LinkController";
|
export * from "./LinkController";
|
||||||
export * from "./UserController";
|
export * from "./UserController";
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { DataSource } from "typeorm";
|
||||||
import { Logger } from "@tsed/logger";
|
import { Logger } from "@tsed/logger";
|
||||||
import { User } from "../entities/User";
|
import { User } from "../entities/User";
|
||||||
import { Link } from "../entities/link/Link";
|
import { Link } from "../entities/link/Link";
|
||||||
import { QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export const SqliteDatasource = Symbol.for("SqliteDatasource");
|
export const SqliteDatasource = Symbol.for("SqliteDatasource");
|
||||||
export type SqliteDatasource = DataSource;
|
export type SqliteDatasource = DataSource;
|
||||||
|
@ -37,48 +36,17 @@ registerProvider<DataSource>({
|
||||||
type: "typeorm:datasource",
|
type: "typeorm:datasource",
|
||||||
deps: [Logger],
|
deps: [Logger],
|
||||||
async useAsyncFactory(logger: Logger) {
|
async useAsyncFactory(logger: Logger) {
|
||||||
await sqliteDatasource.initialize();
|
if (!sqliteDatasource.isInitialized) {
|
||||||
|
await sqliteDatasource.initialize();
|
||||||
logger.info("Connected with typeorm to database: Sqlite");
|
logger.info("Connected with TypeORM to database: Sqlite");
|
||||||
|
}
|
||||||
return sqliteDatasource;
|
return sqliteDatasource;
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
$onDestroy(dataSource) {
|
async $onDestroy(dataSource: DataSource) {
|
||||||
return dataSource.isInitialized && dataSource.destroy();
|
if (dataSource.isInitialized) {
|
||||||
|
await dataSource.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function executeWithRetry<T>(
|
|
||||||
operation: (queryRunner: QueryRunner) => Promise<T>,
|
|
||||||
dataSource: DataSource,
|
|
||||||
maxRetries = 10,
|
|
||||||
delay = 1000
|
|
||||||
): Promise<T> {
|
|
||||||
let retries = 0;
|
|
||||||
while (true) {
|
|
||||||
const queryRunner = dataSource.createQueryRunner();
|
|
||||||
try {
|
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
|
|
||||||
const result = await operation(queryRunner);
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
|
|
||||||
if (error.code === "SQLITE_BUSY" && retries < maxRetries) {
|
|
||||||
retries++;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,12 +17,21 @@ export class User {
|
||||||
@Column({ length: 100 })
|
@Column({ length: 100 })
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
@Required()
|
@Required()
|
||||||
serviceUsername: string;
|
serviceIdentifier: string;
|
||||||
|
|
||||||
@OneToMany(() => Link, (link) => link.user)
|
@OneToMany(() => Link, (link) => link.user)
|
||||||
@CollectionOf(() => Link)
|
@CollectionOf(() => Link)
|
||||||
links: Link[];
|
links: Link[];
|
||||||
|
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column("simple-json", { nullable: true })
|
||||||
|
emails: string[];
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
accessToken: string;
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
generateId() {
|
generateId() {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Property, Required, MaxLength } from "@tsed/schema";
|
import { Property, Required, MaxLength, Enum } from "@tsed/schema";
|
||||||
|
|
||||||
export class CreateLinkDto {
|
export class CreateLinkDto {
|
||||||
@Property()
|
@Property()
|
||||||
@Required()
|
@Required()
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
|
@Enum("github")
|
||||||
service: string;
|
service: string;
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
@Required()
|
@Required()
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
serviceUsername: string;
|
serviceIdentifier: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { MaxLength, Property, Required } from "@tsed/schema";
|
import { Enum, MaxLength, Property, Required } from "@tsed/schema";
|
||||||
import { Column, Entity, ManyToOne, PrimaryColumn, JoinColumn, BeforeInsert } from "typeorm";
|
import { Column, Entity, ManyToOne, PrimaryColumn, JoinColumn, BeforeInsert } from "typeorm";
|
||||||
import { User } from "../User";
|
import { User } from "../User";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export type Service = "github";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Link {
|
export class Link {
|
||||||
@PrimaryColumn("uuid")
|
@PrimaryColumn("uuid")
|
||||||
|
@ -12,12 +14,17 @@ export class Link {
|
||||||
@Column({ length: 100 })
|
@Column({ length: 100 })
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
@Required()
|
@Required()
|
||||||
|
@Enum("github")
|
||||||
service: string;
|
service: string;
|
||||||
|
|
||||||
@Column({ length: 100 })
|
@Column({ length: 100 })
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
@Required()
|
@Required()
|
||||||
serviceUsername: string;
|
serviceIdentifier: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@Required()
|
||||||
|
text: string;
|
||||||
|
|
||||||
@ManyToOne(() => User, (user) => user.links, { onDelete: "SET NULL", onUpdate: "CASCADE" })
|
@ManyToOne(() => User, (user) => user.links, { onDelete: "SET NULL", onUpdate: "CASCADE" })
|
||||||
@JoinColumn({ name: "userId" })
|
@JoinColumn({ name: "userId" })
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest";
|
||||||
import { Server } from "./Server";
|
import { Server } from "./Server";
|
||||||
|
|
||||||
vi.mock("@tsed/common", () => ({
|
vi.mock("@tsed/common", async (importOriginal) => {
|
||||||
$log: {
|
const actual: any = await importOriginal();
|
||||||
error: vi.fn()
|
return {
|
||||||
},
|
...actual,
|
||||||
PlatformApplication: vi.fn()
|
$log: {
|
||||||
}));
|
error: vi.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("@tsed/platform-express", () => ({
|
vi.mock("@tsed/platform-express", async (importOriginal) => {
|
||||||
PlatformExpress: {
|
const actual: any = await importOriginal();
|
||||||
bootstrap: vi.fn()
|
return {
|
||||||
}
|
...actual,
|
||||||
}));
|
PlatformExpress: {
|
||||||
|
bootstrap: vi.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("bootstrap function", () => {
|
describe("bootstrap function", () => {
|
||||||
let bootstrap: () => Promise<void>;
|
let bootstrap: () => Promise<void>;
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterAll, beforeAll } from "vitest";
|
||||||
|
import { PlatformTest, Req } from "@tsed/common";
|
||||||
|
import { UserService } from "../services/UserService";
|
||||||
|
import { Server } from "../Server";
|
||||||
|
import { sqliteDatasource } from "../datasources/SqliteDatasource";
|
||||||
|
import { GithubProtocol } from "./GthubProtocol";
|
||||||
|
|
||||||
|
describe("GithubProtocol", () => {
|
||||||
|
let protocol: GithubProtocol;
|
||||||
|
let userService: UserService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await PlatformTest.create({ platform: Server, imports: [sqliteDatasource] }); // ensure PlatformTest.create() is called
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
return PlatformTest.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
userService = {
|
||||||
|
findOrCreate: vi.fn().mockResolvedValue({ id: "user123", username: "githubuser" })
|
||||||
|
} as unknown as UserService;
|
||||||
|
|
||||||
|
protocol = await PlatformTest.invoke<GithubProtocol>(GithubProtocol, [{ token: UserService, use: userService }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call $onVerify and return a user", async () => {
|
||||||
|
const mockReq = {
|
||||||
|
query: { state: "github-state" }
|
||||||
|
} as unknown as Req;
|
||||||
|
const mockAccessToken = "mock-access-token";
|
||||||
|
const mockProfile = { username: "githubuser" };
|
||||||
|
|
||||||
|
const fetchSpy = vi.spyOn(protocol, "fetchVerifiedEmails").mockResolvedValue([{ value: "user@example.com", verified: true }]);
|
||||||
|
|
||||||
|
const result = await protocol.$onVerify(mockReq, mockAccessToken, "", mockProfile);
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(mockAccessToken);
|
||||||
|
expect(userService.findOrCreate).toHaveBeenCalledWith({
|
||||||
|
service: "github",
|
||||||
|
serviceIdentifier: "github-state",
|
||||||
|
username: "githubuser",
|
||||||
|
emails: [{ value: "user@example.com", verified: true }],
|
||||||
|
accessToken: mockAccessToken
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ id: "user123", username: "githubuser" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if no verified emails are found", async () => {
|
||||||
|
const mockReq = { query: { state: "github-state" } } as unknown as Req;
|
||||||
|
const mockAccessToken = "mock-access-token";
|
||||||
|
const mockProfile = { username: "githubuser" };
|
||||||
|
|
||||||
|
vi.spyOn(protocol, "fetchVerifiedEmails").mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(protocol.$onVerify(mockReq, mockAccessToken, "", mockProfile)).rejects.toThrow("No verified email found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified emails from GitHub", async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
json: vi.fn().mockResolvedValue([
|
||||||
|
{ value: "email1@example.com", verified: true },
|
||||||
|
{ value: "email2@example.com", verified: false }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
const emails = await protocol.fetchVerifiedEmails("mock-access-token");
|
||||||
|
|
||||||
|
expect(emails).toEqual([{ value: "email1@example.com", verified: true }]);
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith("https://api.github.com/user/emails", expect.anything());
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,80 @@
|
||||||
|
// protocols/GithubProtocol.ts
|
||||||
|
import { Protocol, OnVerify, OnInstall } from "@tsed/passport";
|
||||||
|
import { Req } from "@tsed/common";
|
||||||
|
import { Inject } from "@tsed/di";
|
||||||
|
import { UserService } from "../services/UserService";
|
||||||
|
import { Strategy as GithubStrategy } from "passport-github";
|
||||||
|
import { SqliteDatasource } from "../datasources/SqliteDatasource";
|
||||||
|
|
||||||
|
@Protocol({
|
||||||
|
name: "github",
|
||||||
|
useStrategy: GithubStrategy,
|
||||||
|
settings: {
|
||||||
|
clientID: process.env.GITHUB_CLIENT_ID || "your-client-id",
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET || "your-client-secret",
|
||||||
|
callbackURL: "http://localhost:8080/auth/github/callback",
|
||||||
|
scope: ["user:email"],
|
||||||
|
state: true,
|
||||||
|
passReqToCallback: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class GithubProtocol implements OnVerify, OnInstall {
|
||||||
|
@Inject()
|
||||||
|
userService: UserService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
sqliteDatasource: SqliteDatasource;
|
||||||
|
|
||||||
|
async $onVerify(@Req() req: Req, accessToken: string, _refreshToken: string, profile: any) {
|
||||||
|
const emails = await this.fetchVerifiedEmails(accessToken);
|
||||||
|
|
||||||
|
if (!emails.length) {
|
||||||
|
throw new Error("No verified email found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = req.query.state;
|
||||||
|
let identifier: string;
|
||||||
|
|
||||||
|
if (typeof state === "string") {
|
||||||
|
identifier = state;
|
||||||
|
} else if (Array.isArray(state)) {
|
||||||
|
// If state is an array, take the first string element
|
||||||
|
identifier = state.find((s) => typeof s === "string") as string;
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error("Invalid service identifier");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Service identifier is missing or invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userService.findOrCreate({
|
||||||
|
service: "github",
|
||||||
|
serviceIdentifier: identifier.toString(),
|
||||||
|
username: profile.username,
|
||||||
|
emails: emails,
|
||||||
|
accessToken: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchVerifiedEmails(accessToken: string): Promise<any[]> {
|
||||||
|
const response = await fetch("https://api.github.com/user/emails", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${accessToken}`,
|
||||||
|
"User-Agent": "YourAppName"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emails = await response.json();
|
||||||
|
return emails.filter((email: any) => email.verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInstall(strategy: GithubStrategy) {
|
||||||
|
console.log("Github strategy installed");
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.log(strategy);
|
||||||
|
}
|
||||||
|
// Optional: additional strategy configuration
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Inject, Injectable } from "@tsed/di";
|
||||||
|
import { Forbidden, NotFound } from "@tsed/exceptions";
|
||||||
|
import { SqliteDatasource } from "../datasources/SqliteDatasource";
|
||||||
|
import { User } from "../entities/User";
|
||||||
|
import { CreateLinkDto } from "../entities/link/CreateLinkDTO";
|
||||||
|
import { Link } from "../entities/link/Link";
|
||||||
|
import { DataSource, Repository } from "typeorm";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LinkService {
|
||||||
|
private linkRepository: Repository<Link>;
|
||||||
|
|
||||||
|
constructor(@Inject(SqliteDatasource) private dataSource: DataSource) {
|
||||||
|
this.linkRepository = this.dataSource.getRepository(Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLink(linkData: CreateLinkDto, user: User): Promise<Link> {
|
||||||
|
const link = this.linkRepository.create({
|
||||||
|
...linkData,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
return this.linkRepository.save(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLinksForUser(user: User): Promise<Link[]> {
|
||||||
|
return this.linkRepository.find({
|
||||||
|
where: { user: { id: user.id } },
|
||||||
|
relations: ["user"],
|
||||||
|
select: ["id", "service", "serviceIdentifier", "text"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLinkById(id: string): Promise<Link | null> {
|
||||||
|
const link = await this.linkRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ["user"],
|
||||||
|
select: ["id", "service", "serviceIdentifier"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
throw new NotFound("Link not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLinkContentById(id: string, user: User): Promise<string> {
|
||||||
|
const link = await this.linkRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ["user"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
throw new NotFound("Link not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.user.serviceIdentifier !== user.serviceIdentifier) {
|
||||||
|
throw new Forbidden("You are not authorized to view this link content");
|
||||||
|
}
|
||||||
|
|
||||||
|
return link.text;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Inject, Injectable } from "@tsed/di";
|
||||||
|
import { User } from "../entities/User";
|
||||||
|
import { SqliteDatasource } from "../datasources/SqliteDatasource";
|
||||||
|
import { DataSource, Repository } from "typeorm";
|
||||||
|
|
||||||
|
type ProfileData = {
|
||||||
|
service: string;
|
||||||
|
serviceIdentifier: string;
|
||||||
|
username?: string;
|
||||||
|
emails?: string[];
|
||||||
|
accessToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
deps: [SqliteDatasource]
|
||||||
|
})
|
||||||
|
export class UserService {
|
||||||
|
private userRepository: Repository<User>;
|
||||||
|
|
||||||
|
constructor(@Inject(SqliteDatasource) private dataSource: DataSource) {
|
||||||
|
this.userRepository = this.dataSource.getRepository(User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findOrCreate(profileData: ProfileData): Promise<User> {
|
||||||
|
let user = await this.userRepository.findOne({
|
||||||
|
where: { serviceIdentifier: profileData.serviceIdentifier }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = this.userRepository.create({
|
||||||
|
service: profileData.service,
|
||||||
|
serviceIdentifier: profileData.serviceIdentifier,
|
||||||
|
username: profileData.username,
|
||||||
|
emails: profileData.emails,
|
||||||
|
accessToken: profileData.accessToken
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user.emails = profileData.emails || user.emails;
|
||||||
|
user.accessToken = profileData.accessToken || user.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserById(id: string): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ["links"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllUsers(): Promise<User[]> {
|
||||||
|
return this.userRepository.find();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue