diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c9fc6a8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +.git +*.log +npm-debug.log* +.DS_Store +.env +.env.* +coverage +.nyc_output +*.tgz +!protospack.tgz diff --git a/Dockerfile.local b/Dockerfile.local index 03430e4..fc8afaf 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM node:20-alpine AS base_image +FROM node:22-alpine AS base_image RUN npm install -g npm@latest FROM base_image AS build_base diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml index 7312b30..68b8e1d 100644 --- a/deploy/helm-chart/templates/deployment.yaml +++ b/deploy/helm-chart/templates/deployment.yaml @@ -48,6 +48,9 @@ spec: {{- toYaml .Values.resources | nindent 12 }} {{- end }} env: + # Auth Provider Configuration (cognito or keycloak) + - name: AUTH_PROVIDER + value: {{ .Values.maestro.auth_provider | default "cognito" | quote }} - name: AWS_IDENTITY_POOL_ID value: {{ .Values.maestro.aws_identity_pool_id }} - name: AWS_REGION @@ -108,6 +111,8 @@ spec: value: "{{ .Values.maestro.redis_tls }}" - name: PLATFORM_API_URL value: {{ .Values.maestro.platform_api_url }} + - name: STORAGE_EXPLORER_API_URL + value: {{ .Values.maestro.storage_explorer_api_url | quote }} - name: JWT_PRIVATE_KEY valueFrom: secretKeyRef: diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml index 30af31e..3c8ebcc 100644 --- a/deploy/helm-chart/values.yaml +++ b/deploy/helm-chart/values.yaml @@ -27,6 +27,9 @@ resources: cpu: 2000m memory: 2Gi maestro: + # Auth provider: "cognito" (default) or "keycloak" + # Note: maestro doesn't connect to Keycloak directly, only duc does + auth_provider: "cognito" aws_identity_pool_id: "us-east-1_Mrezsw9Sn" duc_url: duc.dadosfera.ai in_factory_url: in-factory.dadosfera.ai @@ -44,6 +47,7 @@ maestro: open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9 platform_api_url: https://oz8v2zid1e.execute-api.us-east-1.amazonaws.com + storage_explorer_api_url: "https://storage-explorer-{customer_id}.prd.dadosfera.ai/api" dedicated_proxy: "" restricted_ip: "" redis_host: "aaapzppmlyamkocqwstpo7zvopczyyiyuy6xzm2g6c5k4mq3a66be4a-0.redis.sa-saopaulo-1.oci.oraclecloud.com" diff --git a/docsfera.json b/docsfera.json index 29c24b7..d13bcd6 100644 --- a/docsfera.json +++ b/docsfera.json @@ -213,14 +213,7 @@ ], "responses": { "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } + "description": "" } }, "tags": [ @@ -722,6 +715,158 @@ ] } }, + "/api-key": { + "post": { + "operationId": "ApiKeyController_create", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponseDto" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponseDto" + } + } + } + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + }, + "get": { + "operationId": "ApiKeyController_findAll", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKeyBaseResponseDto" + } + } + } + } + }, + "default": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKeyBaseResponseDto" + } + } + } + } + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + } + }, + "/api-key/{id}": { + "delete": { + "operationId": "ApiKeyController_remove", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ApiKey" + ], + "security": [ + { + "access-token": [] + } + ] + } + }, "/connections": { "post": { "operationId": "ConnectionController_createConnection", @@ -5461,6 +5606,66 @@ ] } }, + "/customers/{id}/organization-info": { + "get": { + "operationId": "CustomersController_getOrganizationInfo", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization information" + } + }, + "tags": [ + "Customers" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + }, + "put": { + "operationId": "CustomersController_updateOrganizationInfo", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization information updated" + } + }, + "tags": [ + "Customers" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, "/catalog/data-asset/share/{id}": { "get": { "operationId": "ShareController_getShareDataAsset", @@ -6531,158 +6736,6 @@ ] } }, - "/api-key": { - "post": { - "operationId": "ApiKeyController_create", - "parameters": [ - { - "name": "dadosfera-lang", - "in": "header", - "required": false, - "schema": { - "enum": [ - "pt-br", - "en-us" - ], - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyDto" - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyResponseDto" - } - } - } - }, - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyResponseDto" - } - } - } - } - }, - "tags": [ - "ApiKey" - ], - "security": [ - { - "access-token": [] - } - ] - }, - "get": { - "operationId": "ApiKeyController_findAll", - "parameters": [ - { - "name": "dadosfera-lang", - "in": "header", - "required": false, - "schema": { - "enum": [ - "pt-br", - "en-us" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKeyBaseResponseDto" - } - } - } - } - }, - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKeyBaseResponseDto" - } - } - } - } - } - }, - "tags": [ - "ApiKey" - ], - "security": [ - { - "access-token": [] - } - ] - } - }, - "/api-key/{id}": { - "delete": { - "operationId": "ApiKeyController_remove", - "parameters": [ - { - "name": "dadosfera-lang", - "in": "header", - "required": false, - "schema": { - "enum": [ - "pt-br", - "en-us" - ], - "type": "string" - } - }, - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "ApiKey" - ], - "security": [ - { - "access-token": [] - } - ] - } - }, "/identity-providers": { "post": { "operationId": "IdentityProviderController_addIdentityProvider", @@ -7714,6 +7767,748 @@ ] } }, + "/storage-explorer/tables/validate-name": { + "post": { + "operationId": "StorageExplorerController_validateTableName", + "summary": "Validate table name in PostgreSQL and Snowflake", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables": { + "post": { + "operationId": "StorageExplorerController_createTable", + "summary": "Create a new table", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + }, + "get": { + "operationId": "StorageExplorerController_listTables", + "summary": "List all tables with pagination", + "parameters": [ + { + "name": "page", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables/{tableId}": { + "get": { + "operationId": "StorageExplorerController_getTable", + "summary": "Get table details by ID", + "parameters": [ + { + "name": "tableId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables/{tableId}/datasets/{datasetId}": { + "post": { + "operationId": "StorageExplorerController_linkDatasetToTable", + "summary": "Link a dataset to a table", + "parameters": [ + { + "name": "tableId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "datasetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables/{tableId}/datasets": { + "get": { + "operationId": "StorageExplorerController_getTableDatasets", + "summary": "Get all datasets linked to a table", + "parameters": [ + { + "name": "tableId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables/{tableId}/schema": { + "get": { + "operationId": "StorageExplorerController_getTableSchema", + "summary": "Get table schema", + "parameters": [ + { + "name": "tableId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/tables/{tableId}/validate-compatibility/{datasetId}": { + "post": { + "operationId": "StorageExplorerController_validateSchemaCompatibility", + "summary": "Validate schema compatibility between table and dataset", + "parameters": [ + { + "name": "tableId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "datasetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/datasets/{datasetId}/preview": { + "get": { + "operationId": "StorageExplorerController_getDatasetPreview", + "summary": "Get dataset preview data", + "parameters": [ + { + "name": "datasetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/datasets/{datasetId}/schema": { + "get": { + "operationId": "StorageExplorerController_getDatasetSchema", + "summary": "Get dataset schema information", + "parameters": [ + { + "name": "datasetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "force_refresh", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/datasets/upload/{uploadId}": { + "get": { + "operationId": "StorageExplorerController_listDatasetsByUpload", + "summary": "List all datasets for a specific upload", + "parameters": [ + { + "name": "uploadId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/datasets/{datasetId}/refresh-schema": { + "put": { + "operationId": "StorageExplorerController_refreshDatasetSchema", + "summary": "Refresh dataset schema with new parsing options (Excel)", + "parameters": [ + { + "name": "datasetId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/uploads/history": { + "get": { + "operationId": "StorageExplorerController_listFileExplorerUploads", + "summary": "List file explorer uploads with pagination", + "parameters": [ + { + "name": "page", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "limit", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "folder_path", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/browse": { + "get": { + "operationId": "StorageExplorerController_browseStorage", + "summary": "Browse folders and files in storage", + "parameters": [ + { + "name": "path", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/upload/batch": { + "post": { + "operationId": "StorageExplorerController_batchUpload", + "summary": "Upload multiple files to storage", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/folder/create": { + "post": { + "operationId": "StorageExplorerController_createFolder", + "summary": "Create a new folder in storage", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/download": { + "get": { + "operationId": "StorageExplorerController_downloadFile", + "summary": "Download a file from storage", + "parameters": [ + { + "name": "file_path", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/link-to-table": { + "post": { + "operationId": "StorageExplorerController_linkFileToTable", + "summary": "Link an existing file in storage to a table", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, + "/storage-explorer/storage/metadata": { + "get": { + "operationId": "StorageExplorerController_getFileMetadata", + "summary": "Get detailed file metadata", + "parameters": [ + { + "name": "file_path", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Storage Explorer" + ], + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ] + } + }, "/health": { "get": { "operationId": "HealthController_check", @@ -7798,6 +8593,104 @@ "accessToken" ] }, + "CreateApiKeyDto": { + "type": "object", + "properties": { + "permissions": { + "description": "Array of permission IDs", + "type": "array", + "items": { + "type": "number" + } + } + }, + "required": [ + "permissions" + ] + }, + "PermissionDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "CreateApiKeyResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key_mask": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionDto" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key_mask", + "permissions", + "created_at", + "created_by", + "key" + ] + }, + "ApiKeyBaseResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key_mask": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionDto" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "created_by": { + "type": "string" + } + }, + "required": [ + "id", + "key_mask", + "permissions", + "created_at", + "created_by" + ] + }, "CreateConnectionDto": { "type": "object", "properties": { @@ -8021,37 +8914,6 @@ "message" ] }, - "PermissionDto": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "seqid": { - "type": "number" - }, - "name": { - "type": "string" - }, - "claim": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "updatedAt": { - "type": "string" - } - }, - "required": [ - "id", - "seqid", - "name", - "claim", - "createdAt", - "updatedAt" - ] - }, "Customer": { "type": "object", "properties": { @@ -8169,7 +9031,7 @@ "companyName": { "type": "string" }, - "companySite": { + "personalSite": { "type": "string" }, "mfaStatus": { @@ -8192,6 +9054,9 @@ "items": { "type": "string" } + }, + "authProvider": { + "type": "string" } }, "required": [ @@ -8204,7 +9069,7 @@ "hierarchy", "bio", "companyName", - "companySite", + "personalSite", "mfaStatus", "createdAt", "updatedAt", @@ -8344,7 +9209,7 @@ "companyName": { "type": "string" }, - "companySite": { + "personalSite": { "type": "string" }, "customer": { @@ -8376,7 +9241,7 @@ "hierarchy", "bio", "companyName", - "companySite", + "personalSite", "mfaStatus", "createdAt", "updatedAt", @@ -8520,7 +9385,7 @@ "bio": { "type": "string" }, - "companySite": { + "personalSite": { "type": "string" }, "companyName": { @@ -8572,7 +9437,7 @@ "companyName": { "type": "string" }, - "companySite": { + "personalSite": { "type": "string" }, "mfaStatus": { @@ -8600,7 +9465,7 @@ "hierarchy", "bio", "companyName", - "companySite", + "personalSite", "mfaStatus", "createdAt", "updatedAt", @@ -10532,89 +11397,6 @@ "publicKey" ] }, - "CreateApiKeyDto": { - "type": "object", - "properties": { - "permissions": { - "description": "Array of permission IDs", - "type": "array", - "items": { - "type": "number" - } - } - }, - "required": [ - "permissions" - ] - }, - "CreateApiKeyResponseDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "key_mask": { - "type": "string" - }, - "permissions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionDto" - } - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by": { - "type": "string" - }, - "key": { - "type": "string" - } - }, - "required": [ - "id", - "key_mask", - "permissions", - "created_at", - "created_by", - "key" - ] - }, - "ApiKeyBaseResponseDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "key_mask": { - "type": "string" - }, - "permissions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionDto" - } - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "created_by": { - "type": "string" - } - }, - "required": [ - "id", - "key_mask", - "permissions", - "created_at", - "created_by" - ] - }, "CreateIdentityProvider": { "type": "object", "properties": { diff --git a/package-lock.json b/package-lock.json index 612a6b1..4d90eb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@aws-sdk/signature-v4": "^3.370.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.38.0-beta.19", + "@dadosfera/protospack-v2": "^3.38.0-beta.26", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", @@ -1744,10 +1744,9 @@ } }, "node_modules/@dadosfera/protospack-v2": { - "version": "3.38.0-beta.19", - "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.38.0-beta.19.tgz", - "integrity": "sha512-qevGunl1IKv4POa2hhXNaDpqlh/bvnq5Hxo6534OHYPZ//S+XcRZgTrxFnylK3+uoROM8epF3Mey5GwynxY7Tw==", - "license": "ISC", + "version": "3.38.0-beta.26", + "resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.38.0-beta.26.tgz", + "integrity": "sha512-N8NS7+djLGy0wJXk00+4oupqd/wBIQ1f+YBBhK2y9x4guFXYK1KWIrPZhPj+gaU8g2KNkqKoT7SnE9PXNxlLSQ==", "dependencies": { "@grpc/grpc-js": "^1.9.3", "rxjs": "^7.5.5" diff --git a/package.json b/package.json index 2a2c4b8..4fd10bd 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@aws-sdk/signature-v4": "^3.370.0", "@dadosfera/dadosfera-logs": "^1.0.0-beta.4", "@dadosfera/protospack": "2.5.3", - "@dadosfera/protospack-v2": "3.38.0-beta.19", + "@dadosfera/protospack-v2": "^3.38.0-beta.26", "@grpc/grpc-js": "^1.9.3", "@grpc/proto-loader": "^0.7.9", "@nestjs/cli": "^9.5.0", diff --git a/src/app.module.ts b/src/app.module.ts index 27d042c..0865ad2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -34,6 +34,7 @@ import { AssignModule } from './modules/assign/assign.module'; import { ShareMetadataModule } from './modules/share-metadata/share-metadata.module'; import { ApiKeyModule } from './modules/api-key/api-key.module'; import { PlatformApiModule } from './modules/platform-api/platform-api.module'; +import { StorageExplorerModule } from './modules/storage-explorer/storage-explorer.module'; @Module({ providers: [ @@ -75,6 +76,7 @@ import { PlatformApiModule } from './modules/platform-api/platform-api.module'; IdentityProviderModule, NetworkPolicyModule, PlatformApiModule, + StorageExplorerModule, //Always leave HealthModule last, so it is on the bottom of swagger HealthModule, ], diff --git a/src/authentication/permissions.enum.ts b/src/authentication/permissions.enum.ts index 3eeb2a7..ca5e9ac 100644 --- a/src/authentication/permissions.enum.ts +++ b/src/authentication/permissions.enum.ts @@ -668,6 +668,35 @@ export const PERMISSIONS_GROUPS = { }, }, }, + STORAGE_EXPLORER: { + title: { + 'pt-br': 'Storage Explorer', + 'en-us': 'Storage Explorer', + 'es-es': 'Storage Explorer', + }, + permissions: { + READ: { + seqid: 51, + claim: 'storage-explorer:read', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Ler dados do Storage Explorer', + 'en-us': 'Read Storage Explorer data', + 'es-es': 'Leer datos del Storage Explorer', + }, + }, + WRITE: { + seqid: 52, + claim: 'storage-explorer:write', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Escrever dados no Storage Explorer', + 'en-us': 'Write Storage Explorer data', + 'es-es': 'Escribir datos en Storage Explorer', + }, + }, + }, + }, }; export interface DadosferaModule { name: string; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3d86f14..7c3e073 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -55,6 +55,7 @@ import jwt, { JwtPayload } from 'jsonwebtoken'; import { LanguageEnum } from 'src/utils/languages.enum'; import { Language } from 'src/decorators/language.decorator'; import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator'; +import { ApiKeyService } from 'src/modules/api-key/api-key.service'; type CookiesValues = { accessToken?: string; @@ -74,6 +75,7 @@ export class AuthController { @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, private authClient: AuthClientService, + private apiKeyService: ApiKeyService, ) { this.logger = dadosferaLogger.logger; @@ -190,13 +192,14 @@ export class AuthController { ) { this.logger.info('/auth - change-password'); - const { oldPassword, newPassword } = body; + const { oldPassword, newPassword, totpCode } = body; const { authorization: accessToken } = headers; return this.authClient.changePassword({ accessToken, oldPassword, newPassword, + totpCode, }); } @@ -214,7 +217,8 @@ export class AuthController { const { username } = body; - return this.authClient.resetPassword({ username }, metadata); + await this.authClient.resetPassword({ username }, metadata); + return { authProvider: process.env.AUTH_PROVIDER || 'cognito' }; } @ApiInternalOnlyEndpoint() @@ -475,10 +479,29 @@ export class AuthController { async getMe(@Req() req: Request, @Res() res: Response) { this.logger.info('GET /auth/me ') + // Check for API key header first + const apiKey = req.get('X-Api-key'); + if (apiKey) { + this.logger.info('Authenticating via X-Api-key header'); + const { api_key } = await this.apiKeyService.get(apiKey); + + const userDto = { + id: api_key.user_id, + name: api_key.username, + customer: { + id: api_key.customer_id, + name: api_key.customer_name, + tier: api_key.customer_tier, + } + }; + + return res.status(200).json(userDto); + } + // Get token and headers const accessToken = req.cookies['ddf-auth']; - const refreshToken = req.cookies['ddf-refresh-auth']; - const userId = req.cookies['ddf-user-id']; + const refreshToken = req.cookies['ddf-refresh-auth']; + const userId = req.cookies['ddf-user-id']; const resourceHost = req.headers["host"] const hasUserSession = Boolean(accessToken) && Boolean(userId); diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 24e7623..47b59cc 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -8,10 +8,11 @@ import { AuthClientService } from './auth.service'; import { DucClient } from '../duc/client.config'; import { GoogleLoginStrategy } from './passport-strategies/google-strategy'; import { getOauthSecrets } from 'src/utils/OauthSecrets'; +import { ApiKeyModule } from '../api-key/api-key.module'; const client = new DucClient(); @Module({ - imports: [ClientsModule.register([client.providerOptions])], + imports: [ClientsModule.register([client.providerOptions]), ApiKeyModule], controllers: [AuthController], providers: [ AuthClientService, diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index c1b85f6..6d67b95 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -143,6 +143,7 @@ export class AuthClientService implements OnModuleInit { accessToken, oldPassword, newPassword, + totpCode, }: AuthChangePasswordRequest) { this.logger.info('ChangePassword'); @@ -151,6 +152,7 @@ export class AuthClientService implements OnModuleInit { accessToken, oldPassword, newPassword, + totpCode, }), ); } diff --git a/src/modules/customers/customers.controller.ts b/src/modules/customers/customers.controller.ts index 2606ebd..8ac5b3b 100644 --- a/src/modules/customers/customers.controller.ts +++ b/src/modules/customers/customers.controller.ts @@ -136,4 +136,32 @@ export class CustomersController { const result = await this.customersService.getAccessDashboardUrl(user.customer_name, metadata); return result; } + + @Get(':id/organization-info') + @Authenticated() + @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) + @ApiOkResponse({ description: 'Organization information' }) + async getOrganizationInfo(@Param('id') id: string) { + this.logger.info('getOrganizationInfo', { id }); + return this.customersService.getOrganizationInfo(id); + } + + @Put(':id/organization-info') + @Authenticated() + @RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN) + @HttpCode(HttpStatus.OK) + @ApiOkResponse({ description: 'Organization information updated' }) + async updateOrganizationInfo( + @Param('id') id: string, + @Body() body: { + companyName: string; + companySite: string; + domain: string; + cnpj: string; + description: string; + }, + ) { + return this.customersService.updateOrganizationInfo(id, body); + } + } diff --git a/src/modules/customers/customers.service.ts b/src/modules/customers/customers.service.ts index 8b28171..602943b 100644 --- a/src/modules/customers/customers.service.ts +++ b/src/modules/customers/customers.service.ts @@ -223,4 +223,57 @@ export class CustomersService implements OnModuleInit { }) ) } -} + + async updateOrganizationInfo( + customerId: string, + data: { + companyName: string; + companySite: string; + domain: string; + cnpj: string; + description: string; + }, + ) { + try { + const result = await lastValueFrom( + this.customerService.OrganizationUpdate({ + customerId, + companyName: data.companyName || '', + companySite: data.companySite || '', + domain: data.domain || '', + cnpj: data.cnpj || '', + description: data.description || '', + }), + ); + + return result; + } catch (err) { + if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) + throw new HttpException(err.details, HttpStatus.NOT_FOUND); + else throw err; + } + } + + async getOrganizationInfo(customerId: string) { + try { + const customerResponse = await lastValueFrom( + this.customerService.CustomerFindOneById({ id: customerId }) + ); + + const customer = customerResponse.customer; + + return { + companyName: customer.companyName || '', + companySite: customer.companySite || '', + domain: customer.domain || '', + cnpj: customer.cnpj || '', + description: customer.description || '' + }; + } catch (err) { + if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) + throw new HttpException(err.details, HttpStatus.NOT_FOUND); + else throw err; + } + } + +} \ No newline at end of file diff --git a/src/modules/customers/dtos/organization.ts b/src/modules/customers/dtos/organization.ts new file mode 100644 index 0000000..50c3df7 --- /dev/null +++ b/src/modules/customers/dtos/organization.ts @@ -0,0 +1,27 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class OrganizationUpdateRequest { + @ApiProperty() + name: string; + @ApiPropertyOptional() + companySite: string; + @ApiProperty() + domain: string; + @ApiPropertyOptional() + info: string; + @ApiPropertyOptional() + cnpj: string; +} + +export class OrganizationResponse { + @ApiProperty() + name: string; + @ApiPropertyOptional() + companySite: string; + @ApiProperty() + domain: string; + @ApiPropertyOptional() + info: string; + @ApiPropertyOptional() + cnpj: string; +} \ No newline at end of file diff --git a/src/modules/storage-explorer/storage-explorer.config.ts b/src/modules/storage-explorer/storage-explorer.config.ts new file mode 100644 index 0000000..417eb15 --- /dev/null +++ b/src/modules/storage-explorer/storage-explorer.config.ts @@ -0,0 +1,13 @@ +export const STORAGE_EXPLORER_CONFIG = { + getUrl: (customerId: string): string => { + const urlTemplate = process.env.STORAGE_EXPLORER_API_URL; + if (!urlTemplate) { + throw new Error('STORAGE_EXPLORER_API_URL environment variable is not set'); + } + // Replace {customer_id} placeholder with actual customer ID + // For local: http://172.17.0.1:8000/api (no placeholder) + // For prod: https://storage-explorer-{customer_id}.stg.dadosfera.ai/api + return urlTemplate.replace('{customer_id}', customerId); + }, + timeout: parseInt(process.env.STORAGE_EXPLORER_TIMEOUT || '30000', 10), +}; diff --git a/src/modules/storage-explorer/storage-explorer.controller.ts b/src/modules/storage-explorer/storage-explorer.controller.ts new file mode 100644 index 0000000..ef6a071 --- /dev/null +++ b/src/modules/storage-explorer/storage-explorer.controller.ts @@ -0,0 +1,350 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + Inject, + UseInterceptors, + UploadedFiles, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; +import FormData from 'form-data'; + +import { + Authenticated, + RequireAllPermissions, +} from '../../decorators/authentication.decorator'; +import { User, RequestUser } from '../../decorators/user.decorator'; +import { StorageExplorerService } from './storage-explorer.service'; +import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum'; + +@ApiTags('Storage Explorer') +@Controller('storage-explorer') +@Authenticated() +export class StorageExplorerController { + private logger: any; + + constructor( + private readonly storageExplorerService: StorageExplorerService, + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + // ============================================ + // TABLE OPERATIONS + // ============================================ + + @ApiOperation({ summary: 'Validate table name in PostgreSQL and Snowflake' }) + @Post('tables/validate-name') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async validateTableName( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'POST', + '/tables/validate-name', + user, + body, + ); + } + + @ApiOperation({ summary: 'Create a new table' }) + @Post('tables') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async createTable( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'POST', + '/tables/', + user, + body, + ); + } + + @ApiOperation({ summary: 'List all tables with pagination' }) + @Get('tables') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async listTables( + @Query('page') page: number, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + '/tables/', + user, + undefined, + { page }, + ); + } + + @ApiOperation({ summary: 'Get table details by ID' }) + @Get('tables/:tableId') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getTable( + @Param('tableId') tableId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/tables/${tableId}`, + user, + ); + } + + @ApiOperation({ summary: 'Link a dataset to a table' }) + @Post('tables/:tableId/datasets/:datasetId') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async linkDatasetToTable( + @Param('tableId') tableId: string, + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'POST', + `/tables/${tableId}/datasets/${datasetId}`, + user, + ); + } + + @ApiOperation({ summary: 'Get all datasets linked to a table' }) + @Get('tables/:tableId/datasets') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getTableDatasets( + @Param('tableId') tableId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/tables/${tableId}/datasets`, + user, + ); + } + + @ApiOperation({ summary: 'Get table schema' }) + @Get('tables/:tableId/schema') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getTableSchema( + @Param('tableId') tableId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/tables/${tableId}/schema`, + user, + ); + } + + @ApiOperation({ summary: 'Validate schema compatibility between table and dataset' }) + @Post('tables/:tableId/validate-compatibility/:datasetId') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async validateSchemaCompatibility( + @Param('tableId') tableId: string, + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'POST', + `/tables/${tableId}/validate-compatibility/${datasetId}`, + user, + ); + } + + // ============================================ + // DATASET OPERATIONS + // ============================================ + + @ApiOperation({ summary: 'Get dataset preview data' }) + @Get('datasets/:datasetId/preview') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getDatasetPreview( + @Param('datasetId') datasetId: string, + @Query('limit') limit: number, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/datasets/${datasetId}/preview`, + user, + undefined, + { limit }, + ); + } + + @ApiOperation({ summary: 'Get dataset schema information' }) + @Get('datasets/:datasetId/schema') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getDatasetSchema( + @Param('datasetId') datasetId: string, + @Query('force_refresh') forceRefresh: boolean, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/datasets/${datasetId}/schema`, + user, + undefined, + { force_refresh: forceRefresh }, + ); + } + + @ApiOperation({ summary: 'List all datasets for a specific upload' }) + @Get('datasets/upload/:uploadId') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async listDatasetsByUpload( + @Param('uploadId') uploadId: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + `/datasets/upload/${uploadId}`, + user, + ); + } + + @ApiOperation({ summary: 'Refresh dataset schema with new parsing options (Excel)' }) + @Put('datasets/:datasetId/refresh-schema') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async refreshDatasetSchema( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'PUT', + `/datasets/${datasetId}/refresh-schema`, + user, + body, + ); + } + + // ============================================ + // STORAGE OPERATIONS + // ============================================ + + @ApiOperation({ summary: 'List file explorer uploads with pagination' }) + @Get('storage/uploads/history') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async listFileExplorerUploads( + @Query('page') page: number, + @Query('limit') limit: number, + @Query('folder_path') folderPath: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + '/storage/uploads/history', + user, + undefined, + { page, limit, folder_path: folderPath }, + ); + } + + @ApiOperation({ summary: 'Browse folders and files in storage' }) + @Get('storage/browse') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async browseStorage( + @Query('path') path: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + '/storage/browse', + user, + undefined, + { path }, + ); + } + + @ApiOperation({ summary: 'Upload multiple files to storage' }) + @Post('storage/upload/batch') + @ApiConsumes('multipart/form-data') + @UseInterceptors(FilesInterceptor('files')) + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async batchUpload( + @UploadedFiles() files: Array, + @Body('folder_path') folderPath: string, + @User() user: RequestUser, + ) { + // Create FormData to forward files to storage-explorer API + const formData = new FormData(); + + // Add files + if (files && files.length > 0) { + files.forEach((file) => { + formData.append('files', file.buffer, { + filename: file.originalname, + contentType: file.mimetype, + }); + }); + } + + // Add folder_path + if (folderPath) { + formData.append('folder_path', folderPath); + } + + return this.storageExplorerService.proxyFormData( + 'POST', + '/storage/upload/batch', + user, + formData, + ); + } + + @ApiOperation({ summary: 'Create a new folder in storage' }) + @Post('storage/folder/create') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.WRITE) + async createFolder( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'POST', + '/storage/folder/create', + user, + body, + ); + } + + @ApiOperation({ summary: 'Download a file from storage' }) + @Get('storage/download') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async downloadFile( + @Query('file_path') filePath: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + '/storage/download', + user, + undefined, + { file_path: filePath }, + ); + } + + @ApiOperation({ summary: 'Get detailed file metadata' }) + @Get('storage/metadata') + @RequireAllPermissions(PERMISSIONS_GROUPS.STORAGE_EXPLORER.permissions.READ) + async getFileMetadata( + @Query('file_path') filePath: string, + @User() user: RequestUser, + ) { + return this.storageExplorerService.proxy( + 'GET', + '/storage/metadata', + user, + undefined, + { file_path: filePath }, + ); + } +} diff --git a/src/modules/storage-explorer/storage-explorer.module.ts b/src/modules/storage-explorer/storage-explorer.module.ts new file mode 100644 index 0000000..b0f0e86 --- /dev/null +++ b/src/modules/storage-explorer/storage-explorer.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { StorageExplorerController } from './storage-explorer.controller'; +import { StorageExplorerService } from './storage-explorer.service'; + +@Module({ + imports: [], + controllers: [StorageExplorerController], + providers: [StorageExplorerService, DadosferaLogger], + exports: [StorageExplorerService], +}) +export class StorageExplorerModule {} diff --git a/src/modules/storage-explorer/storage-explorer.service.ts b/src/modules/storage-explorer/storage-explorer.service.ts new file mode 100644 index 0000000..ba149c0 --- /dev/null +++ b/src/modules/storage-explorer/storage-explorer.service.ts @@ -0,0 +1,178 @@ +import { Injectable, Inject, HttpException } from '@nestjs/common'; +import axios, { AxiosResponse, Method } from 'axios'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { RequestUser } from '../../decorators/user.decorator'; +import { STORAGE_EXPLORER_CONFIG } from './storage-explorer.config'; + +@Injectable() +export class StorageExplorerService { + private logger: any; + + constructor( + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + async proxy( + method: string, + path: string, + user: RequestUser, + body?: any, + query?: Record, + ): Promise { + // Validate customer_id is present for multi-tenant isolation + if (!user.customer_id) { + throw new HttpException('Customer ID is required for storage operations', 400); + } + + // Get customer-specific storage-explorer URL + const baseUrl = STORAGE_EXPLORER_CONFIG.getUrl(user.customer_id); + const url = new URL(`${baseUrl}${path}`); + + // Add query params + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + 'content-type': 'application/json', + }; + + this.logger.info('Proxying request to storage-explorer', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + storage_url: baseUrl, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: body, + timeout: STORAGE_EXPLORER_CONFIG.timeout, + validateStatus: () => true, // Don't throw on non-2xx + }); + + // Propagate non-2xx responses as HttpExceptions + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Storage Explorer API proxy error', { + error: error.message, + status: error.response?.status, + path, + storage_url: baseUrl, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + if (error.code === 'ECONNREFUSED') { + throw new HttpException('Storage Explorer API service unavailable', 503); + } + + if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') { + throw new HttpException('Storage Explorer API request timeout', 504); + } + + throw new HttpException('Internal server error', 500); + } + } + + /** + * Proxy with file upload support (multipart/form-data) + */ + async proxyFormData( + method: string, + path: string, + user: RequestUser, + formData: any, + query?: Record, + ): Promise { + // Validate customer_id is present for multi-tenant isolation + if (!user.customer_id) { + throw new HttpException('Customer ID is required for storage operations', 400); + } + + // Get customer-specific storage-explorer URL + const baseUrl = STORAGE_EXPLORER_CONFIG.getUrl(user.customer_id); + const url = new URL(`${baseUrl}${path}`); + + // Add query params + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + // Let axios set Content-Type for multipart/form-data with boundary + ...formData.getHeaders?.(), + }; + + this.logger.info('Proxying form data request to storage-explorer', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + storage_url: baseUrl, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: formData, + timeout: STORAGE_EXPLORER_CONFIG.timeout, + maxContentLength: Infinity, + maxBodyLength: Infinity, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Storage Explorer API form data proxy error', { + error: error.message, + status: error.response?.status, + path, + storage_url: baseUrl, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + throw new HttpException('Internal server error', 500); + } + } +} diff --git a/src/modules/users/dtos/entities.ts b/src/modules/users/dtos/entities.ts index 0f0e5a3..eef5ca0 100644 --- a/src/modules/users/dtos/entities.ts +++ b/src/modules/users/dtos/entities.ts @@ -43,7 +43,7 @@ export class User { @ApiProperty() companyName?: string; @ApiProperty() - companySite?: string; + personalSite?: string; @ApiPropertyOptional() customer?: Customer; @ApiProperty() @@ -64,6 +64,8 @@ export class UserNoRolesAndCustomer extends OmitType(UserNoRoles, [ export class IUserByCustomer extends OmitType(User, ['customer']) { @ApiPropertyOptional() permissions?: string[]; + @ApiPropertyOptional() + authProvider?: string; } export class CreateUserReq { @@ -117,7 +119,7 @@ export class UpdateUserReq { @ApiPropertyOptional() bio?: string; @ApiPropertyOptional() - companySite?: string; + personalSite?: string; @ApiPropertyOptional() companyName?: string; @ApiPropertyOptional() diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 278188c..29ec0e3 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -125,6 +125,7 @@ export class UsersService implements OnModuleInit { return { permissions }; }); res.user.permissions = permissions; + res.user.authProvider = process.env.AUTH_PROVIDER || 'cognito'; return res; } @@ -162,7 +163,7 @@ export class UsersService implements OnModuleInit { name: updateUserDTO.name, bio: updateUserDTO.bio, companyName: updateUserDTO.companyName, - companySite: updateUserDTO.companySite, + personalSite: updateUserDTO.personalSite, customerId, id, metabaseUserId: undefined,