From 2bb8387d4b0d1239ae3f326f22a65ea0a2881221 Mon Sep 17 00:00:00 2001 From: Ange Michel Date: Wed, 24 Apr 2024 20:25:39 +0100 Subject: [PATCH] feat(user account verification): Implemention of user account verification to avoid spams - send Verify email to user using nodemailer - verify if token in email url is same with token in database - update isVerified to true - user can login only is verified [Delivers #187419049] --- .env-example | 3 +- .github/workflows/node.js.yml | 5 +++ package-lock.json | 25 +++++++++++++ package.json | 1 + src/__test__/users.test.ts | 39 ++++++++++++++++++-- src/controllers/userController.ts | 7 ++-- src/documention/user/index.ts | 61 +++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 8 deletions(-) diff --git a/.env-example b/.env-example index 6847abc6..24bcd342 100644 --- a/.env-example +++ b/.env-example @@ -24,4 +24,5 @@ SERVICE = < your SERVICE > EMAIL= -PASSWORD= +PASSWORD= + diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 95f63fac..b05e1b1b 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -15,6 +15,11 @@ jobs: runs-on: ubuntu-latest env: + BASE_URL: ${{ secrets.BASE_URL}} + HOST: ${{ secrets.HOST}} + SERVICE: ${{ secrets.SERVICE}} + EMAIL: ${{ secrets.EMAIL}} + PASSWORD: ${{ secrets.PASSWORD}} DB_TEST_URL: ${{ secrets.DB_TEST_URL }} DEV_MODE: ${{ secrets.DEV_MODE }} DB_HOSTED_MODE: ${{ secrets.DB_HOSTED_MODE }} diff --git a/package-lock.json b/package-lock.json index 19e86c8c..4a5d39dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,9 @@ "@types/passport-local": "^1.0.38", "@types/pg": "^8.11.5", "@types/supertest": "^6.0.2", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "dotenv": "^16.4.5", @@ -1920,6 +1923,28 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/validator": { "version": "13.11.9", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", diff --git a/package.json b/package.json index 6dfc4ee1..5cb4bc35 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@types/supertest": "^6.0.2", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "dotenv": "^16.4.5", diff --git a/src/__test__/users.test.ts b/src/__test__/users.test.ts index 42e3b279..bbe8fa7a 100644 --- a/src/__test__/users.test.ts +++ b/src/__test__/users.test.ts @@ -10,6 +10,7 @@ import { NewUser, user_bad_request, } from "../mock/static"; +import { Token } from "../database/models/token"; jest.setTimeout(30000); @@ -34,6 +35,7 @@ describe("USER API TEST", () => { afterAll(async () => { await deleteTableData(User, "users"); + await deleteTableData(Token, "tokens"); }); it("it should register a user and return 201", async () => { const { body } = await Jest_request.post("/api/v1/users/register") @@ -43,7 +45,8 @@ describe("USER API TEST", () => { expect(body.message).toStrictEqual( "Account Created successfully, Plase Verify your Account", ); - token = body.token; + const tokenRecord = await Token.findOne(); + token = tokenRecord?.dataValues.token ?? ""; }); it("it should return a user not found and status 400", async () => { const { body } = await Jest_request.post("/api/v1/users/register") @@ -63,7 +66,7 @@ describe("USER API TEST", () => { const { body } = await Jest_request.get( `/api/v1/users/account/verify/${token}`, - ).expect(200); + ); expect(body.status).toStrictEqual(200); expect(body.message).toStrictEqual("Email verified successfull"); @@ -98,8 +101,8 @@ describe("USER API TEST", () => { it("should return 404 when a user login with wrong credentials", async () => { const { body } = await Jest_request.post("/api/v1/users/login") .send(login_user_wrong_credentials) - .expect(404); - expect(body.status).toStrictEqual("NOT FOUND"); + .expect(403); + expect(body.status).toStrictEqual("FORBIDDEN"); expect(body.message).toStrictEqual("Wrong credentials!"); }); @@ -119,3 +122,31 @@ describe("USER API TEST", () => { expect(body.message).toBeDefined(); }); }); +/** + * -----------------------------------------LOG OUT-------------------------------------- + */ +it("Should log out a user and return 404", async () => { + const { body } = await Jest_request.post("/api/v1/users/logout").send(); + expect(404); + expect(body.status).toStrictEqual("NOT FOUND"); + expect(body.message).toStrictEqual("Token Not Found"); +}); + +it("Should log out a user and return 201", async () => { + const { body } = await Jest_request.post("/api/v1/users/logout") + .send() + .set("Authorization", `Bearer ${token}`); + expect(201); + expect(body.status).toStrictEqual("CREATED"); + expect(body.message).toStrictEqual("Logged out successfully"); + token = token; +}); + +it("Should alert an error and return 401", async () => { + const { body } = await Jest_request.post("/api/v1/users/logout") + .send() + .set("Authorization", `Bearer ${token}`); + expect(401); + expect(body.status).toStrictEqual("UNAUTHORIZED"); + expect(body.message).toStrictEqual("Already logged out"); +}); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a0fe7485..a564ad8c 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -38,7 +38,8 @@ const registerUser = async ( "SUCCESS", "Account Created successfully, Plase Verify your Account", ).response(); - res.status(201).json({ ...response, token }); + + res.status(201).json({ ...response }); }); }, )(req, res, next); @@ -62,8 +63,8 @@ const login = async (req: Request, res: Response, next: NextFunction) => { if (info) { return res - .status(404) - .json(new HttpException("NOT FOUND", info.message)); + .status(403) + .json(new HttpException("FORBIDDEN", info.message)); } (req as any).login(user, (err: Error) => { diff --git a/src/documention/user/index.ts b/src/documention/user/index.ts index 43f0ab8c..b2224874 100644 --- a/src/documention/user/index.ts +++ b/src/documention/user/index.ts @@ -76,6 +76,67 @@ const users = { responses, }, }, + "/users/account/verify/{token}": { + get: { + tags: ["User"], + summary: "Verify user account", + parameters: [ + { + in: "path", + name: "token", + required: true, + type: "string", + description: "Verification token", + }, + ], + responses: { + "200": { + description: "Email verified successfully", + schema: { + type: "object", + properties: { + status: { + type: "integer", + example: 200, + }, + message: { + type: "string", + example: "Email verified successfull", + }, + }, + }, + }, + "400": { + description: "Invalid link or something went wrong", + schema: { + type: "object", + properties: { + status: { + type: "integer", + example: 400, + }, + message: { + type: "string", + example: "Invalid link", + }, + error: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + "/users/logout": { + post: { + tags: ["User"], + security: [{ JWT: [] }], + summary: "Log out a user", + consumes: ["application/json"], + responses, + }, + }, }; export default users;