diff --git a/.env b/.env index d2088d89f..f1c495d9f 100644 --- a/.env +++ b/.env @@ -1,23 +1,37 @@ +# Database +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=postgres +DATABASE_NAME=postgres +# App +API_PORT=3333 +API_NAME=bsafe-api +API_DOCKERFILE= +API_ENVIRONMENT=development +UI_URL=http://localhost:5175 +API_URL=http://localhost:3333 -# Database -DATABASE_HOST = 127.0.0.1 -DATABASE_PORT = 5432 -DATABASE_USERNAME = postgres -DATABASE_PASSWORD = postgres -DATABASE_NAME = postgres +# Admin user +APP_ADMIN_EMAIL=admin_user_email +APP_ADMIN_PASSWORD=admin_user_password -DATABASE_PORT_TEST = 5432 -# APP -API_PORT=3333 -APP_NAME=bsafe-api # ADMIN USER APP_ADMIN_EMAIL=admin_user_email APP_ADMIN_PASSWORD=admin_user_password + # TOKENS ACCESS_TOKEN_SECRET=access_token_secret REFRESH_TOKEN_SECRET=refresh_token_secret + # AWS -UI_URL=https://app.bsafe.pro +AWS_SMTP_USER= +AWS_SMTP_PASS= + +# EMAIL +EMAIL_FROM="Bsafe " +MAIL_TESTING_NOTIFICATIONS=guilhermemigroque@gmail.com + diff --git a/.env.example b/.env.example index e07efffe2..f1c495d9f 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,37 @@ # Database -DATABASE_HOST = 127.0.0.1 -DATABASE_PORT = 5432 -DATABASE_USERNAME = database_username -DATABASE_PASSWORD = database_password -DATABASE_NAME = database_name +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=postgres +DATABASE_NAME=postgres # App -API_PORT = 3333 +API_PORT=3333 +API_NAME=bsafe-api +API_DOCKERFILE= +API_ENVIRONMENT=development +UI_URL=http://localhost:5175 +API_URL=http://localhost:3333 # Admin user -APP_ADMIN_EMAIL = admin_user_email -APP_ADMIN_PASSWORD = admin_user_password +APP_ADMIN_EMAIL=admin_user_email +APP_ADMIN_PASSWORD=admin_user_password -# Tokens -ACCESS_TOKEN_SECRET = access_token_secret -REFRESH_TOKEN_SECRET = refresh_token_secret -TOKEN_EXPIRTATION_TIME = 15 + +# ADMIN USER +APP_ADMIN_EMAIL=admin_user_email +APP_ADMIN_PASSWORD=admin_user_password + +# TOKENS +ACCESS_TOKEN_SECRET=access_token_secret +REFRESH_TOKEN_SECRET=refresh_token_secret + +# AWS +AWS_SMTP_USER= +AWS_SMTP_PASS= + +# EMAIL +EMAIL_FROM="Bsafe " +MAIL_TESTING_NOTIFICATIONS=guilhermemigroque@gmail.com -#UI -UI_URL=http://localhost:5175 -#UI_URL=https://app.bsafe.pro diff --git a/.gitignore b/.gitignore index 4def756fc..e206e2456 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ yarn.lock package-lock.json .editorconfig bsafe/ + #env -.env +.env.staging +.env.prod # Mac OS .DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..df8180d9c --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +prod: + sudo docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d + +stg: + sudo docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d + diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 9cb3b2414..000000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3' -services: - api: - container_name: bsafe_api - ports: - - '3333:3333' - build: - dockerfile: Dockerfile - context: . - working_dir: /api - volumes: - - ./uploads:/api/uploads - environment: - - NODE_ENV=production - # DATABASE - - DATABASE_HOST=roundhouse.proxy.rlwy.net - - DATABASE_PORT=31750 - - DATABASE_USERNAME=postgres - - DATABASE_PASSWORD=f14bEaAF4baEa1cfE26GAa3EEEdaAgBe - - DATABASE_NAME=railway - # APP - - API_PORT=3333 - - APP_NAME=bsafe-api - # ADMIN USER - - APP_ADMIN_EMAIL=admin_user_email - - APP_ADMIN_PASSWORD=admin_user_password - # TOKENS - - ACCESS_TOKEN_SECRET=access_token_secret - - REFRESH_TOKEN_SECRET=refresh_token_secret - # AWS - - UI_URL=https://app.bsafe.pro - - API_URL=https://api-multsig.infinitybase.com - # DISCORD - - DISCORD_WEBHOOK=https://discord.com/api/webhooks/1179911741568733295/Y2Re5SpoxBzwLMZfmZyHqCAIp0sr3NvXc6DCfrpMgNY7KO-bGkOJg2FBiTmJbAnvgD2b diff --git a/docker-compose.stg.yml b/docker-compose.stg.yml deleted file mode 100644 index ca137023b..000000000 --- a/docker-compose.stg.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '3' -services: - api: - container_name: bsafe_api - ports: - - '3333:3333' - build: - dockerfile: Dockerfile.stg - context: . - working_dir: /api - volumes: - - ./uploads:/api/uploads - environment: - - NODE_ENV=staging - # DATABASE - - DATABASE_HOST=containers-us-west-65.railway.app - - DATABASE_PORT=6343 - - DATABASE_USERNAME=postgres - - DATABASE_PASSWORD=5sQtQXIrR6HLECTVB8SI - - DATABASE_NAME=railway - # APP - - API_PORT=3333 - - APP_NAME=bsafe-api - # ADMIN USER - - APP_ADMIN_EMAIL=admin_user_email - - APP_ADMIN_PASSWORD=admin_user_password - # TOKENS - - ACCESS_TOKEN_SECRET=access_token_secret - - REFRESH_TOKEN_SECRET=refresh_token_secret - # AWS - - UI_URL=https://app.bsafe.pro - - API_URL=https://stg-api.bsafe.pro diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..b44244ed3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' +services: + api: + container_name: ${API_NAME} + ports: + - '${API_PORT}:${API_PORT}' + build: + dockerfile: ${API_DOCKERFILE} + context: . + working_dir: /api + volumes: + - ./uploads:/api/uploads + environment: + - NODE_ENV=${API_ENVIRONMENT} + # DATABASE + - DATABASE_HOST=${DATABASE_HOST} + - DATABASE_PORT=${DATABASE_PORT} + - DATABASE_USERNAME=${DATABASE_USERNAME} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_NAME=${DATABASE_NAME} + # APP + - API_PORT=${API_PORT} + - APP_NAME=${API_NAME} + # ADMIN USER + - APP_ADMIN_EMAIL=${APP_ADMIN_EMAIL} + - APP_ADMIN_PASSWORD=${APP_ADMIN_PASSWORD} + # TOKENS + - ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET} + - REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET} + # AWS + - UI_URL=${UI_URL} + - API_URL=${API_URL} + - AWS_SMTP_USER=${AWS_SMTP_USER} + - AWS_SMTP_PASS=${AWS_SMTP_PASS} + # EMAIL + - EMAIL_FROM=${EMAIL_FROM} + - MAIL_TESTING_NOTIFICATIONS=${MAIL_TESTING_NOTIFICATIONS} diff --git a/jest.config.ts b/jest.config.ts index 83baea89f..98df78892 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -14,6 +14,7 @@ const config: JestConfigWithTsJest = { '^@utils/(.*)$': '/src/utils/$1', '^@mocks/(.*)$': '/src/mocks/$1', }, + testPathIgnorePatterns: ['/node_modules/', '/build/'], }; export default config; diff --git a/package.json b/package.json index 0cd27ffb7..7e9b89ead 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "test": "chmod +x ./scripts/init_test.sh && ./scripts/init_test.sh", "start": "pm2-runtime start ./build/server/index.js", "build": "tsc --project . && tscpaths -p tsconfig.json -s ./src -o ./build", + "copyFiles": "copyfiles --error --up 1 src/**/*.html build", + "postbuild": "yarn run copyFiles", "migration:run": "ts-node -r tsconfig-paths/register -r dotenv/config ./node_modules/typeorm/cli.js migration:run", "migration:create": "ts-node -r tsconfig-paths/register -r dotenv/config ./node_modules/typeorm/cli.js migration:create -n", "migration:revert": "ts-node -r tsconfig-paths/register -r dotenv/config ./node_modules/typeorm/cli.js migration:revert", @@ -16,15 +18,19 @@ "database:dev": "make -C ./docker/database start env_file=.env.dev", "database:stop": "make -C ./docker/database start", "database:start": "make -C ./docker/database start", - "postinstall": "patch-package --use-yarn" + "postinstall": "patch-package --use-yarn", + "run:prod": "make -C ./ prod env_file=.env.prod", + "run:stg": "make -C ./ stg env_file=.env.staging" }, "dependencies": { "axios": "1.5.1", "bcrypt": "5.1.0", "body-parser": "1.20.2", "bsafe": "0.0.35", + "cheerio": "1.0.0-rc.12", "class-validator": "0.14.0", "cookie-parser": "1.4.6", + "copyfiles": "2.4.1", "cors": "2.8.5", "date-fns": "2.30.0", "dotenv": "8.2.0", @@ -33,6 +39,7 @@ "joi": "17.4.0", "jsonwebtoken": "9.0.1", "morgan": "1.10.0", + "nodemailer": "6.9.8", "patch-package": "8.0.0", "pg": "8.5.1", "pm2": "5.3.0", diff --git a/src/config/database.ts b/src/config/database.ts index 18eb9d02f..e6c30dd83 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -12,7 +12,6 @@ const { DATABASE_NAME, DATABASE_PORT, NODE_ENV, - DATABASE_PORT_TEST, } = process.env; const [host, port] = String(DATABASE_URL).split(':'); diff --git a/src/database/migrations/1696881120692-create-table-vault-template.ts b/src/database/migrations/1696881120692-create-table-vault-template.ts index b12823867..53b075f26 100644 --- a/src/database/migrations/1696881120692-create-table-vault-template.ts +++ b/src/database/migrations/1696881120692-create-table-vault-template.ts @@ -25,7 +25,7 @@ export class addTableVaultTemplate1696881120692 implements MigrationInterface { isNullable: true, }, { - name: 'min_signers', + name: 'minSigners', type: 'integer', }, { diff --git a/src/database/migrations/1702391257625-add-notifications-columns-to-users-table.ts b/src/database/migrations/1702391257625-add-notifications-columns-to-users-table.ts new file mode 100644 index 000000000..eac14cc04 --- /dev/null +++ b/src/database/migrations/1702391257625-add-notifications-columns-to-users-table.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class addNotificationsColumnsToUsersTable1702391257625 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns('users', [ + new TableColumn({ + name: 'first_login', + type: 'boolean', + default: true, + }), + new TableColumn({ + name: 'notify', + type: 'boolean', + default: false, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumns('users', ['first_login', 'notify']); + } +} diff --git a/src/database/migrations/1703070591761-create-table-workspace.ts b/src/database/migrations/1703070591761-create-table-workspace.ts new file mode 100644 index 000000000..6ee3191e9 --- /dev/null +++ b/src/database/migrations/1703070591761-create-table-workspace.ts @@ -0,0 +1,72 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class createTableWorkspace1703070591761 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'workspace', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + isUnique: true, + generationStrategy: 'uuid', + default: `uuid_generate_v4()`, + }, + { + name: 'name', + type: 'varchar', + isNullable: false, + }, + { + name: 'description', + type: 'text', + isNullable: true, + }, + { + name: 'permissions', + type: 'json', + isNullable: false, + }, + { + name: 'avatar', + type: 'varchar', + isNullable: true, + }, + { + name: 'owner_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + isNullable: true, + }, + { + name: 'deleted_at', + type: 'timestamp', + isNullable: true, + }, + ], + foreignKeys: [ + { + columnNames: ['owner_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('workspace'); + } +} diff --git a/src/database/migrations/1703076380933-add-column-workspace-in-predicates.ts b/src/database/migrations/1703076380933-add-column-workspace-in-predicates.ts new file mode 100644 index 000000000..5ff436d74 --- /dev/null +++ b/src/database/migrations/1703076380933-add-column-workspace-in-predicates.ts @@ -0,0 +1,33 @@ +import { + MigrationInterface, + QueryRunner, + TableColumn, + TableForeignKey, +} from 'typeorm'; + +const colWorkspace = new TableColumn({ + name: 'workspace_id', + type: 'uuid', + isNullable: true, +}); + +const fkWorkspacePredicate = new TableForeignKey({ + name: 'FK-workspace-predicate', + columnNames: ['workspace_id'], + referencedColumnNames: ['id'], + referencedTableName: 'workspace', + onDelete: 'CASCADE', +}); + +export class addColumnWorkspaceInPredicates1703076380933 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('predicates', colWorkspace); + await queryRunner.createForeignKey('predicates', fkWorkspacePredicate); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropForeignKey('predicates', fkWorkspacePredicate); + await queryRunner.dropColumn('predicates', colWorkspace); + } +} diff --git a/src/database/migrations/1703078040646-create-table-pivot-users-workspace.ts b/src/database/migrations/1703078040646-create-table-pivot-users-workspace.ts new file mode 100644 index 000000000..bfb268285 --- /dev/null +++ b/src/database/migrations/1703078040646-create-table-pivot-users-workspace.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class createTablePivotUsersWorkspace1703078040646 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'workspace_users', + columns: [ + { + name: 'workspace_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: false, + }, + ], + foreignKeys: [ + { + name: 'FK-workspace-workspace_users', + columnNames: ['workspace_id'], + referencedColumnNames: ['id'], + referencedTableName: 'workspace', + onDelete: 'CASCADE', + }, + { + name: 'FK-user-workspace_users', + columnNames: ['user_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('workspace_users'); + } +} diff --git a/src/database/migrations/1703078966914-create-table-seeds-monitor.ts b/src/database/migrations/1703078966914-create-table-seeds-monitor.ts new file mode 100644 index 000000000..b2cb8fb4c --- /dev/null +++ b/src/database/migrations/1703078966914-create-table-seeds-monitor.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class createTableSeedsMonitor1703078966914 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'seeds_monitor', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + isUnique: true, + generationStrategy: 'uuid', + default: `uuid_generate_v4()`, + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + isNullable: true, + }, + { + name: 'deleted_at', + type: 'timestamp', + isNullable: true, + }, + { + name: 'filename', + type: 'varchar', + isNullable: false, + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('seeds_monitor'); + } +} diff --git a/src/database/migrations/1703083669692-change-address-book-add-pk.ts b/src/database/migrations/1703083669692-change-address-book-add-pk.ts new file mode 100644 index 000000000..d71120ae4 --- /dev/null +++ b/src/database/migrations/1703083669692-change-address-book-add-pk.ts @@ -0,0 +1,45 @@ +import { + MigrationInterface, + QueryRunner, + TableColumn, + TableForeignKey, +} from 'typeorm'; + +const colWorkspace = new TableColumn({ + name: 'owner_id', + type: 'uuid', + isNullable: true, +}); + +const oldFK = new TableForeignKey({ + name: 'FK-contact-created_by', + columnNames: ['created_by'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', +}); + +const fkWorkspaceAddressBook = new TableForeignKey({ + name: 'FK-workspace-address_book', + columnNames: ['owner_id'], + referencedColumnNames: ['id'], + referencedTableName: 'workspace', + onDelete: 'CASCADE', +}); + +export class changeAddressBookAddPk1703083669692 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.dropForeignKey('address_book', oldFK); + await queryRunner.dropColumn('address_book', 'created_by'); + + await queryRunner.addColumn('address_book', colWorkspace); + await queryRunner.createForeignKey('address_book', fkWorkspaceAddressBook); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropForeignKey('address_book', fkWorkspaceAddressBook); + await queryRunner.dropColumn('address_book', colWorkspace); + + await queryRunner.createForeignKey('address_book', oldFK); + } +} diff --git a/src/database/migrations/1703155001794-add-column-workspace-on-usertoken.ts b/src/database/migrations/1703155001794-add-column-workspace-on-usertoken.ts new file mode 100644 index 000000000..8d1dc8054 --- /dev/null +++ b/src/database/migrations/1703155001794-add-column-workspace-on-usertoken.ts @@ -0,0 +1,32 @@ +import { + MigrationInterface, + QueryRunner, + TableColumn, + TableForeignKey, +} from 'typeorm'; + +const fkWorkspacePredicate = new TableForeignKey({ + name: 'FK-user_token-workspace', + columnNames: ['workspace_id'], + referencedColumnNames: ['id'], + referencedTableName: 'workspace', + onDelete: 'CASCADE', +}); + +const colWorkspace = new TableColumn({ + name: 'workspace_id', + type: 'uuid', +}); + +export class addColumnWorkspaceOnUsertoken1703155001794 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('user_tokens', colWorkspace); + await queryRunner.createForeignKey('user_tokens', fkWorkspacePredicate); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropForeignKey('user_tokens', fkWorkspacePredicate); + await queryRunner.dropColumn('user_tokens', colWorkspace); + } +} diff --git a/src/database/migrations/1703155607667-add-column-single-in-workspace.ts b/src/database/migrations/1703155607667-add-column-single-in-workspace.ts new file mode 100644 index 000000000..50a15a203 --- /dev/null +++ b/src/database/migrations/1703155607667-add-column-single-in-workspace.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +const colSingle = new TableColumn({ + name: 'single', + type: 'boolean', +}); + +export class addColumnSingleInWorkspace1703155607667 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('workspace', colSingle); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('workspace', colSingle); + } +} diff --git a/src/database/migrations/1707395773868-remove-column-utxo-of-column-assets.ts b/src/database/migrations/1707395773868-remove-column-utxo-of-column-assets.ts new file mode 100644 index 000000000..162ae3327 --- /dev/null +++ b/src/database/migrations/1707395773868-remove-column-utxo-of-column-assets.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class removeColumnUtxoOfColumnAssets1707395773868 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('assets', 'utxo'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'assets', + new TableColumn({ + name: 'utxo', + type: 'varchar', + isNullable: true, + }), + ); + } +} diff --git a/src/database/seeders/110820231840-create-users.ts b/src/database/seeders/110820231840-create-users.ts index 394470c54..a6d901707 100644 --- a/src/database/seeders/110820231840-create-users.ts +++ b/src/database/seeders/110820231840-create-users.ts @@ -1,15 +1,27 @@ import { generateInitialUsers } from '@src/mocks/initialSeeds/initialUsers'; +import { + PermissionRoles, + Workspace, + defaultPermissions, +} from '@src/models/Workspace'; +import { UserService } from '@src/modules/user/service'; import { User } from '@models/index'; export default async function () { - const existingUsers = (await User.find()).length >= 3; - - if (existingUsers) { - return; - } const users = await generateInitialUsers(); for await (const user of users) { - await User.create(user).save(); + const _user = await User.create(user).save(); + + await Workspace.create({ + name: `singleWorkspace[${_user.id}]`, + owner: _user, + permissions: { + [_user.id]: defaultPermissions[PermissionRoles.OWNER], + }, + members: [_user], + avatar: await new UserService().randomAvatar(), + single: true, + }).save(); } } diff --git a/src/database/seeders/110820231841-create-predicates.ts b/src/database/seeders/110820231841-create-predicates.ts index 305b933ac..e4203783f 100644 --- a/src/database/seeders/110820231841-create-predicates.ts +++ b/src/database/seeders/110820231841-create-predicates.ts @@ -3,10 +3,5 @@ import { generateInitialPredicate } from '@mocks/initialSeeds/initialPredicate'; import { Predicate } from '@models/index'; export default async function () { - const existingPredicates = (await Predicate.find()).length > 0; - - if (existingPredicates) { - return; - } await Predicate.create(await generateInitialPredicate()).save(); } diff --git a/src/database/seeders/120920231842-create-transactions.ts b/src/database/seeders/120920231842-create-transactions.ts index 614a78981..62d588d6c 100644 --- a/src/database/seeders/120920231842-create-transactions.ts +++ b/src/database/seeders/120920231842-create-transactions.ts @@ -3,10 +3,5 @@ import { generateInitialTransaction } from '@src/mocks/initialSeeds'; import { Transaction } from '@models/index'; export default async function () { - const existing = (await Transaction.find()).length > 0; - - if (existing) { - return; - } await Transaction.create(await generateInitialTransaction()).save(); } diff --git a/src/database/seeders/120920231842-create-vault-template.ts b/src/database/seeders/120920231842-create-vault-template.ts index 7d19c33e6..d135e85f7 100644 --- a/src/database/seeders/120920231842-create-vault-template.ts +++ b/src/database/seeders/120920231842-create-vault-template.ts @@ -1,12 +1,7 @@ import { generateInitialTemplate } from '@mocks/initialSeeds'; -import VaultTemplate from '@src/models/VaultTemplate'; +import { VaultTemplate } from '@src/models/VaultTemplate'; export default async function () { - const existing = (await VaultTemplate.find()).length > 0; - - if (existing) { - return; - } await VaultTemplate.create(await generateInitialTemplate()).save(); } diff --git a/src/database/seeders/151020230935-create-workspace.ts b/src/database/seeders/151020230935-create-workspace.ts new file mode 100644 index 000000000..ab3289c38 --- /dev/null +++ b/src/database/seeders/151020230935-create-workspace.ts @@ -0,0 +1,9 @@ +import { + generateInitialWorkspace, + generateInitialAuxWorkspace, +} from '@src/mocks/initialSeeds/initialWorkspace'; + +export default async function () { + await (await generateInitialWorkspace()).save(); + await (await generateInitialAuxWorkspace()).save(); +} diff --git a/src/database/seeders/151020230936-create-address-book.ts b/src/database/seeders/151020230936-create-address-book.ts index 3919455a6..df4117a08 100644 --- a/src/database/seeders/151020230936-create-address-book.ts +++ b/src/database/seeders/151020230936-create-address-book.ts @@ -2,12 +2,6 @@ import { generateInitialAddressBook } from '@src/mocks/initialSeeds/initialAddre import AddressBook from '@src/models/AddressBook'; export default async function () { - const existing = (await AddressBook.find()).length > 0; - - if (existing) { - return; - } - for await (const addressBook of await generateInitialAddressBook()) { await AddressBook.create(addressBook).save(); } diff --git a/src/database/seeders/151020230937-create-dapps.ts b/src/database/seeders/151020230937-create-dapps.ts index 19fefe3d6..e4fc36392 100644 --- a/src/database/seeders/151020230937-create-dapps.ts +++ b/src/database/seeders/151020230937-create-dapps.ts @@ -3,10 +3,5 @@ import { generateInitialDapp } from '@mocks/initialSeeds'; import { DApp } from '@src/models'; export default async function () { - const existing = (await DApp.find()).length > 0; - - if (existing) { - return; - } await DApp.create(await generateInitialDapp()).save(); } diff --git a/src/database/seeders/index.ts b/src/database/seeders/index.ts index d630f73be..c036443cb 100644 --- a/src/database/seeders/index.ts +++ b/src/database/seeders/index.ts @@ -1,18 +1,43 @@ import glob from 'glob'; import path from 'path'; +import { SeedsMonitor } from '@src/models/SeedsMonitor'; import Bootstrap from '@src/server/bootstrap'; const runSeeders = async () => { await Bootstrap.connectDatabase(); const files = glob.sync(`${__dirname}/**/*.{js,ts}`); - const seeders = files.filter(file => !file.includes('index')); + + const seeders: string[] = []; + files + .filter(file => !file.includes('index')) + .filter(async file => { + const filename = file.replace(__dirname, ''); + !!(await SeedsMonitor.find({ + where: { + filename, + }, + })); + }); + + for (const file of files.filter(file => !file.includes('index'))) { + const filename = file.replace(__dirname, ''); + const r = await SeedsMonitor.findOne({ + where: { + filename, + }, + }); + + !r ? seeders.push(file) : null; + } for (const seeder of seeders) { const [, seedName] = seeder.split('seeders'); const seed = await import(path.resolve(seeder)); await seed.default(); - console.log('[SEEDERS] Seed runned: ', seedName); + await SeedsMonitor.create({ + filename: seedName, + }).save(); } }; diff --git a/src/middlewares/auth/index.ts b/src/middlewares/auth/index.ts index d4a842406..a88fdb3de 100644 --- a/src/middlewares/auth/index.ts +++ b/src/middlewares/auth/index.ts @@ -1,5 +1,8 @@ import { Request, Response, NextFunction } from 'express'; +import { PermissionRoles } from '@src/models/Workspace'; +import { validatePermissionGeneral } from '@src/utils/permissionValidate'; + import { signOutPath } from '@modules/auth/routes'; import { AuthService } from '@modules/auth/services'; @@ -45,6 +48,7 @@ async function authMiddleware(req: Request, res: Response, next: NextFunction) { requestAuth.user = userToken.user; requestAuth.userToken = userToken; + requestAuth.workspace = userToken.workspace; return next(); } catch (e) { @@ -52,4 +56,70 @@ async function authMiddleware(req: Request, res: Response, next: NextFunction) { } } -export { authMiddleware }; +//todo: if required permission to specific vault, check on request this vault ID +function authPermissionMiddleware(permission?: PermissionRoles[]) { + return async function (req: Request, res: Response, next: NextFunction) { + try { + const requestAuth: IAuthRequest = req; + + if (!permission || permission.length === 0) return next(); + const { user, workspace } = requestAuth; + + // if not required info + if (!user || !workspace) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_CREDENTIALS, + detail: 'Some required credentials are missing', + }); + } + + // if not required premission info + if (!workspace.permissions[user.id]) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_PERMISSION, + detail: 'You do not have permission to access this resource', + }); + } + + // DEBUG VALIDATIONS + // const myValidation = `${req.method}-${req.baseUrl}${req.path}`; + // const combination = 'POST-/predicate/'; + + // if (combination === myValidation) { + // console.log('[validacao]: ', { + // //workspace: workspace.permissions, + // user: { + // id: user.id, + // name: user.name, + // address: user.address, + // }, + // permission: permission, + // user_p: workspace.permissions[user.id], + // validations: { + // a: !!workspace.permissions[user.id], + // b: permission.length === 0, + // c: permission.filter(p => + // workspace.permissions[user.id][p].includes('*'), + // ), + // }, + // }); + // } + + if (validatePermissionGeneral(workspace, user.id, permission)) return next(); + + // if not required premissions + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_PERMISSION, + detail: 'You do not have permission to access this resource', + }); + } catch (e) { + return next(e); + //return e; + } + }; +} + +export { authMiddleware, authPermissionMiddleware }; diff --git a/src/middlewares/auth/types.ts b/src/middlewares/auth/types.ts index cb139d5a9..7a24b4b66 100644 --- a/src/middlewares/auth/types.ts +++ b/src/middlewares/auth/types.ts @@ -2,6 +2,8 @@ import { Request } from 'express'; import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import { ParsedQs } from 'qs'; +import { Workspace } from '@src/models/Workspace'; + import UserToken from '@models/UserToken'; import { User } from '@models/index'; @@ -14,6 +16,7 @@ export interface AuthValidatedRequest accessToken?: string; user?: User; userToken?: UserToken; + workspace?: Workspace; } export type IAuthRequest = AuthValidatedRequest; diff --git a/src/mocks/initialSeeds/initialAddressBook.ts b/src/mocks/initialSeeds/initialAddressBook.ts index 56f2fa9b6..78d3b2ed1 100644 --- a/src/mocks/initialSeeds/initialAddressBook.ts +++ b/src/mocks/initialSeeds/initialAddressBook.ts @@ -1,18 +1,21 @@ import { User } from '@src/models'; import AddressBook from '@src/models/AddressBook'; +import { Workspace } from '@src/models/Workspace'; import { accounts } from '../accounts'; export const generateInitialAddressBook = async (): Promise< Partial[] > => { - const owner = await User.findOne({ - where: { address: accounts['USER_1'].address }, + const owner = await Workspace.findOne({ + order: { + createdAt: 'DESC', + }, }); const a1: Partial = { nickname: 'Store', - createdBy: owner, + owner, user: await User.findOne({ where: { address: accounts['STORE'].address }, }), @@ -20,7 +23,7 @@ export const generateInitialAddressBook = async (): Promise< const a2: Partial = { nickname: 'User 2', - createdBy: owner, + owner, user: await User.findOne({ where: { address: accounts['USER_2'].address }, }), diff --git a/src/mocks/initialSeeds/initialAssets.ts b/src/mocks/initialSeeds/initialAssets.ts index 7970dea89..610050f6f 100644 --- a/src/mocks/initialSeeds/initialAssets.ts +++ b/src/mocks/initialSeeds/initialAssets.ts @@ -10,14 +10,12 @@ export const generateInitialAssets = async (): Promise[]> => { const asset1: Partial = { assetId: ETH_id, amount: bn(1000).toString(), - utxo: 'fake_utxo', to: accounts['STORE'].address, }; const asset2: Partial = { assetId: ETH_id, amount: bn(100000).toString(), - utxo: 'fake_utxo', to: accounts['STORE'].address, }; diff --git a/src/mocks/initialSeeds/initialPredicate.ts b/src/mocks/initialSeeds/initialPredicate.ts index fd322ca8a..728275dd8 100644 --- a/src/mocks/initialSeeds/initialPredicate.ts +++ b/src/mocks/initialSeeds/initialPredicate.ts @@ -1,36 +1,40 @@ +import { Vault } from 'bsafe'; import { Address, Provider } from 'fuels'; +import { In } from 'typeorm'; import { User } from '@src/models'; import { Predicate } from '@src/models/Predicate'; -import { accounts } from '../accounts'; -import { networks } from '../networks'; +import { PredicateMock } from '../predicate'; +import { generateInitialUsers } from './initialUsers'; export const generateInitialPredicate = async (): Promise> => { - const pr = await Provider.create(networks['beta4']); + const users = (await generateInitialUsers()).map(u => u.name); + const owner = await User.findOne({ - where: { address: accounts['USER_1'].address }, + where: { name: users[0] }, }); - + const { predicatePayload } = await PredicateMock.create(1, [owner.address]); const members = await User.find({ take: 3, - order: { - createdAt: 'ASC', + where: { + name: In(users), }, }); const predicate1: Partial = { - name: 'fake_name', + name: `fake_name: ${members[0].name}`, predicateAddress: Address.fromRandom().toString(), - description: 'fake_description', - minSigners: 2, - bytes: 'fake_bytes', - abi: 'fake_abi', - configurable: 'fake_configurable', - provider: pr.url, - chainId: pr.getChainId(), + description: `fake_description: ${new Date().getTime()}`, + minSigners: 1, + bytes: predicatePayload.bytes, + abi: predicatePayload.abi, + configurable: predicatePayload.configurable, + provider: predicatePayload.provider, + chainId: predicatePayload.chainId, owner, members, + createdAt: new Date(), }; return predicate1; diff --git a/src/mocks/initialSeeds/initialTemplate.ts b/src/mocks/initialSeeds/initialTemplate.ts index a8895d4af..8d509943c 100644 --- a/src/mocks/initialSeeds/initialTemplate.ts +++ b/src/mocks/initialSeeds/initialTemplate.ts @@ -1,5 +1,5 @@ import { User } from '@src/models'; -import VaultTemplate from '@src/models/VaultTemplate'; +import { VaultTemplate } from '@src/models/VaultTemplate'; import { accounts } from '../accounts'; diff --git a/src/mocks/initialSeeds/initialTransaction.ts b/src/mocks/initialSeeds/initialTransaction.ts index 867febbab..d92964793 100644 --- a/src/mocks/initialSeeds/initialTransaction.ts +++ b/src/mocks/initialSeeds/initialTransaction.ts @@ -9,7 +9,7 @@ import { generateInitialWitness } from './initialWitness'; export interface TTI extends Partial> { assets: Partial[]; - witnesses: Partial[]; + //witnesses: Partial[]; } export const generateInitialTransaction = async (): Promise => { const user = await User.findOne({ @@ -24,9 +24,9 @@ export const generateInitialTransaction = async (): Promise => { }); const assets = await generateInitialAssets(); - const witnesses = await generateInitialWitness(); + //const witnesses = await generateInitialWitness(); const transaction1: TTI = { - name: 'fake_name', + name: `fake_name ${accounts['USER_1'].address} ${TransactionStatus.AWAIT_REQUIREMENTS}`, hash: 'fake_hash', txData: JSON.parse(txData), status: TransactionStatus.AWAIT_REQUIREMENTS, @@ -46,7 +46,7 @@ export const generateInitialTransaction = async (): Promise => { createdBy: user, predicate, assets, - witnesses, + //witnesses, }; return transaction1; diff --git a/src/mocks/initialSeeds/initialUsers.ts b/src/mocks/initialSeeds/initialUsers.ts index a50bcf266..63f86e6ff 100644 --- a/src/mocks/initialSeeds/initialUsers.ts +++ b/src/mocks/initialSeeds/initialUsers.ts @@ -1,5 +1,5 @@ import { User, Languages } from '@src/models'; -import { UserService } from '@src/modules/configs/user/service'; +import { UserService } from '@src/modules/user/service'; import { accounts } from '../accounts'; import { networks } from '../networks'; @@ -8,7 +8,7 @@ export const generateInitialUsers = async (): Promise[]> => { const userService = new UserService(); const user1: Partial = { - name: `[${networks['local']}] ${accounts['STORE'].account}`, + name: `[${networks['local']}] ${accounts['STORE'].address} ${accounts['STORE'].privateKey}`, active: true, email: process.env.APP_ADMIN_EMAIL || '', password: process.env.APP_ADMIN_PASSWORD || '', @@ -16,10 +16,11 @@ export const generateInitialUsers = async (): Promise[]> => { address: accounts['STORE'].address, language: Languages.PORTUGUESE, avatar: await userService.randomAvatar(), + createdAt: new Date(), }; const user2: Partial = { - name: `[${networks['local']}] ${accounts['USER_1'].account}`, + name: `[${networks['local']}] ${accounts['USER_1'].address} ${accounts['USER_1'].privateKey}`, active: true, email: process.env.APP_ADMIN_EMAIL || '', password: process.env.APP_ADMIN_PASSWORD || '', @@ -27,10 +28,11 @@ export const generateInitialUsers = async (): Promise[]> => { provider: networks['local'], address: accounts['USER_1'].address, avatar: await userService.randomAvatar(), + createdAt: new Date(), }; const user3: Partial = { - name: `[${networks['local']}] ${accounts['USER_2'].account}`, + name: `[${networks['local']}] ${accounts['USER_2'].address} ${accounts['USER_2'].privateKey}`, active: true, email: process.env.APP_ADMIN_EMAIL || '', password: process.env.APP_ADMIN_PASSWORD || '', @@ -38,7 +40,20 @@ export const generateInitialUsers = async (): Promise[]> => { provider: networks['local'], address: accounts['USER_2'].address, avatar: await userService.randomAvatar(), + createdAt: new Date(), }; - return [user1, user2, user3]; + const user4: Partial = { + name: `[${networks['local']}] ${accounts['USER_3'].address} ${accounts['USER_3'].privateKey}`, + active: true, + email: process.env.APP_ADMIN_EMAIL || '', + password: process.env.APP_ADMIN_PASSWORD || '', + language: Languages.PORTUGUESE, + provider: networks['local'], + address: accounts['USER_3'].address, + avatar: await userService.randomAvatar(), + createdAt: new Date(), + }; + + return [user1, user2, user3, user4]; }; diff --git a/src/mocks/initialSeeds/initialWorkspace.ts b/src/mocks/initialSeeds/initialWorkspace.ts new file mode 100644 index 000000000..fd204803a --- /dev/null +++ b/src/mocks/initialSeeds/initialWorkspace.ts @@ -0,0 +1,74 @@ +import { randomBytes } from 'crypto'; + +import { User } from '@src/models'; +import { + PermissionRoles, + Workspace, + defaultPermissions, +} from '@src/models/Workspace'; +import { UserService } from '@src/modules/user/service'; + +import { accounts } from '../accounts'; + +export const generateInitialWorkspace = async (): Promise => { + const members = await User.find({ + take: 3, + order: { + createdAt: 'ASC', + }, + }); + return Workspace.create({ + name: randomBytes(10).toString('hex'), + description: `Description ${randomBytes(10).toString('hex')}`, + avatar: await new UserService().randomAvatar(), + members, + owner: members[0], + permissions: { + [members[0].id]: { + [PermissionRoles.ADMIN]: ['*'], + [PermissionRoles.OWNER]: ['*'], + [PermissionRoles.VIEWER]: ['*'], + [PermissionRoles.SIGNER]: ['*'], + }, + [members[1].id]: { + [PermissionRoles.ADMIN]: ['*'], + [PermissionRoles.VIEWER]: ['*'], + [PermissionRoles.SIGNER]: ['*'], + }, + [members[2].id]: { + [PermissionRoles.VIEWER]: ['*'], + [PermissionRoles.SIGNER]: ['*'], + }, + }, + single: false, + }); +}; + +export const generateInitialAuxWorkspace = async (): Promise => { + const members = await User.createQueryBuilder('user') + .where('user.address IN (:...address)', { + address: [ + accounts['USER_1'].address, + accounts['USER_2'].address, + accounts['USER_3'].address, + ], + }) + .getMany(); + + const acc_1 = members.find(m => m.address === accounts['USER_1'].address); + const non_acc = members.filter(m => m.address !== accounts['USER_1'].address); + + return Workspace.create({ + name: `[INITIAL]${randomBytes(10).toString('hex')}`, + description: `Description ${randomBytes(10).toString('hex')}`, + avatar: await new UserService().randomAvatar(), + members, + owner: members[0], + permissions: { + [acc_1.id]: defaultPermissions[PermissionRoles.OWNER], + [non_acc[0].id]: defaultPermissions[PermissionRoles.VIEWER], + [non_acc[1].id]: defaultPermissions[PermissionRoles.VIEWER], + }, + single: false, + }); +}; diff --git a/src/mocks/predicate.ts b/src/mocks/predicate.ts index a2f552ea4..1faa00e69 100644 --- a/src/mocks/predicate.ts +++ b/src/mocks/predicate.ts @@ -9,13 +9,16 @@ import { defaultConfigurable } from '../utils/configurable'; export class PredicateMock { public BSAFEVaultconfigurable: IConfVault; public predicatePayload: Omit; + public vault: Vault; protected constructor( BSAFEVaultConfigurable: IConfVault, predicatePayload: Omit, + vault: Vault, ) { this.BSAFEVaultconfigurable = BSAFEVaultConfigurable; this.predicatePayload = predicatePayload; + this.vault = vault; } public static async create( @@ -43,10 +46,10 @@ export class PredicateMock { minSigners: min, bytes: vault.getBin(), abi: JSON.stringify(vault.getAbi()), - configurable: JSON.stringify(_BSAFEVaultconfigurable), + configurable: JSON.stringify({ ...vault.getConfigurable() }), addresses: _BSAFEVaultconfigurable.SIGNERS.map(signer => signer), }; - return new PredicateMock(_BSAFEVaultconfigurable, predicatePayload); + return new PredicateMock(_BSAFEVaultconfigurable, predicatePayload, vault); } } diff --git a/src/mocks/transaction.ts b/src/mocks/transaction.ts index e9b68fc0d..7557e0106 100644 --- a/src/mocks/transaction.ts +++ b/src/mocks/transaction.ts @@ -1,16 +1,35 @@ -import { bn } from 'fuels'; +import { TransactionStatus, Vault } from 'bsafe'; +import { bn, Address } from 'fuels'; import { accounts } from '@src/mocks/accounts'; import { assets } from '@src/mocks/assets'; +import { sendPredicateCoins } from '@src/utils/testUtils/Wallet'; export const transaction = { name: 'Transaction A', assets: [ { - amount: bn(1_000_000).format(), + amount: bn(1_0).format(), assetId: assets['ETH'], to: accounts['STORE'].address, }, ], witnesses: [], }; + +export const transactionMock = async (vault: Vault) => { + await sendPredicateCoins(vault, bn(1_000_0), 'ETH', accounts['FULL'].privateKey); + + const tx = await vault.BSAFEIncludeTransaction(transaction); + + const payload_transfer = { + predicateAddress: vault.address.toString(), + name: `[TESTE_MOCK] ${Address.fromRandom().toString()}`, + hash: tx.getHashTxId(), + txData: tx.transactionRequest, + status: TransactionStatus.AWAIT_REQUIREMENTS, + assets: transaction.assets, + }; + + return { tx, payload_transfer }; +}; diff --git a/src/models/AddressBook.ts b/src/models/AddressBook.ts index 820a4f88e..2b4b7e3c1 100644 --- a/src/models/AddressBook.ts +++ b/src/models/AddressBook.ts @@ -2,21 +2,21 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { Base } from './Base'; import { User } from './User'; +import { Workspace } from './Workspace'; + +export enum AddressBookType { + PERSONAL = 'PERSONAL', + WORKSPACE = 'WORKSPACE', +} @Entity('address_book') class AddressBook extends Base { @Column() nickname: string; - @Column() - created_by: string; - - @JoinColumn({ name: 'created_by' }) - @ManyToOne(() => User) - createdBy: User; - - @Column() - user_id: string; + @JoinColumn({ name: 'owner_id' }) + @ManyToOne(() => Workspace, { onDelete: 'CASCADE' }) + owner: Workspace; @JoinColumn({ name: 'user_id' }) @OneToOne(() => User) diff --git a/src/models/Asset.ts b/src/models/Asset.ts index a5c1d9619..8ecdd6293 100644 --- a/src/models/Asset.ts +++ b/src/models/Asset.ts @@ -14,9 +14,6 @@ class Asset extends Base { @Column() amount: string; - @Column() - utxo: string; - @Column({ name: 'transaction_id' }) transactionId: string; diff --git a/src/models/Predicate.ts b/src/models/Predicate.ts index c1cca7e38..38c29a186 100644 --- a/src/models/Predicate.ts +++ b/src/models/Predicate.ts @@ -4,6 +4,7 @@ import { JoinColumn, JoinTable, ManyToMany, + ManyToOne, OneToMany, OneToOne, } from 'typeorm'; @@ -11,6 +12,7 @@ import { import { Base } from './Base'; import { Transaction } from './Transaction'; import { User } from './User'; +import { Workspace } from './Workspace'; export interface PredicateMember { avatar: string; @@ -46,13 +48,17 @@ class Predicate extends Base { @Column({ nullable: true }) chainId?: number; - @OneToMany(() => Transaction, transaction => transaction.predicate) - transactions: Transaction[]; - @JoinColumn({ name: 'owner_id' }) @OneToOne(() => User) owner: User; + @JoinColumn({ name: 'workspace_id' }) + @ManyToOne(() => Workspace, workspace => workspace.predicates) + workspace: Workspace; + + @OneToMany(() => Transaction, transaction => transaction.predicate) + transactions: Transaction[]; + @ManyToMany(() => User) @JoinTable({ name: 'predicate_members', diff --git a/src/models/SeedsMonitor.ts b/src/models/SeedsMonitor.ts new file mode 100644 index 000000000..83e1679a6 --- /dev/null +++ b/src/models/SeedsMonitor.ts @@ -0,0 +1,11 @@ +import { Column, Entity } from 'typeorm'; + +import { Base } from './Base'; + +@Entity('seeds_monitor') +class SeedsMonitor extends Base { + @Column({ nullable: false }) + filename: string; +} + +export { SeedsMonitor }; diff --git a/src/models/User.ts b/src/models/User.ts index f5a39d0a1..f65269270 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,16 +1,8 @@ -import { - BeforeInsert, - BeforeUpdate, - Column, - Entity, - JoinColumn, - ManyToOne, -} from 'typeorm'; +import { BeforeInsert, BeforeUpdate, Column, Entity } from 'typeorm'; import { EncryptUtils } from '@utils/index'; import { Base } from './Base'; -import Role from './Role'; export enum Languages { ENGLISH = 'English', @@ -22,6 +14,12 @@ class User extends Base { @Column() name?: string; + @Column({ default: true }) + first_login: boolean; + + @Column({ default: false }) + notify: boolean; + @Column({ default: true }) active: boolean; diff --git a/src/models/UserToken.ts b/src/models/UserToken.ts index ab28a12b3..3ff56b12a 100644 --- a/src/models/UserToken.ts +++ b/src/models/UserToken.ts @@ -1,16 +1,8 @@ -import { - BeforeInsert, - BeforeUpdate, - Column, - Entity, - JoinColumn, - OneToOne, -} from 'typeorm'; - -import { EncryptUtils } from '@utils/index'; +import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; import { Base } from './Base'; import { User } from './User'; +import { Workspace } from './Workspace'; export enum Encoder { fuel = 'fuel', @@ -34,6 +26,10 @@ class UserToken extends Base { @Column() expired_at?: Date; + @JoinColumn({ name: 'workspace_id' }) + @OneToOne(() => Workspace) + workspace: Workspace; + @Column() user_id: string; diff --git a/src/models/VaultTemplate.ts b/src/models/VaultTemplate.ts index 74b0b666a..9728ed36e 100644 --- a/src/models/VaultTemplate.ts +++ b/src/models/VaultTemplate.ts @@ -19,7 +19,7 @@ class VaultTemplate extends Base { @Column() description: string; - @Column({ name: 'min_signers' }) + @Column() minSigners: number; @JoinColumn({ name: 'created_by' }) @@ -35,4 +35,4 @@ class VaultTemplate extends Base { addresses: User[]; } -export default VaultTemplate; +export { VaultTemplate }; diff --git a/src/models/Witness.ts b/src/models/Witness.ts index f7751e8c9..700a69ffb 100644 --- a/src/models/Witness.ts +++ b/src/models/Witness.ts @@ -1,6 +1,4 @@ -import { BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; - -import { TransactionService } from '@src/modules/transaction/services'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { Base } from './Base'; import { Transaction } from './Transaction'; diff --git a/src/models/Workspace.ts b/src/models/Workspace.ts new file mode 100644 index 000000000..8fe0ca61d --- /dev/null +++ b/src/models/Workspace.ts @@ -0,0 +1,124 @@ +import { + BeforeInsert, + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + OneToMany, + OneToOne, +} from 'typeorm'; + +import AddressBook from './AddressBook'; +import { Base } from './Base'; +import { Predicate } from './Predicate'; +import { User } from './User'; + +/** + * vaults -> MUITOS VAULTS <-> 1 WORKSPACE + * book -> MUITOS BOOKS <-> 1 WORKSPACE + * + * users -> MUITOS USERS <-> MUITOS WORKSPACES + */ + +/** + * PERMISSIONS TYPING + */ +export enum PermissionRoles { + OWNER = 'OWNER', // owner of the workspace, THIS ROLE CAN'T BE CHANGED + ADMIN = 'ADMIN', + MANAGER = 'MANAGER', + SIGNER = 'SIGNER', + VIEWER = 'VIEWER', +} + +//todo: change to specific permissions of each role depends the complete flow +export const defaultPermissions = { + [PermissionRoles.OWNER]: { + OWNER: ['*'], + ADMIN: [''], + MANAGER: [''], + SIGNER: [''], + VIEWER: [''], + }, + [PermissionRoles.ADMIN]: { + OWNER: [''], + ADMIN: ['*'], + MANAGER: [''], + SIGNER: [''], + VIEWER: [''], + }, + [PermissionRoles.MANAGER]: { + OWNER: [''], + ADMIN: [''], + MANAGER: ['*'], + SIGNER: [''], + VIEWER: [''], + }, + [PermissionRoles.SIGNER]: { + OWNER: [''], + ADMIN: [''], + MANAGER: [''], + SIGNER: [''], + VIEWER: [''], + }, + [PermissionRoles.VIEWER]: { + OWNER: [''], + ADMIN: [''], + MANAGER: [''], + SIGNER: [''], + VIEWER: ['*'], + }, +}; + +export interface IPermissions { + [key: string]: { + [key in PermissionRoles]: string[]; + }; +} +/** + * PERMISSIONS TYPING + */ + +@Entity('workspace') +class Workspace extends Base { + @Column() + name: string; + + @Column({ nullable: true }) + description: string; + + @Column({ nullable: true }) + avatar: string; + + @JoinColumn({ name: 'owner_id' }) + @OneToOne(() => User, { cascade: true }) + owner: User; + + @Column() // if true, the workspace is a single workspace + single: boolean; + + @Column({ + name: 'permissions', + type: 'jsonb', + }) + permissions: IPermissions; + + @JoinColumn() + @OneToMany(() => Predicate, predicate => predicate.workspace, { cascade: true }) + predicates: Predicate[]; + + @JoinColumn({ name: 'address_book' }) + @OneToMany(() => AddressBook, adb => adb.owner, { cascade: true }) + addressBook: AddressBook[]; + + @ManyToMany(() => User, { cascade: true }) + @JoinTable({ + name: 'workspace_users', + joinColumn: { name: 'workspace_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'user_id', referencedColumnName: 'id' }, + }) + members: User[]; +} + +export { Workspace }; diff --git a/src/modules/addressBook/__tests__/addressBook.tests.ts b/src/modules/addressBook/__tests__/addressBook.tests.ts index ef60646a0..9a27c34f6 100644 --- a/src/modules/addressBook/__tests__/addressBook.tests.ts +++ b/src/modules/addressBook/__tests__/addressBook.tests.ts @@ -1,42 +1,161 @@ +import { Address } from 'fuels'; + import { accounts } from '@src/mocks/accounts'; +import { providers } from '@src/mocks/networks'; import { networks } from '@src/mocks/networks'; import { AuthValidations } from '@src/utils/testUtils/Auth'; describe('[ADDRESS_BOOK]', () => { let api: AuthValidations; + let single_workspace: string; beforeAll(async () => { api = new AuthValidations(networks['local'], accounts['USER_1']); await api.create(); await api.createSession(); + single_workspace = api.workspace.id; }); - test(`List address book of user ${accounts['USER_2'].address}`, async () => { - //todo: fix this request using pagination - // const params = { - // page: 1, - // perPage: 10, - // //orderBy: 'createdAt', - // //sort: 'DESC', - // }; - const { data } = await api.axios.get('/address-book/'); - - expect(data).toHaveProperty('[0]', expect.any(Object)); - //expect(data.addressBook).toHaveLength(1); - }); + test( + 'Create address book using a personal workspace', + async () => { + const nickname = `[FAKE_CONTACT_NAME]: ${Address.fromRandom().toAddress()}`; + const address = Address.fromRandom().toAddress(); + const { data } = await api.axios.post('/address-book/', { + nickname, + address, + }); + + const aux = await api.axios + .post('/address-book/', { + nickname, + address, + }) + .catch(e => e.response.data); + + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('nickname', nickname); + expect(data).toHaveProperty('user.address', address); + + expect(aux).toHaveProperty('detail', 'Duplicated nickname'); + }, + 5 * 1000, + ); test( - 'Create address book', + 'Create address book using a group workspace', async () => { + const { data: data_user1 } = await api.axios.post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()} - Create user test`, + }); + const { data: data_user2 } = await api.axios.post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()} - Create user test`, + }); + + const { data: _data, status } = await api.axios.post(`/workspace/`, { + name: '[ADDBOOK_TEST] Workspace 1', + description: '[ADDBOOK_TEST] Workspace 1 description', + members: [data_user1.id, data_user2.id], + }); + + await api.selectWorkspace(_data.id); + const nickname = `[FAKE_CONTACT_NAME]: ${Address.fromRandom().toAddress()}`; + const address = Address.fromRandom().toAddress(); const { data } = await api.axios.post('/address-book/', { - nickname: 'fake_name', - address: accounts['USER_1'].address, + nickname, + address, }); + const aux = await api.axios + .post('/address-book/', { + nickname, + address, + }) + .catch(e => e.response.data); + expect(data).toHaveProperty('id'); - expect(data).toHaveProperty('name', 'fake_name'); - expect(data).toHaveProperty('address', accounts['USER_2'].address); + expect(data).toHaveProperty('nickname', nickname); + expect(data).toHaveProperty('user.address', address); + + expect(aux).toHaveProperty('detail', 'Duplicated nickname'); }, 5 * 1000, ); + + test(`list addressBook`, async () => { + //list with single workspace [your address book] + await api.axios.get(`/address-book`).then(({ data, status }) => { + expect(status).toBe(200); + data.forEach(element => { + expect(element).toHaveProperty('nickname'); + expect(element.user).toHaveProperty('address'); + + expect(element.owner).toHaveProperty('id', api.workspace.id); + }); + }); + + //list with workspace + const auth = new AuthValidations(networks['local'], accounts['USER_1']); + await auth.create(); + await auth.createSession(); + + const old_workspace = api.workspace.id; + + await auth.axios.get(`/workspace/by-user`).then(({ data, status }) => { + const new_workspace = data.find(i => i.id !== old_workspace); + const owners = [new_workspace.id, api.workspace.id]; + + //with pagination + const page = 1; + const perPage = 8; + auth.axios.get(`/address-book`).then(({ data, status }) => { + expect(status).toBe(200); + expect(data.data).toBeLessThanOrEqual(perPage); + expect(data).toHaveProperty('total'); + expect(data).toHaveProperty('currentPage', page); + expect(data).toHaveProperty('perPage', perPage); + }); + + //with personal contacts + auth.axios.get(`/address-book`).then(({ data, status }) => { + data.forEach(element => { + expect(status).toBe(200); + expect(element).toHaveProperty('id'); + expect(element).toHaveProperty('nickname'); + expect(element.user).toHaveProperty('address'); + expect(element.owner).toHaveProperty('id', new_workspace.id); + }); + }); + + //without personal contacts + auth.selectWorkspace(new_workspace.id); + auth.axios.get(`/address-book`).then(({ data, status }) => { + data.forEach(element => { + expect(status).toBe(200); + expect(element).toHaveProperty('id'); + expect(element).toHaveProperty('nickname'); + expect(element.user).toHaveProperty('address'); + expect(element.owner).toHaveProperty('id', new_workspace.id); + }); + }); + + auth.axios + .get(`/address-book?includePersonal=true`) + .then(({ data, status }) => { + data.forEach(element => { + expect(status).toBe(200); + expect(element).toHaveProperty('id'); + expect(element).toHaveProperty('nickname'); + expect(element.user).toHaveProperty('address'); + expect(element.owner).toHaveProperty('id', new_workspace.id); + const aux = owners.includes(element.owner.id); + expect(aux).toBe(true); + }); + }); + }); + }); }); diff --git a/src/modules/addressBook/controller.ts b/src/modules/addressBook/controller.ts index adfa9c22c..069d32057 100644 --- a/src/modules/addressBook/controller.ts +++ b/src/modules/addressBook/controller.ts @@ -1,11 +1,14 @@ import AddressBook from '@src/models/AddressBook'; import Role from '@src/models/Role'; +import { Workspace } from '@src/models/Workspace'; import Internal from '@src/utils/error/Internal'; import { ErrorTypes, error } from '@utils/error'; import { Responses, bindMethods, successful } from '@utils/index'; -import { IUserService } from '../configs/user/types'; +import { IUserService } from '../user/types'; +import { WorkspaceService } from '../workspace/services'; +import { AddressBookService } from './services'; import { IAddressBookService, ICreateAddressBookRequest, @@ -23,36 +26,35 @@ export class AddressBookController { bindMethods(this); } - async create({ body, user }: ICreateAddressBookRequest) { + async create(req: ICreateAddressBookRequest) { try { - const { address, nickname } = body; + const { address, nickname } = req.body; + const { workspace, user } = req; - const duplicatedNickname = await this.addressBookService + const duplicatedNickname = await new AddressBookService() .filter({ - createdBy: user.id, + owner: [workspace.id], nickname, }) - .paginate(undefined) - .list(); + .list() + .then((response: AddressBook[]) => response.length > 0); - const duplicatedAddress = await this.addressBookService + const duplicatedAddress = await new AddressBookService() .filter({ - createdBy: user.id, + owner: [workspace.id], contactAddress: address, }) .paginate(undefined) - .list(); - const hasDuplicate = - (duplicatedNickname as AddressBook[]).length || - (duplicatedAddress as AddressBook[]).length; + .list() + .then((response: AddressBook[]) => response.length > 0); + + const hasDuplicate = duplicatedNickname || duplicatedAddress; if (hasDuplicate) { throw new Internal({ type: ErrorTypes.Internal, title: 'Error on contact creation', - detail: `Duplicated ${ - (duplicatedNickname as AddressBook[]).length ? 'label' : 'address' - }`, + detail: `Duplicated ${duplicatedNickname ? 'nickname' : 'address'}`, }); } @@ -68,9 +70,9 @@ export class AddressBookController { } const newContact = await this.addressBookService.create({ - ...body, - user_id: savedUser.id, - createdBy: user, + ...req.body, + user: savedUser, + owner: workspace, }); return successful(newContact, Responses.Ok); @@ -83,14 +85,14 @@ export class AddressBookController { try { const duplicatedNickname = await this.addressBookService .filter({ - createdBy: user.id, + owner: [user.id], nickname: body.nickname, }) .list(); const duplicatedAddress = await this.addressBookService .filter({ - createdBy: user.id, + owner: [user.id], contactAddress: body.address, }) .list(); @@ -112,7 +114,6 @@ export class AddressBookController { let savedUser = await this.userService.findByAddress(body.address); if (!savedUser) { - const roles = await Role.find({ where: [{ name: 'Administrador' }] }); savedUser = await this.userService.create({ address: body.address, provider: user.provider, @@ -125,7 +126,7 @@ export class AddressBookController { const updatedContact = await this.addressBookService.update(params.id, { ...rest, - user_id: savedUser.id, + user: savedUser, }); return successful(updatedContact, Responses.Ok); } catch (e) { @@ -142,13 +143,23 @@ export class AddressBookController { } } - async list({ query, user }: IListAddressBookRequest) { - const { id } = user; - const { orderBy, sort, page, perPage, q } = query; + async list(req: IListAddressBookRequest) { + const { workspace, user } = req; + const { orderBy, sort, page, perPage, q, includePersonal } = req.query; try { + const owner = [workspace.id]; + if (includePersonal) { + await new WorkspaceService() + .filter({ + user: user.id, + single: true, + }) + .list() + .then((response: Workspace[]) => owner.push(response[0].id)); + } const response = await this.addressBookService - .filter({ createdBy: id, q }) + .filter({ owner, q }) .ordination({ orderBy, sort }) .paginate({ page, perPage }) .list(); diff --git a/src/modules/addressBook/routes.ts b/src/modules/addressBook/routes.ts index 96ec2f829..e86989645 100644 --- a/src/modules/addressBook/routes.ts +++ b/src/modules/addressBook/routes.ts @@ -1,10 +1,11 @@ import { Router } from 'express'; -import { authMiddleware } from '@src/middlewares'; +import { authMiddleware, authPermissionMiddleware } from '@src/middlewares'; +import { PermissionRoles } from '@src/models/Workspace'; import { handleResponse } from '@utils/index'; -import { UserService } from '../configs/user/service'; +import { UserService } from '../user/service'; import { AddressBookController } from './controller'; import { AddressBookService } from './services'; import { @@ -22,8 +23,26 @@ const { create, update, list, delete: deleteContact } = new AddressBookControlle router.use(authMiddleware); -router.post('/', validateCreateAddressBookPayload, handleResponse(create)); -router.put('/:id', validateUpdateAddressBookPayload, handleResponse(update)); +router.post( + '/', + validateCreateAddressBookPayload, + authPermissionMiddleware([ + PermissionRoles.OWNER, + PermissionRoles.ADMIN, + PermissionRoles.MANAGER, + ]), + handleResponse(create), +); +router.put( + '/:id', + authPermissionMiddleware([ + PermissionRoles.OWNER, + PermissionRoles.ADMIN, + PermissionRoles.MANAGER, + ]), + validateUpdateAddressBookPayload, + handleResponse(update), +); router.delete('/:id', handleResponse(deleteContact)); router.get('/', handleResponse(list)); diff --git a/src/modules/addressBook/services.ts b/src/modules/addressBook/services.ts index d5289837f..b28b552ef 100644 --- a/src/modules/addressBook/services.ts +++ b/src/modules/addressBook/services.ts @@ -55,28 +55,22 @@ export class AddressBookService implements IAddressBookService { const queryBuilder = AddressBook.createQueryBuilder('ab') .select(['ab.id', 'ab.nickname']) .innerJoin('ab.user', 'user') - .innerJoin('ab.createdBy', 'createdBy') - .addSelect([ - 'user.id', - 'user.address', - 'user.avatar', - 'createdBy.id', - 'createdBy.address', - ]); + .innerJoin('ab.owner', 'owner') + .addSelect(['user.id', 'user.address', 'user.avatar', 'owner.id']); const handleInternalError = e => { if (e instanceof GeneralError) throw e; throw new Internal({ type: ErrorTypes.Internal, - title: 'Error on predicate list', + title: 'Error on book contact list', detail: e, }); }; - this._filter.createdBy && - queryBuilder.andWhere('ab.created_by = :createdBy', { - createdBy: `${this._filter.createdBy}`, + this._filter.owner && + queryBuilder.andWhere('ab.owner IN (:...owner)', { + owner: this._filter.owner, }); this._filter.contactAddress && diff --git a/src/modules/addressBook/types.ts b/src/modules/addressBook/types.ts index cfc656cf8..2fef11e2f 100644 --- a/src/modules/addressBook/types.ts +++ b/src/modules/addressBook/types.ts @@ -1,6 +1,7 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import AddressBook from '@src/models/AddressBook'; +import { Workspace } from '@src/models/Workspace'; import { User } from '@models/index'; @@ -23,20 +24,20 @@ export enum Sort { export interface ICreateAddressBookPayload { nickname: string; address: string; - user_id?: string; - createdBy: User; + user: User; + owner: Workspace; } export type IUpdateAddressBookPayload = Omit< ICreateAddressBookPayload, - 'createdBy' | 'address' + 'owner' | 'address' >; -export type IUpdateAddressBookBody = Omit; +export type IUpdateAddressBookBody = Omit; export interface IFilterAddressBookParams { q?: string; - createdBy?: string; + owner?: string[]; contactAddress?: string; nickname?: string; userIds?: string[]; @@ -59,11 +60,12 @@ interface IDeleteAddressBookRequestSchema extends ValidatedRequestSchema { interface IListAddressBookRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Query]: { q: string; - createdBy: string; + owner: string[]; orderBy: OrderBy; sort: Sort; page: string; perPage: string; + includePersonal: boolean; }; } diff --git a/src/modules/auth/__tests__/auth.tests.ts b/src/modules/auth/__tests__/auth.tests.ts index 86420337b..a6db7175f 100644 --- a/src/modules/auth/__tests__/auth.tests.ts +++ b/src/modules/auth/__tests__/auth.tests.ts @@ -1,21 +1,44 @@ -import { Request } from 'supertest'; - import { accounts } from '@src/mocks/accounts'; import { networks } from '@src/mocks/networks'; import { AuthValidations } from '@src/utils/testUtils/Auth'; describe('[AUTH]', () => { test( - 'Sign in', + 'Sign in with personal workspace', async () => { const auth = new AuthValidations(networks['local'], accounts['USER_1']); await auth.create(); - await auth.createSession(); + await auth.createSession().then(() => { + expect(auth.user.address).toBe(accounts['USER_1'].address); + expect(auth.workspace).toHaveProperty('id'); + expect(auth.workspace).toHaveProperty('single', true); + expect(auth.authToken); + }); + }, + 40 * 1000, + ); + + test( + 'Sign in with personal workspace and select other workspace', + async () => { + //crate a session + const _auth = new AuthValidations(networks['local'], accounts['USER_1']); + await _auth.create(); + await _auth.createSession(); + + //select a other workspace + const { data } = await _auth.axios.get(`/workspace/by-user`); + + const w_upgrade = data.find(w => w.id !== _auth.workspace.id); - expect(auth.user.address).toBe(accounts['USER_1'].address); - expect(auth.authToken); + //select workspace + await _auth.selectWorkspace(w_upgrade.id).then(({ data }) => { + expect(_auth.workspace.id).toEqual(w_upgrade.id); + expect(_auth.user).toHaveProperty('address', accounts['USER_1'].address); + expect(_auth.authToken).toHaveProperty('token'); + }); }, 40 * 1000, ); diff --git a/src/modules/auth/controller.ts b/src/modules/auth/controller.ts index efe00ce75..47ec642b2 100644 --- a/src/modules/auth/controller.ts +++ b/src/modules/auth/controller.ts @@ -1,15 +1,21 @@ -import { addMinutes } from 'date-fns'; +import { add, addMinutes } from 'date-fns'; import { Encoder } from '@src/models'; -import GeneralError from '@src/utils/error/GeneralError'; +import { Workspace } from '@src/models/Workspace'; +import GeneralError, { ErrorTypes } from '@src/utils/error/GeneralError'; +import { + Unauthorized, + UnauthorizedErrorTitles, +} from '@src/utils/error/Unauthorized'; import { IAuthRequest } from '@middlewares/auth/types'; -import { error } from '@utils/error'; +import { NotFound, error } from '@utils/error'; import { Responses, successful, bindMethods, Web3Utils } from '@utils/index'; -import { IUserService } from '../configs/user/types'; -import { IAuthService, ISignInRequest } from './types'; +import { IUserService } from '../user/types'; +import { WorkspaceService } from '../workspace/services'; +import { IAuthService, IChangeWorkspaceRequest, ISignInRequest } from './types'; export class AuthController { private authService: IAuthService; @@ -23,7 +29,7 @@ export class AuthController { async signIn(req: ISignInRequest) { try { - const { signature, ...payloadWithoutSignature } = req.body; + const { signature, workspace_id, ...payloadWithoutSignature } = req.body; const expiresIn = process.env.TOKEN_EXPIRATION_TIME ?? '15'; new Web3Utils({ @@ -40,6 +46,18 @@ export class AuthController { await this.authService.signOut(existingToken.user); } + const workspace = await new WorkspaceService() + .filter( + workspace_id + ? { id: workspace_id } + : { + owner: req.body.user_id, + single: true, + }, + ) + .list() + .then((response: Workspace[]) => response[0]); + const userToken = await this.authService.signIn({ token: req.body.signature, encoder: Encoder[req.body.encoder], @@ -47,15 +65,10 @@ export class AuthController { expired_at: addMinutes(req.body.createdAt, Number(expiresIn)), payload: JSON.stringify(payloadWithoutSignature), user: await this.userService.findOne(req.body.user_id), + workspace, }); - return successful( - { - accessToken: userToken.accessToken, - avatar: userToken.avatar, - }, - Responses.Ok, - ); + return successful(userToken, Responses.Ok); } catch (e) { if (e instanceof GeneralError) throw e; @@ -72,4 +85,58 @@ export class AuthController { return error(e.error[0], e.statusCode); } } + + async updateWorkspace(req: IChangeWorkspaceRequest) { + try { + const { workspace: workspaceId, user } = req.body; + + const workspace = await new WorkspaceService() + .filter({ id: workspaceId }) + .list() + .then((response: Workspace[]) => response[0]); + + if (!workspace) + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Workspace not found', + detail: `Workspace not found`, + }); + + const isUserMember = workspace.members.find(m => m.id === user); + + // if (!isUserMember) { + // throw new Unauthorized({ + // type: ErrorTypes.NotFound, + // title: UnauthorizedErrorTitles.INVALID_PERMISSION, + // detail: `User not found`, + // }); + // } + + const token = await this.authService.findToken({ + userId: user, + }); + + if (isUserMember) { + token.workspace = workspace; + } + + const response = await token.save(); + const result = { + workspace: { + id: response.workspace.id, + name: response.workspace.name, + avatar: response.workspace.avatar, + permissions: response.workspace.permissions[response.user.id], + single: response.workspace.single, + }, + token: response.token, + avatar: response.user.avatar, + address: response.user.address, + }; + + return successful(result, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } } diff --git a/src/modules/auth/routes.ts b/src/modules/auth/routes.ts index 2166065c0..58575ad87 100644 --- a/src/modules/auth/routes.ts +++ b/src/modules/auth/routes.ts @@ -1,8 +1,9 @@ import { Router } from 'express'; +import { authMiddleware } from '@src/middlewares'; import { handleResponse } from '@src/utils/index'; -import { UserService } from '../configs/user/service'; +import { UserService } from '../user/service'; import { AuthController } from './controller'; import { AuthService } from './services'; import { validateSignInPayload } from './validations'; @@ -10,15 +11,14 @@ import { validateSignInPayload } from './validations'; const router = Router(); const authService = new AuthService(); const userService = new UserService(); -const authController = new AuthController(authService, userService); +const { signIn, updateWorkspace } = new AuthController(authService, userService); export const signOutPath = '/sign-out'; -router.post( - '/sign-in', - validateSignInPayload, - handleResponse(authController.signIn), -); +router.post('/sign-in', validateSignInPayload, handleResponse(signIn)); + +//todo: verify why do cant use authMiddleware here +router.put('/workspace', handleResponse(updateWorkspace)); //router.delete(signOutPath, authMiddleware, handleResponse(authController.signOut)); diff --git a/src/modules/auth/services.ts b/src/modules/auth/services.ts index 9cf2f05c9..934107a7b 100644 --- a/src/modules/auth/services.ts +++ b/src/modules/auth/services.ts @@ -20,6 +20,15 @@ export class AuthService implements IAuthService { return { accessToken: data.token, avatar: data.user.avatar, + address: data.user.address, + user_id: data.user.id, + workspace: { + id: data.workspace.id, + name: data.workspace.name, + avatar: data.workspace.avatar, + single: data.workspace.single, + permissions: data.workspace.permissions, + }, }; }) .catch(e => { @@ -46,9 +55,9 @@ export class AuthService implements IAuthService { } async findToken(params: IFindTokenParams): Promise { - const queryBuilder = await UserToken.createQueryBuilder( - 'ut', - ).innerJoinAndSelect('ut.user', 'user'); + const queryBuilder = await UserToken.createQueryBuilder('ut') + .innerJoinAndSelect('ut.user', 'user') + .innerJoinAndSelect('ut.workspace', 'workspace'); params.userId && queryBuilder.where('ut.user = :userId', { userId: params.userId }); diff --git a/src/modules/auth/types.ts b/src/modules/auth/types.ts index 70dada5d6..c8e549423 100644 --- a/src/modules/auth/types.ts +++ b/src/modules/auth/types.ts @@ -1,5 +1,7 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; +import { IPermissions, Workspace } from '@src/models/Workspace'; + import UserToken, { Encoder } from '@models/UserToken'; import { User } from '@models/index'; @@ -12,6 +14,7 @@ export interface ICreateUserTokenPayload { encoder: Encoder; provider: string; payload: string; + workspace: Workspace; } export interface ISignInPayload { @@ -22,6 +25,7 @@ export interface ISignInPayload { provider: string; signature: string; user_id: string; + workspace_id?: string; } interface IActiveSessionRequestSchema extends ValidatedRequestSchema { @@ -71,12 +75,28 @@ export interface IFindTokenParams { export interface ISignInResponse { accessToken: string; avatar: string; + user_id: string; + workspace: { + id: string; + name: string; + avatar: string; + permissions: IPermissions; + single: boolean; + }; +} + +export interface IUpgradeWorkspace extends ValidatedRequestSchema { + [ContainerTypes.Body]: { + workspace: string; + user: string; + }; } -export type IActiveSession = AuthValidatedRequest; -export type ISignInRequest = AuthValidatedRequest; export type IListRequest = AuthValidatedRequest; +export type ISignInRequest = AuthValidatedRequest; export type IFindDappRequest = AuthValidatedRequest; +export type IActiveSession = AuthValidatedRequest; +export type IChangeWorkspaceRequest = AuthValidatedRequest; export type IAuthorizeDappRequest = AuthValidatedRequest; export interface IAuthService { diff --git a/src/modules/configs/roles/controller.ts b/src/modules/configs/roles/controller.ts deleted file mode 100644 index aee51ab2a..000000000 --- a/src/modules/configs/roles/controller.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ModulesList } from '@middlewares/permissions/types'; - -import { error } from '@utils/error'; -import { successful, Responses, bindMethods } from '@utils/index'; - -import { - IUpdateRequest, - IListRequest, - ICreateRequest, - IFindOneRequest, - IDeleteRequest, - IRoleService, -} from './types'; - -export class RoleController { - private roleService: IRoleService; - - constructor(roleService: IRoleService) { - this.roleService = roleService; - bindMethods(this); - } - - async create(req: ICreateRequest) { - try { - const { name, active, permissions } = req.body; - - const response = await this.roleService.create({ name, active, permissions }); - - return successful(response, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } - - async find(req: IListRequest) { - try { - const { role, active, page, perPage, orderBy, sort } = req.query; - - const response = await this.roleService - .filter({ role, active }) - .ordination({ orderBy, sort }) - .paginate({ - page, - perPage, - }) - .find(); - - return successful(response, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } - - async findOne(req: IFindOneRequest) { - try { - const { id } = req.params; - - const response = await this.roleService.findOne(id); - - return successful(response, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } - - async update(req: IUpdateRequest) { - try { - const { id } = req.params; - const { name, active, permissions } = req.body; - - const response = await this.roleService.update(id, { - name, - active, - permissions, - }); - - return successful(response, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } - - async delete(req: IDeleteRequest) { - try { - const { id } = req.params; - - const response = await this.roleService.delete(id); - - return successful(response, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } - - async findModules() { - try { - return successful(ModulesList, Responses.Ok); - } catch (e) { - return error(e.error[0], e.statusCode); - } - } -} diff --git a/src/modules/configs/roles/routes.ts b/src/modules/configs/roles/routes.ts deleted file mode 100644 index f63ae18f0..000000000 --- a/src/modules/configs/roles/routes.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Router } from 'express'; - -import { authMiddleware } from '@middlewares/index'; - -import { handleResponse } from '@utils/index'; - -import { RoleController } from './controller'; -import { RoleService } from './services'; -import { PayloadRoleSchema } from './validations'; - -const router = Router(); -const roleService = new RoleService(); -const roleController = new RoleController(roleService); - -router.use(authMiddleware); - -router.get('/modules', handleResponse(roleController.findModules)); -router.get('/', handleResponse(roleController.find)); -router.get('/:id', handleResponse(roleController.findOne)); -router.post('/', PayloadRoleSchema, handleResponse(roleController.create)); -router.put('/:id', PayloadRoleSchema, handleResponse(roleController.update)); -router.delete('/:id', handleResponse(roleController.delete)); - -export default router; diff --git a/src/modules/configs/roles/services.ts b/src/modules/configs/roles/services.ts deleted file mode 100644 index e24360d25..000000000 --- a/src/modules/configs/roles/services.ts +++ /dev/null @@ -1,108 +0,0 @@ -import Role from '@src/models/Role'; -import { ErrorTypes, NotFound } from '@src/utils/error'; -import Internal from '@src/utils/error/Internal'; -import { IOrdination, setOrdination } from '@src/utils/ordination'; -import { IPagination, Pagination, PaginationParams } from '@src/utils/pagination'; - -import { IRolePayload, IFilterParams, IRoleService } from './types'; - -export class RoleService implements IRoleService { - private _pagination: PaginationParams; - private _filter: IFilterParams; - private _ordination: IOrdination; - - filter(filter: IFilterParams) { - this._filter = filter; - return this; - } - - paginate(pagination: PaginationParams) { - this._pagination = pagination; - return this; - } - - ordination(ordination: IOrdination) { - this._ordination = setOrdination(ordination); - return this; - } - - async create(payload: IRolePayload): Promise { - return await Role.create(payload) - .save() - .then(data => data) - .catch(e => { - throw new Internal({ - type: ErrorTypes.Create, - title: 'Error on role create', - detail: e, - }); - }); - } - - async find(): Promise | Role[]> { - try { - const hasPaginationParams = this._pagination.page && this._pagination.perPage; - const queryBuilder = Role.createQueryBuilder('r').select(); - - this._filter.role && - queryBuilder.andWhere('LOWER(r.name) LIKE LOWER(:q)', { - q: `%${this._filter.role}%`, - }); - - this._filter.active && - queryBuilder.andWhere('r.active = :active', { - active: this._filter.active, - }); - - queryBuilder.orderBy(`r.${this._ordination.orderBy}`, this._ordination.sort); - - return hasPaginationParams - ? await Pagination.create(queryBuilder).paginate(this._pagination) - : await queryBuilder.getMany(); - } catch (error) { - throw new Internal({ - type: ErrorTypes.Internal, - title: 'Error on roles find', - detail: error, - }); - } - } - - async findOne(id: string): Promise { - const role = await Role.findOne({ where: { id } }); - - if (!role) { - throw new NotFound({ - type: ErrorTypes.NotFound, - title: 'Role not found', - detail: `Role with id ${id} not found`, - }); - } - - return role; - } - - async update(id: string, payload: IRolePayload): Promise { - return await Role.update({ id }, { ...payload }) - .then(async () => await this.findOne(id)) - .catch(() => { - throw new Internal({ - type: ErrorTypes.Update, - title: 'Role not updated', - detail: `Role ${id} has not been changed`, - }); - }); - } - - async delete(id: string): Promise { - return await Role.update({ id }, { deletedAt: new Date() }) - .then(() => true) - .catch(() => { - throw new Internal({ - type: ErrorTypes.Delete, - title: 'Role not deleted', - detail: `Role ${id} has not been deleted`, - }); - }); - } -} diff --git a/src/modules/configs/roles/types.ts b/src/modules/configs/roles/types.ts deleted file mode 100644 index 43fbab6da..000000000 --- a/src/modules/configs/roles/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; - -import { AuthValidatedRequest } from '@src/middlewares/auth/types'; -import Role, { Permissions } from '@src/models/Role'; -import { IOrdination } from '@src/utils/ordination'; -import { IPagination, PaginationParams } from '@src/utils/pagination'; - -export interface IRolePayload { - name: string; - active?: boolean; - permissions: Permissions; -} - -export interface IFilterParams { - role: string; - active: string; -} - -interface ICreateRequestSchema extends ValidatedRequestSchema { - [ContainerTypes.Body]: IRolePayload; -} - -interface IListRequestSchema extends ValidatedRequestSchema { - [ContainerTypes.Query]: { - role: string; - active: string; - page: string; - perPage: string; - sort: 'ASC' | 'DESC'; - orderBy: 'name' | 'createdAt' | 'active'; - }; -} - -interface IFindOneRequestSchema extends ValidatedRequestSchema { - [ContainerTypes.Params]: { - id: string; - }; -} - -interface IUpdateRequestSchema extends ValidatedRequestSchema { - [ContainerTypes.Params]: { - id: string; - }; - [ContainerTypes.Body]: IRolePayload; -} - -export type ICreateRequest = AuthValidatedRequest; - -export type IListRequest = AuthValidatedRequest; - -export type IFindOneRequest = AuthValidatedRequest; - -export type IUpdateRequest = AuthValidatedRequest; - -export type IDeleteRequest = AuthValidatedRequest; - -export interface IRoleService { - filter(filter: IFilterParams): this; - paginate(pagination: PaginationParams): this; - ordination(ordination: IOrdination): this; - create(payload: IRolePayload): Promise; - find(): Promise | Role[]>; - findOne(id: string): Promise; - update(id: string, payload: IRolePayload): Promise; - delete(id: string): Promise; -} diff --git a/src/modules/configs/roles/validations.ts b/src/modules/configs/roles/validations.ts deleted file mode 100644 index 979b2dc36..000000000 --- a/src/modules/configs/roles/validations.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Joi from 'joi'; - -import { validator } from '@utils/index'; - -const PermissionSchema = Joi.object({ - view: Joi.boolean().required(), - edit: Joi.boolean().required(), - remove: Joi.boolean().required(), -}).required(); - -const PermissionsSchema = Joi.object({ - configs: PermissionSchema, - roles: PermissionSchema, -}).required(); - -export const PayloadRoleSchema = validator.body( - Joi.object({ - name: Joi.string().required(), - active: Joi.boolean().required(), - permissions: PermissionsSchema, - }), -); diff --git a/src/modules/notification/routes.ts b/src/modules/notification/routes.ts index 5b094baa8..ab8a744cd 100644 --- a/src/modules/notification/routes.ts +++ b/src/modules/notification/routes.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { authMiddleware } from '@src/middlewares'; +import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; import { handleResponse } from '@utils/index'; @@ -11,6 +12,30 @@ const router = Router(); const notificationService = new NotificationService(); const { readAll, list } = new NotificationController(notificationService); +// ENDPOINT TO VALIDATE EMAIL SENDING +router.get('/mail', async (_, res) => { + const to = 'guilherme@infinitybase.com'; + const data = { + summary: { + vaultName: 'Vault Name', + transactionName: 'Transaction Name', + name: 'Tester', + }, + }; + + try { + await sendMail(EmailTemplateType.TRANSACTION_CREATED, { to, data }); + await sendMail(EmailTemplateType.TRANSACTION_COMPLETED, { to, data }); + await sendMail(EmailTemplateType.TRANSACTION_DECLINED, { to, data }); + await sendMail(EmailTemplateType.TRANSACTION_SIGNED, { to, data }); + await sendMail(EmailTemplateType.VAULT_CREATED, { to, data }); + } catch (error) { + console.log('🚀 ~ router.get ~ error:', error); + } + + res.status(200).json(); +}); + router.use(authMiddleware); router.get('/', handleResponse(list)); diff --git a/src/modules/predicate/__tests__/predicate.tests.ts b/src/modules/predicate/__tests__/predicate.tests.ts index 83de8c2a0..3b047dc88 100644 --- a/src/modules/predicate/__tests__/predicate.tests.ts +++ b/src/modules/predicate/__tests__/predicate.tests.ts @@ -1,7 +1,9 @@ import { accounts } from '@src/mocks/accounts'; import { networks } from '@src/mocks/networks'; import { PredicateMock } from '@src/mocks/predicate'; +import { PermissionRoles } from '@src/models/Workspace'; import { AuthValidations } from '@src/utils/testUtils/Auth'; +import { generateWorkspacePayload } from '@src/utils/testUtils/Workspace'; describe('[PREDICATE]', () => { let api: AuthValidations; @@ -12,65 +14,173 @@ describe('[PREDICATE]', () => { await api.createSession(); }); - test('Create predicate', async () => { + test( + 'Create predicate', + async () => { + const { + data: data_workspace, + data_user1, + data_user2, + USER_5, + } = await generateWorkspacePayload(api); + const members = [USER_5.address, data_user1.address, data_user2.address]; + const { predicatePayload } = await PredicateMock.create(1, members); + const { data } = await api.axios.post('/predicate', predicatePayload); + + const { data: workspace, status: status_find } = await api.axios.get( + `/workspace/${api.workspace.id}`, + ); + + //predicate validation + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty( + 'predicateAddress', + predicatePayload.predicateAddress, + ); + expect(data).toHaveProperty('owner.address', accounts['USER_1'].address); + + //permissions validation + expect( + workspace.permissions[data_user1.id][PermissionRoles.SIGNER], + ).toContain(data.id); + expect( + workspace.permissions[data_user2.id][PermissionRoles.SIGNER], + ).toContain(data.id); + expect(workspace.permissions[USER_5.id][PermissionRoles.SIGNER]).toContain( + data.id, + ); + }, + 10 * 1000, + ); + + test('Create predicate with invalid owner permission', async () => { + const auth = new AuthValidations(networks['local'], accounts['USER_1']); + await auth.create(); + await auth.createSession(); + + //create a new workspace + const { data: data_workspace } = await generateWorkspacePayload(auth); + + //auth with new account + const auth_aux = new AuthValidations(networks['local'], accounts['USER_5']); + await auth_aux.create(); + await auth_aux.createSession(); + await auth_aux.selectWorkspace(data_workspace.id); + const { predicatePayload } = await PredicateMock.create(1, [ accounts['USER_1'].address, accounts['USER_2'].address, + accounts['USER_3'].address, ]); - const { data } = await api.axios.post('/predicate', predicatePayload); - expect(data).toHaveProperty('id'); - expect(data).toHaveProperty( - 'predicateAddress', - predicatePayload.predicateAddress, + const { status, data: predicate_data } = await auth_aux.axios + .post('/predicate', predicatePayload) + .catch(e => { + return e.response; + }); + + expect(status).toBe(401); + expect(predicate_data.errors.detail).toEqual( + 'You do not have permission to access this resource', ); - expect(data).toHaveProperty('owner.address', accounts['USER_1'].address); - expect(data).toHaveProperty('members[0].address', accounts['USER_1'].address); - expect(data).toHaveProperty('members[1].address', accounts['USER_2'].address); }); - test('Find predicate by ID', async () => { - const { predicatePayload } = await PredicateMock.create(1, [ - accounts['USER_1'].address, - accounts['USER_2'].address, - ]); - const { data } = await api.axios.post('/predicate', predicatePayload); + test('List predicates', async () => { + const auth = new AuthValidations(networks['local'], accounts['USER_1']); + await auth.create(); + await auth.createSession(); - const { data: predicate } = await api.axios.get(`/predicate/${data.id}`); + //on single workspace + await auth.axios.get('/predicate').then(({ data, status }) => { + expect(status).toBe(200); + data.forEach(element => { + expect(element).toHaveProperty('id'); + expect(element.workspace).toHaveProperty('id', api.workspace.id); + }); + }); - expect(predicate).toHaveProperty('id', data.id); - expect(predicate).toHaveProperty('predicateAddress', data.predicateAddress); + //with pagination + const page = 1; + const perPage = 8; + await auth.axios + .get(`/predicate?page=${page}&perPage=${perPage}`) + .then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveProperty('data'); + expect(data.data).toHaveLength(perPage); + expect(data).toHaveProperty('total'); + expect(data).toHaveProperty('currentPage', page); + expect(data).toHaveProperty('perPage', perPage); + }); + + //an another workspace + const { data: data_workspace } = await generateWorkspacePayload(auth); + await auth.selectWorkspace(data_workspace.id); + await auth.axios.get('/predicate').then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveLength(0); + }); }); - test('Find predicate by Address', async () => { - const { predicatePayload } = await PredicateMock.create(1, [ - accounts['USER_1'].address, - accounts['USER_2'].address, - ]); - const { data } = await api.axios.post('/predicate', predicatePayload); + test('Find predicate by id', async () => { + const auth = new AuthValidations(networks['local'], accounts['USER_3']); + await auth.create(); + await auth.createSession(); + + //create a new workspace + const { + data: data_workspace, + USER_5, + data_user1, + data_user2, + } = await generateWorkspacePayload(auth); + await auth.selectWorkspace(data_workspace.id); - const { data: predicate } = await api.axios.get( - `/predicate/by-address/${data.predicateAddress}`, + //create a new nicknames + const { data: n_data5 } = await auth.axios.post('/address-book/', { + address: USER_5.address, + nickname: `[TESTE]${USER_5.address}`, + }); + const { data: n_data1 } = await auth.axios.post('/address-book/', { + address: data_user1.address, + nickname: `[TESTE]${data_user1.address}`, + }); + const { data: n_data2 } = await auth.axios.post('/address-book/', { + address: data_user2.address, + nickname: `[TESTE]${data_user2.address}`, + }); + + //create a vault + const members = [USER_5.address, data_user1.address, data_user2.address]; + + const { predicatePayload } = await PredicateMock.create(3, members); + const { data: data_predicate } = await auth.axios.post( + '/predicate', + predicatePayload, ); - expect(predicate).toHaveProperty('id', data.id); - expect(predicate).toHaveProperty('predicateAddress', data.predicateAddress); - }); + await auth.axios + .get(`/predicate/${data_predicate.id}`) + .then(({ data, status }) => { + const { workspace, members, id } = data; + const n_members = [n_data2.nickname, n_data1.nickname, n_data5.nickname]; + + expect(status).toBe(200); + + //validate workspace members + workspace.addressBook.forEach(element => { + const aux = n_members.includes(element.nickname); + expect(aux).toBe(true); + }); + + //validate members + members.forEach(element => { + const aux = members.find(m => element.id === m.id); + expect(!!aux).toBe(true); + }); - test(`List predicates of user ${accounts['USER_1'].address}`, async () => { - const params = { - page: 1, - perPage: 10, - orderBy: 'createdAt', - sort: 'DESC', - //todo: fix bug to request with owner - //owner: accounts['USER_1'].address, - }; - const { data } = await api.axios.get('/predicate/', { params }); - expect(data).toHaveProperty('currentPage', 1); - expect(data).toHaveProperty('perPage', 10); - expect(data).toHaveProperty('data[0]', expect.any(Object)); - //todo: fix bug to request with owner - //expect(data).toHaveProperty('data[0].owner.id', ); + //validate vault + expect(id).toBe(data_predicate.id); + }); }); }); diff --git a/src/modules/predicate/controller.ts b/src/modules/predicate/controller.ts index f5eecb1bb..529c7f57d 100644 --- a/src/modules/predicate/controller.ts +++ b/src/modules/predicate/controller.ts @@ -3,6 +3,8 @@ import { bn } from 'fuels'; import AddressBook from '@src/models/AddressBook'; import { Predicate } from '@src/models/Predicate'; +import { Workspace } from '@src/models/Workspace'; +import { sendMail, EmailTemplateType } from '@src/utils/EmailSender'; import { Asset, NotificationTitle, Transaction, User } from '@models/index'; @@ -10,9 +12,10 @@ import { error } from '@utils/error'; import { Responses, bindMethods, successful } from '@utils/index'; import { IAddressBookService } from '../addressBook/types'; -import { IUserService } from '../configs/user/types'; import { INotificationService } from '../notification/types'; import { ITransactionService } from '../transaction/types'; +import { IUserService } from '../user/types'; +import { WorkspaceService } from '../workspace/services'; import { ICreatePredicateRequest, IDeletePredicateRequest, @@ -44,9 +47,10 @@ export class PredicateController { bindMethods(this); } - async create({ body: payload, user }: ICreatePredicateRequest) { + async create({ body: payload, user, workspace }: ICreatePredicateRequest) { try { const members: User[] = []; + for await (const member of payload.addresses) { let user = await this.userService.findByAddress(member); @@ -65,9 +69,18 @@ export class PredicateController { ...payload, owner: user, members, + workspace, }); + // include signer permission to vault on workspace + await new WorkspaceService().includeSigner( + members.map(member => member.id), + newPredicate.id, + workspace.id, + ); + const { id, name, members: predicateMembers } = newPredicate; + const summary = { vaultId: id, vaultName: name }; const membersWithoutLoggedUser = predicateMembers.filter( member => member.id !== user.id, ); @@ -76,11 +89,22 @@ export class PredicateController { await this.notificationService.create({ title: NotificationTitle.NEW_VAULT_CREATED, user_id: member.id, - summary: { vaultId: id, vaultName: name }, + summary, }); + + if (member.notify) { + await sendMail(EmailTemplateType.VAULT_CREATED, { + to: member.email, + data: { summary: { ...summary, name: member?.name ?? '' } }, + }); + } } - return successful(newPredicate, Responses.Ok); + const result = await this.predicateService + .filter(undefined) + .findById(newPredicate.id); + + return successful(result, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); } @@ -96,12 +120,12 @@ export class PredicateController { } } - async findById({ params: { id }, user }: IFindByIdRequest) { + async findById({ params: { id }, user, workspace }: IFindByIdRequest) { try { const predicate = await this.predicateService.findById(id, user.address); const membersIds = predicate.members.map(member => member.id); const favorites = (await this.addressBookService - .filter({ createdBy: user.id, userIds: membersIds }) + .filter({ owner: [workspace.id], userIds: membersIds }) .list()) as AddressBook[]; const response = { ...predicate, @@ -143,20 +167,6 @@ export class PredicateController { }) .list() .then((data: Transaction[]) => { - // const a: BN = bn.parseUnits('0'); - // //console.log(data.map((transaction: Transaction) => transaction.assets)); - // data - // .filter( - // (transaction: Transaction) => - // transaction.status == TransactionStatus.AWAIT_REQUIREMENTS || - // transaction.status == TransactionStatus.PENDING_SENDER, - // ) - // .map((_filteredTransactions: Transaction) => { - // _filteredTransactions.assets.map((_assets: Asset) => { - // console.log(_assets.amount, a.add(bn.parseUnits(_assets.amount))); - // return a.add(bn.parseUnits(_assets.amount)); - // }); - // }); return data .filter( (transaction: Transaction) => @@ -166,10 +176,6 @@ export class PredicateController { .reduce((accumulator, transaction: Transaction) => { return accumulator.add( transaction.assets.reduce((assetAccumulator, asset: Asset) => { - console.log( - asset.amount, - assetAccumulator.add(bn.parseUnits(asset.amount)), - ); return assetAccumulator.add(bn.parseUnits(asset.amount)); }, bn.parseUnits('0')), ); @@ -178,9 +184,6 @@ export class PredicateController { .catch(e => { return bn.parseUnits('0'); }); - - //console.log('[HAS_RESERVED_COINS]: ', response.format().toString()); - return successful(response, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); @@ -198,11 +201,35 @@ export class PredicateController { perPage, q, } = req.query; - const { address } = req.user; + const { workspace, user } = req; try { + const singleWorkspace = await new WorkspaceService() + .filter({ + user: user.id, + single: true, + }) + .list() + .then((response: Workspace[]) => response[0]); + + const allWk = await new WorkspaceService() + .filter({ + user: user.id, + }) + .list() + .then((response: Workspace[]) => response.map(wk => wk.id)); + + const hasSingle = singleWorkspace.id === workspace.id; + const response = await this.predicateService - .filter({ address: predicateAddress, signer: address, provider, owner, q }) + .filter({ + address: predicateAddress, + provider, + owner, + q, + workspace: hasSingle ? allWk : [workspace.id], + signer: hasSingle ? user.address : undefined, + }) .ordination({ orderBy, sort }) .paginate({ page, perPage }) .list(); diff --git a/src/modules/predicate/routes.ts b/src/modules/predicate/routes.ts index de22e4f46..14e776207 100644 --- a/src/modules/predicate/routes.ts +++ b/src/modules/predicate/routes.ts @@ -1,13 +1,14 @@ import { Router } from 'express'; -import { authMiddleware } from '@src/middlewares'; +import { authMiddleware, authPermissionMiddleware } from '@src/middlewares'; +import { PermissionRoles } from '@src/models/Workspace'; import { handleResponse } from '@utils/index'; import { AddressBookService } from '../addressBook/services'; -import { UserService } from '../configs/user/service'; import { NotificationService } from '../notification/services'; import { TransactionService } from '../transaction/services'; +import { UserService } from '../user/service'; import { PredicateController } from './controller'; import { PredicateService } from './services'; import { validateAddPredicatePayload } from './validations'; @@ -35,7 +36,16 @@ const { router.use(authMiddleware); -router.post('/', validateAddPredicatePayload, handleResponse(create)); +router.post( + '/', + validateAddPredicatePayload, + authPermissionMiddleware([ + PermissionRoles.OWNER, + PermissionRoles.ADMIN, + PermissionRoles.MANAGER, + ]), + handleResponse(create), +); router.get('/', handleResponse(list)); router.get('/:id', handleResponse(findById)); router.get('/reserved-coins/:address', handleResponse(hasReservedCoins)); diff --git a/src/modules/predicate/services.ts b/src/modules/predicate/services.ts index bd098cb91..dc9838de0 100644 --- a/src/modules/predicate/services.ts +++ b/src/modules/predicate/services.ts @@ -68,50 +68,67 @@ export class PredicateService implements IPredicateService { } async findById(id: string, signer?: string): Promise { - return Predicate.createQueryBuilder('p') - .where({ id }) - .leftJoinAndSelect('p.members', 'members') - .leftJoinAndSelect('p.owner', 'owner') - .select([ - ...this.predicateFieldsSelection, - 'p.configurable', - 'members.id', - 'members.avatar', - 'members.address', - 'owner.id', - 'owner.address', - ]) - .getOne() - - .then(predicate => { - const isNotMember = !predicate.members.map(m => m.address).includes(signer); - - // if (isNotMember) { - // throw new Unauthorized({ - // type: ErrorTypes.Unauthorized, - // title: UnauthorizedErrorTitles.INVALID_PERMISSION, - // detail: `You are not authorized to access requested predicate.`, - // }); - // } - - if (!predicate) { - throw new NotFound({ - type: ErrorTypes.NotFound, - title: 'Predicate not found', - detail: `Predicate with id ${id} not found`, - }); - } - return predicate; - }) - .catch(e => { - if (e instanceof GeneralError) throw e; + return ( + Predicate.createQueryBuilder('p') + .where({ id }) + .leftJoinAndSelect('p.members', 'members') + .leftJoinAndSelect('p.owner', 'owner') + .leftJoin('p.workspace', 'workspace') + .leftJoin('workspace.addressBook', 'addressBook') + .leftJoin('addressBook.user', 'adb_workspace') + .select([ + ...this.predicateFieldsSelection, + 'p.configurable', + 'members.id', + 'members.avatar', + 'members.address', + 'owner.id', + 'owner.address', + 'workspace.id', + 'workspace.name', + 'addressBook.nickname', + 'addressBook.id', + 'addressBook.user_id', + 'adb_workspace.id', + ]) + //.addSelect(['workspace.id', 'workspace.address_book']) + //.innerJoin('workspace.address_book', 'address_book') + // .innerJoin('address_book.user', 'address_book_user') + // .addSelect(['workspace.id', 'address_book.nickname', 'address_book_user.id']) + .getOne() - throw new Internal({ - type: ErrorTypes.Internal, - title: 'Error on predicate findById', - detail: e, - }); - }); + .then(predicate => { + const isNotMember = !predicate.members + .map(m => m.address) + .includes(signer); + + // if (isNotMember) { + // throw new Unauthorized({ + // type: ErrorTypes.Unauthorized, + // title: UnauthorizedErrorTitles.INVALID_PERMISSION, + // detail: `You are not authorized to access requested predicate.`, + // }); + // } + + if (!predicate) { + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Predicate not found', + detail: `Predicate with id ${id} not found`, + }); + } + return predicate; + }) + .catch(e => { + if (e instanceof GeneralError) throw e; + + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on predicate findById', + detail: e, + }); + }) + ); } async list(): Promise | Predicate[]> { @@ -120,12 +137,18 @@ export class PredicateService implements IPredicateService { .select(this.predicateFieldsSelection) .innerJoin('p.members', 'members') .innerJoin('p.owner', 'owner') + .innerJoin('p.workspace', 'workspace') .addSelect([ 'members.id', 'members.address', 'members.avatar', 'owner.id', 'owner.address', + 'workspace.id', + 'workspace.name', + 'workspace.permissions', + 'workspace.single', + 'workspace.avatar', ]); const handleInternalError = e => { @@ -146,7 +169,7 @@ export class PredicateService implements IPredicateService { */ this._filter.address && - queryBuilder.andWhere('p.predicateAddress =:predicateAddress', { + queryBuilder.andWhere('p.predicateAddress = :predicateAddress', { predicateAddress: this._filter.address, }); @@ -155,26 +178,65 @@ export class PredicateService implements IPredicateService { provider: `${this._filter.provider}`, }); - this._filter.owner && - queryBuilder.andWhere('LOWER(p.owner.address) = LOWER(:owner)', { - owner: `${this._filter.owner}`, - }); + // =============== specific for workspace =============== + this._filter.workspace && + !this._filter.signer && + queryBuilder.andWhere( + new Brackets(qb => { + if (this._filter.workspace) { + qb.orWhere('workspace.id IN (:...workspace)', { + workspace: this._filter.workspace, + }); + } + }), + ); + // =============== specific for workspace =============== + //console.log('[PREDICATE_FILTER]: ', this._filter); + // =============== specific for home =============== + (this._filter.workspace || this._filter.signer) && + queryBuilder.andWhere( + new Brackets(qb => { + if (this._filter.workspace) { + qb.orWhere('workspace.id IN (:...workspace)', { + workspace: this._filter.workspace, + }); + } + // Se o filtro signer existe + if (this._filter.signer) { + qb.orWhere(subQb => { + const subQuery = subQb + .subQuery() + .select('1') + .from('predicate_members', 'pm') + .where('pm.predicate_id = p.id') + .andWhere( + '(pm.user_id = (SELECT u.id FROM users u WHERE u.address = :signer))', + { signer: this._filter.signer }, + ) + .getQuery(); - this._filter.signer && - queryBuilder.andWhere(qb => { - const subQuery = qb - .subQuery() - .select('1') - .from('predicate_members', 'pm') - .where('pm.predicate_id = p.id') - .andWhere( - '(pm.user_id = (SELECT u.id FROM users u WHERE u.address = :signer))', - { signer: this._filter.signer }, - ) - .getQuery(); - - return `EXISTS ${subQuery}`; - }); + return `EXISTS ${subQuery}`; + }); + } + }), + ); + // =============== specific for home =============== + + // this._filter.signer && + // queryBuilder.andWhere(qb => { + // const subQuery = qb + // .subQuery() + // .select('1') + // .from('predicate_members', 'pm') + // .where('pm.predicate_id = p.id') + // .andWhere( + // '(pm.user_id = (SELECT u.id FROM users u WHERE u.address = :signer))', + // { signer: this._filter.signer }, + // ) + // .getQuery(); + + // return `EXISTS ${subQuery}`; + // }); this._filter.q && queryBuilder.andWhere( @@ -202,8 +264,14 @@ export class PredicateService implements IPredicateService { .catch(handleInternalError); } - async update(id: string, payload: IPredicatePayload): Promise { - return Predicate.update({ id }, payload) + async update(id: string, payload?: IPredicatePayload): Promise { + return Predicate.update( + { id }, + { + ...payload, + updatedAt: new Date(), + }, + ) .then(() => this.findById(id)) .catch(e => { throw new Internal({ @@ -228,7 +296,12 @@ export class PredicateService implements IPredicateService { async instancePredicate(predicateId: string): Promise { const predicate = await this.findById(predicateId); - const configurable: IConfVault = JSON.parse(predicate.configurable); + + const configurable: IConfVault = { + ...JSON.parse(predicate.configurable), + abi: predicate.abi, + bytecode: predicate.bytes, + }; const provider = await Provider.create(predicate.provider); return Vault.create({ diff --git a/src/modules/predicate/types.ts b/src/modules/predicate/types.ts index 7e4d04f14..c2a77514a 100644 --- a/src/modules/predicate/types.ts +++ b/src/modules/predicate/types.ts @@ -45,6 +45,7 @@ export interface IPredicateFilterParams { signer?: string; provider?: string; owner?: string; + workspace?: string[]; } interface ICreatePredicateRequestSchema extends ValidatedRequestSchema { @@ -95,7 +96,7 @@ export interface IPredicateService { create: (payload: Partial) => Promise; update: (id: string, payload: IPredicatePayload) => Promise; delete: (id: string) => Promise; - findById: (id: string, signer: string) => Promise; + findById: (id: string, signer?: string) => Promise; list: () => Promise | Predicate[]>; instancePredicate: (predicateId: string) => Promise; } diff --git a/src/modules/transaction/__tests__/transactions.tests.ts b/src/modules/transaction/__tests__/transactions.tests.ts index 6fb55c4ce..e4da5388f 100644 --- a/src/modules/transaction/__tests__/transactions.tests.ts +++ b/src/modules/transaction/__tests__/transactions.tests.ts @@ -1,12 +1,12 @@ -import { Vault, defaultConfigurable } from 'bsafe'; -import { Provider, bn, WalletUnlocked } from 'fuels'; +import { TransactionStatus } from 'bsafe'; +import { Address } from 'fuels'; import { accounts } from '@src/mocks/accounts'; import { networks } from '@src/mocks/networks'; import { PredicateMock } from '@src/mocks/predicate'; -import { transaction } from '@src/mocks/transaction'; +import { transaction, transactionMock } from '@src/mocks/transaction'; import { AuthValidations } from '@src/utils/testUtils/Auth'; -import { sendPredicateCoins, signBypK } from '@src/utils/testUtils/Wallet'; +import { generateWorkspacePayload } from '@src/utils/testUtils/Workspace'; describe('[TRANSACTION]', () => { let api: AuthValidations; @@ -18,63 +18,165 @@ describe('[TRANSACTION]', () => { }); test( - 'Create and send a transaction to the vault FLOW', + 'Create transaction', async () => { - const { BSAFEVaultconfigurable } = await PredicateMock.create(1, [ - accounts['USER_1'].address, - ]); - - const vault = await Vault.create({ - configurable: BSAFEVaultconfigurable, - BSAFEAuth: api.authToken, - provider: await Provider.create(defaultConfigurable['provider']), - }); + const user_aux = Address.fromRandom().toString(); + const members = [accounts['USER_1'].address, user_aux]; + const { predicatePayload, vault } = await PredicateMock.create(1, members); + await api.axios.post('/predicate', predicatePayload); + + const { tx, payload_transfer } = await transactionMock(vault); + const { data: data_transaction } = await api.axios.post( + '/transaction', + payload_transfer, + ); - await sendPredicateCoins( - vault, - bn(1_000_000_0), - 'ETH', - accounts['USER_1'].privateKey, + expect(data_transaction).toHaveProperty('id'); + expect(data_transaction).toHaveProperty( + 'predicate.predicateAddress', + vault.address.toString(), ); - // console.log( - // '[VAULT]', - // vault.address, - // (await vault.getBalance()).format().toString(), - // bn(1_000_000).add(bn(5)).format().toString(), - // ); - const tx_1 = await vault.BSAFEIncludeTransaction(transaction); - - console.log('[TRANSACOES_UM]', tx_1.getHashTxId(), tx_1.BSAFETransactionId); - - await api.axios.put(`/transaction/signer/${tx_1.BSAFETransactionId}`, { - signer: await signBypK(tx_1.getHashTxId(), accounts['USER_1'].privateKey), - account: accounts['USER_1'].address, - confirm: true, + expect(data_transaction).toHaveProperty('witnesses'); + expect(data_transaction.witnesses).toHaveLength(members.length); + expect(data_transaction).toHaveProperty('assets'); + expect(tx.getHashTxId()).toEqual(data_transaction.hash); + }, + 60 * 1000, + ); + + test( + 'Create transaction with invalid permission', + async () => { + // logar com usuário inválido no workspace + const auth = new AuthValidations(networks['local'], accounts['USER_3']); + await auth.create(); + await auth.createSession(); + const { + data, + status, + data_user1, + data_user2, + USER_5, + } = await generateWorkspacePayload(auth); + + //gerar um predicate + const members = [data_user1.address, data_user2.address, USER_5.address]; + + const { predicatePayload, vault } = await PredicateMock.create(1, members); + await api.axios.post('/predicate', predicatePayload); + + //gerar uma transacao com um usuário inválido + const { payload_transfer } = await transactionMock(vault); + const { + status: status_transaction, + data: data_transaction, + } = await auth.axios.post('/transaction', payload_transfer).catch(e => { + return e.response; }); - //const txs = await vault.BSAFEGetTransactions(); + //validacoes + expect(status_transaction).toBe(401); + expect(data_transaction).toHaveProperty( + 'detail', + 'You do not have permission to access this resource', + ); + }, + 60 * 1000, + ); - try { - await tx_1.wait(); + test( + 'Create transaction with vault member', + async () => { + // logar com usuário inválido no workspace + const auth = new AuthValidations(networks['local'], accounts['USER_5']); + await auth.create(); + await auth.createSession(); + const { + data, + data_user1, + data_user2, + USER_5, + } = await generateWorkspacePayload(auth); - const tx_2 = await vault.BSAFEIncludeTransaction(transaction); - console.log( - '[TRANSACOES_DOIS]', - tx_2.getHashTxId(), - tx_2.BSAFETransactionId, - ); + //gerar um predicate + const members = [data_user1.address, data_user2.address, USER_5.address]; - await api.axios.put(`/transaction/signer/${tx_2.BSAFETransactionId}`, { - signer: await signBypK(tx_2.getHashTxId(), accounts['USER_1'].privateKey), - account: accounts['USER_1'].address, - confirm: true, - }); + const { predicatePayload, vault } = await PredicateMock.create(1, members); + await api.axios.post('/predicate', predicatePayload); + + //gerar uma transacao com um usuário inválido + const { payload_transfer } = await transactionMock(vault); + const { status: status_transaction } = await auth.axios.post( + '/transaction', + payload_transfer, + ); - await tx_2.wait(); - } catch (e) { - console.log(e); - } + //validacoes + expect(status_transaction).toBe(200); }, - 30 * 1000, + 60 * 1000, ); + + test('List transactions', async () => { + const auth = new AuthValidations(networks['local'], accounts['USER_1']); + await auth.create(); + await auth.createSession(); + + //on single workspace + await auth.axios.get('/transaction').then(({ data, status }) => { + expect(status).toBe(200); + let prev = undefined; + + data.forEach((element, index) => { + const aux = element.predicate; + if (prev && index > 0) { + expect(new Date(prev).getTime()).toBeGreaterThan( + new Date(element.updatedAt).getTime(), + ); + } + prev = element.updatedAt; + + expect(aux).toHaveProperty('id'); + expect(aux).toHaveProperty('predicateAddress'); + expect(aux.workspace).toHaveProperty('id', auth.workspace.id); + expect(aux.workspace).toHaveProperty('name', auth.workspace.name); + }); + }); + + //with pagination + const page = 1; + const perPage = 9; + await auth.axios + .get(`/transaction?page=${page}&perPage=${perPage}`) + .then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveProperty('data'); + expect(data.data.length).toBeLessThanOrEqual(perPage); + expect(data).toHaveProperty('total'); + expect(data).toHaveProperty('currentPage', page); + expect(data).toHaveProperty('perPage', perPage); + }); + + const _status = [ + TransactionStatus.AWAIT_REQUIREMENTS, + TransactionStatus.PENDING_SENDER, + ]; + await auth.axios + .get(`/transaction?status=${_status[0]}&status=${_status[1]}`) + .then(({ data, status }) => { + expect(status).toBe(200); + data.forEach(element => { + const aux = _status.includes(element.status); + expect(aux).toBe(true); + }); + }); + + //an another workspace + const { data: data_workspace } = await generateWorkspacePayload(auth); + await auth.selectWorkspace(data_workspace.id); + await auth.axios.get('/transaction').then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveLength(0); + }); + }); }); diff --git a/src/modules/transaction/controller.ts b/src/modules/transaction/controller.ts index 6441b132d..d27c415d2 100644 --- a/src/modules/transaction/controller.ts +++ b/src/modules/transaction/controller.ts @@ -1,8 +1,17 @@ import { ITransactionResume, TransactionStatus } from 'bsafe'; -import { Provider } from 'fuels'; +import { Provider, Signer, hashMessage } from 'fuels'; import AddressBook from '@src/models/AddressBook'; +import { PermissionRoles, Workspace } from '@src/models/Workspace'; +import { + Unauthorized, + UnauthorizedErrorTitles, +} from '@src/utils/error/Unauthorized'; import { IPagination } from '@src/utils/pagination'; +import { + validatePermissionVault, + validatePermissionGeneral, +} from '@src/utils/permissionValidate'; import { NotificationTitle, @@ -14,12 +23,16 @@ import { import { IPredicateService } from '@modules/predicate/types'; import { IWitnessService } from '@modules/witness/types'; -import { error } from '@utils/error'; +import { ErrorTypes, NotFound, error } from '@utils/error'; import { Responses, bindMethods, successful } from '@utils/index'; import { IAddressBookService } from '../addressBook/types'; import { IAssetService } from '../asset/types'; import { INotificationService } from '../notification/types'; +import { PredicateService } from '../predicate/services'; +import { UserService } from '../user/service'; +import { WorkspaceService } from '../workspace/services'; +import { TransactionService } from './services'; import { ICloseTransactionRequest, ICreateTransactionRequest, @@ -57,18 +70,76 @@ export class TransactionController { bindMethods(this); } - async create({ body: transaction, user }: ICreateTransactionRequest) { - const { predicateAddress, summary } = transaction; - + async pending(req: IListRequest) { try { - const predicate = await this.predicateService + const { workspace, user } = req; + const { predicateId } = req.query; + const { workspaceList, hasSingle } = await new UserService().workspacesByUser( + workspace, + user, + ); + + const result = await new TransactionService() .filter({ - address: predicateAddress, + status: [TransactionStatus.AWAIT_REQUIREMENTS], + signer: hasSingle ? user.address : undefined, + workspaceId: workspaceList, + predicateId, }) - .paginate(undefined) + .list() + .then((result: Transaction[]) => { + return { + ofUser: + result.filter(transaction => + transaction.witnesses.find( + w => + w.account === user.address && + w.status === WitnessesStatus.PENDING, + ), + ).length ?? 0, + transactionsBlocked: + result.filter(transaction => + transaction.witnesses.find( + w => w.status === WitnessesStatus.PENDING, + ), + ).length > 0 ?? false, + }; + }); + return successful(result, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async create({ body: transaction, user, workspace }: ICreateTransactionRequest) { + const { predicateAddress, summary } = transaction; + + try { + const predicate = await new PredicateService() + .filter({ address: predicateAddress }) .list() .then((result: Predicate[]) => result[0]); + // if possible move this next part to a middleware, but we dont have access to body of request + // ======================================================================================================== + const hasPermission = validatePermissionGeneral(workspace, user.id, [ + PermissionRoles.OWNER, + PermissionRoles.ADMIN, + PermissionRoles.MANAGER, + ]); + const isMemberOfPredicate = predicate.members.find( + member => member.id === user.id, + ); + + if (!isMemberOfPredicate && !hasPermission) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_PERMISSION, + detail: 'You do not have permission to access this resource', + }); + } + // ======================================================================================================== + const witnesses = predicate.members.map(member => ({ account: member.address, status: WitnessesStatus.PENDING, @@ -100,6 +171,8 @@ export class TransactionController { summary, }); + await new PredicateService().update(predicate.id); + const { id, name } = newTransaction; const membersWithoutLoggedUser = predicate.members.filter( member => member.id !== user.id, @@ -172,12 +245,44 @@ export class TransactionController { }: ISignByIdRequest) { try { const transaction = await this.transactionService.findById(id); - - const { witnesses, resume, predicate, name, id: transactionId } = transaction; + //console.log('[ASSINATURA] --------->'); + const { + witnesses, + resume, + predicate, + name, + id: transactionId, + hash, + } = transaction; const _resume = resume; const witness = witnesses.find(w => w.account === account); + // console.log( + // '[VALIDACAO DE ASSINATURA]: ', + // Signer.recoverAddress(hashMessage(hash), signer).toString(), + // ); + //validate signature + + // console.log( + // '[VALIDACAO DE ASSINATURA]: ', + // acc_signed, + // Signer.recoverAddress(hashMessage(hash), signer).toString(), + // ); + if (signer && confirm) { + const acc_signed = + Signer.recoverAddress(hashMessage(hash), signer).toString() == + user.address; + if (!acc_signed) { + throw new NotFound({ + type: ErrorTypes.NotFound, + title: UnauthorizedErrorTitles.INVALID_SIGNATURE, + detail: + 'Your signature is invalid or does not match the transaction hash', + }); + } + } + if (witness) { await this.witnessService.update(witness.id, { signature: signer, @@ -185,7 +290,6 @@ export class TransactionController { }), _resume.witnesses.push(signer); - //console.log('[SIGNER_BY_ID_VALIDATE]: ', transaction.status); const statusField = await this.transactionService.validateStatus(id); const result = await this.transactionService.update(id, { @@ -200,7 +304,6 @@ export class TransactionController { await this.transactionService .sendToChain(id) .then(async (result: ITransactionResume) => { - console.log('[SUCCESS SEND]'); return await this.transactionService.update(id, { status: TransactionStatus.PROCESS_ON_CHAIN, sendTime: new Date(), @@ -208,7 +311,6 @@ export class TransactionController { }); }) .catch(async () => { - console.log('[FAILED SEND]'); await this.transactionService.update(id, { status: TransactionStatus.FAILED, sendTime: new Date(), @@ -257,86 +359,138 @@ export class TransactionController { } } - async list(req: IListRequest) { - const { - predicateId, - to, - status, - orderBy, - sort, - page, - perPage, - limit, - endDate, - startDate, - createdBy, - name, - allOfUser, - id, - } = req.query; - const { user } = req; - - const _predicateId = - typeof predicateId == 'string' ? [predicateId] : predicateId; - const hasPagination = !!page && !!perPage; + // async list(req: IListRequest) { + // const { + // predicateId, + // to, + // status, + // orderBy, + // sort, + // page, + // perPage, + // limit, + // endDate, + // startDate, + // createdBy, + // name, + // allOfUser, + // id, + // } = req.query; + // const { user } = req; + + // const _predicateId = + // typeof predicateId == 'string' ? [predicateId] : predicateId; + // const hasPagination = !!page && !!perPage; + + // try { + // const predicateIds: string[] = allOfUser + // ? await this.predicateService + // .filter({ signer: user.address }) + // .paginate(undefined) + // .list() + // .then((data: Predicate[]) => { + // return data.map(predicate => predicate.id); + // }) + // : predicateId + // ? _predicateId + // : undefined; + + // if (predicateIds && predicateIds.length === 0) + // return successful([], Responses.Ok); + + // let response = await this.transactionService + // .filter({ + // predicateId: predicateIds, + // to, + // status, + // endDate, + // startDate, + // createdBy, + // name, + // limit, + // id, + // }) + // .ordination({ orderBy, sort }) + // .paginate({ page, perPage }) + // .list(); + + // let data = hasPagination + // ? (response as IPagination).data + // : (response as Transaction[]); + + // const assets = data.map(i => i.assets); + // const recipientAddresses = assets.flat().map(i => i.to); + // const favorites = (await this.addressBookService + // .filter({ owner: [user.id], contactAddresses: recipientAddresses }) + // .list()) as AddressBook[]; + + // if (favorites.length > 0) { + // data = (data.map(transaction => ({ + // ...transaction, + // assets: transaction.assets.map(asset => ({ + // ...asset, + // recipientNickname: + // favorites?.find(favorite => favorite.user.address === asset.to) + // ?.nickname ?? undefined, + // })), + // })) as unknown) as Transaction[]; + // } + + // response = hasPagination ? { ...response, data } : data; + + // return successful(response, Responses.Ok); + // } catch (e) { + // return error(e.error, e.statusCode); + // } + // } + async list(req: IListRequest) { try { - const predicateIds: string[] = allOfUser - ? await this.predicateService - .filter({ signer: user.address }) - .paginate(undefined) - .list() - .then((data: Predicate[]) => { - return data.map(predicate => predicate.id); - }) - : predicateId - ? _predicateId - : undefined; + const { + to, + status, + orderBy, + sort, + page, + perPage, + createdBy, + predicateId, + name, + } = req.query; + const { workspace, user } = req; + + const singleWorkspace = await new WorkspaceService() + .filter({ + user: user.id, + single: true, + }) + .list() + .then((response: Workspace[]) => response[0]); + + const allWk = await new WorkspaceService() + .filter({ + user: user.id, + }) + .list() + .then((response: Workspace[]) => response.map(wk => wk.id)); - if (predicateIds && predicateIds.length === 0) - return successful([], Responses.Ok); + const hasSingle = singleWorkspace.id === workspace.id; - let response = await this.transactionService + const result = await new TransactionService() .filter({ - predicateId: predicateIds, to, - status, - endDate, - startDate, + status: status ?? undefined, createdBy, name, - limit, - id, + workspaceId: hasSingle ? allWk : [workspace.id], + signer: hasSingle ? user.address : undefined, + predicateId: predicateId ?? undefined, }) .ordination({ orderBy, sort }) .paginate({ page, perPage }) .list(); - let data = hasPagination - ? (response as IPagination).data - : (response as Transaction[]); - - const assets = data.map(i => i.assets); - const recipientAddresses = assets.flat().map(i => i.to); - const favorites = (await this.addressBookService - .filter({ createdBy: user.id, contactAddresses: recipientAddresses }) - .list()) as AddressBook[]; - - if (favorites.length > 0) { - data = (data.map(transaction => ({ - ...transaction, - assets: transaction.assets.map(asset => ({ - ...asset, - recipientNickname: - favorites?.find(favorite => favorite.user.address === asset.to) - ?.nickname ?? undefined, - })), - })) as unknown) as Transaction[]; - } - - response = hasPagination ? { ...response, data } : data; - - return successful(response, Responses.Ok); + return successful(result, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); } @@ -364,7 +518,6 @@ export class TransactionController { const resume = await this.transactionService .sendToChain(id) .then(async (result: ITransactionResume) => { - console.log('[SUCCESS SEND]'); return await this.transactionService.update(id, { status: TransactionStatus.PROCESS_ON_CHAIN, sendTime: new Date(), @@ -372,7 +525,6 @@ export class TransactionController { }); }) .catch(async () => { - console.log('[FAILED SEND]'); await this.transactionService.update(id, { status: TransactionStatus.FAILED, sendTime: new Date(), diff --git a/src/modules/transaction/routes.ts b/src/modules/transaction/routes.ts index d3e26b27d..117a91924 100644 --- a/src/modules/transaction/routes.ts +++ b/src/modules/transaction/routes.ts @@ -10,6 +10,7 @@ import { handleResponse } from '@utils/index'; import { AddressBookService } from '../addressBook/services'; import { AssetService } from '../asset/services'; import { NotificationService } from '../notification/services'; +import { UserService } from '../user/service'; import { TransactionController } from './controller'; import { TransactionService } from './services'; import { @@ -25,15 +26,17 @@ const witnessService = new WitnessService(); const addressBookService = new AddressBookService(); const assetService = new AssetService(); const notificationService = new NotificationService(); +const userService = new UserService(); const { - create, - signByID, list, - findById, + send, close, + create, + pending, + findById, + signByID, findByHash, - send, verifyOnChain, } = new TransactionController( transactionService, @@ -48,6 +51,7 @@ router.use(authMiddleware); router.post('/', validateAddTransactionPayload, handleResponse(create)); router.get('/', handleResponse(list)); +router.get('/pending', handleResponse(pending)); router.get('/:id', handleResponse(findById)); router.get('/by-hash/:hash', handleResponse(findByHash)); router.put('/close/:id', validateCloseTransactionPayload, handleResponse(close)); diff --git a/src/modules/transaction/services.ts b/src/modules/transaction/services.ts index dbb45c2cd..fec0db9f3 100644 --- a/src/modules/transaction/services.ts +++ b/src/modules/transaction/services.ts @@ -12,6 +12,9 @@ import { transactionRequestify, TransactionResponse, } from 'fuels'; +import { Brackets } from 'typeorm'; + +import { sendMail, EmailTemplateType } from '@src/utils/EmailSender'; import { NotificationTitle, @@ -112,20 +115,38 @@ export class TransactionService implements ITransactionService { async list(): Promise | Transaction[]> { const hasPagination = this._pagination?.page && this._pagination?.perPage; - const queryBuilder = Transaction.createQueryBuilder('t').select([ - 't.createdAt', - 't.gasUsed', - 't.hash', - 't.createdAt', - 't.id', - 't.name', - 't.predicateId', - 't.resume', - 't.sendTime', - 't.status', - 't.summary', - 't.updatedAt', - ]); + const queryBuilder = Transaction.createQueryBuilder('t') + .select([ + 't.createdAt', + 't.gasUsed', + 't.hash', + 't.createdAt', + 't.id', + 't.name', + 't.predicateId', + 't.resume', + 't.sendTime', + 't.status', + 't.summary', + 't.updatedAt', + ]) + .leftJoinAndSelect('t.assets', 'assets') + .innerJoin('t.witnesses', 'witnesses') + .innerJoin('t.predicate', 'predicate') + .addSelect([ + 'predicate.name', + 'predicate.id', + 'predicate.minSigners', + 'predicate.predicateAddress', + 'witnesses.id', + 'witnesses.account', + 'witnesses.signature', + 'witnesses.status', + ]) + .innerJoin('predicate.members', 'members') + .addSelect(['members.id', 'members.avatar', 'members.address']) + .innerJoin('predicate.workspace', 'workspace') + .addSelect(['workspace.id', 'workspace.name', 'workspace.single']); this._filter.predicateAddress && this._filter.predicateAddress.length > 0 && @@ -133,6 +154,41 @@ export class TransactionService implements ITransactionService { address: this._filter.predicateAddress, }); + // =============== specific for workspace =============== + this._filter.workspaceId && + !this._filter.signer && + queryBuilder.andWhere( + new Brackets(qb => { + if (this._filter.workspaceId) { + qb.orWhere('workspace.id IN (:...workspace)', { + workspace: this._filter.workspaceId, + }); + } + }), + ); + // =============== specific for workspace =============== + //console.log('[transaction_FILTER]: ', this._filter); + + // =============== specific for home =============== + (this._filter.workspaceId || this._filter.signer) && + queryBuilder.andWhere( + new Brackets(qb => { + if (this._filter.workspaceId) { + qb.orWhere('workspace.id IN (:...workspace)', { + workspace: this._filter.workspaceId, + }); + } + if (this._filter.signer) { + qb.orWhere(subQb => { + subQb.where('witnesses.account = :signer', { + signer: this._filter.signer, + }); + }); + } + }), + ); + // =============== specific for home =============== + this._filter.to && queryBuilder .innerJoin('t.assets', 'asset') @@ -185,20 +241,7 @@ export class TransactionService implements ITransactionService { * */ this._filter.limit && !hasPagination && queryBuilder.take(this._filter.limit); - queryBuilder - .leftJoinAndSelect('t.assets', 'assets') - .leftJoinAndSelect('t.witnesses', 'witnesses') - .innerJoin('t.predicate', 'predicate') - .addSelect([ - 'predicate.name', - 'predicate.id', - 'predicate.description', - 'predicate.minSigners', - 'predicate.predicateAddress', - ]) - .innerJoin('predicate.members', 'members') - .addSelect(['members.id', 'members.avatar', 'members.address']) - .orderBy(`t.${this._ordination.orderBy}`, this._ordination.sort); + queryBuilder.orderBy(`t.${this._ordination.orderBy}`, this._ordination.sort); const handleInternalError = e => { if (e instanceof GeneralError) throw e; @@ -346,11 +389,9 @@ export class TransactionService implements ITransactionService { .toString(), status: TransactionStatus.PROCESS_ON_CHAIN, }; - console.log('[ENVIADO]', resume); return resume; }) .catch(e => { - console.log('[ERRO AO ENVIAR]', e); throw new Internal({ type: ErrorTypes.Internal, title: 'Error on transaction sendToChain', @@ -411,18 +452,27 @@ export class TransactionService implements ITransactionService { // NOTIFY MEMBERS ON TRANSACTIONS SUCCESS const notificationService = new NotificationService(); + const summary = { + vaultId: api_transaction.predicate.id, + vaultName: api_transaction.predicate.name, + transactionId: api_transaction.id, + transactionName: api_transaction.name, + }; + if (result.status.type === TransactionProcessStatus.SUCCESS) { for await (const member of api_transaction.predicate.members) { await notificationService.create({ title: NotificationTitle.TRANSACTION_COMPLETED, - summary: { - vaultId: api_transaction.predicate.id, - vaultName: api_transaction.predicate.name, - transactionId: api_transaction.id, - transactionName: api_transaction.name, - }, + summary, user_id: member.id, }); + + if (member.notify) { + await sendMail(EmailTemplateType.TRANSACTION_COMPLETED, { + to: member.email, + data: { summary: { ...summary, name: member?.name ?? '' } }, + }); + } } } diff --git a/src/modules/transaction/types.ts b/src/modules/transaction/types.ts index 6d19e6946..b67ba71b5 100644 --- a/src/modules/transaction/types.ts +++ b/src/modules/transaction/types.ts @@ -65,6 +65,8 @@ export type ICloseTransactionPayload = { export interface ITransactionFilterParams { predicateId?: string[]; predicateAddress?: string; + signer?: string; // address of logged user + workspaceId?: string[]; to?: string; hash?: string; status?: TransactionStatus[]; @@ -136,7 +138,7 @@ interface IListRequestSchema extends ValidatedRequestSchema { status: TransactionStatus[]; name: string; allOfUser: boolean; - predicateId: string[] | string; + predicateId: string[]; to: string; startDate: string; endDate: string; diff --git a/src/modules/transaction/validations.ts b/src/modules/transaction/validations.ts index ede66541e..973ab88a1 100644 --- a/src/modules/transaction/validations.ts +++ b/src/modules/transaction/validations.ts @@ -19,7 +19,7 @@ export const validateAddTransactionPayload = validator.body( assetId: Joi.string().required(), to: Joi.string().required(), amount: Joi.string().required(), - utxo: Joi.string().required().allow(null, ''), + utxo: Joi.string().allow('').required(), }) .required(), sendTime: Joi.string(), diff --git a/src/modules/user/__tests__/user.tests.ts b/src/modules/user/__tests__/user.tests.ts new file mode 100644 index 000000000..af165886a --- /dev/null +++ b/src/modules/user/__tests__/user.tests.ts @@ -0,0 +1,51 @@ +import axios from 'axios'; +import { accounts } from 'bsafe/dist/cjs/mocks/accounts'; +import { Address } from 'fuels'; + +import { networks, providers } from '@src/mocks/networks'; +import { AuthValidations } from '@src/utils/testUtils/Auth'; + +describe('[USER]', () => { + let api = beforeAll(() => { + api = axios.create({ + baseURL: 'http://localhost:3333', + }); + }); + + test( + 'Create user', + async () => { + await api + .post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()} - Create user test`, + }) + .then(({ data, status }) => { + expect(status).toBe(201); + }); + }, + 40 * 1000, + ); + + test.only('Home endpoint', async () => { + const auth = new AuthValidations(networks['local'], accounts['USER_1']); + await auth.create(); + await auth.createSession(); + + //list by personal workspace + await auth.axios.get('user/me').then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveProperty('predicates'); + expect(data).toHaveProperty('transactions'); + expect(data.predicates.data.length).toBeLessThanOrEqual(8); + expect(data.transactions.data.length).toBeLessThanOrEqual(8); + data.predicates.data.forEach(element => { + expect(element.workspace).toHaveProperty('id', auth.workspace.id); + }); + data.transactions.data.forEach(element => { + expect(element.predicate.workspace).toHaveProperty('id', auth.workspace.id); + }); + }); + }); +}); diff --git a/src/modules/configs/user/controller.ts b/src/modules/user/controller.ts similarity index 54% rename from src/modules/configs/user/controller.ts rename to src/modules/user/controller.ts index 0cd83ed67..834a78a13 100644 --- a/src/modules/configs/user/controller.ts +++ b/src/modules/user/controller.ts @@ -3,11 +3,15 @@ import { bindMethods } from '@src/utils/bindMethods'; import { error } from '@utils/error'; import { Responses, successful } from '@utils/index'; +import { PredicateService } from '../predicate/services'; +import { TransactionService } from '../transaction/services'; +import { UserService } from './service'; import { ICreateRequest, IDeleteRequest, IFindOneRequest, IListRequest, + IMeRequest, IUpdateRequest, IUserService, } from './types'; @@ -36,30 +40,76 @@ export class UserController { } } + async info(req: IListRequest) { + const { user } = req; + + return successful( + { + id: user.id, + name: user.name, + email: user.email, + first_login: user.first_login, + notify: user.notify, + }, + Responses.Ok, + ); + } + + async me(req: IMeRequest) { + try { + //list all 8 last vaults of user + const { workspace, user } = req; + const { workspaceList, hasSingle } = await new UserService().workspacesByUser( + workspace, + user, + ); + + const predicates = await new PredicateService() + .filter({ + workspace: workspaceList, + signer: hasSingle ? user.address : undefined, + }) + .paginate({ page: '0', perPage: '8' }) + .ordination({ orderBy: 'updatedAt', sort: 'DESC' }) + .list(); + + const transactions = await new TransactionService() + .filter({ + workspaceId: workspaceList, + signer: hasSingle ? user.address : undefined, + }) + .paginate({ page: '0', perPage: '6' }) + .ordination({ orderBy: 'createdAt', sort: 'DESC' }) + .list(); + + return successful( + { + workspace: { + id: workspace.id, + name: workspace.name, + avatar: workspace.avatar, + owner: workspace.owner, + description: workspace.description, + }, + predicates, + transactions, + }, + Responses.Ok, + ); + } catch (e) { + return error(e.error, e.statusCode); + } + } + async create(req: ICreateRequest) { try { - const { - name, - email, - password, - active, - language, - role, - address, - provider, - } = req.body; + const { address } = req.body; const existingUser = await this.userService.findByAddress(address); if (existingUser) return successful(existingUser, Responses.Created); const response = await this.userService.create({ - name, - email, - password, - active, - language, - address, - provider, + ...req.body, avatar: await this.userService.randomAvatar(), }); diff --git a/src/modules/configs/user/routes.ts b/src/modules/user/routes.ts similarity index 88% rename from src/modules/configs/user/routes.ts rename to src/modules/user/routes.ts index 7c056d524..e5df0cfef 100644 --- a/src/modules/configs/user/routes.ts +++ b/src/modules/user/routes.ts @@ -16,7 +16,9 @@ router.post('/', PayloadCreateUserSchema, handleResponse(userController.create)) router.use(authMiddleware); +router.get('/me', handleResponse(userController.me)); router.get('/', handleResponse(userController.find)); +router.get('/info', handleResponse(userController.info)); router.get('/:id', handleResponse(userController.findOne)); router.put('/:id', PayloadUpdateUserSchema, handleResponse(userController.update)); router.delete('/:id', handleResponse(userController.delete)); diff --git a/src/modules/configs/user/service.ts b/src/modules/user/service.ts similarity index 78% rename from src/modules/configs/user/service.ts rename to src/modules/user/service.ts index 93a1f3930..927752f93 100644 --- a/src/modules/configs/user/service.ts +++ b/src/modules/user/service.ts @@ -2,12 +2,18 @@ import axios from 'axios'; import { Brackets } from 'typeorm'; import { User } from '@src/models'; +import { + PermissionRoles, + Workspace, + defaultPermissions, +} from '@src/models/Workspace'; import { ErrorTypes, NotFound } from '@src/utils/error'; import GeneralError from '@src/utils/error/GeneralError'; import Internal from '@src/utils/error/Internal'; import { IOrdination, setOrdination } from '@src/utils/ordination'; import { IPagination, Pagination, PaginationParams } from '@src/utils/pagination'; +import { WorkspaceService } from '../workspace/services'; import { IFilterParams, IUserService, IUserPayload } from './types'; const { UI_URL } = process.env; @@ -75,7 +81,17 @@ export class UserService implements IUserService { async create(payload: IUserPayload): Promise { return await User.create(payload) .save() - .then(data => { + .then(async data => { + await new WorkspaceService().create({ + name: `singleWorkspace[${data.id}]`, + owner: data, + members: [data], + avatar: await this.randomAvatar(), + permissions: { + [data.id]: defaultPermissions[PermissionRoles.OWNER], + }, + single: true, + }); delete data.password; return data; }) @@ -154,4 +170,33 @@ export class UserService implements IUserService { const random = Math.floor(Math.random() * avatars.length); return `${url}/${avatars[random]}`; } + + async workspacesByUser(workspace: Workspace, user: User) { + const workspaceList = [workspace.id]; + const singleWorkspace = await new WorkspaceService() + .filter({ + user: user.id, + single: true, + }) + .list() + .then((response: Workspace[]) => response[0]); + const hasSingle = singleWorkspace.id === workspace.id; + + if (hasSingle) { + await new WorkspaceService() + .filter({ + user: user.id, + single: false, + }) + .list() + .then((response: Workspace[]) => + response.map(w => workspaceList.push(w.id)), + ); + } + + return { + workspaceList, + hasSingle, + }; + } } diff --git a/src/modules/configs/user/types.ts b/src/modules/user/types.ts similarity index 89% rename from src/modules/configs/user/types.ts rename to src/modules/user/types.ts index 9e001c17b..a6e4ebd97 100644 --- a/src/modules/configs/user/types.ts +++ b/src/modules/user/types.ts @@ -3,6 +3,7 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import { AuthValidatedRequest } from '@src/middlewares/auth/types'; import { Languages, User } from '@src/models'; import Role from '@src/models/Role'; +import { Workspace } from '@src/models/Workspace'; import { IOrdination } from '@src/utils/ordination'; import { IPagination, PaginationParams } from '@src/utils/pagination'; @@ -12,7 +13,6 @@ export interface IUserPayload { password?: string; active?: boolean; language?: Languages; - role: Role; address: string; provider: string; avatar: string; @@ -61,6 +61,8 @@ export type IUpdateRequest = AuthValidatedRequest; export type IDeleteRequest = AuthValidatedRequest; +export type IMeRequest = AuthValidatedRequest; + export interface IUserService { filter(filter: IFilterParams): this; paginate(pagination: PaginationParams): this; @@ -72,4 +74,11 @@ export interface IUserService { randomAvatar(): Promise; update(id: string, payload: IUserPayload): Promise; delete(id: string): Promise; + workspacesByUser( + worksapce: Workspace, + user: User, + ): Promise<{ + workspaceList: string[]; + hasSingle: boolean; + }>; } diff --git a/src/modules/configs/user/validation.ts b/src/modules/user/validation.ts similarity index 66% rename from src/modules/configs/user/validation.ts rename to src/modules/user/validation.ts index a018e96ac..270580ab0 100644 --- a/src/modules/configs/user/validation.ts +++ b/src/modules/user/validation.ts @@ -19,13 +19,9 @@ export const PayloadCreateUserSchema = validator.body( export const PayloadUpdateUserSchema = validator.body( Joi.object({ - name: Joi.string().required(), - email: Joi.string().email().required(), - password: Joi.string().optional(), - active: Joi.boolean().required(), - language: Joi.string() - .valid(...Object.values(Languages)) - .required(), - role: Joi.number().required(), + name: Joi.string().allow(''), + email: Joi.string().email().allow(''), + notify: Joi.boolean().optional(), + first_login: Joi.boolean().optional(), }), ); diff --git a/src/modules/vaultTemplate/controller.ts b/src/modules/vaultTemplate/controller.ts index 145e5b5d5..ddad2479c 100644 --- a/src/modules/vaultTemplate/controller.ts +++ b/src/modules/vaultTemplate/controller.ts @@ -1,9 +1,10 @@ +import { User } from '@src/models'; import Role from '@src/models/Role'; import { error } from '@utils/error'; import { Responses, bindMethods, successful } from '@utils/index'; -import { IUserService } from '../configs/user/types'; +import { IUserService } from '../user/types'; import { IVaultTemplateService, ICreateVaultTemplateRequest, @@ -26,7 +27,8 @@ export class VaultTemplateController { async create({ body, user }: ICreateVaultTemplateRequest) { try { - const addMembers = body.addresses.map(async address => { + const members: User[] = []; + for await (const address of body.addresses) { let user = await this.userService.findByAddress(address); if (!user) { @@ -37,18 +39,14 @@ export class VaultTemplateController { }); } - return user; - }); + members.push(user); + } - const members = await Promise.all(addMembers); - const newTemplate = await this.vaultTemplateService - .create - // { - // ...body, - // createdBy: user, - // addresses: members, - // } - (); + const newTemplate = await this.vaultTemplateService.create({ + ...body, + createdBy: user, + addresses: members, + }); return successful(newTemplate, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); @@ -79,7 +77,13 @@ export class VaultTemplateController { try { const response = await this.vaultTemplateService.findById(id); - return successful(response, Responses.Ok); + return successful( + { + ...response, + addresses: response.addresses.map(address => address.address), + }, + Responses.Ok, + ); } catch (e) { return error(e.error, e.statusCode); } diff --git a/src/modules/vaultTemplate/routes.ts b/src/modules/vaultTemplate/routes.ts index 0b2a40b35..27d0592fd 100644 --- a/src/modules/vaultTemplate/routes.ts +++ b/src/modules/vaultTemplate/routes.ts @@ -4,7 +4,7 @@ import { authMiddleware } from '@src/middlewares'; import { handleResponse } from '@utils/index'; -import { UserService } from '../configs/user/service'; +import { UserService } from '../user/service'; import { VaultTemplateController } from './controller'; import { VaultTemplateService } from './services'; import { validateCreatePayload } from './validations'; diff --git a/src/modules/vaultTemplate/services.ts b/src/modules/vaultTemplate/services.ts index 3e4e6344f..d16b310e0 100644 --- a/src/modules/vaultTemplate/services.ts +++ b/src/modules/vaultTemplate/services.ts @@ -1,4 +1,4 @@ -import VaultTemplate from '@src/models/VaultTemplate'; +import { VaultTemplate } from '@src/models/VaultTemplate'; import { NotFound } from '@utils/error'; import GeneralError, { ErrorTypes } from '@utils/error/GeneralError'; @@ -35,18 +35,17 @@ export class VaultTemplateService implements IVaultTemplateService { return this; } - async create(): Promise { - return new VaultTemplate(); - // return await VaultTemplate.create(payload) - // .save() - // .then(template => template) - // .catch(e => { - // throw new Internal({ - // type: ErrorTypes.Internal, - // title: 'Error on vault template creation', - // detail: e, - // }); - // }); + async create(payload: ICreatePayload): Promise { + return await VaultTemplate.create(payload) + .save() + .then(template => template) + .catch(e => { + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on vault template creation', + detail: e, + }); + }); } async update(id: string, payload?: IUpdatePayload): Promise { diff --git a/src/modules/vaultTemplate/types.ts b/src/modules/vaultTemplate/types.ts index 1b80f04ef..94a293e8d 100644 --- a/src/modules/vaultTemplate/types.ts +++ b/src/modules/vaultTemplate/types.ts @@ -1,6 +1,6 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; -import VaultTemplate from '@src/models/VaultTemplate'; +import { VaultTemplate } from '@src/models/VaultTemplate'; import { User } from '@models/index'; @@ -24,7 +24,7 @@ export interface ICreatePayload { name: string; description: string; minSigners: number; - addresses: User[] | string[]; + addresses: User[]; createdBy: User; } @@ -40,8 +40,12 @@ export interface IFilterParams { user?: User; } +type ICreatePayloadBody = Omit; + interface ICreateVaultTemplate extends ValidatedRequestSchema { - [ContainerTypes.Body]: ICreatePayload; + [ContainerTypes.Body]: ICreatePayloadBody & { + addresses: string[]; + }; } interface IUpdateVaultTemplate extends ValidatedRequestSchema { @@ -65,6 +69,10 @@ interface IFindByIdVaultTemplate extends ValidatedRequestSchema { }; } +interface IReturnVaultTemplate extends Omit { + addresses: string[]; +} + export type ICreateVaultTemplateRequest = AuthValidatedRequest; export type IUpdateVaultTemplateRequest = AuthValidatedRequest; export type ILisVaultTemplatetRequest = AuthValidatedRequest; @@ -75,7 +83,7 @@ export interface IVaultTemplateService { paginate(pagination?: PaginationParams): this; filter(filter: IFilterParams): this; - create: () => Promise; + create: (payload: ICreatePayload) => Promise; update: (id: string, payload: IUpdatePayload) => Promise; list: () => Promise | VaultTemplate[]>; findById: (id: string) => Promise; diff --git a/src/modules/workspace/__tests__/workspace.tests.ts b/src/modules/workspace/__tests__/workspace.tests.ts new file mode 100644 index 000000000..2f85fa1ce --- /dev/null +++ b/src/modules/workspace/__tests__/workspace.tests.ts @@ -0,0 +1,279 @@ +import { Address } from 'fuels'; + +import { accounts } from '@src/mocks/accounts'; +import { networks, providers } from '@src/mocks/networks'; +import { PermissionRoles, defaultPermissions } from '@src/models/Workspace'; +import { AuthValidations } from '@src/utils/testUtils/Auth'; +import { generateWorkspacePayload } from '@src/utils/testUtils/Workspace'; + +describe('[WORKSPACE]', () => { + let api: AuthValidations; + beforeAll(async () => { + api = new AuthValidations(networks['local'], accounts['USER_1']); + + await api.create(); + await api.createSession(); + }); + + test( + 'List all workspaces to user', + async () => { + //list workspaces + await api.axios.get(`/workspace/by-user`).then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toBeInstanceOf(Array); + + data.forEach(element => { + expect(element).toHaveProperty('id'); + expect(element).toHaveProperty('name'); + expect(element).toHaveProperty('owner'); + expect(element).toHaveProperty('members'); + expect(element).toHaveProperty('single', false); + expect(element).toHaveProperty('permissions'); + const aux = element.members.find( + m => m.address === api.authToken.address, + ); + expect(!!aux).toBe(true); + }); + }); + }, + 40 * 1000, + ); + + test( + 'Create workspace', + async () => { + const { data, status } = await generateWorkspacePayload(api); + const notOwner = data.members.filter(m => m.id !== data.owner.id); + + expect(status).toBe(201); + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('owner'); + expect(data).toHaveProperty('members'); + expect(data).toHaveProperty('single', false); + expect(data.members).toHaveLength(notOwner.length + 1); + for (const member of notOwner) { + expect(data.permissions[member.id]).toStrictEqual( + defaultPermissions[PermissionRoles.VIEWER], + ); + } + expect(data.permissions[data.owner.id]).toStrictEqual( + defaultPermissions[PermissionRoles.OWNER], + ); + }, + 60 * 1000, + ); + + test( + 'Find workspace by ID', + async () => { + const { data } = await generateWorkspacePayload(api); + + const { data: workspace, status: status_find } = await api.axios.get( + `/workspace/${data.id}`, + ); + + expect(status_find).toBe(200); + expect(workspace).toHaveProperty('id'); + expect(workspace.id).toBe(data.id); + expect(workspace).toHaveProperty('owner'); + expect(workspace.owner).toEqual(data.owner); + expect(workspace).toHaveProperty('members'); + expect(workspace.members).toHaveLength(data.members.length); + expect(workspace).toHaveProperty('name', data.name); + }, + 60 * 1000, + ); + + test('Update workspace', async () => { + const { data } = await generateWorkspacePayload(api); + + const { data: workspace, status: status_find } = await api.axios.get( + `/workspace/${data.id}`, + ); + + const { data: workspace_updated, status: status_update } = await api.axios.put( + `/workspace/${data.id}`, + { + name: 'Workspace 1 updated', + description: 'Workspace 1 description updated', + }, + ); + + expect(status_find).toBe(200); + expect(workspace).toHaveProperty('id'); + expect(workspace.id).toBe(data.id); + expect(workspace).toHaveProperty('owner'); + expect(workspace.owner).toEqual(data.owner); + expect(workspace).toHaveProperty('members'); + expect(workspace.members).toHaveLength(data.members.length); + + expect(status_update).toBe(200); + expect(workspace_updated).toHaveProperty('id'); + expect(workspace_updated.id).toBe(data.id); + expect(workspace_updated).toHaveProperty('owner'); + expect(workspace_updated.owner).toEqual(data.owner); + expect(workspace_updated).toHaveProperty('members'); + expect(workspace_updated.members).toHaveLength(data.members.length); + expect(workspace_updated).toHaveProperty('name', 'Workspace 1 updated'); + expect(workspace_updated).toHaveProperty( + 'description', + 'Workspace 1 description updated', + ); + }); + + test( + 'Upgrade workspace permissions', + async () => { + const { data, data_user1, data_user2 } = await generateWorkspacePayload(api); + + const auth_aux = new AuthValidations(networks['local'], accounts['USER_5']); + await auth_aux.create(); + await auth_aux.createSession(); + await auth_aux.selectWorkspace(data.id); + + //update permission + await api.axios + .put(`/workspace/${data.id}/permissions/${data_user1.id}`, { + permissions: defaultPermissions[PermissionRoles.MANAGER], + }) + .then(({ data, status }) => { + expect(status).toBe(200); + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('owner'); + expect(data).toHaveProperty('members'); + expect(data).toHaveProperty('permissions'); + expect(data.permissions[data_user1.id]).toStrictEqual( + defaultPermissions[PermissionRoles.MANAGER], + ); + }); + + //update owner + await api.axios + .put(`/workspace/${data.id}/permissions/${data.owner.id}`, { + permissions: defaultPermissions[PermissionRoles.MANAGER], + }) + .catch(({ response }) => { + expect(response.status).toBe(401); + expect(response.data.detail).toEqual( + 'Owner cannot change his own permissions', + ); + }); + + //update without permission + await auth_aux.axios + .put(`/workspace/${data.id}/permissions/${data_user2.id}`, { + permissions: defaultPermissions[PermissionRoles.MANAGER], + }) + .catch(({ response }) => { + expect(response.status).toBe(401); + expect(response.data.errors.detail).toEqual( + 'You do not have permission to access this resource', + ); + }); + }, + 40 * 1000, + ); + + test('Upgrade workspace members', async () => { + const { data } = await generateWorkspacePayload(api); + + const auth_aux = new AuthValidations(networks['local'], accounts['USER_5']); + await auth_aux.create(); + await auth_aux.createSession(); + await auth_aux.selectWorkspace(data.id); + + const { data: data_user_aux } = await api.axios.post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()}_2 - Create user test`, + }); + + const { data: workspace } = await api.axios.get(`/workspace/${data.id}`); + + let quantityMembers = workspace.members.length; + + //include exists on database member + await api.axios + .post(`/workspace/${data.id}/members/${data_user_aux.id}/include`) + .then(({ data, status }) => { + quantityMembers++; + expect(status).toBe(200); + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('owner'); + expect(data).toHaveProperty('members'); + expect(data.members).toHaveLength(quantityMembers); + expect( + data.members.find(m => m.address === data_user_aux.address), + ).toBeDefined(); + expect(data).toHaveProperty('permissions'); + expect(data.permissions[data_user_aux.id]).toStrictEqual( + defaultPermissions[PermissionRoles.VIEWER], + ); + }); + + //include not exists on database member (create) + const aux_byAddress = Address.fromRandom().toAddress(); + await api.axios + .post(`/workspace/${data.id}/members/${aux_byAddress}/include`) + .then(({ data, status }) => { + quantityMembers++; + const member = data.members.find(m => m.address === aux_byAddress); + expect(status).toBe(200); + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('owner'); + expect(data).toHaveProperty('members'); + expect(data.members.find(m => m.address === aux_byAddress)).toBeDefined(); + expect(data.members).toHaveLength(quantityMembers); + expect(data).toHaveProperty('permissions'); + expect(data.permissions[member.id]).toStrictEqual( + defaultPermissions[PermissionRoles.VIEWER], + ); + }); + + //remove member + await api.axios + .post(`/workspace/${data.id}/members/${data_user_aux.id}/remove`) + .then(({ data, status }) => { + quantityMembers--; + expect(status).toBe(200); + expect(data).toHaveProperty('id'); + expect(data).toHaveProperty('owner'); + expect(data).toHaveProperty('members'); + expect(data.members).toHaveLength(quantityMembers); + expect(data.members).not.toContainEqual(data_user_aux); + expect(data).toHaveProperty('permissions'); + }); + + //remove owner + await api.axios + .post(`/workspace/${data.id}/members/${workspace.owner.id}/remove`) + .catch(({ response }) => { + expect(response.status).toBe(401); + expect(response.data.detail).toEqual( + 'Owner cannot be removed from workspace', + ); + }); + + //update without permission + await auth_aux.axios + .post(`/workspace/${data.id}/members/${data_user_aux.id}/include`) + .catch(({ response }) => { + expect(response.status).toBe(401); + expect(response.data.errors.detail).toEqual( + 'You do not have permission to access this resource', + ); + }); + }); + + // get balance of workspace + test.only( + 'ATUAL', + async () => { + await api.axios.get(`/workspace/balance`).then(({ data, status }) => { + expect(status).toBe(200); + }); + }, + 40 * 1000, + ); +}); diff --git a/src/modules/workspace/controller.ts b/src/modules/workspace/controller.ts new file mode 100644 index 000000000..e11f5a578 --- /dev/null +++ b/src/modules/workspace/controller.ts @@ -0,0 +1,272 @@ +import axios from 'axios'; +import { Vault, defaultConfigurable } from 'bsafe'; +import { BN, Provider, bn } from 'fuels'; +import { parse } from 'path'; + +import { Predicate, User } from '@src/models'; +import { + PermissionRoles, + Workspace, + defaultPermissions, +} from '@src/models/Workspace'; +import Internal from '@src/utils/error/Internal'; +import { + Unauthorized, + UnauthorizedErrorTitles, +} from '@src/utils/error/Unauthorized'; + +import { ErrorTypes, error } from '@utils/error'; +import { Responses, successful } from '@utils/index'; + +import { PredicateService } from '../predicate/services'; +import { UserService } from '../user/service'; +import { WorkspaceService } from './services'; +import { + ICreateRequest, + IGetBalanceRequest, + IListByUserRequest, + IUpdateMembersRequest, + IUpdatePermissionsRequest, + IUpdateRequest, +} from './types'; + +export class WorkspaceController { + async listByUser(req: IListByUserRequest) { + try { + const { user } = req; + + const response = await new WorkspaceService() + .filter({ user: user.id, single: false }) + .list() + .then((response: Workspace[]) => + WorkspaceService.formatToUnloggedUser(response), + ); + + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async create(req: ICreateRequest) { + try { + const { user } = req; + const { members = [] } = req.body; + + const { + _members, + _permissions, + } = await new WorkspaceService().includeMembers(members, req.user); + + const response = await new WorkspaceService().create({ + ...req.body, + owner: user, + members: _members, + permissions: _permissions, + single: false, + avatar: await new UserService().randomAvatar(), + }); + + return successful(response, Responses.Created); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + // todo: implement this by other coins, and use utils of bsafe-sdk + async getBalance(req: IGetBalanceRequest) { + try { + const { workspace } = req; + const predicateService = new PredicateService(); + const balance = await Predicate.find({ + where: { + workspace: workspace.id, + }, + select: ['id'], + }).then(async (response: Predicate[]) => { + let _balance: BN = bn(0); + for await (const predicate of response) { + const vault = await predicateService.instancePredicate(predicate.id); + _balance = _balance.add(await vault.getBalance()); + } + return _balance; + }); + + const priceUSD = await axios + .get( + 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=BTC,USD,EUR', + ) + .then(({ data }) => { + return data.USD; + }) + .catch(() => 0.0); + + const balanceUSD = parseFloat(balance.format().toString()) * priceUSD; + + return successful( + { + balance: balance.format().toString(), + balanceUSD: balanceUSD.toFixed(2), + workspaceId: workspace.id, + }, + Responses.Ok, + ); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async findById(req: IListByUserRequest) { + try { + const { id } = req.params; + + const response = await new WorkspaceService() + .filter({ + id, + }) + .list() + .then((response: Workspace[]) => response[0]); + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async update(req: IUpdateRequest) { + try { + const { id } = req.params; + + const response = await new WorkspaceService() + .update({ + ...req.body, + id, + }) + .then(async () => { + return await new WorkspaceService() + .filter({ id }) + .list() + .then(data => data[0]); + }); + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async updatePermissions(req: IUpdatePermissionsRequest) { + try { + const { id, member } = req.params; + const { permissions } = req.body; + + const response = await new WorkspaceService() + .filter({ id }) + .list() + .then(async (data: Workspace[]) => { + if (!data) { + throw new Internal({ + type: ErrorTypes.NotFound, + title: 'Workspace not found', + detail: `Workspace ${id} not found`, + }); + } + const workspace = data[0]; + if (workspace.owner.id === member) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_PERMISSION, + detail: `Owner cannot change his own permissions`, + }); + } + + workspace.permissions = { + ...workspace.permissions, + [member]: permissions, + }; + + return await workspace.save(); + }); + + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async addMember(req: IUpdateMembersRequest) { + try { + const { id, member } = req.params; + const workspace = await new WorkspaceService() + .filter({ id }) + .list() + .then(data => { + if (!data) { + throw new Internal({ + type: ErrorTypes.NotFound, + title: 'Workspace not found', + detail: `Workspace ${id} not found`, + }); + } + return data[0]; + }); + + const _member = + member.length <= 36 + ? await new UserService().findOne(member) + : await new UserService() + .findByAddress(member) + .then(async (data: User) => { + if (!data) { + return await new UserService().create({ + address: member, + provider: defaultConfigurable['provider'], + avatar: await new UserService().randomAvatar(), + }); + } + return data; + }); + + if (!workspace.members.find(m => m.id === _member.id)) { + workspace.members = [...workspace.members, _member]; + workspace.permissions[_member.id] = + defaultPermissions[PermissionRoles.VIEWER]; + } + + return successful(await workspace.save(), Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async removeMember(req: IUpdateMembersRequest) { + try { + const { id, member } = req.params; + const workspace = await new WorkspaceService() + .filter({ id }) + .list() + .then(data => { + if (!data) { + throw new Internal({ + type: ErrorTypes.NotFound, + title: 'Workspace not found', + detail: `Workspace ${id} not found`, + }); + } + if (data[0].owner.id === member) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_PERMISSION, + detail: `Owner cannot be removed from workspace`, + }); + } + return data[0]; + }); + + workspace.members = workspace.members.filter(m => m.id !== member); + delete workspace.permissions[member]; + + return successful(await workspace.save(), Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } +} diff --git a/src/modules/workspace/routes.ts b/src/modules/workspace/routes.ts new file mode 100644 index 000000000..184e3e533 --- /dev/null +++ b/src/modules/workspace/routes.ts @@ -0,0 +1,67 @@ +import { Router } from 'express'; + +import { authMiddleware, authPermissionMiddleware } from '@src/middlewares'; +import { PermissionRoles } from '@src/models/Workspace'; +import { handleResponse } from '@src/utils'; + +import { WorkspaceController } from './controller'; +import { + PayloadCreateWorkspaceSchema, + PayloadUpdateWorkspaceSchema, + PayloadUpdatePermissionsWorkspaceSchema, + PayloadUpdateWorkspaceParams, +} from './validations'; + +const router = Router(); +const workspaceController = new WorkspaceController(); + +router.use(authMiddleware); + +router.get('/by-user', handleResponse(workspaceController.listByUser)); + +router.get( + '/balance', + authPermissionMiddleware([ + PermissionRoles.OWNER, + PermissionRoles.ADMIN, + PermissionRoles.MANAGER, + PermissionRoles.VIEWER, + ]), + handleResponse(workspaceController.getBalance), +); + +router.post( + '/', + PayloadCreateWorkspaceSchema, + handleResponse(workspaceController.create), +); + +router.get('/:id', handleResponse(workspaceController.findById)); + +router.put( + '/:id', + PayloadUpdateWorkspaceSchema, + authPermissionMiddleware([PermissionRoles.OWNER, PermissionRoles.ADMIN]), + handleResponse(workspaceController.update), +); + +router.put( + '/:id/permissions/:member', + PayloadUpdateWorkspaceParams, + PayloadUpdatePermissionsWorkspaceSchema, + authPermissionMiddleware([PermissionRoles.OWNER, PermissionRoles.ADMIN]), + handleResponse(workspaceController.updatePermissions), +); +router.post( + '/:id/members/:member/remove', + PayloadUpdateWorkspaceParams, + authPermissionMiddleware([PermissionRoles.OWNER, PermissionRoles.ADMIN]), + handleResponse(workspaceController.removeMember), +); +router.post( + '/:id/members/:member/include', + PayloadUpdateWorkspaceParams, + authPermissionMiddleware([PermissionRoles.OWNER, PermissionRoles.ADMIN]), + handleResponse(workspaceController.addMember), +); +export default router; diff --git a/src/modules/workspace/services.ts b/src/modules/workspace/services.ts new file mode 100644 index 000000000..2eb91a767 --- /dev/null +++ b/src/modules/workspace/services.ts @@ -0,0 +1,287 @@ +import { defaultConfigurable } from 'bsafe'; +import { Brackets } from 'typeorm'; + +import { User } from '@src/models'; +import { + IPermissions, + PermissionRoles, + Workspace, + defaultPermissions, +} from '@src/models/Workspace'; +import { ErrorTypes } from '@src/utils/error'; +import GeneralError from '@src/utils/error/GeneralError'; +import Internal from '@src/utils/error/Internal'; +import { IOrdination, setOrdination } from '@src/utils/ordination'; +import { PaginationParams, IPagination, Pagination } from '@src/utils/pagination'; + +import { UserService } from '../user/service'; +import { IFilterParams, IWorkspaceService } from './types'; + +export class WorkspaceService implements IWorkspaceService { + private _ordination: IOrdination = { + orderBy: 'updatedAt', + sort: 'DESC', + }; + private _pagination: PaginationParams; + private _filter: IFilterParams; + + filter(filter: IFilterParams) { + this._filter = filter; + return this; + } + + paginate(pagination?: PaginationParams) { + this._pagination = pagination; + return this; + } + + ordination(ordination?: IOrdination) { + this._ordination = setOrdination(ordination); + return this; + } + + async list(): Promise | Workspace[]> { + try { + const hasPagination = !!this._pagination; + const hasOrdination = !!this._ordination; + const enableSingleFilter = this._filter.single !== undefined; + const queryBuilder = Workspace.createQueryBuilder('w') + .leftJoin('w.owner', 'owner') + .leftJoin('w.members', 'users') + .leftJoin('w.predicates', 'predicates') + .select([ + 'w', // Todos os campos de Workspace + 'owner', // Todos os campos de User (relação owner) + 'users', // Todos os campos de User (relação members) + 'predicates.id', // Seleção específica: apenas o campo 'id' de Predicate com alias + ]); + + enableSingleFilter && + queryBuilder.andWhere('single = :single', { + single: this._filter.single, + }); + + this._filter.q && + queryBuilder.where('LOWER(w.name) LIKE LOWER(:name)', { + name: `%${this._filter.q}%`, + }); + + this._filter.owner && + queryBuilder.andWhere( + `${ + this._filter.owner.length <= 36 ? 'owner.id' : 'owner.address' + } = :owner`, + { + owner: this._filter.owner, + }, + ); + + this._filter.user && + queryBuilder.andWhere(qb => { + const subQuery = qb + .subQuery() + .select('*') + .from('workspace_users', 'wu') + .where('wu.workspace_id = w.id') + .andWhere( + '(wu.user_id = (SELECT u.id FROM users u WHERE u.id = :user))', + { user: this._filter.user }, + ) + .getQuery(); + + return `EXISTS ${subQuery}`; + }); + + this._filter.id && + queryBuilder.andWhere('w.id = :id', { + id: this._filter.id, + }); + + hasOrdination && + queryBuilder.orderBy( + `w.${this._ordination.orderBy}`, + this._ordination.sort, + ); + return hasPagination + ? await Pagination.create(queryBuilder).paginate(this._pagination) + : await queryBuilder.getMany(); + } catch (error) { + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on workspace list', + detail: error, + }); + } + } + + async create(payload: Partial): Promise { + return await Workspace.create(payload) + .save() + .then(data => data) + .catch(error => { + if (error instanceof GeneralError) throw error; + + throw new Internal({ + type: ErrorTypes.Create, + title: 'Error on workspace create', + detail: error, + }); + }); + } + + async update(payload: Partial): Promise { + const w = Object.assign(await Workspace.findOne({ id: payload.id }), payload); + + return w + .save() + .then(() => { + return true; + }) + .catch(error => { + if (error instanceof GeneralError) throw error; + + throw new Internal({ + type: ErrorTypes.Update, + title: 'Error on workspace update', + detail: error, + }); + }); + } + + async includeMembers(members: string[], owner: User, workspace?: string) { + const _members: User[] = []; + + const _permissions: IPermissions = {}; + for await (const member of [...members, owner.id]) { + const m = + member.length <= 36 + ? await new UserService().findOne(member).then(data => data) + : await new UserService() + .findByAddress(member) + .then(async (data: User) => { + if (!data) { + return await new UserService().create({ + address: member, + provider: defaultConfigurable['provider'], + avatar: await new UserService().randomAvatar(), + }); + } + return data; + }); + _members.push(m); + } + + _members.map(m => { + _permissions[m.id] = + m.id === owner.id + ? defaultPermissions[PermissionRoles.OWNER] + : defaultPermissions[PermissionRoles.VIEWER]; + }); + const hasOwner = + workspace && + (await new WorkspaceService() + .filter({ id: workspace }) + .list() + .then(data => { + const { owner, permissions } = data[0]; + _members.map(member => { + _permissions[member.id] = permissions[member.id]; + }); + return _members.find(member => member.id === owner.id); + })); + + if (workspace && !hasOwner) { + throw new Internal({ + type: ErrorTypes.NotFound, + title: 'Owner not found', + detail: `Owner cannot be removed from workspace`, + }); + } + + return { _members, _permissions }; + } + + findById: (id: string) => Promise; + + /** + * Formatar o capo de permissões do workspace, inserindo o assinante + * caso o usuário ainda nao esteja na lista de membros, um novo field é criado, e o id do predicado adicionado + * caso o usuário já esteja na lista de membros, o id do predicado é adicionado + * + * @params signers: string[] - lista de endereços dos signatários + * @params predicate: string - id do predicado + * @params worksapce: string - id do workspace + * + * @return Workspace + * + */ + async includeSigner( + signers: string[], + predicate: string, + worksapce: string, + ): Promise { + return await Workspace.findOne({ id: worksapce }) + .then(async workspace => { + const p = workspace.permissions; + signers.map(s => { + if (p[s]) { + p[s][PermissionRoles.SIGNER] = [ + ...p[s][PermissionRoles.SIGNER].filter(i => i != '*'), + predicate, + ]; + } else { + p[s] = { + ...defaultPermissions[PermissionRoles.SIGNER], + [PermissionRoles.SIGNER]: [predicate], + }; + } + return; + }); + workspace.permissions = p; + await workspace.save(); + return; + }) + .catch(error => { + if (error instanceof GeneralError) throw error; + throw new Internal({ + type: ErrorTypes.Update, + title: 'Error on workspace update', + detail: error, + }); + }); + } + + /** + * Formatar os dados para usuário nao logado, removendo as infos delicadas + * + * @params w: Workspace[] + * + * @return o workspace resumido, apenas com nome, avatar e endereco do owner e membros + * + */ + static formatToUnloggedUser(w: Workspace[]) { + return w.map(workspace => { + return { + id: workspace.id, + name: workspace.name, + avatar: workspace.avatar, + single: workspace.single, + description: workspace.description, + owner: { + name: workspace.owner.name, + avatar: workspace.owner.avatar, + address: workspace.owner.address, + }, + members: workspace.members.map(member => { + return { + name: member.name, + avatar: member.avatar, + address: member.address, + }; + }), + predicates: workspace.predicates.length, + permissions: workspace.permissions, + }; + }); + } +} diff --git a/src/modules/workspace/types.ts b/src/modules/workspace/types.ts new file mode 100644 index 000000000..6b0b77b55 --- /dev/null +++ b/src/modules/workspace/types.ts @@ -0,0 +1,80 @@ +import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; + +import { AuthValidatedRequest } from '@src/middlewares/auth/types'; +import { IPermissions, PermissionRoles, Workspace } from '@src/models/Workspace'; +import { IOrdination } from '@src/utils/ordination'; +import { PaginationParams, IPagination } from '@src/utils/pagination'; + +export interface IFilterParams { + q?: string; + user?: string; + single?: boolean; + owner?: string; + id?: string; +} + +export interface IWorkspacePayload { + name: string; + members?: string[]; + description?: string; + avatar?: string; + single?: boolean; + permissions?: IPermissions; +} + +interface IListRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: { + user: string; + single: boolean; + owner: string; + page: string; + perPage: string; + sort: 'ASC' | 'DESC'; + orderBy: 'name' | 'createdAt'; + }; +} + +interface IFindByIdRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { id: string }; +} + +interface ICreateRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: IWorkspacePayload; +} + +interface IUpdateRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: Pick; + [ContainerTypes.Params]: { id: string }; +} + +interface IUpdateMembersRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { id: string; member: string }; +} + +interface IUpdatePermissionsRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: { + permissions: { + [key in PermissionRoles]: string[]; + }; + }; + [ContainerTypes.Params]: { id: string; member: string }; +} + +export type IListByUserRequest = AuthValidatedRequest; +export type IFindByIdRequest = AuthValidatedRequest; +export type ICreateRequest = AuthValidatedRequest; +export type IUpdateRequest = AuthValidatedRequest; +export type IUpdateMembersRequest = AuthValidatedRequest; +export type IUpdatePermissionsRequest = AuthValidatedRequest; +export type IGetBalanceRequest = AuthValidatedRequest; + +export interface IWorkspaceService { + ordination(ordination?: IOrdination): this; + paginate(pagination?: PaginationParams): this; + filter(filter: IFilterParams): this; + + create: (payload: Partial>) => Promise; + //update: (id: string, payload: IUpdatePayload) => Promise; + list: () => Promise | Workspace[]>; + // findById: (id: string) => Promise; +} diff --git a/src/modules/workspace/validations.ts b/src/modules/workspace/validations.ts new file mode 100644 index 000000000..a5f2d7142 --- /dev/null +++ b/src/modules/workspace/validations.ts @@ -0,0 +1,34 @@ +import Joi from 'joi'; + +import { validator } from '@utils/index'; + +export const PayloadCreateWorkspaceSchema = validator.body( + Joi.object({ + name: Joi.string().required(), + members: Joi.array().items(Joi.string()).optional(), + description: Joi.string().allow('').optional(), + avatar: Joi.string().optional(), + permissions: Joi.object({}).optional(), + }), +); + +export const PayloadUpdateWorkspaceSchema = validator.body( + Joi.object({ + name: Joi.string().optional(), + avatar: Joi.string().optional(), + description: Joi.string().optional(), + }), +); + +export const PayloadUpdateWorkspaceParams = validator.params( + Joi.object({ + member: Joi.string().required(), + id: Joi.string().required(), + }), +); + +export const PayloadUpdatePermissionsWorkspaceSchema = validator.body( + Joi.object({ + permissions: Joi.object().required(), + }), +); diff --git a/src/routes.ts b/src/routes.ts index bcf40b243..68298925a 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,22 +1,19 @@ import { Router } from 'express'; +import users from '@src/modules/user/routes'; + import addressBook from '@modules/addressBook/routes'; import auth from '@modules/auth/routes'; -import roles from '@modules/configs/roles/routes'; -import users from '@modules/configs/user/routes'; import dApp from '@modules/dApps/routes'; import notifications from '@modules/notification/routes'; import predicates from '@modules/predicate/routes'; import transactions from '@modules/transaction/routes'; import vaultTemplate from '@modules/vaultTemplate/routes'; +import workspace from '@modules/workspace/routes'; -import { DAppsService } from './modules/dApps/service'; - -const ses = new DAppsService(); const router = Router(); router.use('/auth', auth); -router.use('/role', roles); router.use('/user', users); router.use('/predicate', predicates); router.use('/transaction', transactions); @@ -24,6 +21,7 @@ router.use('/template', vaultTemplate); router.use('/address-book', addressBook); router.use('/connections', dApp); router.use('/notifications', notifications); +router.use('/workspace', workspace); // ping route router.get('/ping', ({ res }) => diff --git a/src/server/app.ts b/src/server/app.ts index 1663eea8c..eecf47029 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -18,6 +18,7 @@ const { API_PORT, PORT } = process.env; type ServerHooks = { onServerStart?: Callback; + // eslint-disable-next-line @typescript-eslint/no-explicit-any onServerStop?: Callback; }; diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 8a002a4ed..863e775ba 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -4,6 +4,8 @@ import { getConnection } from 'typeorm'; import startConnection from '@database/connection'; import runSeeders from '@database/seeders'; +import config from '../config/database'; + class Bootstrap { static async connectDatabase() { return await startConnection(); @@ -14,15 +16,7 @@ class Bootstrap { } static async start() { - const { - DATABASE_HOST, - DATABASE_PORT, - DATABASE_USERNAME, - DATABASE_PASSWORD, - DATABASE_NAME, - NODE_ENV, - DATABASE_PORT_TEST, - } = process.env; + const { NODE_ENV } = process.env; this.startEnv(); await this.connectDatabase(); @@ -49,7 +43,6 @@ class Bootstrap { } static async runSeeders() { - console.log('[RUN_SEEDERS]'); await runSeeders(); } } diff --git a/src/server/index.ts b/src/server/index.ts index f6f2524b5..39925fbf4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,7 +1,5 @@ import { BSafe } from 'bsafe'; -import { DiscordUtils } from '@src/utils'; - import App from './app'; import Bootstrap from './bootstrap'; @@ -16,19 +14,6 @@ BSafe.setup({ bsafe_url: process.env.UI_URL, }); -if (process.env.NODE_ENV === 'production') { - App.pm2HandleServerStop(); - App.serverHooks({ - onServerStart: () => DiscordUtils.sendStartMessage(), - onServerStop: error => - DiscordUtils.sendErrorMessage({ - name: error.data?.name, - stack: error.data?.stack, - message: error.data?.message, - }), - }); -} - try { start(); } catch (e) { diff --git a/src/socket/calbacks.ts b/src/socket/calbacks.ts index 86218b385..d06fa33a4 100644 --- a/src/socket/calbacks.ts +++ b/src/socket/calbacks.ts @@ -17,10 +17,8 @@ export const popAuth: IEventsExecute = { { content }: ISocketEvent, ) => { try { - console.log('[AUTH_CONFIRMED]', content); const { vaultId, sessionId, name, origin } = content; const predicate = await new PredicateService().findById(vaultId); - console.log('[AUTH_CONFIRMED]', predicate); let dapp = await new DAppsService().findBySessionID(sessionId, origin); const room = `${sessionId}:${origin}`; @@ -83,7 +81,7 @@ export const popAuth: IEventsExecute = { ) => { // add sumary on transaction const { sessionId, origin, operations } = content; - const transaction = await Transaction.findOne({ + await Transaction.findOne({ where: { hash: content.hash }, }).then(async (data: Transaction) => { const session = await new DAppsService().findBySessionID(sessionId, origin); diff --git a/src/templates/transaction-completed.html b/src/templates/transaction-completed.html new file mode 100644 index 000000000..5d39dc058 --- /dev/null +++ b/src/templates/transaction-completed.html @@ -0,0 +1,88 @@ + + + + + + + Transaction Completed + + + + +
+ +
+ +
+ +

Hello, {{summary.name}}

+ +

+ The transaction '{{summary.transactionName}}' has been completed in the + '{{summary.vaultName}}' vault. +

+ + + To view your transaction details, please click + here to log in to your BSAFE account and navigate + to your vault. + + + +
+ + diff --git a/src/templates/transaction-created.html b/src/templates/transaction-created.html new file mode 100644 index 000000000..f4958d6a9 --- /dev/null +++ b/src/templates/transaction-created.html @@ -0,0 +1,88 @@ + + + + + + + Transaction Created + + + + +
+ +
+ +
+ +

Hello, {{summary.name}}

+ +

+ The transaction '{{summary.transactionName}}' has been created on the + '{{summary.vaultName}}' vault. +

+ + + To view your transaction details, please click + here to log in to your BSAFE account and navigate + to your vault. + + + +
+ + diff --git a/src/templates/transaction-declined.html b/src/templates/transaction-declined.html new file mode 100644 index 000000000..cae3e8f9c --- /dev/null +++ b/src/templates/transaction-declined.html @@ -0,0 +1,88 @@ + + + + + + + Transaction Declined + + + + +
+ +
+ +
+ +

Hello, {{summary.name}}

+ +

+ The transaction '{{summary.transactionName}}' has been declined in the + '{{summary.vaultName}}' vault. +

+ + + To view your transaction details, please click + here to log in to your BSAFE account and navigate + to your vault. + + + +
+ + diff --git a/src/templates/transaction-signed.html b/src/templates/transaction-signed.html new file mode 100644 index 000000000..2132ae712 --- /dev/null +++ b/src/templates/transaction-signed.html @@ -0,0 +1,88 @@ + + + + + + + Transaction Signed + + + + +
+ +
+ +
+ +

Hello, {{summary.name}}

+ +

+ The transaction '{{summary.transactionName}}' has been signed in the + '{{summary.vaultName}}' vault. +

+ + + To view your transaction details, please click + here to log in to your BSAFE account and navigate + to your vault. + + + +
+ + diff --git a/src/templates/vault-created.html b/src/templates/vault-created.html new file mode 100644 index 000000000..efb45c0b1 --- /dev/null +++ b/src/templates/vault-created.html @@ -0,0 +1,87 @@ + + + + + + + New Vault Created + + + + +
+ +
+ +
+ +

Hello, {{summary.name}}

+ +

+ The '{{summary.vaultName}}' has been created, and you are a signer! +

+ + + To view your vault details, please click + here to log in to your BSAFE account and navigate + to your vault. + + + +
+ + diff --git a/src/utils/EmailSender.ts b/src/utils/EmailSender.ts new file mode 100644 index 000000000..a6536488d --- /dev/null +++ b/src/utils/EmailSender.ts @@ -0,0 +1,78 @@ +import cheerio from 'cheerio'; +import fs from 'fs'; +import handlebars from 'handlebars'; +import nodemailer, { SendMailOptions } from 'nodemailer'; +import path from 'path'; + +const { AWS_SMTP_USER, AWS_SMTP_PASS, EMAIL_FROM, UI_URL } = process.env; +const YEAR = new Date().getFullYear(); +const LOGO = 'https://besafe-asset.s3.amazonaws.com/darkLogo.png'; + +export interface EmailParams { + [value: string]: unknown; +} + +export interface EmailData extends SendMailOptions { + data: EmailParams; + to: string; +} + +export enum EmailTemplateType { + TRANSACTION_CREATED = 'transaction-created', + TRANSACTION_COMPLETED = 'transaction-completed', + TRANSACTION_DECLINED = 'transaction-declined', + TRANSACTION_SIGNED = 'transaction-signed', + VAULT_CREATED = 'vault-created', +} + +const transporter = nodemailer.createTransport({ + host: 'email-smtp.sa-east-1.amazonaws.com', + port: 587, + auth: { + user: AWS_SMTP_USER, + pass: AWS_SMTP_PASS, + }, +}); + +export const renderTemplate = ( + templateName: EmailTemplateType, + data: EmailParams, +) => { + return new Promise((resolve, reject) => { + fs.readFile( + path.join(__dirname, '../templates/', `${templateName}.html`), + (err, file) => { + if (err) return reject(err); + const compiledTemplate = handlebars.compile(file.toString()); + resolve( + compiledTemplate({ + logo: LOGO, + year: YEAR, + bsafeUrl: UI_URL, + ...data, + }), + ); + }, + ); + }); +}; + +export const getSubject = (html: string) => { + const $ = cheerio.load(html); + const subject = $('title').text(); + return subject; +}; + +export const sendMail = async ( + templateName: EmailTemplateType, + emailData: EmailData, +) => { + const html = await renderTemplate(templateName, emailData.data); + + return transporter.sendMail({ + from: EMAIL_FROM, + subject: getSubject(html), + html, + to: emailData.to, + }); +}; diff --git a/src/utils/configurable.ts b/src/utils/configurable.ts index 6c81b5436..f886ed32f 100644 --- a/src/utils/configurable.ts +++ b/src/utils/configurable.ts @@ -1,5 +1,4 @@ import { defaultConfigurable as conf } from 'bsafe'; -import { bn } from 'fuels'; export const defaultConfigurable = { provider: conf['provider'], diff --git a/src/utils/error/Unauthorized.ts b/src/utils/error/Unauthorized.ts index 312533bde..cf72e3895 100644 --- a/src/utils/error/Unauthorized.ts +++ b/src/utils/error/Unauthorized.ts @@ -19,6 +19,8 @@ export enum UnauthorizedErrorTitles { INVALID_ACCESS_TOKEN = 'Invalid access token', EXPIRED_TOKEN = 'Expired token', INVALID_PERMISSION = 'Invalid permission', + MISSING_PERMISSION = 'Missing permission', + INVALID_SIGNATURE = 'Invalid signature', } export interface UnauthorizedError extends Omit { diff --git a/src/utils/permissionValidate.ts b/src/utils/permissionValidate.ts new file mode 100644 index 000000000..83c89a7e5 --- /dev/null +++ b/src/utils/permissionValidate.ts @@ -0,0 +1,25 @@ +import { PermissionRoles, Workspace } from '@src/models/Workspace'; + +export const validatePermissionVault = ( + workspace: Workspace, + user_id: string, + predicate_id: string, + permission: PermissionRoles, +) => { + return workspace.permissions[user_id][permission].includes(predicate_id) ?? false; +}; + +export const validatePermissionGeneral = ( + workspace: Workspace, + user_id: string, + permission: PermissionRoles[], +) => { + if (permission.length === 0) return true; + + const permissions = !!workspace.permissions[user_id]; + + const validate = + permission.filter(p => workspace.permissions[user_id][p]?.includes('*')) + .length > 0; + return validate && permissions; +}; diff --git a/src/utils/testUtils/Auth.ts b/src/utils/testUtils/Auth.ts index 4096d8ef1..8d4c6b7a0 100644 --- a/src/utils/testUtils/Auth.ts +++ b/src/utils/testUtils/Auth.ts @@ -6,10 +6,16 @@ import { Provider, Wallet } from 'fuels'; import { User, Encoder } from '@src/models'; +//todo: repply this class on SDK to user autentication resource export class AuthValidations { public user: User; public authToken: IBSAFEAuth; public axios: AxiosInstance; + public workspace: { + id: string; + name: string; + avatar: string; + }; constructor( private readonly provider: string, @@ -54,6 +60,25 @@ export class AuthValidations { address, token: data.accessToken, }; + this.workspace = data.workspace; + return data; + } + + async selectWorkspace(workspaceId: string) { + const { data } = await this.axios.put('/auth/workspace', { + workspace: workspaceId, + user: this.user.id, + ...this.authToken, + }); + + this.workspace = data.workspace; + return data; + } + + async listMyWorkspaces() { + const { data } = await this.axios.get(` + /workspace/by-user/${this.account.address}`); + return data; } diff --git a/src/utils/testUtils/Wallet.ts b/src/utils/testUtils/Wallet.ts index 3335f10b1..1b211511d 100644 --- a/src/utils/testUtils/Wallet.ts +++ b/src/utils/testUtils/Wallet.ts @@ -17,6 +17,10 @@ export const sendPredicateCoins = async ( rootWallet, await Provider.create(defaultConfigurable.provider), ); + // console.log( + // '[ROOT_BALANCE]: ', + // (await wallet.getBalance(assets[asset])).toString(), + // ); const deposit = await wallet.transfer( predicate.address, amount, diff --git a/src/utils/testUtils/Workspace.ts b/src/utils/testUtils/Workspace.ts new file mode 100644 index 000000000..fb337e2a0 --- /dev/null +++ b/src/utils/testUtils/Workspace.ts @@ -0,0 +1,41 @@ +import { accounts } from 'bsafe/dist/cjs/mocks/accounts'; +import e from 'express'; +import { Address } from 'fuels'; + +import { providers } from '@src/mocks/networks'; + +import { AuthValidations } from './Auth'; + +const generateWorkspacePayload = async (api: AuthValidations) => { + const { data: data_user1 } = await api.axios.post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()}_1 - Create user test`, + }); + const { data: data_user2 } = await api.axios.post('/user/', { + address: Address.fromRandom().toAddress(), + provider: providers['local'].name, + name: `${new Date()}_2 - Create user test`, + }); + + const { data: USER_5 } = await api.axios.post('/user/', { + address: accounts['USER_5'].address, + provider: providers['local'].name, + name: `${new Date()}_3 - Create user test`, + }); + + const { data, status } = await api.axios.post(`/workspace/`, { + name: `[GENERATED] Workspace 1 ${new Date()}`, + description: '[GENERATED] Workspace 1 description', + members: [ + data_user1.id, + data_user2.id, + USER_5.id, + Address.fromRandom().toAddress(), + ], + }); + + return { data, status, data_user1, data_user2, USER_5 }; +}; + +export { generateWorkspacePayload };