Compare commits

..

2 Commits

19 changed files with 681 additions and 148 deletions

189
package-lock.json generated
View File

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

View File

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

View File

@ -8,13 +8,15 @@ 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/User";
import "./protocols/GthubProtocol";
@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 +30,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",

View File

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

View File

@ -0,0 +1,27 @@
import { Controller, Get, Req, Res, Scope, ProviderScope, Post, BodyParams } from "@tsed/common";
import { Authenticate } from "@tsed/passport";
import { Response, Request } from "express";
import { Returns } from "@tsed/schema";
import { User } from "../../entities/user/User";
@Controller("/auth")
@Scope(ProviderScope.SINGLETON)
export class AuthController {
@Post("/github/login")
@Authenticate("github")
@Returns(200, User)
async githubLogin(@Req() req: Req, @BodyParams("serviceIdentifier") serviceIdentifier: string) {
req.query.state = serviceIdentifier;
return req.user;
}
@Get("/logout")
logout(@Req() req: Request, @Res() res: Response) {
req.logout((err) => {
if (err) {
return res.status(500).send("Error logging out");
}
res.redirect("/");
});
}
}

View File

@ -18,22 +18,28 @@ 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,
text: "some text",
iv: "some iv"
}) })
.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);
}); });
}); });

View File

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

View File

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

View File

@ -1,15 +1,14 @@
import { PathParams } from "@tsed/platform-params"; 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/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);
} }
} }

View File

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

View File

@ -1,9 +1,8 @@
import { registerProvider } from "@tsed/di"; import { registerProvider } from "@tsed/di";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { Logger } from "@tsed/logger"; import { Logger } from "@tsed/logger";
import { User } from "../entities/User"; import { User } from "../entities/user/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();
}
}
}

View File

@ -1,13 +1,23 @@
import { Property, Required, MaxLength } from "@tsed/schema"; import { Property, Required, MaxLength, Enum, Description } 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;
@Property()
@Required()
text: string;
@Property()
@Required()
@Description("The iv used to encrypt the text")
iv: string;
} }

View File

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

View File

@ -1,6 +1,6 @@
import { CollectionOf, MaxLength, Property, Required } from "@tsed/schema"; import { CollectionOf, MaxLength, Property, Required } from "@tsed/schema";
import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { Link } from "./link/Link"; import { Link } from "../link/Link";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@Entity() @Entity()
@ -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) {

View File

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

View File

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

View File

@ -0,0 +1,76 @@
// 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, StrategyOptionsWithRequest } from "passport-github";
@Protocol<StrategyOptionsWithRequest>({
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:8083/auth/github/callback",
scope: ["user:email"],
state: "true",
passReqToCallback: true
}
})
export class GithubProtocol implements OnVerify, OnInstall {
@Inject()
userService: UserService;
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
}
}

View File

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

View File

@ -0,0 +1,55 @@
import { Inject, Injectable } from "@tsed/di";
import { User } from "../entities/user/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();
}
}