diff --git a/package-lock.json b/package-lock.json index fc79ef5..a3ad0d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { - "@athenna/artisan": "^4.43.0", - "@athenna/common": "^4.42.0", + "@athenna/artisan": "^4.44.0", + "@athenna/common": "^4.43.0", "@athenna/config": "^4.22.0", - "@athenna/core": "^4.39.0", - "@athenna/database": "^4.55.0", - "@athenna/http": "^4.35.0", - "@athenna/ioc": "^4.21.0", - "@athenna/logger": "^4.22.0", - "@athenna/mail": "^4.19.0", - "@athenna/queue": "^4.6.0", - "@athenna/validator": "^4.3.0", - "@athenna/view": "^4.23.0", + "@athenna/core": "^4.41.0", + "@athenna/database": "^4.56.0", + "@athenna/http": "^4.36.0", + "@athenna/ioc": "^4.22.0", + "@athenna/logger": "^4.23.0", + "@athenna/mail": "^4.21.0", + "@athenna/queue": "^4.7.0", + "@athenna/validator": "^4.4.0", + "@athenna/view": "^4.24.0", "@fastify/cors": "^8.5.0", "@fastify/helmet": "^11.1.1", "@fastify/rate-limit": "^8.1.1", @@ -35,8 +35,8 @@ "source-map-support": "^0.5.21" }, "devDependencies": { - "@athenna/test": "^4.24.0", - "@athenna/tsconfig": "^4.14.0", + "@athenna/test": "^4.26.0", + "@athenna/tsconfig": "^4.16.0", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.6", "@typescript-eslint/eslint-plugin": "^7.9.0", @@ -100,9 +100,9 @@ } }, "node_modules/@athenna/artisan": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@athenna/artisan/-/artisan-4.43.0.tgz", - "integrity": "sha512-NaAOIUVWqDKVnbBVG/T33p1UToVVQHx50pP2ZJh9haCWMe8VXdaTX6l9ygt97wCloOQdH4P+3PS4b4rwc6kLaw==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@athenna/artisan/-/artisan-4.44.0.tgz", + "integrity": "sha512-IN9Lj1xFFdeZYnuMUQhnFXtzrTFI4VaLlhIat1BxAINSRvSmzVGgAm1rxzTgXDuxwcc6Kk24XURy75uziOVwkg==", "dependencies": { "chalk-rainbow": "^1.0.0", "cli-boxes": "^3.0.0", @@ -110,7 +110,7 @@ "columnify": "^1.6.0", "commander": "^9.5.0", "figlet": "^1.7.0", - "inquirer": "^9.2.21", + "inquirer": "^9.2.22", "log-update": "^5.0.1", "ora": "^6.3.1" }, @@ -119,9 +119,9 @@ } }, "node_modules/@athenna/common": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.42.0.tgz", - "integrity": "sha512-gJUVddtsP58xwNBzcBBcPZwEqmhZiSqDvbXkbrLa7tIQ0pD1HYg9vEoxFrMHCpmAeR4uVSrPOCr87ZameZOYCg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.43.0.tgz", + "integrity": "sha512-r6GKI15PMAzAkZRjKn1HPkUKKd9V9tGwvDgNTuS8OTlj8Pl3rMb2p77nkJ6bTAxLUD4eHNFvhllbS9snrcct4A==", "dependencies": { "@fastify/formbody": "^7.4.0", "bytes": "^3.1.2", @@ -131,9 +131,9 @@ "collect.js": "^4.36.1", "csv-parser": "^3.0.0", "execa": "^8.0.1", - "fastify": "^4.26.2", + "fastify": "^4.27.0", "got": "^12.6.1", - "http-status-codes": "^2.2.0", + "http-status-codes": "^2.3.0", "is-wsl": "^2.2.0", "js-yaml": "^4.1.0", "json-2-csv": "^5.5.1", @@ -142,13 +142,13 @@ "mime-types": "^2.1.35", "minimatch": "^5.1.6", "ms": "^2.1.3", - "parent-module": "^3.0.0", + "parent-module": "^3.1.0", "pluralize": "^8.0.0", "prepend-file": "^2.0.1", "uuid": "^8.3.2", "validator-brazil": "^1.2.2", "youch": "^3.3.3", - "youch-terminal": "^2.2.2" + "youch-terminal": "^2.2.3" }, "engines": { "node": ">=20.0.0" @@ -168,9 +168,9 @@ } }, "node_modules/@athenna/core": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@athenna/core/-/core-4.39.0.tgz", - "integrity": "sha512-9dHGi/Hdh52bToRRVIiCR2CjnNlCiYfOMhC9fP3g/MgF29QHDparoTD5c5Ium/9D/ROCWWlsaFtcCkUMI0EbLQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@athenna/core/-/core-4.41.0.tgz", + "integrity": "sha512-GMiptAGjFx7Z5PsAeJ4Bct74cdLzzuxvxL4IgH2bcZJu7ew9YEwLMaSegZH/sHZOFRSRwq9ruQlbNfC4Xq+eag==", "dependencies": { "pretty-repl": "^3.1.2", "semver": "^7.6.2" @@ -180,9 +180,9 @@ } }, "node_modules/@athenna/database": { - "version": "4.55.0", - "resolved": "https://registry.npmjs.org/@athenna/database/-/database-4.55.0.tgz", - "integrity": "sha512-fHmKlRiasHVWkxxzuwUQetlxosxLeMz7dukle05t/u6zo5jnJwQrLpVv7uhPD4mZON94H9vpAi66v2VxvXA32A==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@athenna/database/-/database-4.56.0.tgz", + "integrity": "sha512-YVvrQj4V8XFiju4uVX7U83FyPWeoZN+aIDi/sA1ANWzN9a6EKuvnNMaHV+flV06NnIfPJ7ADLMdKgwqBuDHCNQ==", "dependencies": { "@faker-js/faker": "^8.4.1", "fast-deep-equal": "^3.1.3" @@ -192,28 +192,28 @@ } }, "node_modules/@athenna/http": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@athenna/http/-/http-4.35.0.tgz", - "integrity": "sha512-L602UD4fA9UrCq/v5w/7mKubgmLjIyQB1orsfHN0N25xbO47QyXcFIMKCgAotYJqDIluuXeHs4qTJ+jVCLFNeA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@athenna/http/-/http-4.36.0.tgz", + "integrity": "sha512-W9Q9ghIZAQSS4Kq2l8QA8pJpO92F0ZeyKLJolOxIF5rwzgSQ4DioX2EOScCFzXJ4B3KpzlM4tIFzFYl6eaQxxA==", "engines": { "node": ">=20.0.0" } }, "node_modules/@athenna/ioc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@athenna/ioc/-/ioc-4.21.0.tgz", - "integrity": "sha512-6cT7+vMTZujvWJO8ZVGKS7ROZvO4MPSEeLjmyf8HHyRtx7OkghGQrArHyGqCn+zO19T5StOhfsNTtC4NvI2pBw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@athenna/ioc/-/ioc-4.22.0.tgz", + "integrity": "sha512-u+f7KzKjvKjE9pi8ug6UaQs845I7ITaCklT8/K+DIltIGKlBaySYbduHB6Ed76ab3KAdlqd2gqDHwfUlmzUZ1w==", "dependencies": { - "awilix": "^7.0.3" + "awilix": "^10.0.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@athenna/logger": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-4.22.0.tgz", - "integrity": "sha512-sMkSDYsy8H2YKkCjlQn0UWCWofvsSRn2FeNE1Mlp6Pf2PAUAAd/K/45elhZ1TGPcK9V+6h62OWJpcmnV4VD8ow==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@athenna/logger/-/logger-4.23.0.tgz", + "integrity": "sha512-b9wEMoDXP4L32vz1+GqTvtugq6f6F5Gy53+CMsc8C516WZoTgBoO2FTXlKhAl1MGKVf9KRXAA1JPxjkfMSwocw==", "dependencies": { "@aws-lambda-powertools/logger": "^1.18.1", "cls-rtracer": "^2.6.3", @@ -224,9 +224,9 @@ } }, "node_modules/@athenna/mail": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@athenna/mail/-/mail-4.19.0.tgz", - "integrity": "sha512-O8uPp3Ar6t2SG9WnIkay9PPO3o9RwLh2yVGcjHoorKwkFxaRdDRSyyYoWQDfLMGXBqel4wOnuggFFSygf0bQ0A==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@athenna/mail/-/mail-4.21.0.tgz", + "integrity": "sha512-qR2vAq7nsoIkX7dnEiAQgnrsnPvyZ9pxAu0pD1bRz9Et5fo0/gvMG8LYZx26B8bJwzqDMy9bHacjwF9OcuoNgw==", "dependencies": { "nodemailer": "^6.9.13", "nodemailer-markdown": "^1.0.3", @@ -237,33 +237,33 @@ } }, "node_modules/@athenna/queue": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@athenna/queue/-/queue-4.6.0.tgz", - "integrity": "sha512-BWvQUC5zxw+1ppBz3+a8/2PXviRKDqXAb6P+u+xl1e9B9daHg/uehRX6cDAPDvOP7os8Idgw9F+nPUX7gaYBbw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@athenna/queue/-/queue-4.7.0.tgz", + "integrity": "sha512-vbVe95FeEd6i5Fvyg4hCRcst/ncZm17Lwu95zlEK9+JXhj5a869+ua0c+nyR5o6AInh/wJfK+omXLoOCNcQ9aQ==", "engines": { "node": ">=20.0.0" } }, "node_modules/@athenna/test": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@athenna/test/-/test-4.24.0.tgz", - "integrity": "sha512-XXn/Th14yf8q4MdPXMP/s0z5f2Lq1lpE8LuNPEEpb6FXKMqo8JfVj5Fmi8FlGnm0DQeAokiJP0ESU55G+zxZwg==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@athenna/test/-/test-4.26.0.tgz", + "integrity": "sha512-ZlalFMDninWPQk3dXUwBcrJj14QB3L2OhQ5UPpJ9lCxthmxSQ+93DQGEl7SI7y9C4kTRmTwt2L8wPkDXpF0MZQ==", "dev": true, "dependencies": { "@japa/assert": "^3.0.0", "@japa/runner": "^3.1.4", "@types/sinon": "^10.0.20", "c8": "^9.1.0", - "sinon": "^15.1.0" + "sinon": "^15.2.0" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@athenna/tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@athenna/tsconfig/-/tsconfig-4.14.0.tgz", - "integrity": "sha512-qytIqdZbjrKJiP9U8mg52i6EZxlImJcBhghBR6XUBVSKhr38bbLM9kermo/EdquU0J0Db3r+ZjuUJRa0RcbhbQ==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@athenna/tsconfig/-/tsconfig-4.16.0.tgz", + "integrity": "sha512-ypcH72jcNF833HBuQZIUXGuliHdZVKcqy40c1CfuUc+5fbUm4kZhnNaeJhbvl2yrCpQZEaESs/jm+lkyc9X32g==", "dev": true, "dependencies": { "copyfiles": "^2.4.1", @@ -282,9 +282,9 @@ "dev": true }, "node_modules/@athenna/validator": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@athenna/validator/-/validator-4.3.0.tgz", - "integrity": "sha512-eJzGyljwruvf83U597YyqeqKhSn15FQrXpL10PbpQFfaJDXD2pJss84ihKUQZsY8Zlh5+H7XgZdMKbovEIDdjA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@athenna/validator/-/validator-4.4.0.tgz", + "integrity": "sha512-m0hldk/N11LOvLHWkzA8aGlx6uETPnYMHd3bPhAHNwJJ6PUeuK2XjLi0MfCEs2OrQ1v0z4Y9wvfoLYFPb5r8Rg==", "dependencies": { "@vinejs/vine": "^2.0.0" }, @@ -293,9 +293,9 @@ } }, "node_modules/@athenna/view": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/@athenna/view/-/view-4.23.0.tgz", - "integrity": "sha512-+om6n6ZzJ9tL7Jh5SJRB8ptBE7d8eA55KloGRgU/wgx22Z+fYKEPYq1g2r+F/ZOiDYje3BrTjZ0Dyn+jaJ3CRA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@athenna/view/-/view-4.24.0.tgz", + "integrity": "sha512-12RELhidrV0qbLUoU9DAzBJ9R4zCc9MSwFKAzFJnEHGwcthB9cY5ag4Q0RoIBLcIpz6osJb+lkEGIsHiawv13w==", "dependencies": { "edge.js": "^6.0.2" }, @@ -2399,15 +2399,15 @@ } }, "node_modules/awilix": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/awilix/-/awilix-7.0.3.tgz", - "integrity": "sha512-4Nmjyh9qloDwXfDK0DBuWd8WyFApyknoaKbE3leQflGLgNfNsBHy2/VYrlyy/mzMobjJ3J8XtNpbjzG3KRkIFQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/awilix/-/awilix-10.0.2.tgz", + "integrity": "sha512-hFatb7eZFdtiWjjmGRSm/K/uxZpmcBlM+YoeMB3VpOPXk3xa6+7zctg3LRbUzoimom5bwGrePF0jXReO6b4zNQ==", "dependencies": { "camel-case": "^4.1.2", - "fast-glob": "^3.2.11" + "fast-glob": "^3.3.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/balanced-match": { diff --git a/package.json b/package.json index 5ad4aa9..13551d4 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,18 @@ "#tests": "./tests/index.js" }, "dependencies": { - "@athenna/artisan": "^4.43.0", - "@athenna/common": "^4.42.0", + "@athenna/artisan": "^4.44.0", + "@athenna/common": "^4.43.0", "@athenna/config": "^4.22.0", - "@athenna/core": "^4.39.0", - "@athenna/database": "^4.55.0", - "@athenna/http": "^4.35.0", - "@athenna/ioc": "^4.21.0", - "@athenna/logger": "^4.22.0", - "@athenna/mail": "^4.19.0", - "@athenna/queue": "^4.6.0", - "@athenna/validator": "^4.3.0", - "@athenna/view": "^4.23.0", + "@athenna/core": "^4.41.0", + "@athenna/database": "^4.56.0", + "@athenna/http": "^4.36.0", + "@athenna/ioc": "^4.22.0", + "@athenna/logger": "^4.23.0", + "@athenna/mail": "^4.21.0", + "@athenna/queue": "^4.7.0", + "@athenna/validator": "^4.4.0", + "@athenna/view": "^4.24.0", "@fastify/cors": "^8.5.0", "@fastify/helmet": "^11.1.1", "@fastify/rate-limit": "^8.1.1", @@ -50,8 +50,8 @@ "source-map-support": "^0.5.21" }, "devDependencies": { - "@athenna/test": "^4.24.0", - "@athenna/tsconfig": "^4.14.0", + "@athenna/test": "^4.26.0", + "@athenna/tsconfig": "^4.16.0", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.6", "@typescript-eslint/eslint-plugin": "^7.9.0", diff --git a/public/css/mail.css b/public/css/mail.css new file mode 100644 index 0000000..07b14c4 --- /dev/null +++ b/public/css/mail.css @@ -0,0 +1,60 @@ +body { + font-family: '-apple-system', 'BlinkMacSystemFont', 'Helvetica', 'Arial', 'sans-serif', 'apple color emoji'; + margin: 0; + padding: 0; + background-color: #252529; + color: #e3e3e3; + text-align: center; +} + +.header { + background-color: #1d1e25; + padding: 20px; +} + +.header h1 { + margin: 0; + font-size: 30px; +} + +.activation { + padding: 10px 100px; +} + +.activation p { + margin: 0; + font-size: 20px; + margin-top: 10px; + margin-bottom: 30px; +} + +.help { + padding: 10px 200px; +} + +.help p { + margin: 0; + font-size: 16px; + margin-top: 10px; + margin-bottom: 30px; +} + +a { + color: #ffff92; +} + +a.button { + padding: 10px 24px; + border: 1px; + font-size: 20px; + border-radius: 3px; + color: #e3e3e3; + font-weight: 500; + background-color: #715bf6; + text-decoration: none; +} + +.minerva-img { + max-width: 200px; + margin-top: 20px; +} diff --git a/public/img/favicons/favicon.ico b/public/img/favicons/favicon.ico new file mode 100644 index 0000000..4a92e13 Binary files /dev/null and b/public/img/favicons/favicon.ico differ diff --git a/public/img/logos/minerva.png b/public/img/logos/minerva.png new file mode 100644 index 0000000..3edc87c Binary files /dev/null and b/public/img/logos/minerva.png differ diff --git a/src/config/http.ts b/src/config/http.ts index e24b546..597d66c 100644 --- a/src/config/http.ts +++ b/src/config/http.ts @@ -120,7 +120,7 @@ export default { */ helmet: { - enabled: true, + enabled: false, global: true }, diff --git a/src/config/view.ts b/src/config/view.ts index 2cead42..b06e87b 100644 --- a/src/config/view.ts +++ b/src/config/view.ts @@ -48,7 +48,9 @@ export default { | */ - components: {}, + components: { + mail: Path.views('components/mail.edge') + }, /* |-------------------------------------------------------------------------- diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index c61d5dd..bd5173b 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -22,7 +22,7 @@ export class AuthController { public async register({ request, response }: Context) { const user = await this.authService.register( - request.only(['name', 'email', 'password']) + request.only(['name', 'email', 'cellphone', 'password']) ) return response.status(201).send(user) @@ -34,30 +34,20 @@ export class AuthController { return response.status(204) } - public async confirmEmailChange({ request, response }: Context) { - await this.authService.confirmEmailChange( - request.query('email'), - request.query('token') - ) + public async resetEmail({ request, response }: Context) { + await this.authService.resetEmail(request.query('token')) return response.status(204) } - public async confirmPasswordChange({ request, response }: Context) { - await this.authService.confirmPasswordChange( - request.query('password'), - request.query('token') - ) + public async resetPassword({ request, response }: Context) { + await this.authService.resetPassword(request.query('token')) return response.status(204) } - public async confirmEmailPasswordChange({ request, response }: Context) { - await this.authService.confirmEmailPasswordChange( - request.query('email'), - request.query('password'), - request.query('token') - ) + public async resetEmailPassword({ request, response }: Context) { + await this.authService.resetEmailPassword(request.query('token')) return response.status(204) } diff --git a/src/database/migrations/2024_04_16_195846_create_users_table.ts b/src/database/migrations/2024_04_16_195846_create_users_table.ts index 65e49c4..2eddc80 100644 --- a/src/database/migrations/2024_04_16_195846_create_users_table.ts +++ b/src/database/migrations/2024_04_16_195846_create_users_table.ts @@ -1,3 +1,5 @@ +import { UserLangEnum } from '#src/enums/userlang.enum' +import { UserStatusEnum } from '#src/enums/userstatus.enum' import { BaseMigration, type DatabaseImpl } from '@athenna/database' export class Users extends BaseMigration { @@ -8,11 +10,16 @@ export class Users extends BaseMigration { builder.increments('id') builder.string('name').notNullable() builder.string('email').unique().notNullable() + builder.string('cellphone').unique() builder.string('password').notNullable() - builder.string('token').unique().notNullable() builder.timestamp('email_verified_at').defaultTo(null) + builder.timestamp('cellphone_verified_at').defaultTo(null) + builder.enum('lang', UserLangEnum.values()).defaultTo(UserLangEnum.PT) builder.timestamps(true, true, false) builder.timestamp('deleted_at').defaultTo(null) + builder + .enum('status', UserStatusEnum.values()) + .defaultTo(UserStatusEnum.PENDENT) }) } diff --git a/src/database/migrations/2024_04_23_172843_create_roles_table.ts b/src/database/migrations/2024_04_23_172843_create_roles_table.ts index fa2b1c5..7ece16e 100644 --- a/src/database/migrations/2024_04_23_172843_create_roles_table.ts +++ b/src/database/migrations/2024_04_23_172843_create_roles_table.ts @@ -1,3 +1,4 @@ +import { RoleEnum } from '#src/enums/role.enum' import { BaseMigration, type DatabaseImpl } from '@athenna/database' export class Roles extends BaseMigration { @@ -6,7 +7,7 @@ export class Roles extends BaseMigration { public async up(db: DatabaseImpl) { return db.createTable(this.tableName, builder => { builder.increments('id') - builder.string('name').unique().notNullable() + builder.enum('name', RoleEnum.values()).notNullable() }) } diff --git a/src/database/migrations/2024_05_18_093031_create_tokens_table.ts b/src/database/migrations/2024_05_18_093031_create_tokens_table.ts new file mode 100644 index 0000000..bac6a4d --- /dev/null +++ b/src/database/migrations/2024_05_18_093031_create_tokens_table.ts @@ -0,0 +1,21 @@ +import { TokenEnum } from '#src/enums/token.enum' +import { BaseMigration, type DatabaseImpl } from '@athenna/database' + +export class Tokens extends BaseMigration { + public tableName = 'tokens' + + public async up(db: DatabaseImpl) { + return db.createTable(this.tableName, builder => { + builder.increments('id') + builder.integer('user_id').unsigned().references('id').inTable('users') + builder.enum('type', TokenEnum.values()).notNullable() + builder.string('token').notNullable() + builder.string('value').nullable() + builder.timestamps(true, true, false) + }) + } + + public async down(db: DatabaseImpl) { + return db.dropTable(this.tableName) + } +} diff --git a/src/enums/role.enum.ts b/src/enums/role.enum.ts index a71d63d..6a701d0 100644 --- a/src/enums/role.enum.ts +++ b/src/enums/role.enum.ts @@ -1,4 +1,6 @@ -export enum RoleEnum { - ADMIN = 'admin', - CUSTOMER = 'customer' +import { Enum } from '@athenna/common' + +export class RoleEnum extends Enum { + static ADMIN = 'admin' + static CUSTOMER = 'customer' } diff --git a/src/enums/token.enum.ts b/src/enums/token.enum.ts new file mode 100644 index 0000000..f5b87e2 --- /dev/null +++ b/src/enums/token.enum.ts @@ -0,0 +1,8 @@ +import { Enum } from '@athenna/common' + +export class TokenEnum extends Enum { + static EMAIL = 'email' + static PASSWORD = 'password' + static EMAIL_PASSWORD = 'email_password' + static CONFIRM_ACCOUNT = 'confirm_account' +} diff --git a/src/enums/userlang.enum.ts b/src/enums/userlang.enum.ts new file mode 100644 index 0000000..5ed23ad --- /dev/null +++ b/src/enums/userlang.enum.ts @@ -0,0 +1,7 @@ +import { Enum } from '@athenna/common' + +export class UserLangEnum extends Enum { + static EN = 'en' + static ES = 'es' + static PT = 'pt' +} diff --git a/src/enums/userstatus.enum.ts b/src/enums/userstatus.enum.ts new file mode 100644 index 0000000..d6fb3cb --- /dev/null +++ b/src/enums/userstatus.enum.ts @@ -0,0 +1,7 @@ +import { Enum } from '@athenna/common' + +export class UserStatusEnum extends Enum { + static PENDENT = 'pendent' + static APPROVED = 'approved' + static BLOCKED = 'blocked' +} diff --git a/src/models/role.ts b/src/models/role.ts index 766914b..f6d8d42 100644 --- a/src/models/role.ts +++ b/src/models/role.ts @@ -12,10 +12,6 @@ export class Role extends BaseModel { @BelongsToMany(() => User, () => RoleUser) public users: User[] - public static attributes(): Partial { - return {} - } - public static async definition(): Promise> { return { name: this.faker.person.jobType() diff --git a/src/models/token.ts b/src/models/token.ts new file mode 100644 index 0000000..72496b1 --- /dev/null +++ b/src/models/token.ts @@ -0,0 +1,41 @@ +import { User } from '#src/models/user' +import { NotFoundException } from '@athenna/http' +import { Column, BaseModel, BelongsTo, type Relation } from '@athenna/database' + +export class Token extends BaseModel { + @Column() + public id: number + + @Column({ isNullable: false }) + public type: string + + @Column({ isNullable: false }) + public token: string + + @Column({ name: 'user_id' }) + public userId: number + + @Column() + public value: string + + @Column({ name: 'created_at', isCreateDate: true }) + public createdAt: Date + + @Column({ name: 'updated_at', isUpdateDate: true }) + public updatedAt: Date + + @BelongsTo(() => User) + public user: Relation + + public static async definition(): Promise> { + return { + token: this.faker.string.uuid() + } + } + + public static async findOrThrowNotFound(token: string): Promise { + return Token.findOr({ token }, () => { + throw new NotFoundException(`Not found any user with token ${token}.`) + }) + } +} diff --git a/src/models/user.ts b/src/models/user.ts index 65a1514..fffe166 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,7 +1,10 @@ import bcrypt from 'bcrypt' +import { Uuid } from '@athenna/common' import { Role } from '#src/models/role' +import { Token } from '#src/models/token' import { RoleUser } from '#src/models/roleuser' -import { Column, BaseModel, BelongsToMany } from '@athenna/database' +import { Column, BaseModel, BelongsToMany, HasMany } from '@athenna/database' +import { TokenEnum } from '#src/enums/token.enum' export class User extends BaseModel { @Column() @@ -13,15 +16,18 @@ export class User extends BaseModel { @Column({ isUnique: true, isNullable: false }) public email: string + @Column({ isUnique: true }) + public cellphone: string + @Column({ isHidden: true, isNullable: false }) public password: string - @Column({ isUnique: true, isNullable: false }) - public token: string - @Column({ name: 'email_verified_at' }) public emailVerifiedAt: Date + @Column({ name: 'cellphone_verified_at' }) + public cellphoneVerifiedAt: Date + @Column({ name: 'created_at', isCreateDate: true }) public createdAt: Date @@ -31,6 +37,9 @@ export class User extends BaseModel { @Column({ name: 'deleted_at', isDeleteDate: true }) public deletedAt: Date + @HasMany(() => Token) + public tokens: Token[] + @BelongsToMany(() => Role, () => RoleUser) public roles: Role[] @@ -58,15 +67,53 @@ export class User extends BaseModel { return bcrypt.compareSync(password, this.password) } - public static attributes(): Partial { - return {} + public async confirmToken() { + const token = await Token.create({ + userId: this.id, + type: TokenEnum.CONFIRM_ACCOUNT, + token: Uuid.generate() + }) + + return token.token + } + + public async resetEmailToken(email: string) { + const token = await Token.create({ + value: email, + userId: this.id, + type: TokenEnum.EMAIL, + token: Uuid.generate() + }) + + return token.token + } + + public async resetPasswordToken(password: string) { + const token = await Token.create({ + value: password, + userId: this.id, + type: TokenEnum.PASSWORD, + token: Uuid.generate() + }) + + return token.token + } + + public async resetEmailPasswordToken(email: string, password: string) { + const token = await Token.create({ + value: JSON.stringify([email, password]), + userId: this.id, + type: TokenEnum.PASSWORD, + token: Uuid.generate() + }) + + return token.token } public static async definition(): Promise> { return { name: this.faker.person.firstName(), email: this.faker.internet.email(), - token: this.faker.string.uuid(), password: await bcrypt.hash('12345', 10) } } diff --git a/src/resources/views/components/mail.edge b/src/resources/views/components/mail.edge new file mode 100644 index 0000000..502c88c --- /dev/null +++ b/src/resources/views/components/mail.edge @@ -0,0 +1,16 @@ + + + + + + Athenna Framework + + + +
+ Minerva Logo +

Welcome to Athenna {{ user.name }}!

+
+ {{{ await $slots.main() }}} + + diff --git a/src/resources/views/mail/change-email-password.edge b/src/resources/views/mail/change-email-password.edge deleted file mode 100644 index 1707dc8..0000000 --- a/src/resources/views/mail/change-email-password.edge +++ /dev/null @@ -1,84 +0,0 @@ - - - - - -Change Email & Password - - - -
- Minerva Logo -

Hey there {{ user.name }}!

-
-
-

- We are sending you this email because you have requested to - change your account email to {{ email }} and also to - change your password. To confirm the update, click the link bellow: -

- CONFIRM EMAIL & PASSWORD CHANGE -
-
-

- If this was not you or if you have any questions, please email us - at support@athenna.io or - visit our FAQS, you can also chat with a real human during our - operating hours. They can answer questions about your account. -

-
- - - diff --git a/src/resources/views/mail/change-email.edge b/src/resources/views/mail/change-email.edge deleted file mode 100644 index cab1cbe..0000000 --- a/src/resources/views/mail/change-email.edge +++ /dev/null @@ -1,84 +0,0 @@ - - - - - -Change Email - - - -
- Minerva Logo -

Hey there {{ user.name }}!

-
-
-

- We are sending you this email because you have requested to - change your account email to {{ email }}. To confirm the update, - click the link bellow: -

- CONFIRM EMAIL CHANGE -
-
-

- If this was not you or if you have any questions, please email us - at support@athenna.io or - visit our FAQS, you can also chat with a real human during our - operating hours. They can answer questions about your account. -

-
- - - diff --git a/src/resources/views/mail/change-password.edge b/src/resources/views/mail/change-password.edge deleted file mode 100644 index 1be2fd1..0000000 --- a/src/resources/views/mail/change-password.edge +++ /dev/null @@ -1,83 +0,0 @@ - - - - - -Change Password - - - -
- Minerva Logo -

Hey there {{ user.name }}!

-
-
-

- We are sending you this email because you have requested to - change your account password. To confirm the update, click the link bellow: -

- CONFIRM PASSWORD CHANGE -
-
-

- If this was not you or if you have any questions, please email us - at support@athenna.io or - visit our FAQS, you can also chat with a real human during our - operating hours. They can answer questions about your account. -

-
- - - diff --git a/src/resources/views/mail/confirm.edge b/src/resources/views/mail/confirm.edge index 83b2ea2..1fcb2fe 100644 --- a/src/resources/views/mail/confirm.edge +++ b/src/resources/views/mail/confirm.edge @@ -1,80 +1,14 @@ - - - - - -Welcome to Athenna - - - -
- Minerva Logo -

Welcome to Athenna {{ user.name }}!

-
-
-

We are happy to have you here. You still need to confirm your account:

- CONFIRM ACCOUNT -
-
-

- If this was not you or if you have any questions, please email us - at support@athenna.io or - visit our FAQS, you can also chat with a real human during our - operating hours. They can answer questions about your account. -

-
- - - +@component('mail', { user }) +
+

We are happy to have you here. You still need to confirm your account:

+ CONFIRM ACCOUNT +
+
+

+ If this was not you or if you have any questions, please email us + at support@athenna.io or + visit our FAQS, you can also chat with a real human during our + operating hours. They can answer questions about your account. +

+
+@end diff --git a/src/resources/views/mail/reset_email.edge b/src/resources/views/mail/reset_email.edge new file mode 100644 index 0000000..aa8e7a8 --- /dev/null +++ b/src/resources/views/mail/reset_email.edge @@ -0,0 +1,18 @@ +@component('mail', { user }) +
+

+ We are sending you this email because you have requested to + change your account email. To confirm the reset, + click the link bellow: +

+ CONFIRM EMAIL CHANGE +
+
+

+ If this was not you or if you have any questions, please email us + at support@athenna.io or + visit our FAQS, you can also chat with a real human during our + operating hours. They can answer questions about your account. +

+
+@end diff --git a/src/resources/views/mail/reset_email_password.edge b/src/resources/views/mail/reset_email_password.edge new file mode 100644 index 0000000..a3dc511 --- /dev/null +++ b/src/resources/views/mail/reset_email_password.edge @@ -0,0 +1,18 @@ +@component('mail', { user }) +
+

+ We are sending you this email because you have requested to + change your account email and password to. To + confirm the reset, click the link bellow: +

+ CONFIRM EMAIL & PASSWORD CHANGE +
+
+

+ If this was not you or if you have any questions, please email us + at support@athenna.io or + visit our FAQS, you can also chat with a real human during our + operating hours. They can answer questions about your account. +

+
+@end diff --git a/src/resources/views/mail/reset_password.edge b/src/resources/views/mail/reset_password.edge new file mode 100644 index 0000000..980b146 --- /dev/null +++ b/src/resources/views/mail/reset_password.edge @@ -0,0 +1,17 @@ +@component('mail', { user }) +
+

+ We are sending you this email because you have requested to + change your account password. To confirm the reset, click the link bellow: +

+ CONFIRM PASSWORD CHANGE +
+
+

+ If this was not you or if you have any questions, please email us + at support@athenna.io or + visit our FAQS, you can also chat with a real human during our + operating hours. They can answer questions about your account. +

+
+@end diff --git a/src/routes/http.ts b/src/routes/http.ts index a2934b1..a467d3d 100644 --- a/src/routes/http.ts +++ b/src/routes/http.ts @@ -1,6 +1,6 @@ import { Route } from '@athenna/http' -Route.view('/mailable', 'mail/register', { user: { name: 'João' } }).helmet({ +Route.view('/mailable', 'mail/confirm', { user: { name: 'João' } }).helmet({ contentSecurityPolicy: false }) @@ -17,12 +17,10 @@ Route.group(() => { }).middleware('auth') Route.get('confirm/account', 'AuthController.confirm') - Route.get('confirm/email', 'AuthController.confirmEmailChange') - Route.get('confirm/password', 'AuthController.confirmPasswordChange') - Route.get( - 'confirm/email/password', - 'AuthController.confirmEmailPasswordChange' - ) + + Route.get('reset/email', 'AuthController.resetEmail') + Route.get('reset/password', 'AuthController.resetPassword') + Route.get('reset/email/password', 'AuthController.resetEmailPassword') Route.post('login', 'AuthController.login').validator('user:login') Route.post('register', 'AuthController.register').validator('user:register') diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index b54d516..3d15cd9 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,11 +1,11 @@ import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' import { Log } from '@athenna/logger' -import { Uuid } from '@athenna/common' import { Queue } from '@athenna/queue' import { Service } from '@athenna/ioc' import { User } from '#src/models/user' import { Config } from '@athenna/config' +import { Token } from '#src/models/token' import { UnauthorizedException } from '@athenna/http' import type { UserService } from '#src/services/user.service' @@ -42,13 +42,14 @@ export class AuthService { } public async register(data: Partial) { - data.token = Uuid.generate() data.password = await bcrypt.hash(data.password, 10) const user = await this.userService.create(data) + const token = await user.confirmToken() await Queue.queue('mail').add({ user, + token, view: 'mail/confirm', subject: 'Athenna Account Confirmation' }) @@ -56,7 +57,8 @@ export class AuthService { return user } - public async confirm(token: string) { + public async confirm(tkn: string) { + const token = await Token.findOrThrowNotFound(tkn) const user = await this.userService.getByToken(token) user.emailVerifiedAt = new Date() @@ -64,33 +66,30 @@ export class AuthService { await user.save() } - public async confirmEmailChange(email: string, token: string) { + public async resetEmail(tkn: string) { + const token = await Token.findOrThrowNotFound(tkn) const user = await this.userService.getByToken(token) - user.email = email + user.email = token.value await user.save() } - public async confirmPasswordChange(password: string, token: string) { + public async resetPassword(tkn: string) { + const token = await Token.findOrThrowNotFound(tkn) const user = await this.userService.getByToken(token) - /** - * Password is already hashed before sending - * the data to queue. - */ - user.password = password + user.password = token.value await user.save() } - public async confirmEmailPasswordChange( - email: string, - password: string, - token: string - ) { + public async resetEmailPassword(tkn: string) { + const token = await Token.findOrThrowNotFound(tkn) const user = await this.userService.getByToken(token) + const [email, password] = JSON.parse(token.value) + user.email = email user.password = password diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 9a448a1..b3decf3 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -3,6 +3,7 @@ import { Service } from '@athenna/ioc' import { User } from '#src/models/user' import { Role } from '#src/models/role' import { Queue } from '@athenna/queue' +import type { Token } from '#src/models/token' import { RoleUser } from '#src/models/roleuser' import { RoleEnum } from '#src/enums/role.enum' import { NotFoundException } from '@athenna/http' @@ -43,11 +44,13 @@ export class UserService { return user } - public async getByToken(token: string) { - const user = await User.query().where('token', token).find() + public async getByToken(token: Token) { + const user = await User.query().where('id', token.userId).find() if (!user) { - throw new NotFoundException(`Not found any user with token ${token}.`) + throw new NotFoundException( + `Not found any user with token ${token.token}.` + ) } return user @@ -56,7 +59,6 @@ export class UserService { public async update(id: number, data: Partial): Promise { const user = await this.getById(id) - const token = user.token const isEmailEqual = user.isEmailEqual(data.email) const isPasswordEqual = user.isPasswordEqual(data.password) @@ -64,36 +66,32 @@ export class UserService { case 'false:true': await Queue.queue('mail').add({ user, - token, - email: data.email, - view: 'mail/change-email', + token: await user.resetEmailToken(data.email), + view: 'mail/reset_email', subject: 'Athenna Email Change' }) break case 'true:false': - // TODO create a password_resets table to save the password - data.password = await bcrypt.hash(data.password, 10) - await Queue.queue('mail').add({ user, - token, - password: data.password, - view: 'mail/change-password', - subject: 'Athenna Email Change' + view: 'mail/reset_password', + subject: 'Athenna Email Change', + token: await user.resetPasswordToken( + await bcrypt.hash(data.password, 10) + ) }) + break case 'false:false': - // TODO create a password_resets table to save the password - data.password = await bcrypt.hash(data.password, 10) - await Queue.queue('mail').add({ user, - token, - email: data.email, - password: data.password, - view: 'mail/change-email-password', - subject: 'Athenna Email & Password Change' + view: 'mail/reset_email_password', + subject: 'Athenna Email & Password Change', + token: await user.resetEmailPasswordToken( + data.email, + await bcrypt.hash(data.password, 10) + ) }) } diff --git a/src/validators/register.validator.ts b/src/validators/register.validator.ts index fb3343a..684ec0a 100644 --- a/src/validators/register.validator.ts +++ b/src/validators/register.validator.ts @@ -5,6 +5,7 @@ import { Validator, BaseValidator } from '@athenna/validator' export class RegisterValidator extends BaseValidator { public schema = this.validator.object({ name: this.validator.string(), + cellphone: this.validator.string().optional(), email: this.validator.string().email().unique({ table: 'users' }), password: this.validator.string().minLength(8).maxLength(32).confirmed() }) @@ -13,6 +14,7 @@ export class RegisterValidator extends BaseValidator { const data = request.only([ 'name', 'email', + 'cellphone', 'password', 'password_confirmation' ]) diff --git a/src/workers/mail.worker.ts b/src/workers/mail.worker.ts index 2b1526d..8b0cbac 100644 --- a/src/workers/mail.worker.ts +++ b/src/workers/mail.worker.ts @@ -6,8 +6,6 @@ type Job = { view: string subject: string user: User - email: string - password: string token: string } @@ -21,12 +19,7 @@ export class MailWorker extends BaseWorker { await Mail.from('noreply@athenna.io') .to(data.user.email) .subject(data.subject) - .view(data.view, { - user: data.user, - email: data.email, - password: data.password, - token: data.token - }) + .view(data.view, { user: data.user, token: data.token }) .send() } } diff --git a/tests/e2e/auth.controller.test.ts b/tests/e2e/auth.controller.test.ts index 3795e60..beb9858 100644 --- a/tests/e2e/auth.controller.test.ts +++ b/tests/e2e/auth.controller.test.ts @@ -2,9 +2,11 @@ import bcrypt from 'bcrypt' import { Queue } from '@athenna/queue' import { User } from '#src/models/user' import { Role } from '#src/models/role' +import { Token } from '#src/models/token' import { SmtpServer } from '@athenna/mail' import { Database } from '@athenna/database' import { RoleUser } from '#src/models/roleuser' +import { TokenEnum } from '#src/enums/token.enum' import { BaseE2ETest } from '#tests/helpers/base.e2e.test' import { Test, type Context, AfterAll, BeforeAll, Cleanup } from '@athenna/test' @@ -130,8 +132,6 @@ export default class AuthControllerTest extends BaseE2ETest { const queue = Queue.queue('mail') - assert.deepEqual(await queue.length(), 1) - assert.isTrue(await User.exists({ email: 'test@athenna.io' })) response.assertStatusCode(201) response.assertBodyContains({ data: { @@ -139,6 +139,9 @@ export default class AuthControllerTest extends BaseE2ETest { email: 'test@athenna.io' } }) + + assert.deepEqual(await queue.length(), 1) + assert.isTrue(await User.exists({ email: 'test@athenna.io' })) } @Test() @@ -173,7 +176,7 @@ export default class AuthControllerTest extends BaseE2ETest { } @Test() - public async shouldThrowValidationErrorWhenPasswordLenghtIsLessThenEightWhenRegisteringUser({ request }: Context) { + public async shouldThrowValidationErrorWhenPasswordLengthIsLessThenEightWhenRegisteringUser({ request }: Context) { const response = await request.post('/api/v1/register', { body: { name: 'Test', @@ -262,11 +265,10 @@ export default class AuthControllerTest extends BaseE2ETest { @Test() public async shouldBeAbleToConfirmUserAccount({ assert, request }: Context) { const user = await User.factory().create({ emailVerifiedAt: null }) + const token = await Token.factory().create({ userId: user.id, type: TokenEnum.CONFIRM_ACCOUNT }) const response = await request.get('/api/v1/confirm/account', { - query: { - token: user.token - } + query: { token: token.token } }) await user.refresh() @@ -278,9 +280,7 @@ export default class AuthControllerTest extends BaseE2ETest { @Test() public async shouldThrowNotFoundExceptionIfTokenDoesNotExistWhenConfirmingAccount({ request }: Context) { const response = await request.get('/api/v1/confirm/account', { - query: { - token: 'not-found' - } + query: { token: 'not-found' } }) response.assertStatusCode(404) @@ -294,28 +294,25 @@ export default class AuthControllerTest extends BaseE2ETest { } @Test() - public async shouldBeAbleToConfirmUserEmail({ assert, request }: Context) { + public async shouldBeAbleToConfirmUserEmailReset({ assert, request }: Context) { + const email = 'newemail@athenna.io' const user = await User.factory().create() + const token = await Token.factory().create({ value: email, userId: user.id, type: TokenEnum.EMAIL }) - const response = await request.get('/api/v1/confirm/email', { - query: { - token: user.token, - email: 'newemail@athenna.io' - } + const response = await request.get('/api/v1/reset/email', { + query: { token: token.token } }) await user.refresh() - assert.deepEqual(user.email, 'newemail@athenna.io') + assert.deepEqual(user.email, email) response.assertStatusCode(204) } @Test() public async shouldThrowNotFoundExceptionIfTokenDoesNotExistWhenConfirmingEmail({ request }: Context) { - const response = await request.get('/api/v1/confirm/email', { - query: { - token: 'not-found' - } + const response = await request.get('/api/v1/reset/email', { + query: { token: 'not-found' } }) response.assertStatusCode(404) @@ -329,14 +326,13 @@ export default class AuthControllerTest extends BaseE2ETest { } @Test() - public async shouldBeAbleToConfirmUserPassword({ assert, request }: Context) { + public async shouldBeAbleToConfirmUserPasswordReset({ assert, request }: Context) { + const password = await bcrypt.hash('1234567', 10) const user = await User.factory().create() + const token = await Token.factory().create({ value: password, userId: user.id, type: TokenEnum.PASSWORD }) - const response = await request.get('/api/v1/confirm/password', { - query: { - token: user.token, - password: await bcrypt.hash('1234567', 10) - } + const response = await request.get('/api/v1/reset/password', { + query: { token: token.token } }) await user.refresh() @@ -347,10 +343,8 @@ export default class AuthControllerTest extends BaseE2ETest { @Test() public async shouldThrowNotFoundExceptionIfTokenDoesNotExistWhenConfirmingPassword({ request }: Context) { - const response = await request.get('/api/v1/confirm/password', { - query: { - token: 'not-found' - } + const response = await request.get('/api/v1/reset/password', { + query: { token: 'not-found' } }) response.assertStatusCode(404) @@ -364,15 +358,19 @@ export default class AuthControllerTest extends BaseE2ETest { } @Test() - public async shouldBeAbleToConfirmUserEmailPassword({ assert, request }: Context) { + public async shouldBeAbleToConfirmUserEmailPasswordReset({ assert, request }: Context) { + const email = 'newemaill@athenna.io' + const password = await bcrypt.hash('1234567', 10) + const emailPassword = JSON.stringify([email, password]) const user = await User.factory().create() + const token = await Token.factory().create({ + value: emailPassword, + userId: user.id, + type: TokenEnum.EMAIL_PASSWORD + }) - const response = await request.get('/api/v1/confirm/email/password', { - query: { - token: user.token, - email: 'newemaill@athenna.io', - password: await bcrypt.hash('1234567', 10) - } + const response = await request.get('/api/v1/reset/email/password', { + query: { token: token.token } }) await user.refresh() @@ -384,10 +382,8 @@ export default class AuthControllerTest extends BaseE2ETest { @Test() public async shouldThrowNotFoundExceptionIfTokenDoesNotExistWhenConfirmingEmailPassword({ request }: Context) { - const response = await request.get('/api/v1/confirm/email/password', { - query: { - token: 'not-found' - } + const response = await request.get('/api/v1/reset/email/password', { + query: { token: 'not-found' } }) response.assertStatusCode(404) diff --git a/tests/unit/auth.service.test.ts b/tests/unit/auth.service.test.ts index 3533320..a193cac 100644 --- a/tests/unit/auth.service.test.ts +++ b/tests/unit/auth.service.test.ts @@ -6,6 +6,7 @@ import { UserService } from '#src/services/user.service' import { AuthService } from '#src/services/auth.service' import { NotFoundException, UnauthorizedException } from '@athenna/http' import { Test, type Context, Mock, AfterEach, BeforeEach } from '@athenna/test' +import { User } from '#src/models/user' export default class AuthServiceTest { private userService: UserService @@ -87,7 +88,7 @@ export default class AuthServiceTest { Mail.when('send').resolve(undefined) Queue.when('queue').return({ add: () => {} }) - Mock.when(this.userService, 'create').resolve(userToRegister) + Mock.when(this.userService, 'create').resolve(User.factory().make(userToRegister)) const authService = new AuthService(this.userService) const user = await authService.register(userToRegister) diff --git a/tests/unit/user.service.test.ts b/tests/unit/user.service.test.ts index 16aace7..c7216ef 100644 --- a/tests/unit/user.service.test.ts +++ b/tests/unit/user.service.test.ts @@ -1,5 +1,5 @@ -import { Uuid } from '@athenna/common' import { User } from '#src/models/user' +import { Token } from '#src/models/token' import { RoleEnum } from '#src/enums/role.enum' import { NotFoundException } from '@athenna/http' import { UserService } from '#src/services/user.service' @@ -90,24 +90,24 @@ export default class UserServiceTest { } @Test() - public async shouldBeAbleToGetAnUserBytoken({ assert }: Context) { - const fakeUser = await User.factory().count(1).make() + public async shouldBeAbleToGetAnUserByToken({ assert }: Context) { + const fakeUser = await User.factory().make() + const token = await Token.factory().make({ userId: fakeUser.id }) - Mock.when(Database.driver, 'where').returnThis() Mock.when(Database.driver, 'find').resolve(fakeUser) - const token = Uuid.generate() const user = await new UserService().getByToken(token) assert.deepEqual(user.toJSON(), fakeUser.toJSON()) - assert.calledWith(Database.driver.where, 'token', token) } @Test() - public async shouldThrowNotFoundExceptionIftokenDoesNotExist({ assert }: Context) { + public async shouldThrowNotFoundExceptionIfTokenDoesNotExist({ assert }: Context) { Mock.when(Database.driver, 'find').resolve(undefined) - await assert.rejects(() => new UserService().getByToken(Uuid.generate()), NotFoundException) + const token = await Token.factory().make() + + await assert.rejects(() => new UserService().getByToken(token), NotFoundException) } @Test()