From 64edba3cf8100a25f6586f0bbef8888544d112a9 Mon Sep 17 00:00:00 2001 From: Silas Date: Sat, 14 Sep 2024 00:36:37 -0400 Subject: [PATCH] WIP: Get tests passing for github oauth, almost fully implement it --- package-lock.json | 189 ++++++++++++++++-- package.json | 5 + src/Server.ts | 7 +- .../rest/AuthController.integration.spec.ts | 76 +++++++ src/controllers/rest/AuthController.ts | 49 +++++ .../rest/LinkController.integration.spec.ts | 22 +- src/controllers/rest/LinkController.ts | 72 +++---- .../rest/UserController.integration.spec.ts | 18 +- src/controllers/rest/UserController.ts | 19 +- src/controllers/rest/index.ts | 1 + src/datasources/SqliteDatasource.ts | 48 +---- src/entities/User.ts | 11 +- src/entities/link/CreateLinkDTO.ts | 5 +- src/entities/link/Link.ts | 11 +- src/index.spec.ts | 29 ++- src/protocols/GithubProtocol.spec.ts | 73 +++++++ src/protocols/GthubProtocol.ts | 80 ++++++++ src/services/LinkService.ts | 63 ++++++ src/services/UserService.ts | 55 +++++ 19 files changed, 690 insertions(+), 143 deletions(-) create mode 100644 src/controllers/rest/AuthController.integration.spec.ts create mode 100644 src/controllers/rest/AuthController.ts create mode 100644 src/protocols/GithubProtocol.spec.ts create mode 100644 src/protocols/GthubProtocol.ts create mode 100644 src/services/LinkService.ts create mode 100644 src/services/UserService.ts diff --git a/package-lock.json b/package-lock.json index a6488ff..ea999a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tsed/ajv": "^7.83.0", "@tsed/common": "^7.83.0", + "@tsed/components-scan": "^7.83.0", "@tsed/core": "^7.83.0", "@tsed/di": "^7.83.0", "@tsed/engines": "^7.83.0", @@ -44,6 +45,8 @@ "express-session": "^1.18.0", "method-override": "^3.0.0", "passport": "^0.7.0", + "passport-github": "^1.1.0", + "passport-oauth2": "^1.8.0", "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", "uuid": "^10.0.0" @@ -62,6 +65,8 @@ "@types/multer": "^1.4.12", "@types/node": "^22.5.4", "@types/passport": "^1.0.16", + "@types/passport-github": "^1.1.12", + "@types/passport-oauth2": "^1.4.17", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.5.0", @@ -943,7 +948,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -957,7 +961,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -967,7 +970,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1800,6 +1802,31 @@ "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": { "version": "7.83.0", "resolved": "https://registry.npmjs.org/@tsed/core/-/core-7.83.0.tgz", @@ -2543,6 +2570,16 @@ "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": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", @@ -2553,6 +2590,30 @@ "@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": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -3390,6 +3451,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "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": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3463,6 +3533,15 @@ ], "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4359,6 +4438,18 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5185,7 +5276,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5229,7 +5319,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5631,7 +5720,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5656,6 +5744,26 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5940,7 +6048,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -6076,7 +6183,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6095,7 +6201,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6572,7 +6677,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -7067,6 +7171,12 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7305,6 +7415,38 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -7391,6 +7533,15 @@ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -7636,7 +7787,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -7795,7 +7945,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -7968,7 +8117,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -8363,6 +8511,15 @@ "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": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -9572,6 +9729,12 @@ "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": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 5ca8963..3328a99 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@tsed/ajv": "^7.83.0", "@tsed/common": "^7.83.0", + "@tsed/components-scan": "^7.83.0", "@tsed/core": "^7.83.0", "@tsed/di": "^7.83.0", "@tsed/engines": "^7.83.0", @@ -53,6 +54,8 @@ "express-session": "^1.18.0", "method-override": "^3.0.0", "passport": "^0.7.0", + "passport-github": "^1.1.0", + "passport-oauth2": "^1.8.0", "reflect-metadata": "^0.2.2", "sqlite3": "^5.1.7", "uuid": "^10.0.0" @@ -71,6 +74,8 @@ "@types/multer": "^1.4.12", "@types/node": "^22.5.4", "@types/passport": "^1.0.16", + "@types/passport-github": "^1.1.12", + "@types/passport-oauth2": "^1.4.17", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.5.0", diff --git a/src/Server.ts b/src/Server.ts index 867f266..64dbfcd 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -8,13 +8,14 @@ import "@tsed/passport"; import { config } from "./config/index"; import * as rest from "./controllers/rest/index"; import * as pages from "./controllers/pages/index"; +import { User } from "./entities/User"; @Configuration({ ...config, acceptMimes: ["application/json"], httpPort: process.env.PORT || 8083, httpsPort: false, // CHANGE - disableComponentsScan: true, + disableComponentsScan: false, ajv: { returnsCoercedValues: true }, @@ -28,6 +29,10 @@ import * as pages from "./controllers/pages/index"; specVersion: "3.0.1" } ], + componentsScan: [`./protocols/*.ts`, `./services/*.ts`], + passport: { + userInfoModel: User + }, middlewares: [ "cors", "cookie-parser", diff --git a/src/controllers/rest/AuthController.integration.spec.ts b/src/controllers/rest/AuthController.integration.spec.ts new file mode 100644 index 0000000..4bc0f42 --- /dev/null +++ b/src/controllers/rest/AuthController.integration.spec.ts @@ -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, [{ 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()); + }); +}); diff --git a/src/controllers/rest/AuthController.ts b/src/controllers/rest/AuthController.ts new file mode 100644 index 0000000..aa1e80b --- /dev/null +++ b/src/controllers/rest/AuthController.ts @@ -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("/"); + }); + } +} diff --git a/src/controllers/rest/LinkController.integration.spec.ts b/src/controllers/rest/LinkController.integration.spec.ts index 9386d2f..f117a40 100644 --- a/src/controllers/rest/LinkController.integration.spec.ts +++ b/src/controllers/rest/LinkController.integration.spec.ts @@ -18,22 +18,26 @@ describe("LinkController", () => { it("should call POST /rest/links and GET /rest/links/:id", async () => { const request = SuperTest(PlatformTest.callback()); const username = `silentsilas-${randomUUID()}`; - const response = await request + const postResponse = await request .post("/rest/links") .send({ service: "github", - serviceUsername: username + serviceIdentifier: username }) .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(response.body.service).toEqual("github"); - expect(response.body.serviceUsername).toEqual(username); + expect(postResponse.body.id).toBeTruthy(); + expect(postResponse.body.service).toEqual("github"); + expect(postResponse.body.serviceIdentifier).toEqual(username); - expect(response2.body.id).toEqual(response.body.userId); - expect(response2.body.service).toEqual("github"); - expect(response2.body.serviceUsername).toEqual(username); + expect(getOneResponse.body.id).toEqual(postResponse.body.userId); + expect(getOneResponse.body.service).toEqual("github"); + expect(getOneResponse.body.serviceIdentifier).toEqual(username); + + expect(getAllResponse.body).toBeInstanceOf(Array); + expect(getAllResponse.body.length).toBeGreaterThan(0); }); }); diff --git a/src/controllers/rest/LinkController.ts b/src/controllers/rest/LinkController.ts index b55c736..9a07348 100644 --- a/src/controllers/rest/LinkController.ts +++ b/src/controllers/rest/LinkController.ts @@ -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 { Controller, Inject } from "@tsed/di"; +import { Authenticate } from "@tsed/passport"; 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 { 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") export class LinkController { - constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {} + constructor( + private linkService: LinkService, // Inject LinkService + private userService: UserService // Inject UserService + ) {} @Post("/") @Summary("Create a new link") @Description("Creates a new link and associates it with a user") @Returns(201, Link) async create(@BodyParams() linkData: CreateLinkDto): Promise { - return executeWithRetry(async (queryRunner) => { - const userRepository = queryRunner.manager.getRepository(User); - const linkRepository = queryRunner.manager.getRepository(Link); + // Delegate user creation logic to UserService + const user = await this.userService.findOrCreate(linkData); - let user = await userRepository.findOne({ - where: { - 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); + // Use LinkService to handle the link creation + return this.linkService.createLink(linkData, user); } @Get("/") - @Summary("Get all links") + @Authenticate("github") + @Summary("Get all links for the authenticated user") @(Returns(200, Array).Of(Link)) - async getList(): Promise { - return executeWithRetry(async (queryRunner) => { - const linkRepository = queryRunner.manager.getRepository(Link); - return linkRepository.find({ relations: ["user"] }); - }, this.sqliteDataSource); + async getList(@Req() req: Req): Promise { + const user = req.user as User; + return this.linkService.getLinksForUser(user); } @Get("/:id") - @Summary("Get a link by ID") + @Summary("Get a link by ID without text content") @Returns(200, Link) async getOne(@PathParams("id") id: string): Promise { - return executeWithRetry(async (queryRunner) => { - const linkRepository = queryRunner.manager.getRepository(Link); - return linkRepository.findOne({ where: { id }, relations: ["user"] }); - }, this.sqliteDataSource); + return this.linkService.getLinkById(id); + } + + @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 { + const user = req.user as User; + return this.linkService.getLinkContentById(id, user); } } diff --git a/src/controllers/rest/UserController.integration.spec.ts b/src/controllers/rest/UserController.integration.spec.ts index 1ff2471..0605e06 100644 --- a/src/controllers/rest/UserController.integration.spec.ts +++ b/src/controllers/rest/UserController.integration.spec.ts @@ -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 { UserController } from "./UserController"; import { User } from "../../entities/User"; -import { DataSource } from "typeorm"; import { v4 as uuidv4 } from "uuid"; import { Server } from "../../Server"; -import { sqliteDatasource } from "src/datasources/SqliteDatasource"; +import { SqliteDatasource, sqliteDatasource } from "src/datasources/SqliteDatasource"; describe("UserController", () => { let controller: UserController; - beforeAll(PlatformTest.bootstrap(Server)); + beforeAll( + PlatformTest.bootstrap(Server, { + imports: [sqliteDatasource] + }) + ); beforeEach(async () => { controller = await PlatformTest.invoke(UserController, [ { - token: DataSource, + token: SqliteDatasource, use: sqliteDatasource } ]); @@ -30,7 +33,8 @@ describe("UserController", () => { const user = new User(); user.id = userId; user.service = "github"; - user.serviceUsername = `silentsilas-${userId}`; + user.serviceIdentifier = `user-${userId}`; + user.links = []; const repo = sqliteDatasource.getRepository(User); await repo.save(user); @@ -39,6 +43,6 @@ describe("UserController", () => { expect(result).toEqual(user); expect(result?.id).toEqual(userId); expect(result?.service).toEqual("github"); - expect(result?.serviceUsername).toEqual(`silentsilas-${userId}`); + expect(result?.serviceIdentifier).toEqual(`user-${userId}`); }); }); diff --git a/src/controllers/rest/UserController.ts b/src/controllers/rest/UserController.ts index 87d9dae..9a86eef 100644 --- a/src/controllers/rest/UserController.ts +++ b/src/controllers/rest/UserController.ts @@ -2,14 +2,13 @@ import { PathParams } from "@tsed/platform-params"; import { Description, Get, Post, Returns, Summary } from "@tsed/schema"; import { Controller, Inject } from "@tsed/di"; 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 { UserService } from "../../services/UserService"; @Controller("/users") export class UserController { - constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {} + @Inject() + service: UserService; // disable the create method and endpoint @Post("/") @@ -24,21 +23,13 @@ export class UserController { @Summary("Get all users") @(Returns(200, Array).Of(User)) async getList(): Promise { - return executeWithRetry(async (queryRunner) => { - return queryRunner.manager.find(User); - }, this.sqliteDataSource); + return this.service.getAllUsers(); } @Get("/:id") @Summary("Get a user by ID") @Returns(200, User) async getOne(@PathParams("id") id: string): Promise { - return executeWithRetry(async (queryRunner) => { - return queryRunner.manager.findOne(User, { - where: { - id - } - }); - }, this.sqliteDataSource); + return this.service.getUserById(id); } } diff --git a/src/controllers/rest/index.ts b/src/controllers/rest/index.ts index c449761..0be9bf9 100644 --- a/src/controllers/rest/index.ts +++ b/src/controllers/rest/index.ts @@ -2,5 +2,6 @@ * @file Automatically generated by barrelsby. */ +export * from "./AuthController"; export * from "./LinkController"; export * from "./UserController"; diff --git a/src/datasources/SqliteDatasource.ts b/src/datasources/SqliteDatasource.ts index a342e3a..31190a8 100644 --- a/src/datasources/SqliteDatasource.ts +++ b/src/datasources/SqliteDatasource.ts @@ -3,7 +3,6 @@ import { DataSource } from "typeorm"; import { Logger } from "@tsed/logger"; import { User } from "../entities/User"; import { Link } from "../entities/link/Link"; -import { QueryRunner } from "typeorm"; export const SqliteDatasource = Symbol.for("SqliteDatasource"); export type SqliteDatasource = DataSource; @@ -37,48 +36,17 @@ registerProvider({ type: "typeorm:datasource", deps: [Logger], async useAsyncFactory(logger: Logger) { - await sqliteDatasource.initialize(); - - logger.info("Connected with typeorm to database: Sqlite"); - + if (!sqliteDatasource.isInitialized) { + await sqliteDatasource.initialize(); + logger.info("Connected with TypeORM to database: Sqlite"); + } return sqliteDatasource; }, hooks: { - $onDestroy(dataSource) { - return dataSource.isInitialized && dataSource.destroy(); + async $onDestroy(dataSource: DataSource) { + if (dataSource.isInitialized) { + await dataSource.destroy(); + } } } }); - -export async function executeWithRetry( - operation: (queryRunner: QueryRunner) => Promise, - dataSource: DataSource, - maxRetries = 10, - delay = 1000 -): Promise { - 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(); - } - } -} diff --git a/src/entities/User.ts b/src/entities/User.ts index fc082bb..911da6f 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -17,12 +17,21 @@ export class User { @Column({ length: 100 }) @MaxLength(100) @Required() - serviceUsername: string; + serviceIdentifier: string; @OneToMany(() => Link, (link) => link.user) @CollectionOf(() => Link) links: Link[]; + @Column({ length: 100, nullable: true }) + username: string; + + @Column("simple-json", { nullable: true }) + emails: string[]; + + @Column({ nullable: true }) + accessToken: string; + @BeforeInsert() generateId() { if (!this.id) { diff --git a/src/entities/link/CreateLinkDTO.ts b/src/entities/link/CreateLinkDTO.ts index 359cb65..5befb64 100644 --- a/src/entities/link/CreateLinkDTO.ts +++ b/src/entities/link/CreateLinkDTO.ts @@ -1,13 +1,14 @@ -import { Property, Required, MaxLength } from "@tsed/schema"; +import { Property, Required, MaxLength, Enum } from "@tsed/schema"; export class CreateLinkDto { @Property() @Required() @MaxLength(100) + @Enum("github") service: string; @Property() @Required() @MaxLength(100) - serviceUsername: string; + serviceIdentifier: string; } diff --git a/src/entities/link/Link.ts b/src/entities/link/Link.ts index 0298683..c45f0f3 100644 --- a/src/entities/link/Link.ts +++ b/src/entities/link/Link.ts @@ -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 { User } from "../User"; import { v4 as uuidv4 } from "uuid"; +export type Service = "github"; + @Entity() export class Link { @PrimaryColumn("uuid") @@ -12,12 +14,17 @@ export class Link { @Column({ length: 100 }) @MaxLength(100) @Required() + @Enum("github") service: string; @Column({ length: 100 }) @MaxLength(100) @Required() - serviceUsername: string; + serviceIdentifier: string; + + @Column({ nullable: true }) + @Required() + text: string; @ManyToOne(() => User, (user) => user.links, { onDelete: "SET NULL", onUpdate: "CASCADE" }) @JoinColumn({ name: "userId" }) diff --git a/src/index.spec.ts b/src/index.spec.ts index b337c1e..6aa6503 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,18 +1,25 @@ import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest"; import { Server } from "./Server"; -vi.mock("@tsed/common", () => ({ - $log: { - error: vi.fn() - }, - PlatformApplication: vi.fn() -})); +vi.mock("@tsed/common", async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + $log: { + error: vi.fn() + } + }; +}); -vi.mock("@tsed/platform-express", () => ({ - PlatformExpress: { - bootstrap: vi.fn() - } -})); +vi.mock("@tsed/platform-express", async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + PlatformExpress: { + bootstrap: vi.fn() + } + }; +}); describe("bootstrap function", () => { let bootstrap: () => Promise; diff --git a/src/protocols/GithubProtocol.spec.ts b/src/protocols/GithubProtocol.spec.ts new file mode 100644 index 0000000..f702e80 --- /dev/null +++ b/src/protocols/GithubProtocol.spec.ts @@ -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, [{ 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()); + }); +}); diff --git a/src/protocols/GthubProtocol.ts b/src/protocols/GthubProtocol.ts new file mode 100644 index 0000000..4c13916 --- /dev/null +++ b/src/protocols/GthubProtocol.ts @@ -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 { + 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 + } +} diff --git a/src/services/LinkService.ts b/src/services/LinkService.ts new file mode 100644 index 0000000..622f3e4 --- /dev/null +++ b/src/services/LinkService.ts @@ -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; + + constructor(@Inject(SqliteDatasource) private dataSource: DataSource) { + this.linkRepository = this.dataSource.getRepository(Link); + } + + async createLink(linkData: CreateLinkDto, user: User): Promise { + const link = this.linkRepository.create({ + ...linkData, + user + }); + return this.linkRepository.save(link); + } + + async getLinksForUser(user: User): Promise { + return this.linkRepository.find({ + where: { user: { id: user.id } }, + relations: ["user"], + select: ["id", "service", "serviceIdentifier", "text"] + }); + } + + async getLinkById(id: string): Promise { + 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 { + 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; + } +} diff --git a/src/services/UserService.ts b/src/services/UserService.ts new file mode 100644 index 0000000..b40b036 --- /dev/null +++ b/src/services/UserService.ts @@ -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; + + constructor(@Inject(SqliteDatasource) private dataSource: DataSource) { + this.userRepository = this.dataSource.getRepository(User); + } + + public async findOrCreate(profileData: ProfileData): Promise { + 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 { + return this.userRepository.findOne({ + where: { id }, + relations: ["links"] + }); + } + + public async getAllUsers(): Promise { + return this.userRepository.find(); + } +}