diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml deleted file mode 100644 index f547ac4..0000000 --- a/.github/workflows/deploy-k8s.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Deploy K8S Modifications - -on: - push: - branches: - - main - - beta - -jobs: - extract_environment: - runs-on: ubuntu-22.04 - outputs: - environment: ${{ steps.extract_environment.outputs.environment }} - steps: - - name: Extract Environment - run: | - if [ ${GITHUB_REF} == "refs/heads/main" ]; then - echo "environment=prd" >> $GITHUB_OUTPUT - elif [ ${GITHUB_REF} == "refs/heads/beta" ]; then - echo "environment=stg" >> $GITHUB_OUTPUT - fi - id: extract_environment - - helmfile-deploy: - needs: [extract_environment] - runs-on: [self-hosted, "prd-azure"] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Helm - uses: azure/setup-helm@v1 - with: - version: 'v3.9.0' - - - name: Install Azure ClI - run: | - curl -sL https://aka.ms/InstallAzureCLIDeb | bash - - - uses: azure/login@v2 - with: - creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}' - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - - name: Install Helmfile - run: | - wget https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz - tar -xzf helmfile_0.148.0_linux_amd64.tar.gz - mv helmfile /usr/local/bin/ - helmfile --version - - - name: Install Helm Diff Plugin - run: helm plugin install https://github.com/databus23/helm-diff || true - - - name: Setup kubectl - uses: azure/setup-kubectl@v1 - with: - version: 'v1.30.1' - - - name: Authenticate with cluster - env: - CLUSTER_NAME: platform-${{ needs.extract_environment.outputs.environment }} - run: az aks get-credentials --resource-group dadosfera-prd --name ${CLUSTER_NAME} --overwrite-existing - - - name: Run Helmfile Apply - env: - ENV: ${{ needs.extract_environment.outputs.environment }} - run: helmfile -f helmfiles/${ENV}.yaml sync diff --git a/README.md b/README.md index 028ca81..bb50d3a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@

+ # Maestro Maestro é a API principal da Dadosfera. É responsável pela comunicação do Frontend com nossos microsserviços. diff --git a/docsfera.json b/docsfera.json index 0922da4..33988d7 100644 --- a/docsfera.json +++ b/docsfera.json @@ -5431,6 +5431,117 @@ ] } }, + "/open-data/sharing-ocean-data": { + "post": { + "operationId": "OpenDataController_createUser", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + }, + { + "name": "language", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "OpenData" + ] + } + }, + "/customers/{id}/theme": { + "post": { + "operationId": "ThemeController_saveCustomertheme", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerThemeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerThemeResponse" + } + } + } + } + }, + "tags": [ + "Theme" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + }, + "get": { + "operationId": "ThemeController_getCustomerTheme", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerThemeResponse" + } + } + } + } + }, + "tags": [ + "Theme" + ] + } + }, "/health": { "get": { "operationId": "HealthController_check", @@ -5508,6 +5619,9 @@ "items": { "type": "string" } + }, + "themeEnabled": { + "type": "boolean" } }, "required": [ @@ -5516,7 +5630,8 @@ "name", "tier", "scheduleLimit", - "links" + "links", + "themeEnabled" ] }, "AuthUser": { @@ -8234,8 +8349,61 @@ "required": [ "links" ] + }, + "CustomerThemeRequest": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "textColor": { + "type": "string" + } + }, + "required": [ + "displayName", + "backgroundColor", + "textColor" + ] + }, + "CustomerTheme": { + "type": "object", + "properties": { + "backgroundColor": { + "type": "string" + }, + "textColor": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "logoLogin": { + "type": "string" + } + }, + "required": [ + "backgroundColor", + "textColor", + "logo", + "logoLogin" + ] + }, + "CustomerThemeResponse": { + "type": "object", + "properties": { + "theme": { + "$ref": "#/components/schemas/CustomerTheme" + } + }, + "required": [ + "theme" + ] } } } } -} +} \ No newline at end of file diff --git a/environment.d.ts b/environment.d.ts index 727c484..c910516 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -12,6 +12,8 @@ declare global { INTERNAL_SWAGGER: 'true' | 'false'; AWS_REGION: string; + OPEN_GROUP_ID: string; + OPEN_CUSTOMER_ID: string; } } } diff --git a/helmfiles/prd.yaml b/helmfiles/prd.yaml index 8076b3b..344cb88 100644 --- a/helmfiles/prd.yaml +++ b/helmfiles/prd.yaml @@ -13,4 +13,8 @@ charts: - name: maestro.in_factory_url value: in-factory.dadosfera.ai - name: maestro.tr_factory_url - value: in-factory.dadosfera.ai \ No newline at end of file + value: in-factory.dadosfera.ai + - name: maestro.open_customer_id + value: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e + - name: maestro.open_group_id + value: 401573bb-334f-44b2-b30e-88d4cea31ae9 \ No newline at end of file diff --git a/helmfiles/stg.yaml b/helmfiles/stg.yaml index eb1fc69..263570f 100644 --- a/helmfiles/stg.yaml +++ b/helmfiles/stg.yaml @@ -5,12 +5,18 @@ charts: - ../maestro/values.yaml set: - name: maestro.duc_url - value: duc-temp.dadosfera.ai + value: duc.stg.dadosfera.ai - name: hostname - value: maestro-temp.dadosfera.ai + value: maestro.stg.dadosfera.ai - name: maestro.pi_factory_url - value: pi-factory-temp.dadosfera.ai + value: pi-factory.dadosfera.ai - name: maestro.in_factory_url - value: in-factory-temp.dadosfera.ai + value: in-factory.dadosfera.ai - name: maestro.tr_factory_url - value: in-factory-temp.dadosfera.ai \ No newline at end of file + value: in-factory.dadosfera.ai + - name: maestro.open_customer_id + value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d + - name: maestro.open_group_id + value: e3f98a2f-7748-4981-8505-7695c8ca8218 + - name: replicaCount + value: 1 \ No newline at end of file diff --git a/maestro/templates/deployment.yaml b/maestro/templates/deployment.yaml index 18fad50..74a7a5c 100644 --- a/maestro/templates/deployment.yaml +++ b/maestro/templates/deployment.yaml @@ -36,6 +36,11 @@ spec: operator: In values: - backend + tolerations: + - key: "kubernetes.azure.com/scalesetpriority" + operator: "Equal" + value: "spot" + effect: "NoSchedule" containers: - name: maestro @@ -90,6 +95,10 @@ spec: value: {{ .Values.maestro.tr_factory_url }} - name: UPLOAD_FILE_AGENT_CONNECTION value: {{ .Values.maestro.upload_file_agent_connection }} + - name: OPEN_CUSTOMER_ID + value: {{ .Values.maestro.open_customer_id }} + - name: OPEN_GROUP_ID + value: {{ .Values.maestro.open_group_id }} - name: JWT_PRIVATE_KEY valueFrom: secretKeyRef: diff --git a/maestro/values.yaml b/maestro/values.yaml index a8d745c..070c533 100644 --- a/maestro/values.yaml +++ b/maestro/values.yaml @@ -40,7 +40,8 @@ maestro: sm_oauth_path: prd/root/oauth_applications tr_factory_url: in-factory.dadosfera.ai upload_file_agent_connection: cbc2f881-58c4-4d60-8003-0979b0b5b911 - + open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e + open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9 autoscaling: enabled: false minReplicas: 1 diff --git a/package-lock.json b/package-lock.json index 61c7a9a..9539649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.34.0", + "@dadosfera/protospack-v2": "3.37.0-beta.5", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -53,7 +53,7 @@ "@types/jest": "27.0.2", "@types/jsonwebtoken": "^8.5.9", "@types/jwk-to-pem": "^2.0.1", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.12", "@types/node": "^16.18.52", "@types/passport-facebook": "^2.1.11", "@types/passport-google-oauth20": "^2.0.11", @@ -1403,9 +1403,10 @@ } }, "node_modules/@dadosfera/protospack-v2": { - "version": "3.34.0", - "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.34.0.tgz", - "integrity": "sha512-VUjpoHg5/uNNkg1cTxWbUv+20t5CTAf1tB4dhBmGSYjgb+Efo5gi3aO/p3ajYuzOOMOxF+EhjQVouU6BD3E+Xg==", + "version": "3.37.0-beta.5", + "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.37.0-beta.5.tgz", + "integrity": "sha512-dtHMzMU2Qa9c6DpygWr7fib19l1zqbYgBi2wGkiJ6uMAvl1CGu9V0p+JA1FwGTQ0QRy7EkkRVi6X6sH5u/ICpA==", + "license": "ISC", "dependencies": { "@grpc/grpc-js": "^1.9.3", "rxjs": "^7.5.5" @@ -3526,10 +3527,11 @@ "dev": true }, "node_modules/@types/multer": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", - "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } diff --git a/package.json b/package.json index fbe7eea..072bfb6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@aws-sdk/client-secrets-manager": "^3.414.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.34.0", + "@dadosfera/protospack-v2": "3.37.0-beta.5", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -74,7 +74,7 @@ "@types/jest": "27.0.2", "@types/jsonwebtoken": "^8.5.9", "@types/jwk-to-pem": "^2.0.1", - "@types/multer": "^1.4.7", + "@types/multer": "^1.4.12", "@types/node": "^16.18.52", "@types/passport-facebook": "^2.1.11", "@types/passport-google-oauth20": "^2.0.11", diff --git a/src/app.module.ts b/src/app.module.ts index fa831fd..3e54fde 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -26,9 +26,10 @@ import { PipelinesV2Module } from './modules/pipelinesV2/pipelines.module'; import { ProductboardModule } from './modules/productboard/productboard.module'; import { MixpanelModule } from './modules/mixpanel/mixpanel.module'; import { CustomersModule } from './modules/customers/customers.module'; +import { OpenDataModule } from './modules/open-data/open-data.module'; +import { ThemeModule } from './modules/theme/theme.module'; @Module({ - controllers: [], providers: [ DadosferaLogger, { @@ -58,8 +59,10 @@ import { CustomersModule } from './modules/customers/customers.module'; ProductboardModule, MixpanelModule, CustomersModule, + OpenDataModule, + ThemeModule, //Always leave HealthModule last, so it is on the bottom of swagger - HealthModule, + HealthModule ], }) export class AppModule {} diff --git a/src/modules/auth/dtos/login.ts b/src/modules/auth/dtos/login.ts index 33a779b..6e87606 100644 --- a/src/modules/auth/dtos/login.ts +++ b/src/modules/auth/dtos/login.ts @@ -77,6 +77,8 @@ export class AuthCustomer { scheduleLimit: string; @ApiProperty() links: Link[]; + @ApiProperty() + themeEnabled: boolean; } export class AuthSignInReq implements AuthSignInRequest { diff --git a/src/modules/customers/customers.controller.ts b/src/modules/customers/customers.controller.ts index 04073ec..1bd9835 100644 --- a/src/modules/customers/customers.controller.ts +++ b/src/modules/customers/customers.controller.ts @@ -1,5 +1,4 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; -import { IdResponse } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; import { Body, Controller, @@ -18,7 +17,6 @@ import { Authenticated, RequireAllPermissions, } from 'src/decorators/authentication.decorator'; -import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator'; import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; import { CustomersService } from './customers.service'; import { CustomerLinkRequest, CustomerLinksResponse } from './dtos/customers'; @@ -28,7 +26,6 @@ import { PackTheMetadata } from 'src/utils/ PackTheMetadata'; @ApiTags('Customers') @Controller('customers') -@Authenticated() @UseFilters(GrpcToHttpExceptionFilter) export class CustomersController { logger: DadosferaLogger; @@ -42,6 +39,7 @@ export class CustomersController { } @Get(':id/links') + @Authenticated() @ApiOkResponse({ type: CustomerLinksResponse }) async getCustomerLinks(@Param('id') id: string) { this.logger.info('getCustomerLinks', { id }); @@ -50,6 +48,7 @@ export class CustomersController { } @Put(':id/links') + @Authenticated() @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) @ApiOkResponse() @HttpCode(HttpStatus.OK) @@ -63,6 +62,7 @@ export class CustomersController { } @Get('token') + @Authenticated() @RequireAllPermissions(PERMISSIONS_GROUPS.AUTH.permissions.GENERATE_TOKEN) @ApiProduces('text/plain') async getCustomerToken( @@ -79,6 +79,7 @@ export class CustomersController { } @Get('monitoring-dashboard') + @Authenticated() @RequireAllPermissions( PERMISSIONS_GROUPS.CUSTOMER.permissions.MONITORING_DASHBOARD, ) diff --git a/src/modules/customers/customers.service.ts b/src/modules/customers/customers.service.ts index 5102db5..cd7f472 100644 --- a/src/modules/customers/customers.service.ts +++ b/src/modules/customers/customers.service.ts @@ -28,19 +28,24 @@ import { } from '@dadosfera/protospack-v2/dist/lib/PipelineV2'; import { Metadata } from '@grpc/grpc-js'; import { PipelinesClientConfiguration } from '../pipelinesV2/pipelines-client'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; // This function will accept any string, which may result in a bug. @Injectable() export class CustomersService implements OnModuleInit { private customerService: CustomersProtoService; - + private logger: DadosferaLogger; private pipelineReadService: ReadService.PipelineV2ReadService; constructor( @Inject(DucClient.name) private readonly grpcClient: ClientGrpc, @Inject(PipelinesClientConfiguration.name) private readonly pipelinesGrpcClient: ClientGrpc, - ) {} + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } onModuleInit() { this.customerService = this.grpcClient.getService( diff --git a/src/modules/customers/dtos/customers.ts b/src/modules/customers/dtos/customers.ts index c3b3070..db56ee3 100644 --- a/src/modules/customers/dtos/customers.ts +++ b/src/modules/customers/dtos/customers.ts @@ -20,3 +20,4 @@ export class CustomerLinksResponse { @ApiProperty({ type: [CustomerLink] }) links: CustomerLink[]; } + diff --git a/src/modules/duc/client.config.ts b/src/modules/duc/client.config.ts index 25ce28b..4cf4124 100644 --- a/src/modules/duc/client.config.ts +++ b/src/modules/duc/client.config.ts @@ -29,6 +29,8 @@ export class DucClient { objects: true, arrays: true, }, + maxSendMessageLength: 15 * 1024 * 1024, // 15 MB por mensagem + maxReceiveMessageLength: 15 * 1024 * 1024, }, }; diff --git a/src/modules/open-data/dto/wordpres-form.ts b/src/modules/open-data/dto/wordpres-form.ts new file mode 100644 index 0000000..39b6ed7 --- /dev/null +++ b/src/modules/open-data/dto/wordpres-form.ts @@ -0,0 +1,48 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class CreateUserOpenDataDTO { + @ApiProperty() + firstName: string; + @ApiProperty() + lastName: string; + @ApiProperty() + email: string; + @ApiProperty() + organization: string; + @ApiProperty() + enquiryType: string; +} + +type FormField = { + id: string; + type: string; + title: string; + value: string; + raw_value: string; + required: string; +}; + +type MetaData = { + title: string; + value: string; +}; + +export type WordpressForm = { + form: { + id: string; + name: string; + }; + fields: { + [key: string]: FormField; + }; + meta: { + date: MetaData; + time: MetaData; + page_url: MetaData; + user_agent: MetaData; + remote_ip: MetaData; + credit: MetaData; + }; +}; + + diff --git a/src/modules/open-data/open-data.controller.ts b/src/modules/open-data/open-data.controller.ts new file mode 100644 index 0000000..b999060 --- /dev/null +++ b/src/modules/open-data/open-data.controller.ts @@ -0,0 +1,98 @@ +import DadosferaLogger from '@dadosfera/dadosfera-logs'; +import { Body, Controller, ForbiddenException, Header, HttpCode, Inject, Param, Post, Query, Req, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; +import { ApiCreatedResponse, ApiHeaders, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator'; +import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; +import { LanguageEnum } from 'src/utils/languages.enum'; +import { UsersService } from '../users/users.service'; +import { Language } from 'src/decorators/language.decorator'; +import { OpenDataService } from './open-data.service'; +import { CreateUserOpenDataDTO, WordpressForm } from './dto/wordpres-form'; +import { Metadata } from '@grpc/grpc-js'; +import { PackTheMetadata } from 'src/utils/ PackTheMetadata'; +import { request } from 'http'; +import { Request } from 'express'; + +@Controller('open-data') +@ApiInternalOnlyController() +@ApiTags('OpenData') +@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }]) +@UseFilters(GrpcToHttpExceptionFilter) +export class OpenDataController { + logger: DadosferaLogger; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private openDataService: OpenDataService, + ) { + this.logger = dadosferaLogger.logger; + } + + @Post("/sharing-ocean-data") + @HttpCode(200) + @Header('content-type', 'application/json') + @ApiOkResponse() + async createUser( + @Body() + body: WordpressForm, + @Query('language') + language: string, + @Req() + request: Request, + ) { + if (!request.headers['user-agent'].includes('WordPress/6.7.1; https://devsbm.dadosfera.io')) { + throw new ForbiddenException(); + } + this.logger.info('createUser for open data'); + + this.logger.info('user-agent', request.headers['user-agent']); + + const OPENDATA_CUSTOMER_ID = process.env.OPEN_CUSTOMER_ID; + const OPENDATA_GROUP_ID = process.env.OPEN_GROUP_ID; + const roles = [process.env.OPEN_GROUP_ID]; + const metadata = PackTheMetadata({ + language: language || 'en-us' + }); + + const data = {} + + try { + Object.keys(body.fields) + .filter(key => body.fields[key].required === "1") + .forEach(key => { + const field = body.fields[key] + data[field.id] = field.value + }); + } catch (e) { + this.logger.error('user data ' + e.message); + } + + const user: CreateUserOpenDataDTO = { + email: data["email"], + enquiryType: data["enquiry_type"], + firstName: data["first_name"], + lastName: data["last_name"], + organization: data["organization"] + } + this.logger.info(`user request to group ${OPENDATA_CUSTOMER_ID} with role ${OPENDATA_GROUP_ID}`); + + try { + const id = await this.openDataService.createUser(OPENDATA_CUSTOMER_ID, user, roles, metadata); + this.logger.info('user created with id: '+ id); + return { + success: true, + status: 'success', + message: 'user created with succesfull' + } + } catch (e) { + this.logger.error('failed with exception: ' + e.message); + return { + success: false, + status: 'failed', + message: e.message + }; + } + } + +} diff --git a/src/modules/open-data/open-data.module.ts b/src/modules/open-data/open-data.module.ts new file mode 100644 index 0000000..f82b125 --- /dev/null +++ b/src/modules/open-data/open-data.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { OpenDataController } from './open-data.controller'; +import { UsersService } from '../users/users.service'; +import { ClientsModule } from '@nestjs/microservices' +import { DucClient } from '../duc/client.config'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; +import { RolesModule } from '../roles/roles.module'; +import { PermissionsModule } from '../permissions/permissions.module'; +import { OpenDataService } from './open-data.service'; + +const client = new DucClient(); + +@Module({ + controllers: [OpenDataController], + imports: [ + ClientsModule.register([client.providerOptions]), + RolesModule, + // PermissionsModule, + ], + providers: [DadosferaLogger, UsersService, OpenDataService] +}) +export class OpenDataModule {} diff --git a/src/modules/open-data/open-data.service.ts b/src/modules/open-data/open-data.service.ts new file mode 100644 index 0000000..4c927fa --- /dev/null +++ b/src/modules/open-data/open-data.service.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { lastValueFrom } from 'rxjs'; +import { CreateUserOpenDataDTO } from './dto/wordpres-form'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; +import { UsersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { DucClient } from '../duc/client.config'; +import { ClientGrpc } from '@nestjs/microservices'; +import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; +import { Metadata } from '@grpc/grpc-js'; + +@Injectable() +export class OpenDataService implements OnModuleInit { + logger: DadosferaLogger; + + private usersClientService: UsersProtoService; + constructor( + @Inject(DadosferaLogger) + private dadosferaLogger: DadosferaLogger, + @Inject(DucClient.name) + private readonly grpcClient: ClientGrpc, + ) { + this.logger = dadosferaLogger.logger; + } + + onModuleInit() { + this.usersClientService = this.grpcClient.getService( + ProtoServices.UsersProtoService, + ); + + } + + async createUser(customerId: string, data: CreateUserOpenDataDTO, roleIds: string[], metadata: Metadata) { + const body = { + email: data.email, + name: data.firstName + " " + data.lastName, + department: data.organization, + jobTitle: data.enquiryType, + customerId: customerId, + roleIds: roleIds + } + + try { + const { user } = await lastValueFrom( + this.usersClientService.SimpleUserCreate(body, metadata), + ); + return user.id; + } catch(err) { + return err; + } + } +} diff --git a/src/modules/theme/dtos/customers.ts b/src/modules/theme/dtos/customers.ts new file mode 100644 index 0000000..05f5305 --- /dev/null +++ b/src/modules/theme/dtos/customers.ts @@ -0,0 +1,47 @@ +import { Link, Theme } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CustomerLink implements Link { + @ApiProperty() + href: string; + @ApiProperty() + name: string; + @ApiProperty() + description: string; + @ApiPropertyOptional() + iconSrc: string; +} +export class CustomerLinkRequest { + @ApiProperty({ type: [CustomerLink] }) + links: CustomerLink[]; +} + +export class CustomerLinksResponse { + @ApiProperty({ type: [CustomerLink] }) + links: CustomerLink[]; +} + +export class CustomerTheme implements Theme { + @ApiProperty() + backgroundColor: string; + @ApiProperty() + textColor: string; + @ApiProperty() + logo: string; + @ApiProperty() + logoLogin: string; +} + +export class CustomerThemeResponse { + @ApiProperty() + theme: CustomerTheme; +} + +export class CustomerThemeRequest { + @ApiProperty() + displayName: string; + @ApiProperty() + backgroundColor: string; + @ApiProperty() + textColor: string; +} diff --git a/src/modules/theme/theme.controller.ts b/src/modules/theme/theme.controller.ts new file mode 100644 index 0000000..327c5be --- /dev/null +++ b/src/modules/theme/theme.controller.ts @@ -0,0 +1,127 @@ +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Inject, + Param, + Post, + Put, + Query, + UploadedFiles, + UseFilters, + UseInterceptors, + HttpCode +} from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum'; +import { + Authenticated, + RequireAllPermissions, +} from 'src/decorators/authentication.decorator'; +import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter'; +import { CustomerThemeRequest, CustomerThemeResponse } from './dtos/customers'; + +import ErrorCodes from 'src/utils/errorCodes'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; +import { ThemeService } from './theme.service'; + +@ApiTags('Theme') +@Controller('customers') +@UseFilters(GrpcToHttpExceptionFilter) +export class ThemeController { + logger: DadosferaLogger; + + constructor( + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + private themeService: ThemeService, + ) { + this.logger = dadosferaLogger.logger; + } + + + @Post('/:id/theme') + @Authenticated() + @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) + @ApiOkResponse({ type: CustomerThemeResponse }) + @UseInterceptors(AnyFilesInterceptor()) + @HttpCode(HttpStatus.OK) + async saveCustomertheme( + @Param('id') id: string, + @Body() data: CustomerThemeRequest, + @UploadedFiles() files: Array + ) { + + this.logger.info('saveCustomertheme' + JSON.stringify({ + id, + })); + + const logo = files.find(file => file.fieldname === 'logo'); + const logoLogin = files.find(file => file.fieldname === 'logoLogin'); + + this.validFileSize(logo); + this.validFileSize(logoLogin); + this.validMimeType(logo); + this.validMimeType(logoLogin); + + try { + const theme = await this.themeService.createThemeByCustomer(id, { + ...data, + logo, + logoLogin + }); + this.logger.info('saveCustomertheme' + JSON.stringify(theme)); + return theme; + } catch (err) { + if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) { + this.logger.error('Error - saveCustomertheme - Expect CUSTOMER.NOT_FOUND'); + throw new HttpException(err.details, HttpStatus.NOT_FOUND); + } else { + this.logger.error('Error - saveCustomertheme Unknown Error:' + err?.message); + return { theme: null }; + }; + } + } + + private validFileSize(file: Express.Multer.File) { + const maxFileSize = 10 * 1024 * 1024; // 10MB + + if (file && file.size > maxFileSize) { + throw new HttpException(`O Arquivo ${file.filename} possui mais de 10MB`, HttpStatus.BAD_REQUEST); + } + } + + private validMimeType(file: Express.Multer.File) { + const mimeTypesValid = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml']; + + if (file && !mimeTypesValid.includes(file.mimetype)) { + throw new HttpException(`O Arquivo ${file.fieldname} deve ser jpeg, jpg, png ou svg`, HttpStatus.BAD_REQUEST); + } + } + + @Get('/:id/theme') + @ApiOkResponse({ type: CustomerThemeResponse }) + async getCustomerTheme(@Param('id') id: string) { + this.logger.info('getCustomerTheme with id' + id); + + try { + const data = await this.themeService.getThemeByCustomer(id); + this.logger.info('Success - getCustomerTheme'+ JSON.stringify(data)); + if (data?.theme) return data; + + return { theme: null }; + }catch (err) { + if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) { + this.logger.error('Error - getCustomerTheme - Expect CUSTOMER.NOT_FOUND'); + throw new HttpException(err.details, HttpStatus.NOT_FOUND); + } else { + this.logger.error('Error - getCustomerTheme Unknown Error:' + err?.message); + return { theme: null }; + }; + } + } + +} diff --git a/src/modules/theme/theme.module.ts b/src/modules/theme/theme.module.ts new file mode 100644 index 0000000..4fc3d53 --- /dev/null +++ b/src/modules/theme/theme.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; +import { ClientsModule } from '@nestjs/microservices'; +import { ThemeController } from './theme.controller'; +import { ThemeService } from './theme.service'; +import { DucClient } from '../duc/client.config'; + +const ducClient = new DucClient(); + +@Module({ + imports: [ + ClientsModule.register([ + ducClient.providerOptions, + ]), + ], + controllers: [ThemeController], + providers: [ThemeService, DadosferaLogger], + exports: [ThemeService], +}) +export class ThemeModule {} diff --git a/src/modules/theme/theme.service.ts b/src/modules/theme/theme.service.ts new file mode 100644 index 0000000..b62338b --- /dev/null +++ b/src/modules/theme/theme.service.ts @@ -0,0 +1,158 @@ +import { + OnModuleInit, + Inject, + Injectable, + HttpException, + HttpStatus, + InternalServerErrorException, +} from '@nestjs/common'; + +import { firstValueFrom, lastValueFrom, ReplaySubject } from 'rxjs'; +import { Link } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/entities'; +import { DucClient } from '../duc/client.config'; +import { ClientGrpc } from '@nestjs/microservices'; +import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; +import { CustomerUpdateRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; +import { ThemeProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; +import { ThemeRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; +import { CustomerThemeRequest, CustomerThemeResponse } from './dtos/customers'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; +import { resolve } from 'path'; +import { Readable } from 'stream'; + +type Files = { + logo: Express.Multer.File, + logoLogin: Express.Multer.File, +} + +@Injectable() +export class ThemeService implements OnModuleInit { + private themeService: ThemeProtoService; + private logger: DadosferaLogger; + + constructor( + @Inject(DucClient.name) private readonly grpcClient: ClientGrpc, + @Inject(DadosferaLogger) + dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + onModuleInit() { + this.themeService = this.grpcClient.getService( + ProtoServices.ThemeProtoService, + ); + } + + async createThemeByCustomer(id: string, theme: CustomerThemeRequest & Files) { + if (!id) { + this.logger.error('Error - saveCustomertheme - not found id:' + id); + throw new HttpException(null, HttpStatus.BAD_REQUEST); + } + + const customerThemeRequest$ = new ReplaySubject(); + + customerThemeRequest$.next({ + customerId: id, + displayName: theme.displayName, + backgroundColor: theme.backgroundColor, + textColor: theme.textColor, + isMetadata: true, + filename: '', + chunk: Buffer.alloc(0) + }) + + if(theme.logo) { + await this.sendFile(theme.logo, customerThemeRequest$); + } + + if(theme.logoLogin) { + await this.sendFile(theme.logoLogin, customerThemeRequest$); + } + customerThemeRequest$.complete(); + + const stream = this.themeService.CustomerCreateTheme(customerThemeRequest$); + + return lastValueFrom(stream); + } + + async getThemeByCustomer(id: string): Promise { + if (!id) { + this.logger.error('Error - getCustomerTheme - not found id:' + id); + throw new HttpException(null, HttpStatus.BAD_REQUEST); + } + + const { theme } = await firstValueFrom( + this.themeService.CustomerGetTheme({ + id + }), + ); + + return { + theme + } + } + + private async sendFile(file: Express.Multer.File, stream$: ReplaySubject) { + const chunkSize = 4 * 1024 * 1024; + const bufferStream = new CustomBufferStream(file.buffer, chunkSize); + const parseMimitypeForExtension = { + 'image/jpeg': '.jpeg', + 'image/jpg': '.jpg', + 'image/png': '.png', + 'image/svg+xml': '.svg', + } + + const extension = parseMimitypeForExtension[file.mimetype]; + + return new Promise((resolve, reject) => { + bufferStream.on('data', (chunk) => { + const filename = file.fieldname.concat(extension); + stream$.next({ + customerId: '', + displayName: '', + backgroundColor: '', + textColor: '', + isMetadata: false, + filename: filename, + chunk: chunk + }); + }); + + bufferStream.on('end', () => { + resolve(file.filename) + }); + + bufferStream.on('error', (err) => { + reject(err); + }); + }); + } + +} + +class CustomBufferStream extends Readable { + buffer: Buffer; + offset: number; + chunkSize: number; + + constructor(buffer: Buffer, chunkSize: number) { + super({ highWaterMark: chunkSize }); // Configura o tamanho do chunk + this.buffer = buffer; + this.offset = 0; + this.chunkSize = chunkSize; + } + + _read() { + if (this.offset < this.buffer.length) { + const end = Math.min(this.offset + this.chunkSize, this.buffer.length); + + const copiedBuf = Uint8Array.prototype.slice.call(this.buffer); + const chunk = copiedBuf.slice(this.offset, end); + this.offset = end; + this.push(chunk); + } else { + this.push(null); + } + } +}