diff --git a/README.md b/README.md index 12972d2..8caeff1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ This decorator requires that **all permissions** listed are granted to the reque ```ts import { RequireAllPermissions } from '../../authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; @Controller('foo') class FooController { @@ -129,7 +129,7 @@ In the following case, the user is required to have **at least one** listed perm ```ts import { RequireSomePermission } from '../../authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; @Controller('foo') @RequireSomePermission(Permissions.FOO.MANAGE, Permissions.FOO.CREATE) @@ -184,7 +184,7 @@ class FooController { RequireSomePermission, AuthenticateCondition, } from '../../authentication/authentication.decorator'; - import { Permissions } from '../../authentication/permissions.enum'; + import { PERMISSIONS } from '../../authentication/permissions.enum'; import { Waa, Baz } from './authenticationFunctions' @Controller('foo') diff --git a/nest-cli.json b/nest-cli.json index 2edc9db..69ccf04 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -3,7 +3,11 @@ "sourceRoot": "src", "compilerOptions": { "assets": [ - "**/*.proto" + "**/*.proto", + { + "include": "i18n/**/*", + "watchAssets": true + } ], "plugins": [ "@nestjs/swagger" diff --git a/package.json b/package.json index 6b136d7..67f5ba6 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,13 @@ "json", "ts" ], - "rootDir": "src", + "rootDir": ".", + "modulePaths": [ + "" + ], + "moduleDirectories": [ + "node_modules" + ], "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/src/app.module.ts b/src/app.module.ts index 10442f0..3de9691 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,10 @@ import { Module } from '@nestjs/common'; import { ClientsModule } from '@nestjs/microservices'; import { ConfigModule } from '@nestjs/config'; - import { InputsController } from './modules/inputs/inputs.controller'; import { TransformationsController } from './modules/transformations/transformations.controller'; import { OutputsController } from './modules/outputs/outputs.controllers'; import { PipelinesController } from './modules/pipelines/pipelines.controller'; -import { AuthController } from './modules/auth/auth.controller'; import { HealthController } from './modules/health/health.controller'; import { InputsService } from './modules/inputs/inputs.service'; @@ -35,6 +33,8 @@ import { APP_GUARD } from '@nestjs/core'; import { GoogleStrategy } from './modules/oauth/passport-strategies/google-strategy'; import { MailChimpStrategy } from './modules/oauth/passport-strategies/mailchimp-strategy'; import { SalesforceStrategy } from './modules/oauth/passport-strategies/salesforce-strategy'; +import { UsersModule } from './modules/users/users.module'; +import { RolesModule } from './modules/roles/roles.module'; import { ConnectorModule } from './modules/connector/connector.module'; import { AuthModule } from './modules/auth/auth.module'; @@ -57,7 +57,6 @@ const transformationClient = new TransformationsClientConfiguration(); TransformationsController, OutputsController, PipelinesController, - AuthController, HealthController, CatalogController, OauthController, @@ -115,6 +114,10 @@ const transformationClient = new TransformationsClientConfiguration(); ...pipelineClient.config(), }, ]), + + UsersModule, + + RolesModule, ], }) export class AppModule {} diff --git a/src/authentication/authentication.decorator.ts b/src/authentication/authentication.decorator.ts index 6c86f13..5bebd81 100644 --- a/src/authentication/authentication.decorator.ts +++ b/src/authentication/authentication.decorator.ts @@ -1,6 +1,6 @@ import { Request } from 'express'; import { CustomDecorator } from '@nestjs/common'; -import { Permission } from './permissions.enum'; +import { PermissionsObjectPermission } from './permissions.enum'; import { RequestUser } from './user.decorator'; export const AUTH_FUNCTION_KEY = '__AUTH_FUNCTION__'; @@ -31,13 +31,17 @@ function SetMultipleMetadata(metadataKey, metadataValue): CustomDecorator { return decoratorFactory as any; } -export function RequireAllPermissions(...permissions: Permission[]) { +export function RequireAllPermissions( + ...permissions: PermissionsObjectPermission[] +) { return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) => permissions.every(({ seqid }) => user.permissions.includes(seqid)), ); } -export function RequireSomePermission(...permissions: Permission[]) { +export function RequireSomePermission( + ...permissions: PermissionsObjectPermission[] +) { return SetMultipleMetadata(AUTH_FUNCTION_KEY, (req, user: RequestUser) => permissions.some(({ seqid }) => user.permissions.includes(seqid)), ); diff --git a/src/authentication/authentication.guard.spec.ts b/src/authentication/authentication.guard.spec.ts index 5400831..6ee76d0 100644 --- a/src/authentication/authentication.guard.spec.ts +++ b/src/authentication/authentication.guard.spec.ts @@ -13,11 +13,15 @@ import { RequireSomePermission, } from './authentication.decorator'; import { AuthenticationGuard } from './authentication.guard'; -import { Permissions } from './permissions.enum'; +import { PERMISSIONS } from './permissions.enum'; import { AuthClientService } from '../modules/auth/auth.service'; import ErrorCodes from '../utils/errorCodes'; +const logger = { + info: (...args) => args, + error: (...args) => args, +}; @Controller('no-class-auth') class NoClassAuthController { @Get('body') @@ -32,19 +36,25 @@ class NoClassAuthController { } @Get('required-permission') - @RequireSomePermission(Permissions.ZENDESK.OPEN) + @RequireSomePermission(PERMISSIONS.ZENDESK.permissions.OPEN) async requiredPermission(@Body() body) { return { body }; } @Get('has-all-permissions') - @RequireAllPermissions(Permissions.ZENDESK.OPEN, Permissions.METABASE.OPEN) + @RequireAllPermissions( + PERMISSIONS.ZENDESK.permissions.OPEN, + PERMISSIONS.METABASE.permissions.OPEN, + ) async hasAllPermissions(@Body() body) { return { body }; } @Get('has-some-permission') - @RequireSomePermission(Permissions.ZENDESK.OPEN, Permissions.METABASE.OPEN) + @RequireSomePermission( + PERMISSIONS.ZENDESK.permissions.OPEN, + PERMISSIONS.METABASE.permissions.OPEN, + ) async hasSomePermission(@Body() body) { return { body }; } @@ -52,7 +62,7 @@ class NoClassAuthController { @Controller('class-auth-condition') @AuthenticateCondition((req) => req.get('x-on-class') === 'ok') -@RequireSomePermission(Permissions.METABASE.OPEN) +@RequireSomePermission(PERMISSIONS.METABASE.permissions.OPEN) class ClassAuthConditionController { @Get('body') async getBody(@Body() body) { @@ -67,7 +77,7 @@ class ClassAuthConditionController { @Get('required-permission') @AuthenticateCondition((req) => req.get('x-on-route') === 'ok') - @RequireSomePermission(Permissions.ZENDESK.OPEN) + @RequireSomePermission(PERMISSIONS.ZENDESK.permissions.OPEN) async requiredPermission(@Body() body) { return { body }; } @@ -104,7 +114,7 @@ describe('authentication.guard', () => { }, { provide: DadosferaLogger, - useValue: { logger: console }, + useValue: { logger }, }, { provide: APP_GUARD, @@ -430,17 +440,17 @@ describe('authentication.guard', () => { NoClassAuthTest(null, null); ClassAuthConditionTest(null, null); - const tokenZ = CreateToken([Permissions.ZENDESK.OPEN]); + const tokenZ = CreateToken([PERMISSIONS.ZENDESK.permissions.OPEN]); NoClassAuthTest(tokenZ, ['zendesk']); ClassAuthConditionTest(tokenZ, ['zendesk']); - const tokenM = CreateToken([Permissions.METABASE.OPEN]); + const tokenM = CreateToken([PERMISSIONS.METABASE.permissions.OPEN]); NoClassAuthTest(tokenM, ['metabase']); ClassAuthConditionTest(tokenM, ['metabase']); const tokenZM = CreateToken([ - Permissions.ZENDESK.OPEN, - Permissions.METABASE.OPEN, + PERMISSIONS.ZENDESK.permissions.OPEN, + PERMISSIONS.METABASE.permissions.OPEN, ]); NoClassAuthTest(tokenZM, ['zendesk', 'metabase']); ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']); diff --git a/src/authentication/authentication.guard.ts b/src/authentication/authentication.guard.ts index b663a3b..3fca4da 100644 --- a/src/authentication/authentication.guard.ts +++ b/src/authentication/authentication.guard.ts @@ -3,7 +3,6 @@ import { CanActivate, OnApplicationBootstrap, ExecutionContext, - Logger, Inject, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @@ -96,7 +95,7 @@ export class AuthenticationGuard );*/ assert( - accessTokenDecoded.payload.token_use === 'access', + accessTokenDecoded?.payload?.token_use === 'access', 'should be an access token', ); @@ -108,7 +107,7 @@ export class AuthenticationGuard } catch (err) { // log errors if authentication is required if (mustBeAuthenticated) { - this.logger.error('Unable to verify duc access token\n' + err.stack); + this.logger.error('Unable to verify duc access token\n' + err?.stack); } return false; @@ -122,6 +121,7 @@ export class AuthenticationGuard customer_id: accessTokenPayload.customer_id, customer_name: accessTokenPayload.customer_name, customer_tier: accessTokenPayload.customer_tier, + access_token: accessToken, }; // TODO: for backwards compatibility. remove in the future request.body.info = { diff --git a/src/authentication/permissions.enum.ts b/src/authentication/permissions.enum.ts index 2c8a47d..cc7d4be 100644 --- a/src/authentication/permissions.enum.ts +++ b/src/authentication/permissions.enum.ts @@ -1,5 +1,6 @@ import { Logger } from '@nestjs/common'; import assert from 'assert'; +import { LanguageEnum } from 'src/utils/languages.enum'; const logger = new Logger(); @@ -8,10 +9,24 @@ export enum PermissionUsages { INTERNAL = 'internal', } -export interface Permission { +export interface PermissionsObjectPermission { seqid: number; claim: string; usage: PermissionUsages; + name?: Record; +} +export interface PermissionsObjectPermissionTranslated { + seqid: number; + claim: string; + usage: PermissionUsages; + name?: string; +} +export interface PermissionsObjectGroup { + title: { + 'pt-br': string; + 'en-us': string; + }; + permissions: Record; } /* @@ -22,216 +37,422 @@ export interface Permission { /*export const Permissions: { [P in keyof any]: { [Q in keyof any]: Permission }; } = {*/ -export const Permissions = { +export const PERMISSIONS = { AUTH: { - CREATE: { - seqid: 18, - claim: 'POST /auth', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Autenticação', + 'en-us': 'Authentication', + }, + permissions: { + CREATE: { + seqid: 18, + claim: 'POST /auth', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': '', + 'en-us': '', + }, + }, }, }, PIPELINE: { - CREATE: { - seqid: 23, - claim: 'POST /pipelines', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Pipelines', + 'en-us': 'Pipelines', }, - GET: { - seqid: 13, - claim: 'GET /pipelines', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 29, - claim: 'PUT /pipelines', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 5, - claim: 'DELETE /pipelines', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 23, + claim: 'POST /pipelines', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Criar pipelines', + 'en-us': 'Create pipelines', + }, + }, + GET: { + seqid: 13, + claim: 'GET /pipelines', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Buscar pipelines', + 'en-us': 'Get pipelines', + }, + }, + UPDATE: { + seqid: 29, + claim: 'PUT /pipelines', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Atualizar pipeline', + 'en-us': 'Update pipeline', + }, + }, + DELETE: { + seqid: 5, + claim: 'DELETE /pipelines', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Remover pipeline', + 'en-us': 'Delete pipeline', + }, + }, }, }, INPUT: { - CREATE: { - seqid: 21, - claim: 'POST /inputs', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Conexões', + 'en-us': 'Connections', }, - GET: { - seqid: 9, - claim: 'GET /inputs', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 27, - claim: 'PUT /inputs', - usage: PermissionUsages.PUBLIC, - }, - UPDATE_PARTIAL: { - seqid: 17, - claim: 'PATCH /inputs', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 3, - claim: 'DELETE /inputs', - usage: PermissionUsages.PUBLIC, - }, - GET_ENTITIES: { - seqid: 10, - claim: 'GET /inputs/available-entities', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 21, + claim: 'POST /inputs', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Criar conexões', + 'en-us': 'Create connections', + }, + }, + GET: { + seqid: 9, + claim: 'GET /inputs', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Buscar conexões', + 'en-us': 'Get connections', + }, + }, + UPDATE: { + seqid: 27, + claim: 'PUT /inputs', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Atualizar conexões', + 'en-us': 'Create connections', + }, + }, + DELETE: { + seqid: 3, + claim: 'DELETE /inputs', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Remover conexões', + 'en-us': 'Delete connections', + }, + }, + GET_ENTITIES: { + seqid: 10, + claim: 'GET /inputs/available-entities', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Remover conexões', + 'en-us': 'Delete connections', + }, + }, }, }, OUTPUT: { - CREATE: { - seqid: 22, - claim: 'POST /outputs', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Output', + 'en-us': 'Output', }, - GET: { - seqid: 12, - claim: 'GET /outputs', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 28, - claim: 'PUT /outputs', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 4, - claim: 'DELETE /outputs', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 22, + claim: 'POST /outputs', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Criar outputs', + 'en-us': 'Create outputs', + }, + }, + GET: { + seqid: 12, + claim: 'GET /outputs', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Buscar outputs', + 'en-us': 'Get outputs', + }, + }, + UPDATE: { + seqid: 28, + claim: 'PUT /outputs', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Editar outputs', + 'en-us': 'Edit outputs', + }, + }, + DELETE: { + seqid: 4, + claim: 'DELETE /outputs', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Remover outputs', + 'en-us': 'Remove outputs', + }, + }, }, }, TRANSFORMATIONS: { - CREATE: { - seqid: 24, - claim: 'POST /transformations', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Micro-transformações', + 'en-us': 'Micro-transformations', }, - GET: { - seqid: 15, - claim: 'GET /transformations', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 30, - claim: 'PUT /transformations', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 6, - claim: 'DELETE /transformations', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 24, + claim: 'POST /transformations', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Criar micro-transformações', + 'en-us': 'Create micro-transformations', + }, + }, + GET: { + seqid: 15, + claim: 'GET /transformations', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Buscar micro-transformações', + 'en-us': 'Get micro-transformations', + }, + }, + UPDATE: { + seqid: 30, + claim: 'PUT /transformations', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Editar micro-transformações', + 'en-us': 'Edit micro-transformations', + }, + }, + DELETE: { + seqid: 6, + claim: 'DELETE /transformations', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Remover micro-transformações', + 'en-us': 'Remove micro-transformations', + }, + }, }, }, CATALOG: { - CREATE: { - seqid: 19, - claim: 'POST /catalog', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Catálogo', + 'en-us': 'Catalog', }, - GET: { - seqid: 7, - claim: 'GET /catalog', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 25, - claim: 'PUT /catalog', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 1, - claim: 'DELETE /catalog', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 19, + claim: 'POST /catalog', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Criar atributos no catálogo', + 'en-us': 'Create catalog attributes', + }, + }, + GET: { + seqid: 7, + claim: 'GET /catalog', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Buscar catálogos', + 'en-us': 'Get catalogs', + }, + }, + UPDATE: { + seqid: 25, + claim: 'PUT /catalog', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Editar catálogo', + 'en-us': 'Edit catalogs', + }, + }, + DELETE: { + seqid: 1, + claim: 'DELETE /catalog', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Remover catálogo', + 'en-us': 'Remove catalogs', + }, + }, }, }, CONNECTORS: { - CREATE: { - seqid: 20, - claim: 'POST /connectors', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Conectores', + 'en-us': 'Connectors', }, - GET: { - seqid: 8, - claim: 'GET /connectors', - usage: PermissionUsages.PUBLIC, - }, - UPDATE: { - seqid: 26, - claim: 'PUT /connectors', - usage: PermissionUsages.PUBLIC, - }, - DELETE: { - seqid: 2, - claim: 'DELETE /connectors', - usage: PermissionUsages.PUBLIC, + permissions: { + CREATE: { + seqid: 20, + claim: 'POST /connectors', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Criar conectores', + 'en-us': 'Create connectors', + }, + }, + GET: { + seqid: 8, + claim: 'GET /connectors', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Buscar conectores', + 'en-us': 'Get connectors', + }, + }, + UPDATE: { + seqid: 26, + claim: 'PUT /connectors', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Editar conectores', + 'en-us': 'Edit connectors', + }, + }, + DELETE: { + seqid: 2, + claim: 'DELETE /connectors', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Remover conectores', + 'en-us': 'Remove connectors', + }, + }, }, }, SNOWFLAKE: { - OPEN: { - seqid: 14, - claim: 'GET /snowflake', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Snowflake', + 'en-us': 'Snowflake', + }, + permissions: { + OPEN: { + seqid: 14, + claim: 'GET /snowflake', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Acessar Snowflake', + 'en-us': 'Access Snowflake', + }, + }, }, }, ZENDESK: { - OPEN: { - seqid: 16, - claim: 'GET /zendesk', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Zendesk', + 'en-us': 'Zendesk', + }, + permissions: { + OPEN: { + seqid: 16, + claim: 'GET /zendesk', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Acessar Zendesk', + 'en-us': 'Access Zendesk', + }, + }, }, }, METABASE: { - OPEN: { - seqid: 11, - claim: 'GET /metabase', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Metabase', + 'en-us': 'Metabase', + }, + permissions: { + OPEN: { + seqid: 11, + claim: 'GET /metabase', + usage: PermissionUsages.INTERNAL, + name: { + 'pt-br': 'Acessar Metabase', + 'en-us': 'Access Metabase', + }, + }, }, }, ORCHEST: { - INTELLIGENCE: { - seqid: 31, - claim: 'intelligence:open', - usage: PermissionUsages.PUBLIC, + title: { + 'pt-br': 'Orchest', + 'en-us': 'Orchest', }, - APP_BUILDER: { - seqid: 32, - claim: 'app-builder:open', - usage: PermissionUsages.PUBLIC, + permissions: { + INTELLIGENCE: { + seqid: 31, + claim: 'intelligence:open', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Acessar inteligência', + 'en-us': 'Access intelligence', + }, + }, + APP_BUILDER: { + seqid: 32, + claim: 'app-builder:open', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Acessar App Builder', + 'en-us': 'Access App Builder', + }, + }, + MACHINE_LEARNING: { + seqid: 33, + claim: 'machine-learning:open', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Acessar Machine Learning', + 'en-us': 'Access Machine Learning', + }, + }, }, - MACHINE_LEARNING: { - seqid: 33, - claim: 'machine-learning:open', - usage: PermissionUsages.PUBLIC, + }, + USERS: { + title: { + 'pt-br': 'Usuários', + 'en-us': 'Users', + }, + permissions: { + ADMIN: { + seqid: 34, + claim: 'users:admin', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Administrar usuários e grupos', + 'en-us': 'Manage user and roles', + }, + }, }, }, }; // traverses the object searching for duplicate seqids or claims (executes at runtime) let nextAvailableSeqid = 0; -const seqids = Object.values(Permissions).flatMap((namespace) => - Object.values(namespace).map(({ seqid }) => seqid), +const seqids = Object.values(PERMISSIONS).flatMap((namespace) => + Object.values(namespace.permissions).map(({ seqid }) => seqid), ); -const claims = Object.values(Permissions).flatMap((namespace) => - Object.values(namespace).map(({ claim }) => claim), +const claims = Object.values(PERMISSIONS).flatMap((namespace) => + Object.values(namespace.permissions).map(({ claim }) => claim), ); -Object.values(Permissions).map((namespace) => - Object.values(namespace).map(({ seqid, claim }) => { +Object.values(PERMISSIONS).map((namespace) => + Object.values(namespace.permissions).map(({ seqid, claim }) => { const seqidsCount = seqids.filter((x) => x === seqid).length; assert(seqidsCount === 1, `seqid ${seqid} count is not 1`); diff --git a/src/authentication/user.decorator.spec.ts b/src/authentication/user.decorator.spec.ts index cc57a88..9af2ebd 100644 --- a/src/authentication/user.decorator.spec.ts +++ b/src/authentication/user.decorator.spec.ts @@ -11,8 +11,12 @@ import { AuthClientService } from '../modules/auth/auth.service'; import ErrorCodes from '../utils/errorCodes'; import { User } from './user.decorator'; -import { Permissions } from './permissions.enum'; +import { PERMISSIONS } from './permissions.enum'; +const logger = { + info: (...args) => args, + error: (...args) => args, +}; @Controller('user') class UserController { @Get() @@ -45,10 +49,13 @@ describe('user.decorator', () => { const fakeUserPayload = { user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125', username: 'super.admin', - permissions: [Permissions.METABASE.OPEN].map(({ seqid }) => seqid), + permissions: [PERMISSIONS.METABASE.permissions.OPEN].map( + ({ seqid }) => seqid, + ), customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11', customer_name: 'dadosfera', customer_tier: 'BASIC', + access_token: '', }; beforeAll(async () => { @@ -64,7 +71,7 @@ describe('user.decorator', () => { }, { provide: DadosferaLogger, - useValue: { logger: console }, + useValue: { logger }, }, { provide: APP_GUARD, @@ -170,5 +177,6 @@ describe('user.decorator', () => { UserTest(null); const token = CreateToken(); + fakeUserPayload.access_token = token; UserTest(token); }); diff --git a/src/authentication/user.decorator.ts b/src/authentication/user.decorator.ts index 31041ff..cf7709b 100644 --- a/src/authentication/user.decorator.ts +++ b/src/authentication/user.decorator.ts @@ -10,6 +10,7 @@ export interface RequestUser { customer_id: string; customer_name: string; customer_tier: string; + access_token: string; } export const User: (options?: { required?: boolean }) => ParameterDecorator = diff --git a/src/clients/outputs/client.service.ts b/src/clients/outputs/client.service.ts index 7f0b959..abfa662 100644 --- a/src/clients/outputs/client.service.ts +++ b/src/clients/outputs/client.service.ts @@ -1,10 +1,7 @@ import { Controller, Inject, OnModuleInit } from '@nestjs/common'; import { ClientGrpc, Payload } from '@nestjs/microservices'; import { DadosferaLogger } from 'dadosfera-logs'; -import { - OutputServicesNames, - OutputsServiceInterface, -} from 'protospack'; +import { OutputServicesNames, OutputsServiceInterface } from 'protospack'; import { objectCamelToSnake, objectSnakeToCamel, diff --git a/src/clients/pipelines/client.config.ts b/src/clients/pipelines/client.config.ts index 01083eb..3f5b72d 100644 --- a/src/clients/pipelines/client.config.ts +++ b/src/clients/pipelines/client.config.ts @@ -1,8 +1,5 @@ import { ClientOptions, Transport } from '@nestjs/microservices'; -import { - PipelinePackages, - PipelineProtoFilePath, -} from 'protospack'; +import { PipelinePackages, PipelineProtoFilePath } from 'protospack'; import { credentials } from '@grpc/grpc-js'; export class PipelinesClientConfiguration { diff --git a/src/clients/pipelines/client.service.ts b/src/clients/pipelines/client.service.ts index c197ca7..829462b 100644 --- a/src/clients/pipelines/client.service.ts +++ b/src/clients/pipelines/client.service.ts @@ -255,7 +255,7 @@ export class PipelinesClientService implements OnModuleInit { reject(err); }, complete() { - this.logger.info('Done'); + //this.logger.info('Done'); }, }); }) diff --git a/src/main.ts b/src/main.ts index 788cf4c..add8d4c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,11 @@ async function bootstrap() { .setDescription('Documentation for Maestro gateway') .setVersion('1.0') .addBearerAuth() + .addServer( + process.env.ENV === 'local' + ? 'http://127.0.0.1:3333' + : 'https://maestro.dev.dadosfera.ai', + ) .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/modules/catalog/catalog.controller.ts b/src/modules/catalog/catalog.controller.ts index 7c44f81..b627f73 100644 --- a/src/modules/catalog/catalog.controller.ts +++ b/src/modules/catalog/catalog.controller.ts @@ -10,7 +10,7 @@ import { } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthenticateCondition } from '../../authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; import { CatalogService } from './catalog.service'; import { DadosferaLogger } from 'dadosfera-logs'; @@ -32,7 +32,9 @@ import { DadosferaLogger } from 'dadosfera-logs'; action = req.method; } - return user.permissions.includes(Permissions.CATALOG[action].seqid); + return user.permissions.includes( + PERMISSIONS.CATALOG.permissions[action].seqid, + ); }) export class CatalogController { logger: any; diff --git a/src/modules/connection/connection.controller.ts b/src/modules/connection/connection.controller.ts index 792c557..39ffc65 100644 --- a/src/modules/connection/connection.controller.ts +++ b/src/modules/connection/connection.controller.ts @@ -18,7 +18,7 @@ import { Authenticated, RequireAllPermissions, } from 'src/authentication/authentication.decorator'; -import { Permissions } from 'src/authentication/permissions.enum'; +import { PERMISSIONS } from 'src/authentication/permissions.enum'; import { User } from 'src/authentication/user.decorator'; import { ValidationPipe } from '../../pipes/object-validation.pipe'; import { UpdateConnectionDto } from './dtos/update-connection'; diff --git a/src/modules/connector/connector.controller.ts b/src/modules/connector/connector.controller.ts index 1141ac5..230f3b4 100644 --- a/src/modules/connector/connector.controller.ts +++ b/src/modules/connector/connector.controller.ts @@ -29,7 +29,7 @@ import { Authenticated, RequireAllPermissions, } from 'src/authentication/authentication.decorator'; -import { Permissions } from 'src/authentication/permissions.enum'; +import { PERMISSIONS } from 'src/authentication/permissions.enum'; @ApiTags('connectors') @ApiBearerAuth() @@ -46,7 +46,7 @@ export class ConnectorController { } @Post() - @RequireAllPermissions(Permissions.CONNECTORS.CREATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.CREATE) @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file')) async uploadConnector( @@ -64,7 +64,7 @@ export class ConnectorController { } @Post('/uploads') - @RequireAllPermissions(Permissions.CONNECTORS.CREATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.CREATE) @ApiConsumes('multipart/form-data') @UseInterceptors(FilesInterceptor('files')) async uploadFile(@UploadedFiles() files) { @@ -81,7 +81,7 @@ export class ConnectorController { } @Post('/multiple-connectors-upload') - @RequireAllPermissions(Permissions.CONNECTORS.CREATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.CREATE) @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file')) async uploadConnectorsFile(@UploadedFile() file: UploadConnectorsFileDto) { @@ -95,7 +95,7 @@ export class ConnectorController { } @Get() - @RequireAllPermissions(Permissions.CONNECTORS.GET) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.GET) async getAllConnectors( @Headers('dadosfera-lang') language, @Query() queries: GetAllDto, @@ -121,13 +121,13 @@ export class ConnectorController { } @Get('/tags') - @RequireAllPermissions(Permissions.CONNECTORS.GET) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.GET) async getConnectorsTags() { return await this.connectorClientService.getConnectorsTags(); } @Get('/:plugin') - @RequireAllPermissions(Permissions.CONNECTORS.GET) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.GET) async getConnector( @Headers('dadosfera-lang') language, @Param('plugin') plugin: string, @@ -149,7 +149,7 @@ export class ConnectorController { } @Get('/:plugin/details') - @RequireAllPermissions(Permissions.CONNECTORS.GET) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.GET) async getConnectorDetails( @Headers('dadosfera-lang') language, @Param('plugin') plugin: string, @@ -171,7 +171,7 @@ export class ConnectorController { } @Put('/:plugin') - @RequireAllPermissions(Permissions.CONNECTORS.UPDATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.UPDATE) @ApiConsumes('multipart/form-data') async updateConnector( @Param('plugin') plugin: string, @@ -193,7 +193,7 @@ export class ConnectorController { } @Put('/:plugin/add-tag') - @RequireAllPermissions(Permissions.CONNECTORS.UPDATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.UPDATE) async addTagOnConnector( @Param('plugin') plugin: string, @Body() body: AddTagDto, @@ -220,7 +220,7 @@ export class ConnectorController { } @Put('/:plugin/remove-tag') - @RequireAllPermissions(Permissions.CONNECTORS.UPDATE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.UPDATE) async removeTagOnConnector( @Param('plugin') plugin: string, @Body() body: RemoveTagDto, @@ -248,7 +248,7 @@ export class ConnectorController { } @Delete('/:plugin') - @RequireAllPermissions(Permissions.CONNECTORS.DELETE) + @RequireAllPermissions(PERMISSIONS.CONNECTORS.permissions.DELETE) async deleteConnector( @Param('plugin') plugin: string, @Body() { version }: DeleteConnectorDto, diff --git a/src/modules/inputs/inputs.controller.ts b/src/modules/inputs/inputs.controller.ts index 7ac86e3..ce2ed85 100644 --- a/src/modules/inputs/inputs.controller.ts +++ b/src/modules/inputs/inputs.controller.ts @@ -12,7 +12,7 @@ import { InputsService } from './inputs.service'; import { DadosferaLogger } from 'dadosfera-logs'; import { InputsClientService } from 'src/clients/inputs/client.service'; import { UpdateInputRequest } from 'src/clients/inputs/interfaces'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { @@ -41,7 +41,7 @@ import { break; case 'PATCH': - action = 'UPDATE_PARTIAL'; + action = 'UPDATE'; break; default: @@ -55,7 +55,7 @@ import { action = 'GET_ENTITIES'; } - return user.permissions.includes(Permissions.INPUT[action].seqid); + return user.permissions.includes(PERMISSIONS.INPUT.permissions[action].seqid); }) export class InputsController { inputService: InputsService; diff --git a/src/modules/inputs/inputs.service.ts b/src/modules/inputs/inputs.service.ts index 29d9750..812e189 100644 --- a/src/modules/inputs/inputs.service.ts +++ b/src/modules/inputs/inputs.service.ts @@ -7,7 +7,7 @@ import { IIdRequest, Info } from 'src/clients/inputs/interfaces'; @Injectable() export class InputsService { - constructor(private inputClient: InputsClientService) { } + constructor(private inputClient: InputsClientService) {} secondsInADay = 60 * 60 * 24; secondsInAnHour = 60 * 60; @@ -54,7 +54,7 @@ export class InputsService { switch (data.plugin.toLowerCase()) { case 'csv': case 'json': - case 'parquet': + case 'parquet': { const { info, ...input } = data; const inputPayload = this.generateInputS3Payload(input); response = await this.inputClient.createS3Inputs({ @@ -62,6 +62,7 @@ export class InputsService { info, }); break; + } case 'oracle': case 'mysql': case 'postgresql': diff --git a/src/modules/outputs/outputs.controllers.ts b/src/modules/outputs/outputs.controllers.ts index e13bf70..45b07f1 100644 --- a/src/modules/outputs/outputs.controllers.ts +++ b/src/modules/outputs/outputs.controllers.ts @@ -12,7 +12,7 @@ import { Payload } from '@nestjs/microservices'; import { ApiTags } from '@nestjs/swagger'; import { OutputsClientService } from 'src/clients/outputs/client.service'; import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; import { OutputsService } from './outputs.service'; import { DadosferaLogger } from 'dadosfera-logs'; @@ -34,7 +34,9 @@ import { DadosferaLogger } from 'dadosfera-logs'; action = req.method; } - return user.permissions.includes(Permissions.OUTPUT[action].seqid); + return user.permissions.includes( + PERMISSIONS.OUTPUT.permissions[action].seqid, + ); }) export class OutputsController { logger: any; diff --git a/src/modules/permissions/dto/entities.ts b/src/modules/permissions/dto/entities.ts new file mode 100644 index 0000000..faa4a3b --- /dev/null +++ b/src/modules/permissions/dto/entities.ts @@ -0,0 +1,49 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PermissionGroupPermission { + @ApiProperty() + name: string; + @ApiProperty() + id: number; +} + +export class PermissionGroup { + @ApiProperty() + title: string; + @ApiProperty({ type: [PermissionGroupPermission] }) + permissions: PermissionGroupPermission[]; +} + +export class Permission { + @ApiProperty() + id: string; + @ApiProperty() + seqid: number; + @ApiProperty() + name: string; + @ApiProperty() + claim: string; + @ApiProperty() + createdAt: string; + @ApiProperty() + updatedAt: string; +} + +export class PermissionDto { + @ApiProperty() + id: number; + @ApiProperty() + seqid: number; + @ApiProperty() + name: string; + @ApiProperty() + claim: string; + @ApiProperty() + createdAt: string; + @ApiProperty() + updatedAt: string; +} +export class GetPublicPermissionsRes { + @ApiProperty({ type: [PermissionGroup] }) + permissionGroups: PermissionGroup[]; +} diff --git a/src/modules/permissions/permissions.controller.spec.ts b/src/modules/permissions/permissions.controller.spec.ts new file mode 100644 index 0000000..9bf04b1 --- /dev/null +++ b/src/modules/permissions/permissions.controller.spec.ts @@ -0,0 +1,42 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DucClient } from 'src/clients/duc/client.config'; +import { PermissionsController } from './permissions.controller'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { PermissionsService } from './permissions.service'; + +const ducClient = new DucClient(); +const logger = { + info: (...args) => args, + error: (...args) => args, +}; +describe('PermissionsController', () => { + let controller: PermissionsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + ], + controllers: [PermissionsController], + providers: [ + { + provide: DadosferaLogger, + useValue: { logger }, + }, + PermissionsService, + ], + }).compile(); + + controller = module.get(PermissionsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/permissions/permissions.controller.ts b/src/modules/permissions/permissions.controller.ts new file mode 100644 index 0000000..4b32ec0 --- /dev/null +++ b/src/modules/permissions/permissions.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Headers, Inject } from '@nestjs/common'; +import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { + Authenticated, + RequireAllPermissions, +} from 'src/authentication/authentication.decorator'; +import { + PERMISSIONS, + PermissionUsages, +} from 'src/authentication/permissions.enum'; +import { RequestUser, User } from 'src/authentication/user.decorator'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { GetPublicPermissionsRes } from './dto/entities'; +import { PermissionsService } from './permissions.service'; + +@ApiTags('Permissions') +@Controller('permissions') +@ApiHeader({ + name: 'dadosfera-lang', + enum: LanguageEnum, + required: false, + description: 'Defaults to pt-br', +}) +@Authenticated() +export class PermissionsController { + logger: any; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private permissionsService: PermissionsService, + ) { + this.logger = dadosferaLogger.logger; + } + + @Get() + @ApiOkResponse({ type: GetPublicPermissionsRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + getPublicPermissions( + @User() user: RequestUser, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('getPublicPermissions', { user }); + const permissionGroups = this.permissionsService.getPermissionsGrouped( + PermissionUsages.PUBLIC, + language, + ); + return { permissionGroups }; + } +} diff --git a/src/modules/permissions/permissions.module.ts b/src/modules/permissions/permissions.module.ts index f390558..4475a81 100644 --- a/src/modules/permissions/permissions.module.ts +++ b/src/modules/permissions/permissions.module.ts @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common'; import { ClientsModule } from '@nestjs/microservices'; import { DadosferaLogger } from 'dadosfera-logs'; -import { PermissionsClientService } from './permissions.service'; +import { PermissionsService } from './permissions.service'; import { DucClient } from '../../clients/duc/client.config'; +import { PermissionsController } from './permissions.controller'; + const ducClient = new DucClient(); @Module({ @@ -16,6 +18,8 @@ const ducClient = new DucClient(); }, ]), ], - providers: [PermissionsClientService, DadosferaLogger], + providers: [PermissionsService, DadosferaLogger], + controllers: [PermissionsController], + exports: [PermissionsService], }) export class PermissionsModule {} diff --git a/src/modules/permissions/permissions.service.ts b/src/modules/permissions/permissions.service.ts index 9887b27..f6d27bc 100644 --- a/src/modules/permissions/permissions.service.ts +++ b/src/modules/permissions/permissions.service.ts @@ -11,16 +11,23 @@ import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; import { PermissionsProtoService as PermissionsServiceInterface } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; import { InjectPermissionsRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; import { DadosferaLogger } from 'dadosfera-logs'; -import { Permissions } from '../../authentication/permissions.enum'; +import { + PERMISSIONS, + PermissionUsages, + PermissionsObjectGroup, + PermissionsObjectPermissionTranslated, +} from '../../authentication/permissions.enum'; import ErrorBuilder from '../../utils/ErrorBuilder'; +import { PermissionGroupPermission, PermissionGroup } from './dto/entities'; +import { LanguageEnum } from 'src/utils/languages.enum'; @Injectable() -export class PermissionsClientService +export class PermissionsService implements OnModuleInit, OnApplicationBootstrap { logger: any; - private permissionsService: PermissionsServiceInterface; + private permissionsClientService: PermissionsServiceInterface; constructor( @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, @@ -30,7 +37,7 @@ export class PermissionsClientService } onModuleInit() { - this.permissionsService = + this.permissionsClientService = this.grpcClient.getService( ProtoServices.PermissionsProtoService, ); @@ -40,8 +47,8 @@ export class PermissionsClientService async onApplicationBootstrap() { this.logger.info('sending permissions to DUC...'); - const permissions = Object.values(Permissions).flatMap((namespace) => - Object.values(namespace), + const permissions = Object.values(PERMISSIONS).flatMap((namespace) => + Object.values(namespace.permissions), ); return this.injectPermissions({ @@ -64,7 +71,48 @@ export class PermissionsClientService this.logger.info('InjectPermissions'); return lastValueFrom( - this.permissionsService.InjectPermissions({ permissions }), + this.permissionsClientService.InjectPermissions({ permissions }), ); } + getPermissionsGrouped( + permissionUsage?: PermissionUsages, + language: LanguageEnum = LanguageEnum.ptbr, + ): PermissionGroup[] { + const groups: PermissionGroup[] = []; + for (const key in PERMISSIONS) { + const permissionGroup: PermissionsObjectGroup = PERMISSIONS[key]; + const title = permissionGroup.title[language]; + const permissions: PermissionGroupPermission[] = Object.values( + permissionGroup.permissions, + ) + .filter(({ usage }) => + permissionUsage ? usage === permissionUsage : true, + ) + .map(({ seqid, name }) => ({ id: seqid, name: name[language] })); + if (permissions?.length) + groups.push({ + title, + permissions, + }); + } + return groups; + } + getAllPermissions(language: LanguageEnum = LanguageEnum.ptbr) { + let permissions: PermissionsObjectPermissionTranslated[] = []; + for (const key in PERMISSIONS) { + const permissionGroup: PermissionsObjectGroup = PERMISSIONS[key]; + const permissionGroupPermissions: PermissionsObjectPermissionTranslated[] = + Object.values(permissionGroup.permissions).map((p) => ({ + ...p, + name: p.name[language], + })); + permissions = permissions.concat(permissionGroupPermissions); + } + return permissions; + } + getPermissionsBySeqId(seqIds: number[]) { + const permissionGroupPermissions = this.getAllPermissions(); + + return permissionGroupPermissions.filter((p) => seqIds.includes(p.seqid)); + } } diff --git a/src/modules/pipelines/pipelines.controller.ts b/src/modules/pipelines/pipelines.controller.ts index 1c40836..5267807 100644 --- a/src/modules/pipelines/pipelines.controller.ts +++ b/src/modules/pipelines/pipelines.controller.ts @@ -11,7 +11,7 @@ import { import { ApiTags } from '@nestjs/swagger'; import { PipelinesClientService } from 'src/clients/pipelines/client.service'; import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; import { PipelinesService } from './pipelines.service'; import { DadosferaLogger } from 'dadosfera-logs'; @@ -33,7 +33,9 @@ import { DadosferaLogger } from 'dadosfera-logs'; action = req.method; } - return user.permissions.includes(Permissions.PIPELINE[action].seqid); + return user.permissions.includes( + PERMISSIONS.PIPELINE.permissions[action].seqid, + ); }) export class PipelinesController { logger: any; diff --git a/src/modules/roles/dto/entities.ts b/src/modules/roles/dto/entities.ts new file mode 100644 index 0000000..5652684 --- /dev/null +++ b/src/modules/roles/dto/entities.ts @@ -0,0 +1,115 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { PermissionDto } from 'src/modules/permissions/dto/entities'; +import { + Customer, + UserNoRolesAndCustomer, +} from 'src/modules/users/dtos/entities'; +export class RoleDto { + @ApiProperty() + id: string; + @ApiProperty() + name: string; + @ApiProperty() + description: string; + @ApiPropertyOptional({ type: () => [PermissionDto] }) + permissions: PermissionDto[]; + @ApiPropertyOptional({ type: () => [UserNoRolesAndCustomer] }) + users?: UserNoRolesAndCustomer[]; + @ApiProperty() + createdAt: string; + @ApiProperty() + updatedAt: string; + @ApiProperty() + isPublic: boolean; + @ApiProperty({ type: () => Customer }) + customer?: Customer; +} + +export class CustomerRole extends OmitType(RoleDto, ['customer']) {} +export class UserRole extends OmitType(RoleDto, ['users']) {} + +export enum TrueOrFalseEnum { + true = 'true', + false = 'false', +} +export class GetRolesByCustomerReq { + @ApiPropertyOptional() + permissionId: string[]; + @ApiPropertyOptional({ enum: TrueOrFalseEnum }) + getUsers: TrueOrFalseEnum; +} + +export class GetRolesByCustomerRes { + @ApiProperty({ type: [RoleDto] }) + roles: RoleDto[]; +} +export class CreateRoleReq { + @ApiProperty() + name: string; + @ApiProperty() + permissionIds: number[]; + @ApiPropertyOptional() + userIds: string[]; + @ApiProperty() + description: string; +} +export class CreateRoleRes { + @ApiProperty() + role: CustomerRole; +} + +export class GrantPermissionsToRoleReq { + @ApiProperty() + roleId: string; + @ApiProperty() + permissionIds: number[]; +} +export class GrantPermissionsToRoleRes { + @ApiProperty() + role: RoleDto; +} + +export class RevokePermissionsFromRoleReq { + @ApiProperty() + roleId: string; + @ApiProperty() + permissionIds: number[]; +} +export class RevokePermissionsFromRoleRes { + @ApiProperty() + role: RoleDto; +} + +export class UpdateRoleReq { + @ApiPropertyOptional() + name: string; + @ApiPropertyOptional() + description: string; +} + +export class UpdateRoleRes { + @ApiProperty() + role: RoleDto; +} + +export class SetRolePermissionsReq { + @ApiProperty() + roleId: string; + @ApiProperty() + permissionIds: number[]; +} +export class SetRolePermissionsRes { + @ApiProperty() + role: RoleDto; +} + +export class SetRoleUsersReq { + @ApiProperty() + roleId: string; + @ApiProperty() + userIds: string[]; +} +export class SetRoleUsersRes { + @ApiProperty() + role: RoleDto; +} diff --git a/src/modules/roles/roles.controller.spec.ts b/src/modules/roles/roles.controller.spec.ts new file mode 100644 index 0000000..38448bc --- /dev/null +++ b/src/modules/roles/roles.controller.spec.ts @@ -0,0 +1,46 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DucClient } from 'src/clients/duc/client.config'; +import { RolesController } from './roles.controller'; +import { RolesService } from './roles.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +const logger = { + info: (...args) => args, + error: (...args) => args, +}; +describe('RolesController', () => { + let controller: RolesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + PermissionsModule, + ], + controllers: [RolesController], + providers: [ + RolesService, + { + provide: DadosferaLogger, + useValue: { logger }, + }, + ], + exports: [RolesService], + }).compile(); + + controller = module.get(RolesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/roles/roles.controller.ts b/src/modules/roles/roles.controller.ts new file mode 100644 index 0000000..98ecbbe --- /dev/null +++ b/src/modules/roles/roles.controller.ts @@ -0,0 +1,186 @@ +import { + Body, + Controller, + Delete, + Get, + Headers, + Inject, + Param, + Patch, + Post, + Put, + Query, + UseFilters, +} from '@nestjs/common'; +import { + ApiCreatedResponse, + ApiHeader, + ApiOkResponse, + ApiTags, +} from '@nestjs/swagger'; +import { RequestUser, User } from 'src/authentication/user.decorator'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { RolesService } from './roles.service'; +import { + GrantPermissionsToRoleReq, + GrantPermissionsToRoleRes, + CreateRoleReq, + CreateRoleRes, + GetRolesByCustomerReq, + GetRolesByCustomerRes, + RevokePermissionsFromRoleReq, + RevokePermissionsFromRoleRes, + UpdateRoleReq, + UpdateRoleRes, + RoleDto, + SetRolePermissionsReq, + SetRolePermissionsRes, + SetRoleUsersRes, + SetRoleUsersReq, +} from './dto/entities'; +import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; +import { PERMISSIONS } from 'src/authentication/permissions.enum'; +import { + Authenticated, + RequireAllPermissions, +} from 'src/authentication/authentication.decorator'; +import { LanguageEnum } from 'src/utils/languages.enum'; + +@ApiTags('Roles') +@ApiHeader({ + name: 'dadosfera-lang', + enum: LanguageEnum, + required: false, + description: 'Defaults to pt-br', +}) +@UseFilters(new GrpcToHttpExceptionFilter()) +@Authenticated() +@Controller('roles') +export class RolesController { + logger: any; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private rolesService: RolesService, + ) { + this.logger = dadosferaLogger.logger; + } + + @Get() + @ApiOkResponse({ type: GetRolesByCustomerRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async searchRoles( + @User() user: RequestUser, + @Query() filters: GetRolesByCustomerReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('searchRoles', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.roleSearch(filters, user); + } + + @Post() + @ApiCreatedResponse({ type: CreateRoleRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async createRole( + @User() user: RequestUser, + @Body() body: CreateRoleReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('createRole', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.createRole(body, user); + } + + @Put('permissions') + @ApiOkResponse({ type: GrantPermissionsToRoleRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async grantPermissionsToRole( + @User() user: RequestUser, + @Body() body: GrantPermissionsToRoleReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('grantPermissionsToRole', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.grantPermissionsToRole(body, user); + } + + @Delete('permissions') + @ApiOkResponse({ type: RevokePermissionsFromRoleRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async revokePermissionsFromRole( + @User() user: RequestUser, + @Body() body: RevokePermissionsFromRoleReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('revokePermissionsToRole', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.revokePermissionFromRole(body, user); + } + + @Patch('permissions') + @ApiOkResponse({ type: SetRolePermissionsRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async setRolePermissions( + @User() user: RequestUser, + @Body() body: SetRolePermissionsReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('setRolePermissions', { user }); + this.rolesService.setLanguage(language); + return this.rolesService.setRolePermissions(body, user); + } + + @Patch('users') + @ApiOkResponse({ type: SetRoleUsersRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async setRoleUsers( + @User() user: RequestUser, + @Body() body: SetRoleUsersReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('setRoleUsers', { user }); + this.rolesService.setLanguage(language); + return this.rolesService.setRoleUsers(body, user); + } + + @Get(':id') + @ApiOkResponse({ type: RoleDto }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async getRoleById( + @User() user: RequestUser, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('getRoleById', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.getRolesById(id, user); + } + + @Delete(':id') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async deleteRole( + @User() user: RequestUser, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('deleteRole', { user }); + this.rolesService.setLanguage(language); + return await this.rolesService.removeRole(id, user); + } + + @Patch(':id') + @ApiOkResponse({ type: UpdateRoleRes }) + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async updateRole( + @User() user: RequestUser, + @Param('id') id: string, + @Body() body: UpdateRoleReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('updateRole', { user }); + this.rolesService.setLanguage(language); + return this.rolesService.updateRole(id, body, user); + } +} diff --git a/src/modules/roles/roles.module.ts b/src/modules/roles/roles.module.ts new file mode 100644 index 0000000..2ec4a05 --- /dev/null +++ b/src/modules/roles/roles.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule } from '@nestjs/microservices'; +import { DucClient } from 'src/clients/duc/client.config'; +import { RolesController } from './roles.controller'; +import { RolesService } from './roles.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + PermissionsModule, + ], + controllers: [RolesController], + providers: [RolesService, DadosferaLogger], + exports: [RolesService], +}) +export class RolesModule {} diff --git a/src/modules/roles/roles.service.spec.ts b/src/modules/roles/roles.service.spec.ts new file mode 100644 index 0000000..4d8b206 --- /dev/null +++ b/src/modules/roles/roles.service.spec.ts @@ -0,0 +1,46 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RolesController } from './roles.controller'; +import { RolesService } from './roles.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { DucClient } from 'src/clients/duc/client.config'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +const logger = { + info: (...args) => args, + error: (...args) => args, +}; +describe('RolesService', () => { + let service: RolesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + PermissionsModule, + ], + controllers: [RolesController], + providers: [ + RolesService, + { + provide: DadosferaLogger, + useValue: { logger }, + }, + ], + exports: [RolesService], + }).compile(); + + service = module.get(RolesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/roles/roles.service.ts b/src/modules/roles/roles.service.ts new file mode 100644 index 0000000..e41ff8d --- /dev/null +++ b/src/modules/roles/roles.service.ts @@ -0,0 +1,260 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; +import { RolesProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { + CreateRoleReq, + GetRolesByCustomerReq, + GrantPermissionsToRoleReq, + RevokePermissionsFromRoleReq, + RoleDto, + SetRolePermissionsReq, + SetRoleUsersReq, + UpdateRoleReq, +} from './dto/entities'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { Metadata } from '@grpc/grpc-js'; +import { ClientGrpc } from '@nestjs/microservices'; +import { Permission } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities'; +import { lastValueFrom } from 'rxjs'; +import { PermissionsService } from '../permissions/permissions.service'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { RequestUser } from 'src/authentication/user.decorator'; + +interface GetRolesPermissionsName { + id: string; + name: string; + description: string; + permissions: Permission[]; + isPublic?: boolean; + createdAt: string; + updatedAt: string; +} +@Injectable() +export class RolesService { + logger: any; + private language: LanguageEnum = LanguageEnum.ptbr; + private rolesClientService: RolesProtoService; + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + @Inject('DUC_PACKAGE') private readonly grpcClient: ClientGrpc, + private permissionsService: PermissionsService, + ) { + this.logger = dadosferaLogger.logger; + } + setLanguage(lang: LanguageEnum = LanguageEnum.ptbr) { + if (Object.values(LanguageEnum).includes(lang)) this.language = lang; + this.language = lang; + } + + onModuleInit() { + this.rolesClientService = this.grpcClient.getService( + ProtoServices.RolesProtoService, + ); + } + + async getRolesById(id: string, { access_token }: RequestUser) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const role = await lastValueFrom( + this.rolesClientService.RoleFindOneById( + { + id, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async roleSearch( + filters: GetRolesByCustomerReq, + { customer_id, access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + let { permissionId: permissionIds } = filters; + if (permissionIds && !Array.isArray(permissionIds)) + permissionIds = [permissionIds]; + const { getUsers } = filters; + const isGetUsers = getUsers === 'true'; + const permissionSeqIds = + permissionIds?.map((id) => { + const seqId = Number(id); + if (isNaN(seqId)) + throw new BadRequestException(`permissionId ${id} not valid`); + return seqId; + }) || []; + const roles = await lastValueFrom( + this.rolesClientService.RoleSearch( + { + customerId: customer_id, + getUsers: isGetUsers, + permissionIds: [], + permissionSeqIds, + }, + meta, + ), + ); + const rolesTreated = this.getRolesPermissionsName(roles.roles); + return { roles: rolesTreated }; + } + + async createRole( + data: CreateRoleReq, + { customer_id, access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const { description, name, permissionIds, userIds } = data; + const role = await lastValueFrom( + this.rolesClientService.RoleCreate( + { + customerId: customer_id, + name, + permissionIds: [], + permissionSeqIds: permissionIds, + description, + userIds, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async removeRole(id: string, { access_token }: RequestUser) { + const meta = new Metadata(); + meta.add('access_token', access_token); + return lastValueFrom(this.rolesClientService.RoleRemove({ id }, meta)); + } + async grantPermissionsToRole( + { roleId, permissionIds }: GrantPermissionsToRoleReq, + { access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const role = await lastValueFrom( + this.rolesClientService.GrantPermissionsToRole( + { + permissionIds: [], + roleId, + permissionSeqIds: permissionIds, + }, + meta, + ), + ); + + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async revokePermissionFromRole( + { roleId, permissionIds }: RevokePermissionsFromRoleReq, + { access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const role = await lastValueFrom( + this.rolesClientService.RevokePermissionsFromRole( + { + permissionIds: [], + roleId, + permissionSeqIds: permissionIds, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async updateRole( + id: string, + req: UpdateRoleReq, + { access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const { description, name } = req; + const role = await lastValueFrom( + this.rolesClientService.RoleUpdate( + { + id, + description, + name, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async setRolePermissions( + req: SetRolePermissionsReq, + { access_token }: RequestUser, + ) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const { roleId: id, permissionIds } = req; + const clearPermissions = + Array.isArray(permissionIds) && permissionIds.length === 0; + const role = await lastValueFrom( + this.rolesClientService.RoleSetPermissions( + { + id, + permissionSeqIds: permissionIds, + clearPermissions, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + + async setRoleUsers(req: SetRoleUsersReq, { access_token }: RequestUser) { + const meta = new Metadata(); + meta.add('access_token', access_token); + const { roleId: id, userIds } = req; + const clearUsers = Array.isArray(userIds) && userIds.length === 0; + const role = await lastValueFrom( + this.rolesClientService.RoleSetUsers( + { + id, + userIds, + clearUsers, + }, + meta, + ), + ); + const [roleTreated] = this.getRolesPermissionsName([role.role]); + return { role: roleTreated }; + } + getRolesPermissionsName(roles: GetRolesPermissionsName[]): RoleDto[] { + const newRoles: RoleDto[] = []; + for (const role of roles) { + const allPermissions = this.permissionsService.getAllPermissions( + this.language, + ); + const newPermissions = role.permissions.map((p) => { + const permission = allPermissions.find((per) => per.seqid === p.seqid); + return { + ...p, + name: permission.name, + id: p.seqid, + }; + }); + const newRole: RoleDto = { + ...role, + permissions: newPermissions, + isPublic: role.isPublic, + }; + newRoles.push(newRole); + } + return newRoles; + } +} diff --git a/src/modules/transformations/transformations.controller.ts b/src/modules/transformations/transformations.controller.ts index 71b2d21..d4dea00 100644 --- a/src/modules/transformations/transformations.controller.ts +++ b/src/modules/transformations/transformations.controller.ts @@ -13,7 +13,7 @@ import { ApiTags } from '@nestjs/swagger'; import { TransformationsClientService } from 'src/clients/transformations/client.service'; import { IIdRequest } from 'src/clients/transformations/interfaces'; import { AuthenticateCondition } from 'src/authentication/authentication.decorator'; -import { Permissions } from '../../authentication/permissions.enum'; +import { PERMISSIONS } from '../../authentication/permissions.enum'; import { TransformationsService } from './transformations.service'; import { DadosferaLogger } from 'dadosfera-logs'; @@ -35,7 +35,9 @@ import { DadosferaLogger } from 'dadosfera-logs'; action = req.method; } - return user.permissions.includes(Permissions.TRANSFORMATIONS[action].seqid); + return user.permissions.includes( + PERMISSIONS.TRANSFORMATIONS.permissions[action].seqid, + ); }) export class TransformationsController { logger: any; diff --git a/src/modules/users/dtos/entities.ts b/src/modules/users/dtos/entities.ts new file mode 100644 index 0000000..5eda738 --- /dev/null +++ b/src/modules/users/dtos/entities.ts @@ -0,0 +1,171 @@ +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { UserRole } from 'src/modules/roles/dto/entities'; + +export class Hierarchy { + @ApiProperty() + id: string; + @ApiProperty() + name: string; +} +export class Customer { + @ApiProperty() + id: string; + @ApiProperty() + name: string; + @ApiProperty() + domain: string; + @ApiProperty() + tier: string; + @ApiProperty() + info: string; + @ApiProperty() + createdAt: string; + @ApiProperty() + updatedAt: string; +} +export class User { + @ApiProperty() + id: string; + @ApiProperty() + name: string; + @ApiProperty() + email: string; + @ApiProperty({ type: [UserRole] }) + roles: UserRole[]; + @ApiProperty() + jobTitle?: string; + @ApiProperty() + department?: string; + @ApiProperty() + hierarchy?: string; + @ApiPropertyOptional() + customer?: Customer; + @ApiProperty() + mfaStatus: string; + @ApiProperty() + createdAt: string; + @ApiProperty() + updatedAt: string; + @ApiProperty() + status: string; + @ApiPropertyOptional() + lastLogin?: string; +} +export class UserNoRoles extends OmitType(User, ['roles']) {} +export class UserNoRolesAndCustomer extends OmitType(UserNoRoles, [ + 'customer', +]) {} +export class UserByCustomer extends OmitType(User, ['customer']) {} + +export class CreateUserReq { + @ApiPropertyOptional() + name?: string; + @ApiProperty() + email: string; + @ApiPropertyOptional() + roleNames?: string[]; + @ApiPropertyOptional() + jobTitle?: string; + @ApiPropertyOptional() + department?: string; + @ApiPropertyOptional() + hierarchy?: string; +} +export class CreateUserRes { + @ApiProperty() + user: User; +} +export class BatchCreateUserReq { + @ApiProperty({ type: [CreateUserReq] }) + users: CreateUserReq[]; +} + +class ErrorUsers { + @ApiProperty() + user: CreateUserReq; + @ApiProperty() + error: string; +} + +export class BatchCreateUserRes { + @ApiProperty({ type: [User] }) + usersCreated: User[]; + @ApiProperty({ type: [ErrorUsers] }) + errorUsers: ErrorUsers[]; +} + +export class UpdateUserReq { + @ApiPropertyOptional() + name?: string; + @ApiPropertyOptional() + jobTitle?: string; + @ApiPropertyOptional() + department?: string; + @ApiPropertyOptional() + hierarchy?: string; + @ApiPropertyOptional() + roleNames?: string[]; +} + +export class UpdateUserRes { + @ApiProperty() + user: UserByCustomer; +} +export class GetAllUsersByCustomerIdRes { + @ApiProperty({ type: [UserByCustomer] }) + users: UserByCustomer[]; + @ApiProperty() + departments: string[]; + @ApiProperty() + jobTitles: string[]; +} + +export class AssignRoleToUserReq { + @ApiProperty() + userId: string; + @ApiProperty() + roleId: string; +} +export class UnassignRoleToUserReq { + @ApiProperty() + userId: string; + @ApiProperty() + roleId: string; +} +export class DeleteUserRes { + @ApiProperty() + id: string; +} + +export class SearchUserReq { + @ApiProperty() + s: string; +} + +export class SetUserRolesReq { + @ApiProperty() + userId: string; + @ApiProperty() + roleNames: string[]; +} + +export class SetUserRolesRes { + @ApiProperty() + user: UserByCustomer; +} + +export class SearchUserRes extends GetAllUsersByCustomerIdRes {} + +export class GetAllHierarchiesRes { + @ApiProperty({ type: [Hierarchy] }) + hierarchies: Hierarchy[]; +} + +export class GetAllJobTitlesRes { + @ApiProperty() + jobTitles: string[]; +} +export class GetAllDepartmentsRes { + @ApiProperty() + departments: string[]; +} diff --git a/src/modules/users/hierarchies.ts b/src/modules/users/hierarchies.ts new file mode 100644 index 0000000..9f942dd --- /dev/null +++ b/src/modules/users/hierarchies.ts @@ -0,0 +1,79 @@ +import { LanguageEnum } from 'src/utils/languages.enum'; + +interface InternalHierarchyType { + name: Record; +} +export const HIERARCHIES: Record = { + presidence: { + name: { + 'pt-br': 'Presidência', + 'en-us': 'Presidence', + }, + }, + directory: { + name: { + 'pt-br': 'Diretoria', + 'en-us': 'Directory', + }, + }, + management: { + name: { + 'pt-br': 'Gerência', + 'en-us': 'Management', + }, + }, + specialist: { + name: { + 'pt-br': 'Especialista', + 'en-us': 'Specialist', + }, + }, + supervisor: { + name: { + 'pt-br': 'Coordenador ou Supervisor', + 'en-us': 'Supervisor', + }, + }, + trainee: { + name: { + 'pt-br': 'Trainee', + 'en-us': 'Trainee', + }, + }, + 'senior-analyst': { + name: { + 'pt-br': 'Analista Sênior', + 'en-us': 'Senior Analyst', + }, + }, + 'midlevel-analyst': { + name: { + 'pt-br': 'Analista Pleno', + 'en-us': 'Mid-level Analyst', + }, + }, + 'junior-analyst': { + name: { + 'pt-br': 'Analista Júnior', + 'en-us': 'Junior Analyst', + }, + }, + intern: { + name: { + 'pt-br': 'Estagiário', + 'en-us': 'Intern', + }, + }, + 'youth-apprentice': { + name: { + 'pt-br': 'Jovem aprendiz', + 'en-us': 'Youth Apprentice', + }, + }, + other: { + name: { + 'pt-br': 'Outro', + 'en-us': 'Other', + }, + }, +}; diff --git a/src/modules/users/users.controller.spec.ts b/src/modules/users/users.controller.spec.ts new file mode 100644 index 0000000..610e16e --- /dev/null +++ b/src/modules/users/users.controller.spec.ts @@ -0,0 +1,47 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DucClient } from 'src/clients/duc/client.config'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { RolesModule } from '../roles/roles.module'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +const logger = { + info: (...args) => args, + error: (...args) => args, +}; +describe('UsersController', () => { + let controller: UsersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + RolesModule, + PermissionsModule, + ], + controllers: [UsersController], + providers: [ + UsersService, + { + provide: DadosferaLogger, + useValue: { logger }, + }, + ], + }).compile(); + + controller = module.get(UsersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100644 index 0000000..a6b6e87 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,238 @@ +import { + Body, + Controller, + Delete, + Get, + Headers, + HttpCode, + HttpStatus, + Inject, + Param, + Patch, + Post, + Put, + Req, + UseFilters, +} from '@nestjs/common'; +import { + ApiCreatedResponse, + ApiHeader, + ApiOkResponse, + ApiTags, +} from '@nestjs/swagger'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { RequireAllPermissions } from 'src/authentication/authentication.decorator'; +import { PERMISSIONS } from 'src/authentication/permissions.enum'; +import { RequestUser, User } from 'src/authentication/user.decorator'; +import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { + AssignRoleToUserReq, + BatchCreateUserReq, + BatchCreateUserRes, + CreateUserReq, + CreateUserRes, + DeleteUserRes, + GetAllDepartmentsRes, + GetAllHierarchiesRes, + GetAllJobTitlesRes, + GetAllUsersByCustomerIdRes, + SetUserRolesReq, + SetUserRolesRes, + UnassignRoleToUserReq, + UpdateUserReq, + UpdateUserRes, + UserByCustomer, +} from './dtos/entities'; +import { UsersService } from './users.service'; + +// TODO GET de hierarquias e do PATCH em usuário +@ApiTags('Users') +@Controller('users') +@ApiHeader({ + name: 'dadosfera-lang', + enum: LanguageEnum, + required: false, + description: 'Defaults to pt-br', +}) +@UseFilters(GrpcToHttpExceptionFilter) +export class UsersController { + logger: any; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private userService: UsersService, + ) { + this.logger = dadosferaLogger.logger; + } + + @Get() + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ + type: GetAllUsersByCustomerIdRes, + description: 'List of users of a customer', + }) + async getAllUsersByCustomerId( + @User() user: RequestUser, + @Headers('dadosfera-lang') language, + ): Promise { + this.logger.info('getAllUsersByCustomerId', { user }); + this.userService.setLanguage(language); + return await this.userService.findAllUsersByCustomerId(user.customer_id); + } + + @Get('hierarchies') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: GetAllHierarchiesRes }) + async getAllHierarchies( + @User() user: RequestUser, + @Headers('dadosfera-lang') language, + ): Promise { + this.logger.info('getAllHierarchies', { user }); + this.userService.setLanguage(language); + return await this.userService.getAllHierarchies(); + } + + @Get('departments') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: GetAllDepartmentsRes }) + async getAllDepartments( + @User() user: RequestUser, + @Headers('dadosfera-lang') language, + ): Promise { + this.logger.info('getAllHierarchies', { user }); + this.userService.setLanguage(language); + return await this.userService.getAllDepartments(user.customer_id); + } + + @Get('jobTitles') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: GetAllDepartmentsRes }) + async getAllJobTitles( + @User() user: RequestUser, + @Headers('dadosfera-lang') language, + ): Promise { + this.logger.info('getAllHierarchies', { user }); + this.userService.setLanguage(language); + return await this.userService.getAllJobTitles(user.customer_id); + } + + @Get(':id') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ + type: UserByCustomer, + }) + async findOneById( + @User() user: RequestUser, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('findOneById', { user }); + this.userService.setLanguage(language); + return await this.userService.findOneById(id); + } + + @Post() + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiCreatedResponse({ type: CreateUserRes }) + async createUser( + @User() user: RequestUser, + @Body() body: CreateUserReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('createUser', { user }); + this.userService.setLanguage(language); + return await this.userService.createUser(body); + } + + @Post('batch') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiCreatedResponse({ type: BatchCreateUserRes }) + async batchCreateUser( + @User() user: RequestUser, + @Body() body: BatchCreateUserReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('batchCreateUser', { user }); + this.userService.setLanguage(language); + return await this.userService.batchCreateUser(body); + } + + @Post(':id/resend-invite') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @HttpCode(HttpStatus.OK) + async resendInvite( + @User() user: RequestUser, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('resendInvite', { user }); + this.userService.setLanguage(language); + return this.userService.resendInvite({ id }); + } + + @Delete(':id') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: DeleteUserRes }) + async deleteUser( + @User() user: RequestUser, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('deleteUser', { user }); + this.userService.setLanguage(language); + return await this.userService.deleteUser({ id }); + } + + @Patch('roles') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: SetUserRolesRes }) + async setUserRoles( + @User() user: RequestUser, + @Body() body: SetUserRolesReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('createUser', { user }); + this.userService.setLanguage(language); + return await this.userService.setRoles(body, user.customer_id); + } + + @Put('role') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async assignRoleToUser( + @User() user: RequestUser, + @Body() body: AssignRoleToUserReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('assignRoleToUser', { user }); + this.userService.setLanguage(language); + return await this.userService.assignRoleToUser(body); + } + + @Delete('role') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + async unassignRoleToUser( + @User() user: RequestUser, + @Req() request: UnassignRoleToUserReq, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('unassignRoleToUser', { user }); + this.userService.setLanguage(language); + return await this.userService.unassignRoleToUser(request); + } + + @Patch(':id') + @RequireAllPermissions(PERMISSIONS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: UpdateUserRes }) + async updateUser( + @User() user: RequestUser, + @Body() body: UpdateUserReq, + @Param('id') id: string, + @Headers('dadosfera-lang') language, + ) { + this.logger.info('updateUser', { user }); + this.userService.setLanguage(language); + return await this.userService.updateUser(body, id, user.customer_id); + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts new file mode 100644 index 0000000..8ff3a49 --- /dev/null +++ b/src/modules/users/users.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { ClientsModule } from '@nestjs/microservices'; +import { DucClient } from 'src/clients/duc/client.config'; +import { RolesModule } from '../roles/roles.module'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + RolesModule, + PermissionsModule, + ], + controllers: [UsersController], + providers: [UsersService, DadosferaLogger], +}) +export class UsersModule {} diff --git a/src/modules/users/users.service.spec.ts b/src/modules/users/users.service.spec.ts new file mode 100644 index 0000000..6b665f5 --- /dev/null +++ b/src/modules/users/users.service.spec.ts @@ -0,0 +1,47 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { DadosferaLogger } from 'dadosfera-logs'; +import { DucClient } from 'src/clients/duc/client.config'; +import { RolesModule } from '../roles/roles.module'; +import { PermissionsModule } from '../permissions/permissions.module'; + +const ducClient = new DucClient(); + +const logger = { + info: (...args) => args, + error: (...args) => args, +}; +describe('UsersService', () => { + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ClientsModule.register([ + { + name: 'DUC_PACKAGE', + ...ducClient.config(), + }, + ]), + RolesModule, + PermissionsModule, + ], + controllers: [UsersController], + providers: [ + UsersService, + { + provide: DadosferaLogger, + useValue: { logger }, + }, + ], + }).compile(); + + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts new file mode 100644 index 0000000..4c5fbf7 --- /dev/null +++ b/src/modules/users/users.service.ts @@ -0,0 +1,202 @@ +import { OnModuleInit, Inject, Injectable } from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; +import { DadosferaLogger } from 'dadosfera-logs'; + +import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; +import { UsersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { + AssignRoleToUserRequest, + IdRequest, + UnassignRoleFromUserRequest, + UserCreateRequest, +} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; +import { lastValueFrom } from 'rxjs'; +import { + BatchCreateUserReq, + CreateUserReq, + Hierarchy, + SetUserRolesReq, + UpdateUserReq, +} from './dtos/entities'; +import { RolesService } from '../roles/roles.service'; +import { HIERARCHIES } from './hierarchies'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { UserByCustomer } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities'; +import { EnrichErrorCode } from 'src/utils/ErrorBuilder'; + +@Injectable() +export class UsersService implements OnModuleInit { + logger: any; + language: LanguageEnum = LanguageEnum.ptbr; + + private usersClientService: UsersProtoService; + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + @Inject('DUC_PACKAGE') private readonly grpcClient: ClientGrpc, + private rolesService: RolesService, + ) { + this.logger = dadosferaLogger.logger; + } + + onModuleInit() { + this.usersClientService = this.grpcClient.getService( + ProtoServices.UsersProtoService, + ); + } + + setLanguage(l: LanguageEnum = LanguageEnum.ptbr) { + if (Object.values(LanguageEnum).includes(l)) this.language = l; + this.rolesService.setLanguage(l); + } + + async findAllUsersByCustomerId(customerId: string) { + const { users } = await lastValueFrom( + this.usersClientService.UserFindAllByCustomerId({ customerId }), + ); + const usersAdj = this.adjustUsersPayload(users); + const dintinctDepartments = [...new Set(usersAdj.map((u) => u.department))] + .filter((value) => value != null) + .sort(); + const dintinctJobTitles = [...new Set(usersAdj.map((u) => u.jobTitle))] + .filter((value) => value != null) + .sort(); + return { + users: usersAdj, + jobTitles: dintinctJobTitles, + departments: dintinctDepartments, + }; + } + + async findOneById(id: string) { + const { user } = await lastValueFrom( + this.usersClientService.UserFindOneById({ id }), + ); + return { user: this.adjustUsersPayload([user])[0] }; + } + + async createUser(req: CreateUserReq) { + const { email, department, hierarchy, jobTitle, name, roleNames } = req; + const { user } = await lastValueFrom( + this.usersClientService.UserCreate({ + email, + hierarchy, + name, + roleIds: [], + department, + jobTitle, + roleNames, + }), + ); + return { user: this.adjustUsersPayload([user])[0] }; + } + + async updateUser(req: UpdateUserReq, id: string, customerId: string) { + const { department, hierarchy, jobTitle, name, roleNames } = req; + if (roleNames) { + await this.setRoles({ roleNames, userId: id }, customerId); + } + const { user } = await lastValueFrom( + this.usersClientService.UserUpdate({ + name, + customerId, + id, + department, + hierarchy, + jobTitle, + }), + ); + return { user: this.adjustUsersPayload([user])[0] }; + } + + async setRoles(body: SetUserRolesReq, customerId: string) { + const { roleNames, userId } = body; + const clearRoles = roleNames && roleNames.length === 0; + const { user } = await lastValueFrom( + this.usersClientService.UserSetRoles({ + userId, + roleIds: [], + roleNames, + clearRoles, + customerId, + }), + ); + + return { user: this.adjustUsersPayload([user])[0] }; + } + + async batchCreateUser({ users }: BatchCreateUserReq) { + const usersToCreate: UserCreateRequest[] = users.map( + ({ roleNames, email, department, hierarchy, jobTitle, name }) => ({ + roleNames, + email, + department, + hierarchy, + jobTitle, + name, + roleIds: [], + }), + ); + const { errorUsers, usersCreated } = await lastValueFrom( + this.usersClientService.UserBatchCreate({ + users: usersToCreate, + }), + ); + + return { + usersCreated: this.adjustUsersPayload(usersCreated), + // mapping error code to message + errorUsers: errorUsers.map(({ user, error }) => ({ + user, + error: EnrichErrorCode(error).message, + })), + }; + } + + async resendInvite(body: IdRequest) { + return lastValueFrom(this.usersClientService.UserResendInvite(body)); + } + + async assignRoleToUser(body: AssignRoleToUserRequest) { + return lastValueFrom(this.usersClientService.AssignRoleToUser(body)); + } + + async unassignRoleToUser(body: UnassignRoleFromUserRequest) { + return lastValueFrom(this.usersClientService.UnassignRoleFromUser(body)); + } + + async deleteUser(id: IdRequest) { + return lastValueFrom(this.usersClientService.UserRemove(id)); + } + + getAllHierarchies(): { hierarchies: Hierarchy[] } { + const hierarchies = Object.keys(HIERARCHIES).map((key) => ({ + id: key, + name: HIERARCHIES[key]?.name[this.language] || key, + })); + return { hierarchies }; + } + + async getAllDepartments(customerId?: string) { + return lastValueFrom( + this.usersClientService.FindDistinctDepartments({ customerId }), + ); + } + + async getAllJobTitles(customerId?: string) { + return lastValueFrom( + this.usersClientService.FindDistinctJobTitles({ customerId }), + ); + } + + private adjustUsersPayload(users: UserByCustomer[]) { + return users.map((u) => { + if (!u) return undefined; + return { + ...u, + roles: this.rolesService.getRolesPermissionsName(u.roles), + hierarchy: HIERARCHIES[u.hierarchy]?.name[this.language] || u.hierarchy, + }; + }); + } +} diff --git a/src/utils/ErrorBuilder.ts b/src/utils/ErrorBuilder.ts index 50d8640..e6c0eb8 100644 --- a/src/utils/ErrorBuilder.ts +++ b/src/utils/ErrorBuilder.ts @@ -128,6 +128,49 @@ export function EnrichErrorCode(code: string) { code, }; + case ErrorCodes.ROLE.ALREADY_EXIST: + return { + statusCode: HttpStatus.BAD_REQUEST, + error: 'Grupo já existe', + message: 'Não é possível criar um grupo com esse nome', + code, + }; + case ErrorCodes.ROLE.NOT_FOUND: + case ErrorCodes.ROLE.FROM_ANOTHER_CUSTOMER: + return { + statusCode: HttpStatus.NOT_FOUND, + error: 'Grupo não encontrado', + message: 'Grupo solicitado não encontrado', + code: ErrorCodes.ROLE.NOT_FOUND, + }; + case ErrorCodes.ROLE.CANNOT_EDIT_PUBLIC_ROLE: + return { + statusCode: HttpStatus.FORBIDDEN, + error: 'Grupo público', + message: 'Você não tem permissão para editar esse grupo', + code, + }; + case ErrorCodes.USER.NOT_FOUND: + return { + statusCode: HttpStatus.NOT_FOUND, + error: 'Usuário não encontrado', + message: 'Usuário solicitado não encontrado', + code, + }; + case ErrorCodes.USER.ALREADY_EXISTS: + return { + statusCode: HttpStatus.NOT_FOUND, + error: 'Usuário encontrado', + message: 'Nome de usuário já existente', + code, + }; + case ErrorCodes.USER.INVALID_ID: + return { + statusCode: HttpStatus.NOT_FOUND, + error: 'Id inválido', + message: 'Id encontra-se num formato inválido', + code, + }; case ErrorCodes.INTERNAL: case ErrorCodes.UNKNOWN: default: @@ -137,6 +180,7 @@ export function EnrichErrorCode(code: string) { message: 'Erro desconhecido. Tente novamente ou entre em contato com o suporte', code: ErrorCodes.UNKNOWN, + details: code, }; } } @@ -146,16 +190,22 @@ const logger = new Logger(); export default class ErrorBuilder extends HttpException { code: string; - constructor(code: string | RpcException) { + constructor(code: string | RpcException = '') { if (typeof code !== 'string') { - logger.log(code.stack); + logger.log(code?.stack); code = (code as any).details as string; } else { logger.log(code); } - const { statusCode, message, error, code: rCode } = EnrichErrorCode(code); - super({ statusCode, message, error, code: rCode }, statusCode); + const { + statusCode, + message, + error, + code: rCode, + details, + } = EnrichErrorCode(code); + super({ statusCode, message, error, code: rCode, details }, statusCode); this.code = code; } } diff --git a/src/utils/errorCodes.ts b/src/utils/errorCodes.ts index 0293678..b679510 100644 --- a/src/utils/errorCodes.ts +++ b/src/utils/errorCodes.ts @@ -13,7 +13,26 @@ export const AUTH = { RESET_PASSWORD_CODE_INVALID: 'AUTH.RESET_PASSWORD_CODE_INVALID', WEAK_NEW_PASSWORD: 'AUTH.WEAK_NEW_PASSWORD', }; +export const ROLE = { + ALREADY_EXIST: 'ROLE.ALREADY_EXISTS', + NOT_FOUND: 'ROLE.NOT_FOUND', + FROM_ANOTHER_CUSTOMER: 'ROLE.FROM_ANOTHER_CUSTOMER', + CANNOT_EDIT_PUBLIC_ROLE: 'ROLE.CANNOT_EDIT_PUBLIC_ROLE', +}; +export const USER = { + INVALID_ID: 'USER.INVALID_ID', + NOT_FOUND: 'USER.NOT_FOUND', + ALREADY_EXISTS: 'USER.ALREADY_EXISTS', + ROLE_ALREADY_ASSIGNED: 'USER.ROLE_ALREADY_ASSIGNED', + ROLE_NOT_ASSIGNED: 'USER.ROLE_NOT_ASSIGNED', +}; + +export const CUSTOMER = { + INVALID_ID: 'CUSTOMER.INVALID_ID', + NOT_FOUND: 'CUSTOMER.NOT_FOUND', + ALREADY_EXISTS: 'CUSTOMER.ALREADY_EXISTS', +}; const TERMS_OF_USE = { UP_TO_DATE: 'TERMS_OF_USE.UP_TO_DATE', REQUIRED: 'TERMS_OF_USE.REQUIRED', @@ -24,7 +43,10 @@ const ErrorCodes = { RATE_LIMIT: 'RATE_LIMIT', INTERNAL: 'INTERNAL', AUTH, + ROLE, TERMS_OF_USE, + USER, + CUSTOMER, }; export default ErrorCodes; diff --git a/src/utils/languages.enum.ts b/src/utils/languages.enum.ts new file mode 100644 index 0000000..1fb3bc1 --- /dev/null +++ b/src/utils/languages.enum.ts @@ -0,0 +1,4 @@ +export enum LanguageEnum { + ptbr = 'pt-br', + enus = 'en-us', +}