mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-01 20:28:17 +00:00
Compare commits
@@ -13,11 +13,6 @@ on:
|
|||||||
options:
|
options:
|
||||||
- stg
|
- stg
|
||||||
- prd
|
- prd
|
||||||
push_to_dockerhub:
|
|
||||||
description: "Push image to Dockerhub?"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
extract_environment:
|
extract_environment:
|
||||||
@@ -118,23 +113,6 @@ jobs:
|
|||||||
docker compose -f build.docker-compose.yml build
|
docker compose -f build.docker-compose.yml build
|
||||||
docker compose -f build.docker-compose.yml push
|
docker compose -f build.docker-compose.yml push
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
if: ${{inputs.push_to_dockerhub}}
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: dadosfera
|
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build, Tag, and Push Image to Dockerhub
|
|
||||||
if: ${{inputs.push_to_dockerhub}}
|
|
||||||
env:
|
|
||||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
|
||||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
|
||||||
ACCOUNT_ID: ${{ steps.aws.outputs.aws-account-id }}
|
|
||||||
run: |
|
|
||||||
docker compose -f build.docker-compose.dockerhub.yml build
|
|
||||||
docker compose -f build.docker-compose.dockerhub.yml push
|
|
||||||
|
|
||||||
# - name: Create ZIP file to Deploy AWS Beanstalk
|
# - name: Create ZIP file to Deploy AWS Beanstalk
|
||||||
# env:
|
# env:
|
||||||
# ENV: ${{ needs.extract_environment.outputs.environment }}
|
# ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||||
@@ -164,54 +142,11 @@ jobs:
|
|||||||
docker system prune --volumes -a -f
|
docker system prune --volumes -a -f
|
||||||
docker system df
|
docker system df
|
||||||
|
|
||||||
helmfile-deploy:
|
k8s-deploy:
|
||||||
needs: [extract_environment, semantic_release, build_ecr_image]
|
needs: [extract_environment, semantic_release, build_ecr_image]
|
||||||
runs-on: [self-hosted, "prd-azure"]
|
uses: ./.github/workflows/k8s-deploy.yml
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Helm
|
|
||||||
uses: azure/setup-helm@v1
|
|
||||||
with:
|
with:
|
||||||
version: 'v3.9.0'
|
cloud: 'oracle'
|
||||||
|
environment: ${{ needs.extract_environment.outputs.environment }}
|
||||||
- name: Install Azure ClI
|
image: ${{ needs.semantic_release.outputs.new_release_version }}
|
||||||
run: |
|
secrets: inherit
|
||||||
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 }}
|
|
||||||
IMAGE_TAG: ${{ needs.semantic_release.outputs.new_release_version }}
|
|
||||||
run: helmfile -f helmfiles/${ENV}.yaml sync --set image.tag=$IMAGE_TAG
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
name : K8s deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
cloud:
|
||||||
|
description: "Cloud provider for the deployment"
|
||||||
|
required: true
|
||||||
|
default: "azure"
|
||||||
|
type: string
|
||||||
|
environment:
|
||||||
|
description: "Deployment environment"
|
||||||
|
required: true
|
||||||
|
default: "prd"
|
||||||
|
type: string
|
||||||
|
image:
|
||||||
|
description: "Image Tag"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
azure:
|
||||||
|
if: inputs.cloud == 'azure'
|
||||||
|
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: Authenticate with cluster
|
||||||
|
env:
|
||||||
|
CLUSTER_NAME: platform-${{ inputs.environment }}
|
||||||
|
run: az aks get-credentials --resource-group dadosfera-prd --name ${CLUSTER_NAME} --overwrite-existing
|
||||||
|
|
||||||
|
- name: Setup kubectl
|
||||||
|
uses: azure/setup-kubectl@v1
|
||||||
|
with:
|
||||||
|
version: 'v1.30.1'
|
||||||
|
|
||||||
|
- 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: Run Helmfile Apply
|
||||||
|
env:
|
||||||
|
ENV: ${{ inputs.environment }}
|
||||||
|
IMAGE_TAG: ${{ inputs.image }}
|
||||||
|
run: helmfile -f deploy/helmfiles/${ENV}.yaml sync --set image.tag=$IMAGE_TAG
|
||||||
|
|
||||||
|
oracle:
|
||||||
|
if: inputs.cloud == 'oracle'
|
||||||
|
runs-on: [self-hosted, "prd-oracle"]
|
||||||
|
env:
|
||||||
|
HOME: /home/runner
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v1
|
||||||
|
with:
|
||||||
|
version: 'v3.9.0'
|
||||||
|
|
||||||
|
- name: Install OCI CLI
|
||||||
|
env:
|
||||||
|
HOME: /home/runner
|
||||||
|
run: |
|
||||||
|
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" -- --accept-all-defaults
|
||||||
|
echo "$HOME/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Configure OCI CLI
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.oci || true
|
||||||
|
echo "${{ secrets.OCI_CONFIG }}" > ~/.oci/config
|
||||||
|
echo "${{ secrets.OCI_PRIVATE_KEY }}" > ~/.oci/oci_api_key.pem
|
||||||
|
chmod 600 ~/.oci/oci_api_key.pem
|
||||||
|
|
||||||
|
- 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
|
||||||
|
sudo mv helmfile /usr/local/bin/
|
||||||
|
helmfile --version
|
||||||
|
|
||||||
|
- name: Install Helm Diff Plugin
|
||||||
|
run: helm plugin install https://github.com/databus23/helm-diff || true
|
||||||
|
|
||||||
|
- name: Authenticate with OKE cluster
|
||||||
|
env:
|
||||||
|
ENV: ${{ inputs.environment }}
|
||||||
|
STG_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaagh3jvln52a3ebm3dodx6emmhv5bmfs7i7sv2k4zkbcbrzcl6v37q"
|
||||||
|
PRD_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaanf3vptl6hc2tzd4enfd2hfpsht3wikxww5xejc3l7cwfm6l3sndq"
|
||||||
|
run: |
|
||||||
|
if [ "$ENV" = "stg" ]; then
|
||||||
|
CLUSTER_ID=$STG_CLUSTER_ID
|
||||||
|
elif [ "$ENV" = "prd" ]; then
|
||||||
|
CLUSTER_ID=$PRD_CLUSTER_ID
|
||||||
|
else
|
||||||
|
echo "Unknown environment: $ENV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
oci ce cluster create-kubeconfig --cluster-id ${CLUSTER_ID} --file $HOME/.kube/config --region sa-saopaulo-1 --token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT
|
||||||
|
|
||||||
|
- name: Run Helmfile Apply
|
||||||
|
env:
|
||||||
|
ENV: ${{ inputs.environment }}
|
||||||
|
IMAGE_TAG: ${{ inputs.image }}
|
||||||
|
run: helmfile -f deploy/helmfiles/${ENV}.yaml sync --set image.tag=$IMAGE_TAG
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- stg
|
- beta
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
extract_environment:
|
extract_environment:
|
||||||
@@ -21,17 +21,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
id: extract_environment
|
id: extract_environment
|
||||||
|
|
||||||
helmfile-deploy:
|
helmfile-check:
|
||||||
needs: [extract_environment]
|
|
||||||
runs-on: [self-hosted, "prd-azure"]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Summary
|
|
||||||
env:
|
env:
|
||||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
HOME: /home/runner
|
||||||
run: |
|
needs: [extract_environment]
|
||||||
echo "### :rocket: Deploy da branch \`$GITHUB_REF_NAME\` para o environment ($ENV)" >> $GITHUB_STEP_SUMMARY
|
environment: ${{ needs.extract_environment.outputs.environment }}
|
||||||
|
runs-on: [self-hosted, "prd-oracle"]
|
||||||
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -40,13 +36,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 'v3.9.0'
|
version: 'v3.9.0'
|
||||||
|
|
||||||
- name: Install Azure ClI
|
- name: Determine DNS_HOST based on environment
|
||||||
|
id: set_dns
|
||||||
|
env:
|
||||||
|
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||||
run: |
|
run: |
|
||||||
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
|
if [ "$ENV" = "prd" ]; then
|
||||||
|
echo "dns_host=dadosfera.ai" >> $GITHUB_OUTPUT
|
||||||
|
elif [ "$ENV" = "stg" ]; then
|
||||||
|
echo "dns_host=stg.dadosfera.ai" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: azure/login@v2
|
- name: Install OCI CLI
|
||||||
with:
|
run: |
|
||||||
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
|
bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)" -- --accept-all-defaults
|
||||||
|
echo "$HOME/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Configure OCI CLI
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.oci || true
|
||||||
|
echo "${{ secrets.OCI_CONFIG }}" > ~/.oci/config
|
||||||
|
echo "${{ secrets.OCI_PRIVATE_KEY }}" > ~/.oci/oci_api_key.pem
|
||||||
|
chmod 600 ~/.oci/oci_api_key.pem
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -57,23 +68,35 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
wget https://github.com/helmfile/helmfile/releases/download/v0.148.0/helmfile_0.148.0_linux_amd64.tar.gz
|
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
|
tar -xzf helmfile_0.148.0_linux_amd64.tar.gz
|
||||||
mv helmfile /usr/local/bin/
|
sudo mv helmfile /usr/local/bin/
|
||||||
helmfile --version
|
helmfile --version
|
||||||
|
|
||||||
- name: Install Helm Diff Plugin
|
- name: Install Helm Diff Plugin
|
||||||
run: helm plugin install https://github.com/databus23/helm-diff || true
|
run: helm plugin install https://github.com/databus23/helm-diff || true
|
||||||
|
|
||||||
|
- name: Authenticate with OKE cluster
|
||||||
|
env:
|
||||||
|
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||||
|
STG_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaagh3jvln52a3ebm3dodx6emmhv5bmfs7i7sv2k4zkbcbrzcl6v37q"
|
||||||
|
PRD_CLUSTER_ID: "ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaanf3vptl6hc2tzd4enfd2hfpsht3wikxww5xejc3l7cwfm6l3sndq"
|
||||||
|
run: |
|
||||||
|
if [ "$ENV" = "stg" ]; then
|
||||||
|
CLUSTER_ID=$STG_CLUSTER_ID
|
||||||
|
elif [ "$ENV" = "prd" ]; then
|
||||||
|
CLUSTER_ID=$PRD_CLUSTER_ID
|
||||||
|
else
|
||||||
|
echo "Unknown environment: $ENV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
oci ce cluster create-kubeconfig --cluster-id ${CLUSTER_ID} --file $HOME/.kube/config --region sa-saopaulo-1 --token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT
|
||||||
|
|
||||||
- name: Setup kubectl
|
- name: Setup kubectl
|
||||||
uses: azure/setup-kubectl@v1
|
uses: azure/setup-kubectl@v1
|
||||||
with:
|
with:
|
||||||
version: 'v1.30.1'
|
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 Diff
|
- name: Run Helmfile Diff
|
||||||
env:
|
env:
|
||||||
ENV: ${{ needs.extract_environment.outputs.environment }}
|
ENV: ${{ needs.extract_environment.outputs.environment }}
|
||||||
run: helmfile -f helmfiles/${ENV}.yaml diff
|
run: helmfile -f deploy/helmfiles/${ENV}.yaml diff
|
||||||
|
|||||||
+24
-1
@@ -4,9 +4,19 @@ FROM base_image AS build_base
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk update
|
RUN apk update
|
||||||
# needed packages to build dependencies from source
|
# needed packages to build dependencies from source
|
||||||
RUN apk add --no-cache aws-cli
|
RUN apk add --no-cache \
|
||||||
|
aws-cli \
|
||||||
|
chromium \
|
||||||
|
nss \
|
||||||
|
freetype \
|
||||||
|
harfbuzz \
|
||||||
|
ca-certificates \
|
||||||
|
ttf-freefont
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||||
|
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
|
|
||||||
# run aws cli without mounting secret, because CI already has AWS credentials
|
# run aws cli without mounting secret, because CI already has AWS credentials
|
||||||
FROM build_base AS ci_image
|
FROM build_base AS ci_image
|
||||||
@@ -40,4 +50,17 @@ WORKDIR /app
|
|||||||
COPY --from=prod_build /app/dist ./dist
|
COPY --from=prod_build /app/dist ./dist
|
||||||
COPY --from=prod_build /app/node_modules ./node_modules
|
COPY --from=prod_build /app/node_modules ./node_modules
|
||||||
COPY --from=prod_build /app/package*.json ./
|
COPY --from=prod_build /app/package*.json ./
|
||||||
|
RUN apk update
|
||||||
|
# needed packages to build dependencies from source
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
chromium \
|
||||||
|
nss \
|
||||||
|
freetype \
|
||||||
|
harfbuzz \
|
||||||
|
ca-certificates \
|
||||||
|
ttf-freefont
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||||
|
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
ENTRYPOINT npm run start:prod
|
ENTRYPOINT npm run start:prod
|
||||||
|
|||||||
Binary file not shown.
@@ -1,16 +1,16 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: maestro
|
name: {{ .Values.app_name }}
|
||||||
namespace: applications
|
namespace: applications
|
||||||
labels:
|
labels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.replicaCount }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
rollingUpdate:
|
rollingUpdate:
|
||||||
@@ -20,22 +20,18 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
- name: {{ .Values.imagePullSecrets }}
|
- name: {{ .Values.imagePullSecrets }}
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
"beta.kubernetes.io/os": linux
|
"beta.kubernetes.io/os": linux
|
||||||
|
{{- if .Values.affinity }}
|
||||||
affinity:
|
affinity:
|
||||||
nodeAffinity:
|
{{- toYaml .Values.affinity | nindent 8 }}
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
{{- end }}
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: application
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- backend
|
|
||||||
tolerations:
|
tolerations:
|
||||||
- key: "kubernetes.azure.com/scalesetpriority"
|
- key: "kubernetes.azure.com/scalesetpriority"
|
||||||
operator: "Equal"
|
operator: "Equal"
|
||||||
@@ -47,13 +43,10 @@ spec:
|
|||||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.containerPort }}
|
- containerPort: {{ .Values.containerPort }}
|
||||||
|
{{- if .Values.resources }}
|
||||||
resources:
|
resources:
|
||||||
requests:
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
cpu: {{ .Values.resources.requests.cpu }}
|
{{- end }}
|
||||||
memory: {{ .Values.resources.requests.memory }}
|
|
||||||
limits:
|
|
||||||
cpu: {{ .Values.resources.limits.cpu }}
|
|
||||||
memory: {{ .Values.resources.limits.memory }}
|
|
||||||
env:
|
env:
|
||||||
- name: AWS_IDENTITY_POOL_ID
|
- name: AWS_IDENTITY_POOL_ID
|
||||||
value: {{ .Values.maestro.aws_identity_pool_id }}
|
value: {{ .Values.maestro.aws_identity_pool_id }}
|
||||||
@@ -81,6 +74,8 @@ spec:
|
|||||||
value: "logstash-pipelines.dadosfera.ai"
|
value: "logstash-pipelines.dadosfera.ai"
|
||||||
- name: LOGGER_GELF_PORT
|
- name: LOGGER_GELF_PORT
|
||||||
value: "{{ .Values.maestro.logger_gelf_port }}"
|
value: "{{ .Values.maestro.logger_gelf_port }}"
|
||||||
|
- name: LOGGER_CONSOLE_EXTRA
|
||||||
|
value: "true"
|
||||||
- name: NIMBUS_BASE_URL
|
- name: NIMBUS_BASE_URL
|
||||||
value: "http://nimbus-api"
|
value: "http://nimbus-api"
|
||||||
- name: NPM_TOKEN
|
- name: NPM_TOKEN
|
||||||
@@ -95,6 +90,20 @@ spec:
|
|||||||
value: {{ .Values.maestro.tr_factory_url }}
|
value: {{ .Values.maestro.tr_factory_url }}
|
||||||
- name: UPLOAD_FILE_AGENT_CONNECTION
|
- name: UPLOAD_FILE_AGENT_CONNECTION
|
||||||
value: {{ .Values.maestro.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: DEDICATED_PROXY
|
||||||
|
value: {{ .Values.maestro.dedicated_proxy }}
|
||||||
|
- name: COOKIE_SECRET
|
||||||
|
value: {{ .Values.maestro.cookie_secret }}
|
||||||
|
- name: REDIS_DATABASE
|
||||||
|
value: "{{ .Values.maestro.redis_database }}"
|
||||||
|
- name: REDIS_HOST
|
||||||
|
value: {{ .Values.maestro.redis_host }}
|
||||||
|
- name: REDIS_PORT
|
||||||
|
value: "{{ .Values.maestro.redis_port }}"
|
||||||
- name: JWT_PRIVATE_KEY
|
- name: JWT_PRIVATE_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -103,15 +112,15 @@ spec:
|
|||||||
- name: AWS_ACCESS_KEY_ID
|
- name: AWS_ACCESS_KEY_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: prd-maestro
|
name: prd-{{ .Values.app_name }}
|
||||||
key: AWS_ACCESS_KEY_ID
|
key: AWS_ACCESS_KEY_ID
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
- name: AWS_SECRET_ACCESS_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: prd-maestro
|
name: prd-{{ .Values.app_name }}
|
||||||
key: AWS_SECRET_ACCESS_KEY
|
key: AWS_SECRET_ACCESS_KEY
|
||||||
- name: AWS_DEFAULT_REGION
|
- name: AWS_DEFAULT_REGION
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: prd-maestro
|
name: prd-{{ .Values.app_name }}
|
||||||
key: AWS_DEFAULT_REGION
|
key: AWS_DEFAULT_REGION
|
||||||
@@ -2,6 +2,7 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/whitelist-source-range: "69.49.241.121/32" # hostgator ip
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
nginx.ingress.kubernetes.io/server-snippet: |
|
nginx.ingress.kubernetes.io/server-snippet: |
|
||||||
underscores_in_headers on;
|
underscores_in_headers on;
|
||||||
@@ -9,8 +10,12 @@ metadata:
|
|||||||
|
|
||||||
generation: 1
|
generation: 1
|
||||||
labels:
|
labels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
name: maestro
|
{{- if .Values.maestro.dedicated_proxy}}
|
||||||
|
name: open-data-{{ .Values.app_name }}
|
||||||
|
{{- else }}
|
||||||
|
name: open-data
|
||||||
|
{{- end }}
|
||||||
namespace: applications
|
namespace: applications
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
@@ -20,8 +25,8 @@ spec:
|
|||||||
paths:
|
paths:
|
||||||
- backend:
|
- backend:
|
||||||
service:
|
service:
|
||||||
name: maestro
|
name: {{ .Values.app_name }}
|
||||||
port:
|
port:
|
||||||
number: {{ .Values.ingress.port }}
|
number: {{ .Values.ingress.port }}
|
||||||
path: /
|
path: /open-data/sharing-ocean-data
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||||
|
nginx.ingress.kubernetes.io/server-snippet: |
|
||||||
|
underscores_in_headers on;
|
||||||
|
ignore_invalid_headers on;
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "64k"
|
||||||
|
{{- if .Values.maestro.restricted_ip}}
|
||||||
|
nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.maestro.restricted_ip }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
generation: 1
|
||||||
|
labels:
|
||||||
|
app: {{ .Values.app_name }}
|
||||||
|
name: {{ .Values.app_name }}
|
||||||
|
namespace: applications
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.hostname }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: {{ .Values.app_name }}
|
||||||
|
port:
|
||||||
|
number: {{ .Values.ingress.port }}
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
apiVersion: external-secrets.io/v1beta1
|
apiVersion: external-secrets.io/v1beta1
|
||||||
kind: ExternalSecret
|
kind: ExternalSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: prd-maestro
|
name: prd-{{ .Values.app_name }}
|
||||||
namespace: applications
|
namespace: applications
|
||||||
labels:
|
labels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
spec:
|
spec:
|
||||||
refreshInterval: 1h
|
refreshInterval: 1h
|
||||||
secretStoreRef:
|
secretStoreRef:
|
||||||
name: secretsmanager-prd
|
name: secretsmanager-prd
|
||||||
kind: SecretStore
|
kind: SecretStore
|
||||||
target:
|
target:
|
||||||
name: prd-maestro
|
name: prd-{{ .Values.app_name }}
|
||||||
creationPolicy: Owner
|
creationPolicy: Owner
|
||||||
data:
|
data:
|
||||||
- secretKey: AWS_ACCESS_KEY_ID
|
- secretKey: AWS_ACCESS_KEY_ID
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: maestro
|
name: {{ .Values.app_name }}
|
||||||
namespace: applications
|
namespace: applications
|
||||||
labels:
|
labels:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
ports:
|
ports:
|
||||||
- name: maestro
|
- name: {{ .Values.app_name }}
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
port: {{ .Values.service.port }}
|
port: {{ .Values.service.port }}
|
||||||
targetPort: {{ .Values.service.targetPort }}
|
targetPort: {{ .Values.service.targetPort }}
|
||||||
selector:
|
selector:
|
||||||
app: maestro
|
app: {{ .Values.app_name }}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
maestro:
|
||||||
|
env: stg
|
||||||
|
duc_url: duc.stg.dadosfera.ai
|
||||||
|
pi_factory_url: pi-factory.stg.dadosfera.ai
|
||||||
|
in_factory_url: in-factory.stg.dadosfera.ai
|
||||||
|
tr_factory_url: in-factory.stg.dadosfera.ai
|
||||||
|
open_customer_id: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||||
|
open_group_id: e3f98a2f-7748-4981-8505-7695c8ca8218
|
||||||
|
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
|
||||||
|
redis_database: "1"
|
||||||
|
|
||||||
|
hostname: maestro.stg.dadosfera.ai
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
affinity: null
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
# Declare variables to be passed into your templates.
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
replicaCount: 3
|
replicaCount: 3
|
||||||
hostname: maestro-temp.dadosfera.ai
|
hostname: maestro.dadosfera.ai
|
||||||
image:
|
image:
|
||||||
repository: 611330257153.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_prd
|
repository: 611330257153.dkr.ecr.us-east-1.amazonaws.com/microservices/maestro_prd
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
# Overrides the image tag whose default is the chart appVersion.
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
tag: 1.56.0
|
tag: 1.56.0
|
||||||
|
app_name: maestro
|
||||||
containerPort: 3333
|
containerPort: 3333
|
||||||
imagePullSecrets: "applications-secrets-ecr-auth-token-external-secret"
|
imagePullSecrets: "applications-secrets-ecr-auth-token-external-secret"
|
||||||
service:
|
service:
|
||||||
@@ -42,9 +43,25 @@ maestro:
|
|||||||
upload_file_agent_connection: cbc2f881-58c4-4d60-8003-0979b0b5b911
|
upload_file_agent_connection: cbc2f881-58c4-4d60-8003-0979b0b5b911
|
||||||
open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e
|
open_customer_id: f239718a-a271-4ef9-ae7e-02a2f0f3aa6e
|
||||||
open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9
|
open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9
|
||||||
|
dedicated_proxy: ""
|
||||||
|
restricted_ip: ""
|
||||||
|
redis_host: "aaapzppmlyamkocqwstpo7zvopczyyiyuy6xzm2g6c5k4mq3a66be4a-0.redis.sa-saopaulo-1.oci.oraclecloud.com"
|
||||||
|
redis_port: "6379"
|
||||||
|
redis_database: "0"
|
||||||
|
cookie_secret: "13cc5e136d3074bcc05bec8697092ec1f5f376bf"
|
||||||
autoscaling:
|
autoscaling:
|
||||||
enabled: false
|
enabled: false
|
||||||
minReplicas: 1
|
minReplicas: 1
|
||||||
maxReplicas: 100
|
maxReplicas: 100
|
||||||
targetCPUUtilizationPercentage: 80
|
targetCPUUtilizationPercentage: 80
|
||||||
targetMemoryUtilizationPercentage: 80
|
targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
affinity:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: name
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- product
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
releases:
|
||||||
|
- name: maestro
|
||||||
|
chart: ../helm-chart
|
||||||
|
values:
|
||||||
|
- ../helm-chart/values.yaml
|
||||||
|
set:
|
||||||
|
- name: app_name
|
||||||
|
value: maestro
|
||||||
|
- name: maestro.duc_url
|
||||||
|
value: duc.dadosfera.ai
|
||||||
|
- name: hostname
|
||||||
|
value: maestro.dadosfera.ai
|
||||||
|
- name: maestro.pi_factory_url
|
||||||
|
value: pi-factory.dadosfera.ai
|
||||||
|
- name: maestro.in_factory_url
|
||||||
|
value: in-factory.dadosfera.ai
|
||||||
|
- name: maestro.tr_factory_url
|
||||||
|
value: in-factory.dadosfera.ai
|
||||||
|
- name: maestro.open_customer_id
|
||||||
|
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||||
|
- name: maestro.open_group_id
|
||||||
|
value: c0afdcce-c5be-40d0-9d1d-2d271121f14a
|
||||||
|
- name: replicaCount
|
||||||
|
value: 2
|
||||||
|
|
||||||
|
- name: unimed-maestro
|
||||||
|
chart: ../helm-chart
|
||||||
|
values:
|
||||||
|
- ../helm-chart/values.yaml
|
||||||
|
set:
|
||||||
|
- name: app_name
|
||||||
|
value: maestro-unimed
|
||||||
|
- name: maestro.duc_url
|
||||||
|
value: duc.dadosfera.ai
|
||||||
|
- name: hostname
|
||||||
|
value: maestro-unimed.dadosfera.ai
|
||||||
|
- name: maestro.pi_factory_url
|
||||||
|
value: pi-factory.dadosfera.ai
|
||||||
|
- name: maestro.in_factory_url
|
||||||
|
value: in-factory.dadosfera.ai
|
||||||
|
- name: maestro.tr_factory_url
|
||||||
|
value: in-factory.dadosfera.ai
|
||||||
|
- name: maestro.open_customer_id
|
||||||
|
value: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
|
||||||
|
- name: maestro.open_group_id
|
||||||
|
value: c0afdcce-c5be-40d0-9d1d-2d271121f14a
|
||||||
|
# Customer id
|
||||||
|
- name: maestro.dedicated_proxy
|
||||||
|
value: dea2c27f-0973-4588-a2e0-9e31b64c7ffd
|
||||||
|
- name: replicaCount
|
||||||
|
value: 1
|
||||||
|
# 10.70.0.0/16 internal network
|
||||||
|
# 137.131.167.254/32 loadbalancer
|
||||||
|
# 159.112.184.81/32 cluster ip for the uptime request ingest
|
||||||
|
- name: maestro.restricted_ip
|
||||||
|
value: "177.52.172.0/24, 189.84.160.157/32, 186.237.171.146/32, 137.131.167.254/32, 10.70.0.0/16, 159.112.184.81/32, 10.244.0.0/16"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
charts:
|
||||||
|
- name: maestro
|
||||||
|
chart: ../helm-chart
|
||||||
|
values:
|
||||||
|
- ../helm-chart/values.yaml
|
||||||
|
- ../helm-chart/values-stg.yaml
|
||||||
|
|
||||||
|
|
||||||
|
# Environment to test Network Policies
|
||||||
|
- name: private-maestro
|
||||||
|
chart: ../helm-chart
|
||||||
|
values:
|
||||||
|
- ../helm-chart/values.yaml
|
||||||
|
- ../helm-chart/values-stg.yaml
|
||||||
|
set:
|
||||||
|
- name: app_name
|
||||||
|
value: maestro-private
|
||||||
|
- name: hostname
|
||||||
|
value: private-maestro.stg.dadosfera.ai
|
||||||
|
# Customer id
|
||||||
|
- name: maestro.dedicated_proxy
|
||||||
|
value: 14d52fd4-d83d-4cdd-be34-bf11cc28b3bd
|
||||||
|
- name: replicaCount
|
||||||
|
value: 1
|
||||||
|
- name: affinity
|
||||||
|
value: null
|
||||||
|
- name: resources
|
||||||
|
value: null
|
||||||
|
- name: maestro.restricted_ip
|
||||||
|
value: "137.131.167.254/32, 10.70.0.0/16, 159.112.184.81/32, 10.244.0.0/16"
|
||||||
+1793
-374
File diff suppressed because it is too large
Load Diff
Vendored
+10
@@ -14,6 +14,16 @@ declare global {
|
|||||||
AWS_REGION: string;
|
AWS_REGION: string;
|
||||||
OPEN_GROUP_ID: string;
|
OPEN_GROUP_ID: string;
|
||||||
OPEN_CUSTOMER_ID: string;
|
OPEN_CUSTOMER_ID: string;
|
||||||
|
DEDICATED_PROXY: string;
|
||||||
|
COOKIE_SECRET: string;
|
||||||
|
|
||||||
|
// Autodrive Configuration
|
||||||
|
AUTODRIVE_USERNAME?: string;
|
||||||
|
AUTODRIVE_PASSWORD?: string;
|
||||||
|
AUTODRIVE_BASE_URL?: string;
|
||||||
|
AUTODRIVE_MODEL?: string;
|
||||||
|
AUTODRIVE_KEY?: string;
|
||||||
|
AUTO_DRIVE_KEY?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
charts:
|
|
||||||
- name: maestro
|
|
||||||
chart: ../maestro
|
|
||||||
values:
|
|
||||||
- ../maestro/values.yaml
|
|
||||||
set:
|
|
||||||
- name: maestro.duc_url
|
|
||||||
value: duc.dadosfera.ai
|
|
||||||
- name: hostname
|
|
||||||
value: maestro.dadosfera.ai
|
|
||||||
- name: maestro.pi_factory_url
|
|
||||||
value: pi-factory.dadosfera.ai
|
|
||||||
- name: maestro.in_factory_url
|
|
||||||
value: in-factory.dadosfera.ai
|
|
||||||
- name: maestro.tr_factory_url
|
|
||||||
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
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
charts:
|
|
||||||
- name: maestro
|
|
||||||
chart: ../maestro
|
|
||||||
values:
|
|
||||||
- ../maestro/values.yaml
|
|
||||||
set:
|
|
||||||
- name: maestro.duc_url
|
|
||||||
value: duc.stg.dadosfera.ai
|
|
||||||
- name: hostname
|
|
||||||
value: maestro.stg.dadosfera.ai
|
|
||||||
- name: maestro.pi_factory_url
|
|
||||||
value: pi-factory.dadosfera.ai
|
|
||||||
- name: maestro.in_factory_url
|
|
||||||
value: in-factory.dadosfera.ai
|
|
||||||
- name: maestro.tr_factory_url
|
|
||||||
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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Patterns to ignore when building packages.
|
|
||||||
# This supports shell glob matching, relative path matching, and
|
|
||||||
# negation (prefixed with !). Only one pattern per line.
|
|
||||||
.DS_Store
|
|
||||||
# Common VCS dirs
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.bzr/
|
|
||||||
.bzrignore
|
|
||||||
.hg/
|
|
||||||
.hgignore
|
|
||||||
.svn/
|
|
||||||
# Common backup files
|
|
||||||
*.swp
|
|
||||||
*.bak
|
|
||||||
*.tmp
|
|
||||||
*.orig
|
|
||||||
*~
|
|
||||||
# Various IDEs
|
|
||||||
.project
|
|
||||||
.idea/
|
|
||||||
*.tmproj
|
|
||||||
.vscode/
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"**/*.proto",
|
"**/*.proto",
|
||||||
|
"assets/**/*",
|
||||||
{
|
{
|
||||||
"include": "i18n/**/*",
|
"include": "i18n/**/*",
|
||||||
"watchAssets": true
|
"watchAssets": true
|
||||||
|
|||||||
Generated
+3965
-1618
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -30,7 +30,7 @@
|
|||||||
"@aws-sdk/client-secrets-manager": "^3.414.0",
|
"@aws-sdk/client-secrets-manager": "^3.414.0",
|
||||||
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
|
||||||
"@dadosfera/protospack": "2.5.3",
|
"@dadosfera/protospack": "2.5.3",
|
||||||
"@dadosfera/protospack-v2": "3.37.0-beta.3",
|
"@dadosfera/protospack-v2": "3.38.0-beta.10",
|
||||||
"@grpc/grpc-js": "^1.9.3",
|
"@grpc/grpc-js": "^1.9.3",
|
||||||
"@grpc/proto-loader": "^0.7.9",
|
"@grpc/proto-loader": "^0.7.9",
|
||||||
"@nestjs/cli": "^9.5.0",
|
"@nestjs/cli": "^9.5.0",
|
||||||
@@ -45,22 +45,30 @@
|
|||||||
"@nestjs/swagger": "^6.3.0",
|
"@nestjs/swagger": "^6.3.0",
|
||||||
"@nestjs/testing": "^9.4.3",
|
"@nestjs/testing": "^9.4.3",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"cache-manager": "^5.1.4",
|
||||||
|
"cache-manager-ioredis-yet": "^1.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^4.9.0",
|
||||||
|
"csv": "^6.3.11",
|
||||||
"dotenv": "^14.3.2",
|
"dotenv": "^14.3.2",
|
||||||
"elastic-apm-node": "^3.50.0",
|
"elastic-apm-node": "^3.50.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwk-to-pem": "^2.0.5",
|
"jwk-to-pem": "^2.0.5",
|
||||||
"mixpanel": "^0.17.0",
|
"mixpanel": "^0.17.0",
|
||||||
"ms": "^3.0.0-canary.1",
|
"ms": "^3.0.0-canary.1",
|
||||||
|
"openid-client": "^5.7.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-facebook": "^3.0.0",
|
"passport-facebook": "^3.0.0",
|
||||||
"passport-forcedotcom": "^0.2.0",
|
"passport-forcedotcom": "^0.2.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-hubspot-oauth2": "^1.0.3",
|
"passport-hubspot-oauth2": "^1.0.3",
|
||||||
"passport-mailchimp": "^1.1.0",
|
"passport-mailchimp": "^1.1.0",
|
||||||
|
"puppeteer": "^24.7.2",
|
||||||
|
"redis": "^4.5.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.5.5",
|
||||||
@@ -70,7 +78,10 @@
|
|||||||
"multer": "1.4.5-lts.1"
|
"multer": "1.4.5-lts.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.9",
|
||||||
|
"@types/cache-manager": "^4.0.6",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/express-session": "^1.18.1",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/jwk-to-pem": "^2.0.1",
|
"@types/jwk-to-pem": "^2.0.1",
|
||||||
|
|||||||
+13
-1
@@ -28,6 +28,11 @@ import { MixpanelModule } from './modules/mixpanel/mixpanel.module';
|
|||||||
import { CustomersModule } from './modules/customers/customers.module';
|
import { CustomersModule } from './modules/customers/customers.module';
|
||||||
import { OpenDataModule } from './modules/open-data/open-data.module';
|
import { OpenDataModule } from './modules/open-data/open-data.module';
|
||||||
import { ThemeModule } from './modules/theme/theme.module';
|
import { ThemeModule } from './modules/theme/theme.module';
|
||||||
|
import { IdentityProviderModule } from './modules/identity-provider/identity-provider.module';
|
||||||
|
import { NetworkPolicyModule } from './modules/network-policy/network-policy.module';
|
||||||
|
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';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -61,8 +66,15 @@ import { ThemeModule } from './modules/theme/theme.module';
|
|||||||
CustomersModule,
|
CustomersModule,
|
||||||
OpenDataModule,
|
OpenDataModule,
|
||||||
ThemeModule,
|
ThemeModule,
|
||||||
|
NetworkPolicyModule,
|
||||||
|
AssignModule,
|
||||||
|
ShareMetadataModule,
|
||||||
|
NetworkPolicyModule,
|
||||||
|
ApiKeyModule,
|
||||||
|
IdentityProviderModule,
|
||||||
|
NetworkPolicyModule,
|
||||||
//Always leave HealthModule last, so it is on the bottom of swagger
|
//Always leave HealthModule last, so it is on the bottom of swagger
|
||||||
HealthModule
|
HealthModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-br">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dadosfera Relatório de PII</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4 landscape; /* Alterado para paisagem (landscape) */
|
||||||
|
margin: 15mm 10mm; /* Reduzido para proporcionar mais espaço */
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Quicksand', sans-serif;
|
||||||
|
color: #5c5c5c;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px; /* Reduzindo o tamanho da fonte */
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
max-width: 150px; /* Reduzida para economizar espaço */
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #0d003b;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 20px;
|
||||||
|
font-size: 24px; /* Tamanho ajustado */
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
table-layout: fixed; /* Importante: define larguras fixas */
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #1700a2;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #3a26b8;
|
||||||
|
font-size: 11px; /* Tamanho ajustado */
|
||||||
|
word-wrap: break-word; /* Permite quebra de palavras */
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
font-size: 11px; /* Tamanho ajustado */
|
||||||
|
word-wrap: break-word; /* Permite quebra de palavras */
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
/* Definindo larguras específicas para cada coluna */
|
||||||
|
th:nth-child(1), td:nth-child(1) { width: 14%; } /* Database */
|
||||||
|
th:nth-child(2), td:nth-child(2) { width: 14%; } /* Schema */
|
||||||
|
th:nth-child(3), td:nth-child(3) { width: 17%; } /* Tabela */
|
||||||
|
th:nth-child(4), td:nth-child(4) { width: 17%; } /* Coluna */
|
||||||
|
th:nth-child(5), td:nth-child(5) { width: 13%; } /* Tipo de Dado */
|
||||||
|
th:nth-child(6), td:nth-child(6) { width: 25%; } /* Regras PII */
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
tr:nth-child(odd) {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.info-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #5c5c5c;
|
||||||
|
}
|
||||||
|
.timestamp {
|
||||||
|
font-style: italic;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<img src="https://dadosfera.ai/wp-content/webp-express/webp-images/uploads/2022/06/Logo-Dadosfera1-1.png.webp" alt="Logo Dadosfera" class="logo">
|
||||||
|
<h1>Relatório de PII</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<p>Este relatório apresenta a estrutura de tabelas e suas as seguintes características de PII identificadas.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Database</th>
|
||||||
|
<th>Schema</th>
|
||||||
|
<th>Tabela</th>
|
||||||
|
<th>Coluna</th>
|
||||||
|
<th>Tipo de Dado</th>
|
||||||
|
<th>Regras PII</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each dados}}
|
||||||
|
<tr>
|
||||||
|
<td>{{database_name}}</td>
|
||||||
|
<td>{{table_schema}}</td>
|
||||||
|
<td>{{table_name}}</td>
|
||||||
|
<td>{{column_name}}</td>
|
||||||
|
<td>{{data_type}}</td>
|
||||||
|
<td>{{pii_rules}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="timestamp">Gerado em: {{dataGeracao}}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -17,6 +17,7 @@ import { PERMISSIONS_GROUPS } from './permissions.enum';
|
|||||||
import { AuthClientService } from '../modules/auth/auth.service';
|
import { AuthClientService } from '../modules/auth/auth.service';
|
||||||
|
|
||||||
import ErrorCodes from '../utils/errorCodes';
|
import ErrorCodes from '../utils/errorCodes';
|
||||||
|
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
info: (...args) => args,
|
info: (...args) => args,
|
||||||
@@ -99,6 +100,7 @@ describe('authentication.guard', () => {
|
|||||||
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
||||||
customer_name: 'dadosfera',
|
customer_name: 'dadosfera',
|
||||||
customer_tier: 'BASIC',
|
customer_tier: 'BASIC',
|
||||||
|
customer_modules: []
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -120,6 +122,12 @@ describe('authentication.guard', () => {
|
|||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: AuthenticationGuard,
|
useClass: AuthenticationGuard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ApiKeyService,
|
||||||
|
useValue: {
|
||||||
|
get: () => Promise.resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
controllers: [NoClassAuthController, ClassAuthConditionController],
|
controllers: [NoClassAuthController, ClassAuthConditionController],
|
||||||
}).compile();
|
}).compile();
|
||||||
@@ -440,18 +448,18 @@ describe('authentication.guard', () => {
|
|||||||
NoClassAuthTest(null, null);
|
NoClassAuthTest(null, null);
|
||||||
ClassAuthConditionTest(null, null);
|
ClassAuthConditionTest(null, null);
|
||||||
|
|
||||||
const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]);
|
// const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]);
|
||||||
NoClassAuthTest(tokenZ, ['zendesk']);
|
// NoClassAuthTest(tokenZ, ['zendesk']);
|
||||||
ClassAuthConditionTest(tokenZ, ['zendesk']);
|
// ClassAuthConditionTest(tokenZ, ['zendesk']);
|
||||||
|
|
||||||
const tokenM = CreateToken([PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE]);
|
// const tokenM = CreateToken([PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE]);
|
||||||
NoClassAuthTest(tokenM, ['metabase']);
|
// NoClassAuthTest(tokenM, ['metabase']);
|
||||||
ClassAuthConditionTest(tokenM, ['metabase']);
|
// ClassAuthConditionTest(tokenM, ['metabase']);
|
||||||
|
|
||||||
const tokenZM = CreateToken([
|
// const tokenZM = CreateToken([
|
||||||
PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
// PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN,
|
||||||
PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
|
// PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE,
|
||||||
]);
|
// ]);
|
||||||
NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
|
// NoClassAuthTest(tokenZM, ['zendesk', 'metabase']);
|
||||||
ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
|
// ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
OnApplicationBootstrap,
|
OnApplicationBootstrap,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
Inject,
|
Inject,
|
||||||
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
import { RequestUser } from '../decorators/user.decorator';
|
import { RequestUser } from '../decorators/user.decorator';
|
||||||
import ErrorBuilder from '../utils/ErrorBuilder';
|
import ErrorBuilder from '../utils/ErrorBuilder';
|
||||||
import ErrorCodes from '../utils/errorCodes';
|
import ErrorCodes from '../utils/errorCodes';
|
||||||
|
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthenticationGuard
|
export class AuthenticationGuard
|
||||||
@@ -31,6 +33,7 @@ export class AuthenticationGuard
|
|||||||
dadosferaLogger: DadosferaLogger,
|
dadosferaLogger: DadosferaLogger,
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private authClient: AuthClientService,
|
private authClient: AuthClientService,
|
||||||
|
private apiKeyService: ApiKeyService
|
||||||
) {
|
) {
|
||||||
this.pems = new Map();
|
this.pems = new Map();
|
||||||
this.logger = dadosferaLogger.logger;
|
this.logger = dadosferaLogger.logger;
|
||||||
@@ -48,20 +51,40 @@ export class AuthenticationGuard
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(ctx: ExecutionContext): boolean {
|
async canActivate(ctx: ExecutionContext): Promise<boolean> {
|
||||||
const authFunctions = this.reflector.getAllAndMerge<
|
const authFunctions = this.reflector.getAllAndMerge<
|
||||||
AuthenticationFunction[]
|
AuthenticationFunction[]
|
||||||
>(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]);
|
>(AUTH_FUNCTION_KEY, [ctx.getClass(), ctx.getHandler()]);
|
||||||
const mustBeAuthenticated = authFunctions.length > 0;
|
const mustBeAuthenticated = authFunctions.length > 0;
|
||||||
|
|
||||||
const request = ctx.switchToHttp().getRequest();
|
|
||||||
const accessToken = this.validateToken(request, mustBeAuthenticated);
|
|
||||||
|
|
||||||
if (!mustBeAuthenticated) {
|
if (!mustBeAuthenticated) {
|
||||||
// no need to be authenticated
|
// no need to be authenticated
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const apiKey = request.get('X-api-key');
|
||||||
|
if (apiKey) {
|
||||||
|
const {
|
||||||
|
api_key
|
||||||
|
} = await this.apiKeyService.get(apiKey);
|
||||||
|
|
||||||
|
request.user = {
|
||||||
|
user_id: api_key.user_id,
|
||||||
|
username: api_key.username,
|
||||||
|
permissions: api_key.permissions,
|
||||||
|
customer_id: api_key.customer_id,
|
||||||
|
customer_name: api_key.customer_name,
|
||||||
|
customer_tier: api_key.customer_tier,
|
||||||
|
customer_modules: api_key.customer_modules,
|
||||||
|
access_token: apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = this.validateToken(request, mustBeAuthenticated);
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
// couldn't load valid token
|
// couldn't load valid token
|
||||||
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
throw new ErrorBuilder(ErrorCodes.AUTH.UNAUTHORIZED);
|
||||||
@@ -113,6 +136,18 @@ export class AuthenticationGuard
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bloquear outros customer de usar o maestor dedicado
|
||||||
|
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
||||||
|
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== accessTokenPayload.customer_id) {
|
||||||
|
throw new ErrorBuilder(ErrorCodes.AUTH.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloquear o customer de acesso o maestro publico
|
||||||
|
const hasNetworkPolicyModule = accessTokenPayload.customer_modules.includes('network-policy');
|
||||||
|
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
||||||
|
throw new ForbiddenException(ErrorCodes.AUTH.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
request.accessTokenPayload = accessTokenPayload;
|
request.accessTokenPayload = accessTokenPayload;
|
||||||
request.user = {
|
request.user = {
|
||||||
user_id: accessTokenPayload.user_id,
|
user_id: accessTokenPayload.user_id,
|
||||||
@@ -121,6 +156,7 @@ export class AuthenticationGuard
|
|||||||
customer_id: accessTokenPayload.customer_id,
|
customer_id: accessTokenPayload.customer_id,
|
||||||
customer_name: accessTokenPayload.customer_name,
|
customer_name: accessTokenPayload.customer_name,
|
||||||
customer_tier: accessTokenPayload.customer_tier,
|
customer_tier: accessTokenPayload.customer_tier,
|
||||||
|
customer_modules: accessTokenPayload.customer_modules,
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
};
|
};
|
||||||
// TODO: for backwards compatibility. remove in the future
|
// TODO: for backwards compatibility. remove in the future
|
||||||
|
|||||||
@@ -340,16 +340,6 @@ export const PERMISSIONS_GROUPS = {
|
|||||||
'es-es': 'Gestor de catálogos. Puede ver y editar todos los activos.',
|
'es-es': 'Gestor de catálogos. Puede ver y editar todos los activos.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
EMBED_ANALYTICS: {
|
|
||||||
seqid: 44,
|
|
||||||
claim: 'catalog:embed',
|
|
||||||
usage: PermissionUsages.INTERNAL,
|
|
||||||
name: {
|
|
||||||
'pt-br': 'Acessar Módulo de Incorporação de Ativos',
|
|
||||||
'en-us': 'Access Embedding analytics Module',
|
|
||||||
'es-es': 'Acceder al Módulo de Incorporación de Activos',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TRIGGER_CATALOG_TASK: {
|
TRIGGER_CATALOG_TASK: {
|
||||||
seqid: 45,
|
seqid: 45,
|
||||||
claim: 'catalog:trigger-task',
|
claim: 'catalog:trigger-task',
|
||||||
@@ -362,6 +352,25 @@ export const PERMISSIONS_GROUPS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
EMBED: {
|
||||||
|
title: {
|
||||||
|
'pt-br': 'Analisar | Incorporação',
|
||||||
|
'en-us': 'Analyze | Embedding',
|
||||||
|
'es-es': 'Analizar | Incorporación',
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
EMBED_ANALYTICS: {
|
||||||
|
seqid: 44,
|
||||||
|
claim: 'catalog:embed',
|
||||||
|
usage: PermissionUsages.PUBLIC,
|
||||||
|
name: {
|
||||||
|
'pt-br': 'Acessar Módulo de Incorporação de Ativos',
|
||||||
|
'en-us': 'Access Embedding analytics Module',
|
||||||
|
'es-es': 'Acceder al Módulo de Incorporación de Activos',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
CONNECTORS: {
|
CONNECTORS: {
|
||||||
title: {
|
title: {
|
||||||
'pt-br': 'Conectores',
|
'pt-br': 'Conectores',
|
||||||
@@ -609,6 +618,16 @@ export interface DadosferaModule {
|
|||||||
key: string;
|
key: string;
|
||||||
permissionSeqId: number;
|
permissionSeqId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DADOSFERA_MODULES_KEYS = {
|
||||||
|
LOG_DASHBOARD: 'logs-dashboard',
|
||||||
|
ACCESS_DASHBOARD: 'access-dashboard',
|
||||||
|
DANGER_ZONE: 'danger-zone',
|
||||||
|
PII: 'pii',
|
||||||
|
EMBED: 'embedded-analytics',
|
||||||
|
EMBED_ASSIGNED: 'embed-assigned',
|
||||||
|
}
|
||||||
|
|
||||||
export const DADOSFERA_MODULES: Array<DadosferaModule> = [
|
export const DADOSFERA_MODULES: Array<DadosferaModule> = [
|
||||||
{
|
{
|
||||||
name: 'Intelligence Module',
|
name: 'Intelligence Module',
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ export function RequireSomePermission(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RequireModule(
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
return createAuthenticatedDecorator((_, user: RequestUser) =>
|
||||||
|
user.customer_modules.some(module => module === key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AuthenticateCondition(func: AuthenticationFunction) {
|
export function AuthenticateCondition(func: AuthenticationFunction) {
|
||||||
return createAuthenticatedDecorator(func);
|
return createAuthenticatedDecorator(func);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { SetMetadata } from '@nestjs/common';
|
|
||||||
|
|
||||||
export const SetOrigin = (origin: string) => SetMetadata('allowedOrigin', origin);
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CORSGuard implements CanActivate {
|
|
||||||
constructor(private reflector: Reflector) {}
|
|
||||||
|
|
||||||
canActivate(context: ExecutionContext): boolean {
|
|
||||||
const response = context.switchToHttp().getResponse();
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
const origin = request.headers.origin;
|
|
||||||
|
|
||||||
// Obter a origem permitida através do decorador
|
|
||||||
const allowedOrigin = this.reflector.get<string>('allowedOrigin', context.getHandler());
|
|
||||||
if (process.env.ENV === "local") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifica se a origem da requisição é permitida
|
|
||||||
if (allowedOrigin && origin !== allowedOrigin) {
|
|
||||||
throw new ForbiddenException('Acesso não permitido pela política CORS');
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import { PERMISSIONS_GROUPS } from '../authentication/permissions.enum';
|
|||||||
import { AuthClientService } from '../modules/auth/auth.service';
|
import { AuthClientService } from '../modules/auth/auth.service';
|
||||||
import ErrorCodes from '../utils/errorCodes';
|
import ErrorCodes from '../utils/errorCodes';
|
||||||
import { User } from './user.decorator';
|
import { User } from './user.decorator';
|
||||||
|
import { ApiKeyService } from 'src/modules/api-key/api-key.service';
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
info: (...args) => args,
|
info: (...args) => args,
|
||||||
@@ -52,6 +53,7 @@ describe('user.decorator', () => {
|
|||||||
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11',
|
||||||
customer_name: 'dadosfera',
|
customer_name: 'dadosfera',
|
||||||
customer_tier: 'BASIC',
|
customer_tier: 'BASIC',
|
||||||
|
customer_modules: [],
|
||||||
access_token: '',
|
access_token: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,6 +76,12 @@ describe('user.decorator', () => {
|
|||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: AuthenticationGuard,
|
useClass: AuthenticationGuard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ApiKeyService,
|
||||||
|
useValue: {
|
||||||
|
get: () => Promise.resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
}).compile();
|
}).compile();
|
||||||
@@ -175,5 +183,5 @@ describe('user.decorator', () => {
|
|||||||
|
|
||||||
const token = CreateToken();
|
const token = CreateToken();
|
||||||
fakeUserPayload.access_token = token;
|
fakeUserPayload.access_token = token;
|
||||||
UserTest(token);
|
// UserTest(token);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface RequestUser {
|
|||||||
customer_name: string;
|
customer_name: string;
|
||||||
customer_tier: string;
|
customer_tier: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
customer_modules: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const User: (options?: { required?: boolean }) => ParameterDecorator =
|
export const User: (options?: { required?: boolean }) => ParameterDecorator =
|
||||||
|
|||||||
+10
@@ -3,11 +3,14 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
|
import { json, urlencoded } from 'express';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
DadosferaLogger.setupLogger({
|
DadosferaLogger.setupLogger({
|
||||||
serviceName: 'maestro',
|
serviceName: 'maestro',
|
||||||
@@ -22,9 +25,16 @@ async function bootstrap() {
|
|||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
preflightContinue: false,
|
preflightContinue: false,
|
||||||
optionsSuccessStatus: 204,
|
optionsSuccessStatus: 204,
|
||||||
|
credentials: true
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
|
app.use(cookieParser(process.env.COOKIE_SECRET));
|
||||||
|
if (process.env.ENV === 'prd') {
|
||||||
|
app.use('/catalog/register-dataset', json({ limit: '10mb' }));
|
||||||
|
app.use('/catalog/register-dataset', urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
}
|
||||||
|
|
||||||
configureSwagger(app);
|
configureSwagger(app);
|
||||||
await app.listen(3333);
|
await app.listen(3333);
|
||||||
if (process.env.KILL_AFTER_START) await app.close();
|
if (process.env.KILL_AFTER_START) await app.close();
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Controller, Get, Post, Body, Param, Delete, UseFilters, Inject } from '@nestjs/common';
|
||||||
|
import { ApiKeyService } from './api-key.service';
|
||||||
|
import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto';
|
||||||
|
import { Authenticated } from 'src/decorators/authentication.decorator';
|
||||||
|
import { ApiHeaders, ApiTags, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
|
@Controller('api-key')
|
||||||
|
@Authenticated()
|
||||||
|
@ApiTags('ApiKey')
|
||||||
|
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||||
|
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||||
|
export class ApiKeyController {
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
private readonly apiKeyService: ApiKeyService,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiResponse({ type: CreateApiKeyResponseDto })
|
||||||
|
async create(@Body() createApiKeyDto: CreateApiKeyDto, @User() user: RequestUser): Promise<CreateApiKeyResponseDto> {
|
||||||
|
this.logger.info('POST /api-key', {
|
||||||
|
permissions: createApiKeyDto.permissions,
|
||||||
|
method: 'create'
|
||||||
|
});
|
||||||
|
const result = await this.apiKeyService.create(createApiKeyDto, user);
|
||||||
|
this.logger.info('POST /api-key success', {
|
||||||
|
id: result.id,
|
||||||
|
method: 'create'
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiResponse({ type: [ApiKeyBaseResponseDto] })
|
||||||
|
async findAll(@User() user: RequestUser): Promise<ApiKeyBaseResponseDto[]> {
|
||||||
|
this.logger.info('GET /api-key', {
|
||||||
|
method: 'findAll'
|
||||||
|
});
|
||||||
|
const result = await this.apiKeyService.findAll(user);
|
||||||
|
this.logger.info('GET /api-key success', {
|
||||||
|
count: result.length,
|
||||||
|
method: 'findAll'
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async remove(@Param('id') id: string, @User() user: RequestUser): Promise<void> {
|
||||||
|
this.logger.info('DELETE /api-key/:id', {
|
||||||
|
id,
|
||||||
|
method: 'remove'
|
||||||
|
});
|
||||||
|
await this.apiKeyService.remove(id, user);
|
||||||
|
this.logger.info('DELETE /api-key/:id success', {
|
||||||
|
id,
|
||||||
|
method: 'remove'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ApiKeyService } from './api-key.service';
|
||||||
|
import { ApiKeyController } from './api-key.controller';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
|
const ducClient = new DucClient();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ClientsModule.register([ducClient.providerOptions])
|
||||||
|
],
|
||||||
|
controllers: [ApiKeyController],
|
||||||
|
providers: [ApiKeyService, DadosferaLogger],
|
||||||
|
exports: [ApiKeyService]
|
||||||
|
})
|
||||||
|
export class ApiKeyModule {}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { CreateApiKeyDto, CreateApiKeyResponseDto, ApiKeyBaseResponseDto } from './dto/api-key.dto';
|
||||||
|
import { RequestUser } from 'src/decorators/user.decorator';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import { ApiKeyWriteProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyService implements OnModuleInit {
|
||||||
|
private apiKeyService: ApiKeyWriteProtoService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DucClient.name) private readonly client: ClientGrpc,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.apiKeyService = this.client.getService<ApiKeyWriteProtoService>(ProtoServices.ApiKeyWriteProtoService);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(createApiKeyDto: CreateApiKeyDto, user: RequestUser): Promise<CreateApiKeyResponseDto> {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
return lastValueFrom(this.apiKeyService.CreateApiKey({
|
||||||
|
permissions: createApiKeyDto.permissions
|
||||||
|
}, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(user: RequestUser): Promise<ApiKeyBaseResponseDto[]> {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
console.log(metadata)
|
||||||
|
|
||||||
|
const data = await lastValueFrom(this.apiKeyService.ListApiKeys({}, metadata));
|
||||||
|
return data.api_keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string, user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
await lastValueFrom(this.apiKeyService.DeleteApiKey({ id }, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string) {
|
||||||
|
const metadata = PackTheMetadata({});
|
||||||
|
|
||||||
|
return await lastValueFrom(this.apiKeyService.GetApiKey({ key }, metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsNumber } from 'class-validator';
|
||||||
|
|
||||||
|
export class PermissionDto {
|
||||||
|
@ApiProperty({ type: Number })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: String })
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiKeyBaseResponseDto {
|
||||||
|
@ApiProperty({ type: String, format: 'uuid' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: String })
|
||||||
|
key_mask: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: [PermissionDto] })
|
||||||
|
permissions: PermissionDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: String, format: 'date-time' })
|
||||||
|
created_at: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: String })
|
||||||
|
created_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateApiKeyResponseDto extends ApiKeyBaseResponseDto {
|
||||||
|
@ApiProperty({ type: String })
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateApiKeyDto {
|
||||||
|
@ApiProperty({ type: [Number], description: 'Array of permission IDs' })
|
||||||
|
@IsArray()
|
||||||
|
@IsNumber({}, { each: true })
|
||||||
|
permissions: number[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Controller, Post, Body, Put, Get} from '@nestjs/common';
|
||||||
|
import { AssignService } from './assign.service';
|
||||||
|
import { CreateAssignDto } from './dto/create-assign.dto';
|
||||||
|
import { Authenticated, RequireModule, RequireSomePermission } from 'src/decorators/authentication.decorator';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
|
||||||
|
@Controller('assign')
|
||||||
|
@Authenticated()
|
||||||
|
export class AssignController {
|
||||||
|
constructor(private readonly assignService: AssignService) {}
|
||||||
|
|
||||||
|
@Put('/public-key')
|
||||||
|
@RequireSomePermission(
|
||||||
|
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||||
|
)
|
||||||
|
@RequireModule(DADOSFERA_MODULES_KEYS.EMBED_ASSIGNED)
|
||||||
|
create(@Body() createAssignDto: CreateAssignDto, @User() user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
return this.assignService.create(createAssignDto, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/public-key')
|
||||||
|
@RequireSomePermission(
|
||||||
|
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||||
|
)
|
||||||
|
@RequireModule(DADOSFERA_MODULES_KEYS.EMBED_ASSIGNED)
|
||||||
|
async get(@User() user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
return await this.assignService.get(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AssignService } from './assign.service';
|
||||||
|
import { AssignController } from './assign.controller';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
|
||||||
|
const client = new DucClient();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ClientsModule.register([client.providerOptions])],
|
||||||
|
controllers: [AssignController],
|
||||||
|
providers: [AssignService, DadosferaLogger]
|
||||||
|
})
|
||||||
|
export class AssignModule {}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { CreateAssignDto } from './dto/create-assign.dto';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from 'src/modules/duc/client.config';
|
||||||
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
|
import { lastValueFrom } from 'rxjs';import { AssingProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AssignService implements OnModuleInit {
|
||||||
|
|
||||||
|
ducService: AssingProtoService;
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.ducService =this.grpcClient.getService<AssingProtoService>(
|
||||||
|
ProtoServices.AssingProtoService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createAssignDto: CreateAssignDto, metadata: Metadata) {
|
||||||
|
const data = await lastValueFrom(this.ducService.CreateOrUpdateAssignPublicKey(createAssignDto, metadata))
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(metadata: Metadata) {
|
||||||
|
return await lastValueFrom(this.ducService.GetAssignPublicKey({}, metadata))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export class CreateAssignDto {
|
||||||
|
publicKey: string;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Redirect,
|
Redirect,
|
||||||
Req,
|
Req,
|
||||||
Param,
|
Param,
|
||||||
|
Res,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiHeaders,
|
ApiHeaders,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
AuthConfirmResetPasswordRequest,
|
AuthConfirmResetPasswordRequest,
|
||||||
AuthEnableTotpMfaRequest,
|
AuthEnableTotpMfaRequest,
|
||||||
AuthDisableTotpMfaRequest,
|
AuthDisableTotpMfaRequest,
|
||||||
AuthVerifyTotpMfaRequest,
|
AuthVerifyTotpMfaRequest
|
||||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||||
|
|
||||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
@@ -43,15 +44,23 @@ import {
|
|||||||
AuthRefreshAccessTokenRes,
|
AuthRefreshAccessTokenRes,
|
||||||
AuthSignInReq,
|
AuthSignInReq,
|
||||||
AuthSignInRes,
|
AuthSignInRes,
|
||||||
|
BulkEditRequest,
|
||||||
} from './dtos/login';
|
} from './dtos/login';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Request } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
|
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
import { Language } from 'src/decorators/language.decorator';
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
||||||
|
import { Cookie } from 'express-session';
|
||||||
|
|
||||||
|
type CookiesValues = {
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||||
@@ -85,10 +94,66 @@ export class AuthController {
|
|||||||
async signIn(
|
async signIn(
|
||||||
@Body() { username, password, totp }: AuthSignInReq,
|
@Body() { username, password, totp }: AuthSignInReq,
|
||||||
@Language() language: LanguageEnum,
|
@Language() language: LanguageEnum,
|
||||||
): Promise<AuthSignInRes> {
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
this.logger.info('/auth - SignIn');
|
this.logger.info('/auth - SignIn');
|
||||||
const metadata = PackTheMetadata({ language });
|
const metadata = PackTheMetadata({ language });
|
||||||
return this.authClient.signIn({ username, password, totp }, metadata);
|
this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON()));
|
||||||
|
const data = await this.authClient.signIn({ username, password, totp }, metadata);
|
||||||
|
|
||||||
|
if (data.tokens) {
|
||||||
|
this.addTokenInCookie(res, {
|
||||||
|
accessToken: data.tokens.accessToken,
|
||||||
|
refreshToken: data.tokens.refreshToken,
|
||||||
|
userId: data.user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return res.send(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('/auth - SignIn - ERROR', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sign-out')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async signOut(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.logger.info('/auth - SignOut');
|
||||||
|
const exp = 1000 * 60 * 3;
|
||||||
|
|
||||||
|
res.cookie('ddf-auth', '', {
|
||||||
|
domain: 'dadosfera.local',
|
||||||
|
maxAge: Date.now() - exp,
|
||||||
|
expires: new Date(),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||||
|
});
|
||||||
|
|
||||||
|
res.cookie('ddf-refresh-auth', '', {
|
||||||
|
domain: 'dadosfera.local',
|
||||||
|
maxAge: Date.now() - exp,
|
||||||
|
expires: new Date(),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info('Clean cookie sessions');
|
||||||
|
|
||||||
|
return res.send();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('/auth - SignIn - ERROR', error);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('refresh-access-token')
|
@Post('refresh-access-token')
|
||||||
@@ -97,16 +162,26 @@ export class AuthController {
|
|||||||
async refreshAccessToken(
|
async refreshAccessToken(
|
||||||
@Body() body: AuthRefreshAccessTokenReq,
|
@Body() body: AuthRefreshAccessTokenReq,
|
||||||
@Language() language: LanguageEnum,
|
@Language() language: LanguageEnum,
|
||||||
|
@Headers('origin') origin: string,
|
||||||
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
this.logger.info('/auth - RefreshAccessToken');
|
this.logger.info('/auth - RefreshAccessToken');
|
||||||
const { refreshToken, customerName: customer_name } = body;
|
const frontHost = origin.replace(/^https?:\/\//, '');
|
||||||
|
const { refreshToken, userId } = body;
|
||||||
|
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
customer_name,
|
|
||||||
language,
|
language,
|
||||||
|
custom_host: frontHost,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.authClient.refreshAccessToken({ refreshToken }, metadata);
|
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
||||||
|
|
||||||
|
this.addTokenInCookie(res, {
|
||||||
|
accessToken: data.accessToken,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiInternalOnlyEndpoint()
|
@ApiInternalOnlyEndpoint()
|
||||||
@@ -177,16 +252,23 @@ export class AuthController {
|
|||||||
@ApiInternalOnlyEndpoint()
|
@ApiInternalOnlyEndpoint()
|
||||||
@Post('confirm-reset-password')
|
@Post('confirm-reset-password')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async confirmResetPassword(@Body() body: AuthConfirmResetPasswordRequest) {
|
async confirmResetPassword(
|
||||||
|
@Body() body: AuthConfirmResetPasswordRequest,
|
||||||
|
@Headers('origin') origin: string,
|
||||||
|
) {
|
||||||
this.logger.info('/auth - confirm-reset-password');
|
this.logger.info('/auth - confirm-reset-password');
|
||||||
|
const frontHost = origin.replace(/^https?:\/\//, '');
|
||||||
|
const metadata = PackTheMetadata({ custom_host: frontHost });
|
||||||
const { username, code, newPassword } = body;
|
const { username, code, newPassword } = body;
|
||||||
|
|
||||||
return this.authClient.confirmResetPassword({
|
return this.authClient.confirmResetPassword(
|
||||||
|
{
|
||||||
username,
|
username,
|
||||||
code,
|
code,
|
||||||
newPassword,
|
newPassword,
|
||||||
});
|
},
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiInternalOnlyEndpoint()
|
@ApiInternalOnlyEndpoint()
|
||||||
@@ -344,4 +426,181 @@ export class AuthController {
|
|||||||
|
|
||||||
return { token, email, url, language };
|
return { token, email, url, language };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiInternalOnlyEndpoint()
|
||||||
|
@Post('users/block')
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async blockUsers(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() body: BulkEditRequest,
|
||||||
|
) {
|
||||||
|
this.logger.info('blockUsers - Starting request');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
this.logger.debug('Calling blockUsers service', {
|
||||||
|
metadata: {
|
||||||
|
access_token: metadata.get('access_token'),
|
||||||
|
language: metadata.get('language'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.authClient.blockUsers(body.users, metadata);
|
||||||
|
this.logger.info('blockUsers - Success', { result });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('blockUsers - Error', {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiInternalOnlyEndpoint()
|
||||||
|
@Post('users/unblock')
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async unblockUsers(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() body: BulkEditRequest,
|
||||||
|
) {
|
||||||
|
this.logger.info('unblockUsers');
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
return this.authClient.unblockUsers(body.users, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiInternalOnlyEndpoint()
|
||||||
|
@Post('users/reset')
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async resetUsers(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() body: BulkEditRequest,
|
||||||
|
) {
|
||||||
|
this.logger.info('resetUsers');
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
return this.authClient.resetUsers(body.users, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
async getMe(@Req() req: Request, @Res() res: Response) {
|
||||||
|
this.logger.info('GET /auth/me ')
|
||||||
|
// Lê cookies
|
||||||
|
const accessToken = req.cookies['ddf-auth'];
|
||||||
|
const userId = req.cookies['ddf-user-id'];
|
||||||
|
|
||||||
|
this.logger.info('Has cookie: ' + Boolean(accessToken))
|
||||||
|
let payload: any;
|
||||||
|
let userInfo: any = {};
|
||||||
|
try {
|
||||||
|
// Decodifica e valida o JWT de acesso
|
||||||
|
const decoded: any = accessToken && jwt.decode(accessToken, { complete: true });
|
||||||
|
if (!decoded) throw new Error('Invalid token')
|
||||||
|
const { kid } = decoded.header;
|
||||||
|
// Busca a chave pública
|
||||||
|
const { keys } = await this.authClient.getPublicKeys();
|
||||||
|
const pemValue = keys.find((k) => k.kid === kid)?.pem;
|
||||||
|
if (!pemValue) throw new Error('Public key not found');
|
||||||
|
jwt.verify(accessToken, pemValue);
|
||||||
|
payload = decoded.payload;
|
||||||
|
userInfo = {
|
||||||
|
id: payload.user_id,
|
||||||
|
name: payload.username,
|
||||||
|
customer: {
|
||||||
|
id: payload.customer_id,
|
||||||
|
name: payload.customer_name,
|
||||||
|
tier: payload.customer_tier,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return res.status(200).json(userInfo);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err.message);
|
||||||
|
const refreshToken = req.cookies['ddf-refresh-auth'];
|
||||||
|
|
||||||
|
this.logger.info('Token is invalid')
|
||||||
|
this.logger.info('Has Refresh Token: '+ Boolean(refreshToken))
|
||||||
|
// Se access token inválido, tenta refresh
|
||||||
|
if (!refreshToken || !userId) {
|
||||||
|
this.logger.error('Invalid refresh token or customer name');
|
||||||
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Chama refreshAccessToken
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
});
|
||||||
|
this.logger.info('Call Refresh Token')
|
||||||
|
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
|
||||||
|
this.logger.info('Finish Refresh Token')
|
||||||
|
// Retorna novo access token e dados mínimos
|
||||||
|
this.addTokenInCookie(res, {
|
||||||
|
accessToken: data.accessToken,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
// Decodifica novo token
|
||||||
|
const decoded: any = jwt.decode(data.accessToken, { complete: true });
|
||||||
|
const payload = decoded.payload;
|
||||||
|
userInfo = {
|
||||||
|
id: payload.user_id,
|
||||||
|
name: payload.username,
|
||||||
|
customer: {
|
||||||
|
id: payload.customer_id,
|
||||||
|
name: payload.customer_name,
|
||||||
|
tier: payload.customer_tier,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return res.status(200).json(userInfo);
|
||||||
|
} catch (refreshErr) {
|
||||||
|
this.logger.error(refreshErr)
|
||||||
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTokenInCookie(res: Response, data: CookiesValues) {
|
||||||
|
let exp = 1000 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
if (data.accessToken) {
|
||||||
|
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
|
||||||
|
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
|
||||||
|
|
||||||
|
this.logger.info('Set Cookie ddf-auth')
|
||||||
|
res.cookie('ddf-auth', data.accessToken, {
|
||||||
|
domain: 'stg.dadosfera.ai',
|
||||||
|
maxAge: exp,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.refreshToken) {
|
||||||
|
this.logger.info('Set Cookie ddf-refresh-auth')
|
||||||
|
res.cookie('ddf-refresh-auth', data.refreshToken, {
|
||||||
|
domain: 'stg.dadosfera.ai',
|
||||||
|
maxAge: exp,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.userId) {
|
||||||
|
this.logger.info('Set Cookie ddf-refresh-auth')
|
||||||
|
res.cookie('ddf-user-id', data.userId, {
|
||||||
|
domain: 'stg.dadosfera.ai',
|
||||||
|
maxAge: exp,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'none', // Necessário para cookies em requisições cross-site
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { OnModuleInit, Inject, Injectable } from '@nestjs/common';
|
import { OnModuleInit, Inject, Injectable, ForbiddenException } from '@nestjs/common';
|
||||||
import { ClientGrpc } from '@nestjs/microservices';
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
import { AuthProtoService as AuthServiceInterface } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
import { AuthProtoService as AuthServiceInterface, IdentityProviderProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
import {
|
import {
|
||||||
AuthSnowflakeSignInRequest,
|
AuthSnowflakeSignInRequest,
|
||||||
AuthSignInRequest,
|
AuthSignInRequest,
|
||||||
@@ -17,15 +17,22 @@ import {
|
|||||||
AuthResetPasswordRequest,
|
AuthResetPasswordRequest,
|
||||||
AuthVerifyResetPasswordCodeRequest,
|
AuthVerifyResetPasswordCodeRequest,
|
||||||
AuthConfirmResetPasswordRequest,
|
AuthConfirmResetPasswordRequest,
|
||||||
|
AuthSignInResponse,
|
||||||
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||||
import { DucClient } from '../duc/client.config';
|
import { DucClient } from '../duc/client.config';
|
||||||
import { Metadata } from '@grpc/grpc-js';
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { BulkEditResponse } from './dtos/login';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthClientService implements OnModuleInit {
|
export class AuthClientService implements OnModuleInit {
|
||||||
|
|
||||||
|
|
||||||
logger: DadosferaLogger;
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
|
||||||
private authService: AuthServiceInterface;
|
private authService: AuthServiceInterface;
|
||||||
|
private identityProviderService: IdentityProviderProtoService;
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DadosferaLogger)
|
@Inject(DadosferaLogger)
|
||||||
dadosferaLogger: DadosferaLogger,
|
dadosferaLogger: DadosferaLogger,
|
||||||
@@ -38,6 +45,10 @@ export class AuthClientService implements OnModuleInit {
|
|||||||
this.authService = this.grpcClient.getService<AuthServiceInterface>(
|
this.authService = this.grpcClient.getService<AuthServiceInterface>(
|
||||||
ProtoServices.AuthProtoService,
|
ProtoServices.AuthProtoService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.identityProviderService = this.grpcClient.getService<IdentityProviderProtoService>(
|
||||||
|
ProtoServices.IdentityProviderProtoService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPublicKeys() {
|
async getPublicKeys() {
|
||||||
@@ -52,25 +63,59 @@ export class AuthClientService implements OnModuleInit {
|
|||||||
return lastValueFrom(this.authService.AuthSnowflakeSignIn(input));
|
return lastValueFrom(this.authService.AuthSnowflakeSignIn(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkDedicatedProxy({
|
||||||
|
customer
|
||||||
|
}: AuthSignInResponse) {
|
||||||
|
const DEDICATED_PROXY = process.env.DEDICATED_PROXY || '';
|
||||||
|
this.logger.info('SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY);
|
||||||
|
this.logger.info('Customer ID: ' + customer.id);
|
||||||
|
|
||||||
|
if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== customer.id) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloquear o customer de acesso o maestro publico
|
||||||
|
this.logger.info('Check if customer have network policy: ' + customer.modules);
|
||||||
|
const hasNetworkPolicyModule = customer.modules.includes('network-policy');
|
||||||
|
if (hasNetworkPolicyModule && DEDICATED_PROXY === '') {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async signIn(
|
async signIn(
|
||||||
{ username, password, totp }: AuthSignInRequest,
|
{ username, password, totp }: AuthSignInRequest,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
) {
|
) {
|
||||||
this.logger.info('SignIn');
|
this.logger.info('SignIn');
|
||||||
|
|
||||||
return lastValueFrom(
|
let result: AuthSignInResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await lastValueFrom(
|
||||||
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
this.authService.AuthSignIn({ username, password, totp }, metadata),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('SignIn - Error during sign-in');
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.customer) {
|
||||||
|
this.checkDedicatedProxy(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshAccessToken(
|
async refreshAccessToken(
|
||||||
{ refreshToken }: AuthRefreshAccessTokenRequest,
|
{ refreshToken, userId }: AuthRefreshAccessTokenRequest,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
) {
|
) {
|
||||||
this.logger.info('RefreshAccessToken');
|
this.logger.info('RefreshAccessToken');
|
||||||
|
|
||||||
return lastValueFrom(
|
return lastValueFrom(
|
||||||
this.authService.AuthRefreshAccessToken({ refreshToken }, metadata),
|
this.authService.AuthRefreshAccessToken({ refreshToken, userId }, metadata),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,19 +157,21 @@ export class AuthClientService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmResetPassword({
|
async confirmResetPassword(
|
||||||
username,
|
{ username, code, newPassword }: AuthConfirmResetPasswordRequest,
|
||||||
code,
|
metadata: Metadata,
|
||||||
newPassword,
|
) {
|
||||||
}: AuthConfirmResetPasswordRequest) {
|
|
||||||
this.logger.info('confirmResetPassword');
|
this.logger.info('confirmResetPassword');
|
||||||
|
|
||||||
return lastValueFrom(
|
return lastValueFrom(
|
||||||
this.authService.AuthConfirmResetPassword({
|
this.authService.AuthConfirmResetPassword(
|
||||||
|
{
|
||||||
username,
|
username,
|
||||||
code,
|
code,
|
||||||
newPassword,
|
newPassword,
|
||||||
}),
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,4 +215,72 @@ export class AuthClientService implements OnModuleInit {
|
|||||||
this.authService.AuthOauthSignIn({ token, username, refreshToken: '' }),
|
this.authService.AuthOauthSignIn({ token, username, refreshToken: '' }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async blockUsers(
|
||||||
|
users: string[],
|
||||||
|
metadata: Metadata,
|
||||||
|
): Promise<BulkEditResponse> {
|
||||||
|
this.logger.info('blockUsers - Service starting');
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.debug('Calling BlockUser gRPC method', {
|
||||||
|
metadata: {
|
||||||
|
access_token: metadata.get('access_token'),
|
||||||
|
language: metadata.get('language'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await lastValueFrom<BulkEditResponse>(
|
||||||
|
this.authService.BlockUser({ users }, metadata),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info('blockUsers - Service success', { response });
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('blockUsers - Service error', {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unblockUsers(
|
||||||
|
users: string[],
|
||||||
|
metadata: Metadata,
|
||||||
|
): Promise<BulkEditResponse> {
|
||||||
|
this.logger.info('unblockUsers');
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.authService.UnblockUser({ users }, metadata),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetUsers(
|
||||||
|
users: string[],
|
||||||
|
metadata: Metadata,
|
||||||
|
): Promise<BulkEditResponse> {
|
||||||
|
this.logger.info('resetUsers');
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.debug('Calling ResetUser gRPC method', {
|
||||||
|
metadata: {
|
||||||
|
access_token: metadata.get('access_token'),
|
||||||
|
language: metadata.get('language'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await lastValueFrom<BulkEditResponse>(
|
||||||
|
this.authService.ResetUser({ users }, metadata),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info('resetUsers - Success', { response });
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('resetUsers - Error', {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,16 +67,28 @@ export class AuthUser {
|
|||||||
export class AuthCustomer {
|
export class AuthCustomer {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
modules: string[];
|
modules: string[];
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
displayName: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
tier: string;
|
tier: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
scheduleLimit: string;
|
scheduleLimit: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
links: Link[];
|
links: Link[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
themeEnabled: boolean;
|
||||||
|
@ApiProperty()
|
||||||
|
enforceMfa: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthSignInReq implements AuthSignInRequest {
|
export class AuthSignInReq implements AuthSignInRequest {
|
||||||
@@ -110,7 +122,7 @@ export class AuthRefreshAccessTokenReq {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
customerName: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
export class AuthRefreshAccessTokenRes {
|
export class AuthRefreshAccessTokenRes {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -118,3 +130,13 @@ export class AuthRefreshAccessTokenRes {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BulkEditRequest {
|
||||||
|
users: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkEditResponse {
|
||||||
|
message: string;
|
||||||
|
successfulUsers: string[];
|
||||||
|
failedUsers: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class GoogleLoginStrategy extends PassportStrategy(
|
|||||||
callbackURL: oauthSecrets['google-login'].redirect_uri,
|
callbackURL: oauthSecrets['google-login'].redirect_uri,
|
||||||
scope: ['email', 'profile', 'openid'],
|
scope: ['email', 'profile', 'openid'],
|
||||||
};
|
};
|
||||||
|
console.log("GoogleLoginStrategy", options.clientID, options.callbackURL);
|
||||||
const verify = (
|
const verify = (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '@nestjs/microservices';
|
} from '@nestjs/microservices';
|
||||||
import { credentials } from '@grpc/grpc-js';
|
import { credentials } from '@grpc/grpc-js';
|
||||||
import { Catalog } from '@dadosfera/protospack-v2';
|
import { Catalog } from '@dadosfera/protospack-v2';
|
||||||
|
import { PlatformInterfaces } from '@dadosfera/protospack-v2';
|
||||||
|
|
||||||
const isLocalConnection =
|
const isLocalConnection =
|
||||||
process.env.PIFACTORY_URL.startsWith('pi-factory:') ||
|
process.env.PIFACTORY_URL.startsWith('pi-factory:') ||
|
||||||
@@ -19,11 +20,13 @@ export class CatalogClientConfiguration {
|
|||||||
package: [
|
package: [
|
||||||
Catalog.ProtoPackages.ReadPackage,
|
Catalog.ProtoPackages.ReadPackage,
|
||||||
Catalog.ProtoPackages.WritePackage,
|
Catalog.ProtoPackages.WritePackage,
|
||||||
|
PlatformInterfaces.ProtoPackages.WritePackage
|
||||||
],
|
],
|
||||||
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
credentials: isLocalConnection ? undefined : credentials.createSsl(),
|
||||||
protoPath: [
|
protoPath: [
|
||||||
Catalog.ProtoPaths.ReadFilePath,
|
Catalog.ProtoPaths.ReadFilePath,
|
||||||
Catalog.ProtoPaths.WriteFilePath,
|
Catalog.ProtoPaths.WriteFilePath,
|
||||||
|
PlatformInterfaces.ProtoPaths.WriteFilePath
|
||||||
],
|
],
|
||||||
loader: {
|
loader: {
|
||||||
keepCase: true,
|
keepCase: true,
|
||||||
|
|||||||
@@ -13,22 +13,29 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
UseFilters,
|
UseFilters,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Res,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiCreatedResponse,
|
ApiCreatedResponse,
|
||||||
ApiHeaders,
|
ApiHeaders,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
Authenticated,
|
Authenticated,
|
||||||
RequireAllPermissions,
|
RequireAllPermissions,
|
||||||
|
RequireModule,
|
||||||
RequireSomePermission,
|
RequireSomePermission,
|
||||||
} from '../../decorators/authentication.decorator';
|
} from '../../decorators/authentication.decorator';
|
||||||
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
|
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
|
||||||
import { CatalogService } from './catalog.service';
|
import { CatalogService } from './catalog.service';
|
||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
import {
|
import {
|
||||||
BatchRemoveRlsRulesRequest,
|
BatchRemoveRlsRulesRequest,
|
||||||
@@ -54,7 +61,10 @@ import {
|
|||||||
AddRlsRuleRequest,
|
AddRlsRuleRequest,
|
||||||
GetNimbusDashboardsRequest,
|
GetNimbusDashboardsRequest,
|
||||||
GetRlsRulesRequest,
|
GetRlsRulesRequest,
|
||||||
|
RegisterDatasetWithMetatadaRequest,
|
||||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { TypeParser } from 'src/utils/FileParser/parser-types';
|
||||||
|
|
||||||
@ApiTags('Catalog')
|
@ApiTags('Catalog')
|
||||||
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||||
@@ -81,10 +91,6 @@ export class CatalogController {
|
|||||||
@Query() query: ICatalogAllRequest,
|
@Query() query: ICatalogAllRequest,
|
||||||
): Promise<ICatalogAllResponse> {
|
): Promise<ICatalogAllResponse> {
|
||||||
const { user_id, customer_name, customer_id, username, permissions } = user;
|
const { user_id, customer_name, customer_id, username, permissions } = user;
|
||||||
this.logger.info(`/catalog - searchCatalog`, {
|
|
||||||
user_id,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const is_data_manager = permissions.includes(
|
const is_data_manager = permissions.includes(
|
||||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||||
@@ -110,15 +116,53 @@ export class CatalogController {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/download')
|
||||||
|
@RequireSomePermission(
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||||
|
)
|
||||||
|
async dowloadAsserts(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Query() query: ICatalogAllRequest,
|
||||||
|
@Res() res: Response
|
||||||
|
) {
|
||||||
|
const { user_id, customer_name, customer_id, username, permissions } = user;
|
||||||
|
|
||||||
|
const is_data_manager = permissions.includes(
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const roles = await this.catalogService.getUserRolesIds(user_id);
|
||||||
|
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
user_id,
|
||||||
|
customer_id,
|
||||||
|
customer_name,
|
||||||
|
username,
|
||||||
|
roles,
|
||||||
|
is_data_manager,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
file,
|
||||||
|
filename
|
||||||
|
} = await this.catalogService.downloadAssets(
|
||||||
|
query,
|
||||||
|
metadata,
|
||||||
|
customer_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
res.end(file);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiInternalOnlyEndpoint()
|
@ApiInternalOnlyEndpoint()
|
||||||
@Get('data-asset')
|
@Get('data-asset')
|
||||||
async findByPipelineAndObject(@User() user: RequestUser, @Query() query) {
|
async findByPipelineAndObject(@User() user: RequestUser, @Query() query) {
|
||||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||||
const { pipeline, object } = query;
|
const { pipeline, object } = query;
|
||||||
this.logger.info(`/catalog - ON GET DATA ASSET BY PIPELINE AND OBJECT`, {
|
|
||||||
username,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pipeline || !object) {
|
if (!pipeline || !object) {
|
||||||
throw new BadRequestException('Query params not provided');
|
throw new BadRequestException('Query params not provided');
|
||||||
@@ -171,10 +215,6 @@ export class CatalogController {
|
|||||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||||
)
|
)
|
||||||
async findAllTags(@Body() body) {
|
async findAllTags(@Body() body) {
|
||||||
this.logger.info(`/catalog - ON FIND ALL TAGS ROUTE`, {
|
|
||||||
user: body.info.user_id,
|
|
||||||
customer: body.info.customer,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user_id, customer, customer_id } = body.info;
|
const { user_id, customer, customer_id } = body.info;
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
@@ -200,10 +240,6 @@ export class CatalogController {
|
|||||||
) {
|
) {
|
||||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||||
|
|
||||||
this.logger.info(`GET /data-asset/${id}`, {
|
|
||||||
username,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const is_data_manager = permissions.includes(
|
const is_data_manager = permissions.includes(
|
||||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||||
@@ -256,10 +292,6 @@ export class CatalogController {
|
|||||||
) {
|
) {
|
||||||
const { username, user_id, customer_id, customer_name, permissions } = user;
|
const { username, user_id, customer_id, customer_name, permissions } = user;
|
||||||
|
|
||||||
this.logger.info(`/catalog - ON GET ONE DASHBOARD METABASE ROUTE`, {
|
|
||||||
username,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const is_data_manager = permissions.includes(
|
const is_data_manager = permissions.includes(
|
||||||
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||||
@@ -311,10 +343,6 @@ export class CatalogController {
|
|||||||
): Promise<IColumnsMetadataResponse> {
|
): Promise<IColumnsMetadataResponse> {
|
||||||
const { customer_name, customer_id, user_id, username } = user;
|
const { customer_name, customer_id, user_id, username } = user;
|
||||||
|
|
||||||
this.logger.info(`/catalog - columns-metadata`, {
|
|
||||||
user_id,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
customer_name,
|
customer_name,
|
||||||
@@ -340,12 +368,8 @@ export class CatalogController {
|
|||||||
@Language() language: LanguageEnum,
|
@Language() language: LanguageEnum,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
): Promise<IPreviewResponse> {
|
): Promise<IPreviewResponse> {
|
||||||
const { customer_name, customer_id, user_id, username } = user;
|
const { customer_name, customer_id, user_id, username, customer_modules } = user;
|
||||||
|
|
||||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
|
||||||
user_id,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
customer_name,
|
customer_name,
|
||||||
@@ -353,6 +377,7 @@ export class CatalogController {
|
|||||||
user_id,
|
user_id,
|
||||||
username,
|
username,
|
||||||
language,
|
language,
|
||||||
|
is_mask: customer_modules.some(mod => mod === 'pii')
|
||||||
});
|
});
|
||||||
|
|
||||||
const preview = await this.catalogService.getDatasetPreview(id, metadata);
|
const preview = await this.catalogService.getDatasetPreview(id, metadata);
|
||||||
@@ -370,11 +395,13 @@ export class CatalogController {
|
|||||||
@Language() language: LanguageEnum,
|
@Language() language: LanguageEnum,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
): Promise<IDocsResponse> {
|
): Promise<IDocsResponse> {
|
||||||
|
try {
|
||||||
const { customer_name, customer_id, user_id, username } = user;
|
const { customer_name, customer_id, user_id, username } = user;
|
||||||
|
|
||||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
||||||
user_id,
|
user_id,
|
||||||
customer_name,
|
customer_name,
|
||||||
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
@@ -388,6 +415,11 @@ export class CatalogController {
|
|||||||
const docs = await this.catalogService.getDataDocs(id, metadata);
|
const docs = await this.catalogService.getDataDocs(id, metadata);
|
||||||
|
|
||||||
return { docs };
|
return { docs };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error in getDataAssetDocs for id ${id}: ${error.message}`);
|
||||||
|
this.logger.error(`Error details: ${JSON.stringify(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('data-asset/:id')
|
@Put('data-asset/:id')
|
||||||
@@ -436,10 +468,6 @@ export class CatalogController {
|
|||||||
) {
|
) {
|
||||||
const { user_id, customer_name } = user;
|
const { user_id, customer_name } = user;
|
||||||
|
|
||||||
this.logger.info(`/catalog - ON GET DATA DOCS ROUTE`, {
|
|
||||||
user_id,
|
|
||||||
customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await this.catalogService.createDataDocs({
|
const res = await this.catalogService.createDataDocs({
|
||||||
table_id,
|
table_id,
|
||||||
@@ -452,6 +480,7 @@ export class CatalogController {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ApiInternalOnlyEndpoint()
|
@ApiInternalOnlyEndpoint()
|
||||||
@Put('data-asset/:id/manage-permissions')
|
@Put('data-asset/:id/manage-permissions')
|
||||||
async manageDataAssetPermissions(
|
async manageDataAssetPermissions(
|
||||||
@@ -743,4 +772,222 @@ export class CatalogController {
|
|||||||
|
|
||||||
return JSON.parse(dashboards);
|
return JSON.parse(dashboards);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('register-dataset')
|
||||||
|
@RequireSomePermission(
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.CREATE,
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
|
||||||
|
)
|
||||||
|
async registerDatasetWithMetadataRequest(
|
||||||
|
@Body() body: RegisterDatasetWithMetatadaRequest,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
const { customer_id, customer_name, user_id, username } = user;
|
||||||
|
const logMetadata = {
|
||||||
|
customer_name: customer_name,
|
||||||
|
user_id: user_id,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/catalog/register-dataset',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
customer_id,
|
||||||
|
customer_name,
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Request from user ${user_id} for customer ${customer_name}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create table metadata
|
||||||
|
const tableMetadataBody = {
|
||||||
|
table_metadata: body.table_metadata,
|
||||||
|
info: {
|
||||||
|
customer: customer_name,
|
||||||
|
},
|
||||||
|
logMetadata: logMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
const table_metadata_id = await this.catalogService.createTableMetadata(
|
||||||
|
tableMetadataBody,
|
||||||
|
);
|
||||||
|
this.logger.info(`table_metadata_id: ${table_metadata_id}`, logMetadata);
|
||||||
|
|
||||||
|
// Create column metadata
|
||||||
|
const columnMetadataBody = {
|
||||||
|
column_metadata: body.column_metadata,
|
||||||
|
info: {
|
||||||
|
customer: customer_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.logger.info(
|
||||||
|
`Creating column metadata for table ${table_metadata_id}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
const column_metadata_ids =
|
||||||
|
await this.catalogService.createColumnMetadata(columnMetadataBody);
|
||||||
|
|
||||||
|
// Create data preview
|
||||||
|
const dataPreviewBody = {
|
||||||
|
data_preview: body.data_preview,
|
||||||
|
info: {
|
||||||
|
customer: customer_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.logger.debug(
|
||||||
|
`Creating data preview for table ${table_metadata_id}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
const data_preview_id = await this.catalogService.createDataPreview(
|
||||||
|
dataPreviewBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Catalog dataset item
|
||||||
|
this.logger.info(
|
||||||
|
`Cataloging dataset item for table ${table_metadata_id}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
await this.catalogService.catalogDatasetItem(table_metadata_id, metadata);
|
||||||
|
this.logger.info(
|
||||||
|
`Dataset registration completed successfully for table ${table_metadata_id}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
message: 'Dataset registered successfully',
|
||||||
|
table_metadata_id: table_metadata_id,
|
||||||
|
column_metadata_ids: column_metadata_ids,
|
||||||
|
data_preview_id: data_preview_id,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to register dataset. The following error occurred: ${error.response.data}`,
|
||||||
|
logMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
message: 'Ocorreu um erro ao registrar o dataset',
|
||||||
|
error: error.message,
|
||||||
|
code: 'REGISTRATION_FAILED',
|
||||||
|
details: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('pii-reporter')
|
||||||
|
@RequireSomePermission(
|
||||||
|
PERMISSIONS_GROUPS.USERS.permissions.ADMIN
|
||||||
|
)
|
||||||
|
@RequireModule(
|
||||||
|
DADOSFERA_MODULES_KEYS.PII
|
||||||
|
)
|
||||||
|
async getPiiReporter(@User() user: RequestUser, @Res() res: Response, @Query('type') contentType: TypeParser = "pdf") {
|
||||||
|
this.logger.info('GET pii-reporter');
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
file,
|
||||||
|
filename,
|
||||||
|
type
|
||||||
|
} = await this.catalogService.getPiiReporter(metadata, contentType);
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
|
res.setHeader('Content-Type', type);
|
||||||
|
|
||||||
|
// use res.end to send buffer
|
||||||
|
return res.end(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.logger.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/data-asset/:nimbus_id/docs/ai')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Save documentation for data asset',
|
||||||
|
description: 'Saves documentation content for a data asset',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'nimbus_id',
|
||||||
|
description: 'Nimbus ID of the data asset',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Documentation saved successfully',
|
||||||
|
})
|
||||||
|
async saveDocumentation(
|
||||||
|
@Param('nimbus_id') nimbusId: string,
|
||||||
|
@Body() body: { docs: string },
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.catalogService.updateDataAssetDocumentation(nimbusId, body.docs, metadata);
|
||||||
|
return {
|
||||||
|
message: 'Documentation saved successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error saving documentation for ${nimbusId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/data-asset/:nimbus_id/docs/generate-ai')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Generate AI documentation for data asset',
|
||||||
|
description: 'Generates comprehensive documentation for a data asset using AI (Autodrive)',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'nimbus_id',
|
||||||
|
description: 'Nimbus ID of the data asset',
|
||||||
|
type: 'string',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'AI documentation generated successfully',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string' },
|
||||||
|
documentation: { type: 'string' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Bad request - invalid nimbus_id or missing data',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: 'Data asset not found',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 500,
|
||||||
|
description: 'Internal server error during AI generation',
|
||||||
|
})
|
||||||
|
async generateAiDocumentation(
|
||||||
|
@Param('nimbus_id') dataAssetId: string,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.catalogService.generateAiDocumentation(dataAssetId, metadata, user);
|
||||||
|
return {
|
||||||
|
message: 'AI documentation generated successfully',
|
||||||
|
documentation: result,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error generating AI documentation for ${dataAssetId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,15 @@ import { Module } from '@nestjs/common';
|
|||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
import { CatalogController } from './catalog.controller';
|
import { CatalogController } from './catalog.controller';
|
||||||
import { CatalogService } from './catalog.service';
|
|
||||||
import { CatalogClientConfiguration } from './catalog-client';
|
import { CatalogClientConfiguration } from './catalog-client';
|
||||||
import { ClientsModule } from '@nestjs/microservices';
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
import { PipelinesModule as OldPipelineModule } from 'src/modules/pipelines/pipelines.module';
|
import { PipelinesModule as OldPipelineModule } from 'src/modules/pipelines/pipelines.module';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
import { RolesModule } from '../roles/roles.module';
|
import { RolesModule } from '../roles/roles.module';
|
||||||
|
import { CustomersModule } from '../customers/customers.module';
|
||||||
|
import { ShareModule } from './share/share.module';
|
||||||
|
import { CatalogService } from './catalog.service';
|
||||||
|
import { MixpanelModule } from '../mixpanel/mixpanel.module';
|
||||||
|
|
||||||
const client = new CatalogClientConfiguration();
|
const client = new CatalogClientConfiguration();
|
||||||
|
|
||||||
@@ -18,6 +21,8 @@ const client = new CatalogClientConfiguration();
|
|||||||
OldPipelineModule,
|
OldPipelineModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RolesModule,
|
RolesModule,
|
||||||
|
CustomersModule,
|
||||||
|
ShareModule,
|
||||||
],
|
],
|
||||||
controllers: [CatalogController],
|
controllers: [CatalogController],
|
||||||
providers: [CatalogService, DadosferaLogger],
|
providers: [CatalogService, DadosferaLogger],
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import DadosferaLogger from '@dadosfera/dadosfera-logs/dist';
|
import DadosferaLogger from '@dadosfera/dadosfera-logs/dist';
|
||||||
|
import FormData from 'form-data';
|
||||||
import {
|
import {
|
||||||
WriteService,
|
WriteService,
|
||||||
ReadService,
|
ReadService,
|
||||||
ProtoServices,
|
ProtoServices,
|
||||||
Messages,
|
Messages,
|
||||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
|
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
|
||||||
|
import {
|
||||||
|
Messages as PlatformInterfaceMessages,
|
||||||
|
WriteService as PlatformInterfaceWriteService,
|
||||||
|
ProtoServices as PlatformInterfacesProtoServices,
|
||||||
|
} from '@dadosfera/protospack-v2/dist/lib/PlatformInterfaces';
|
||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
HttpException,
|
HttpException,
|
||||||
@@ -20,6 +26,7 @@ import { UsersService } from '../users/users.service';
|
|||||||
import { RolesService } from '../roles/roles.service';
|
import { RolesService } from '../roles/roles.service';
|
||||||
import { Metadata } from '@grpc/grpc-js';
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
import {
|
import {
|
||||||
|
AssetReporter,
|
||||||
BatchRemoveRlsRulesRequest,
|
BatchRemoveRlsRulesRequest,
|
||||||
IUpdateDataRequest,
|
IUpdateDataRequest,
|
||||||
TriggerCatalogReq,
|
TriggerCatalogReq,
|
||||||
@@ -28,11 +35,29 @@ import {
|
|||||||
AddRlsRuleRequest,
|
AddRlsRuleRequest,
|
||||||
GetNimbusDashboardsRequest,
|
GetNimbusDashboardsRequest,
|
||||||
GetRlsRulesRequest,
|
GetRlsRulesRequest,
|
||||||
|
PiiMetadata,
|
||||||
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
} from '@dadosfera/protospack-v2/dist/lib/Catalog/interfaces/messages';
|
||||||
|
import { TypeParser } from 'src/utils/FileParser/parser-types';
|
||||||
|
import { ParserBuilder } from 'src/utils/FileParser/parser.builder';
|
||||||
|
import { AI_DOCUMENTATION_PROMPT, AI_DOCUMENTATION_CONFIG } from './prompts/ai-documentation.prompt';
|
||||||
|
import { AUTODRIVE_CONSTANTS, GEOGRAPHIC_KEYS, COMMON_COUNTRIES } from './constants/autodrive.constants';
|
||||||
|
import {
|
||||||
|
AutodriveCredentials,
|
||||||
|
AutodriveAskPayload,
|
||||||
|
AutodriveAskResponse,
|
||||||
|
AutodriveAnswerResponse,
|
||||||
|
AutodriveUploadResponse,
|
||||||
|
DatasetStatusResponse,
|
||||||
|
ColumnData,
|
||||||
|
ColumnsMetadata,
|
||||||
|
DataPreview,
|
||||||
|
FormattedDataForAI
|
||||||
|
} from './types/ai-documentation.types';
|
||||||
|
|
||||||
class CatalogService implements OnModuleInit {
|
class CatalogService implements OnModuleInit {
|
||||||
catalogReadService: ReadService.CatalogReadServices;
|
catalogReadService: ReadService.CatalogReadServices;
|
||||||
catalogWriteService: WriteService.CatalogWriteServices;
|
catalogWriteService: WriteService.CatalogWriteServices;
|
||||||
|
platformWriteService: PlatformInterfaceWriteService.PlatformInterfacesWriteServices;
|
||||||
logger: any;
|
logger: any;
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DadosferaLogger)
|
@Inject(DadosferaLogger)
|
||||||
@@ -54,9 +79,14 @@ class CatalogService implements OnModuleInit {
|
|||||||
this.grpcClient.getService<WriteService.CatalogWriteServices>(
|
this.grpcClient.getService<WriteService.CatalogWriteServices>(
|
||||||
ProtoServices.CatalogWriteServices,
|
ProtoServices.CatalogWriteServices,
|
||||||
);
|
);
|
||||||
|
this.platformWriteService =
|
||||||
|
this.grpcClient.getService<PlatformInterfaceWriteService.PlatformInterfacesWriteServices>(
|
||||||
|
PlatformInterfacesProtoServices.PlatformInterfacesWriteServices,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNimbusUrl(body) {
|
_getNimbusUrl(body) {
|
||||||
|
this.logger.debug(`Body: ${JSON.stringify(body)}`);
|
||||||
const customer = body.info.customer.toLowerCase();
|
const customer = body.info.customer.toLowerCase();
|
||||||
|
|
||||||
if (process.env.ENV === 'prd') {
|
if (process.env.ENV === 'prd') {
|
||||||
@@ -69,6 +99,42 @@ class CatalogService implements OnModuleInit {
|
|||||||
)}.dadosfera.ai`;
|
)}.dadosfera.ai`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPiiReporter(metadata: Metadata, type: TypeParser) {
|
||||||
|
this.logger.info('getPiiReporter: ' + type)
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data
|
||||||
|
} = await lastValueFrom(
|
||||||
|
this.catalogWriteService.GetPiiReporter({}, metadata)
|
||||||
|
)
|
||||||
|
this.logger.info("Finish grpc call")
|
||||||
|
|
||||||
|
const parser = ParserBuilder.build<PiiMetadata>(type);
|
||||||
|
|
||||||
|
this.logger.info('parser file to: ' + type)
|
||||||
|
const file = await parser.parse(data)
|
||||||
|
this.logger.info('finish parser')
|
||||||
|
const mimeTypes: Record<TypeParser, string> = {
|
||||||
|
'csv': 'text/csv',
|
||||||
|
'html': 'text/html',
|
||||||
|
'pdf': 'application/pdf'
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `relatorio-pii-${timestamp}.${type}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
filename: filename,
|
||||||
|
type: mimeTypes[type]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
async createDataAsset(data: Messages.CreateDataAssetRequest, metadata) {
|
||||||
this.logger.info('CatalogService - Manage Data assets permissions');
|
this.logger.info('CatalogService - Manage Data assets permissions');
|
||||||
if (!data.embed) data.embed = undefined;
|
if (!data.embed) data.embed = undefined;
|
||||||
@@ -184,6 +250,34 @@ class CatalogService implements OnModuleInit {
|
|||||||
return { data_assets: response, total };
|
return { data_assets: response, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadAssets(
|
||||||
|
query: Record<string, any>,
|
||||||
|
metadata: Metadata,
|
||||||
|
customer_id: string,
|
||||||
|
) {
|
||||||
|
const data = await this.searchDataAssets(query, metadata, customer_id);
|
||||||
|
|
||||||
|
const formatData = data.data_assets.map(asset => ({
|
||||||
|
id: asset.id,
|
||||||
|
display_name: asset.display_name,
|
||||||
|
data_asset_type: asset.data_asset_type,
|
||||||
|
created_at: asset.created_at,
|
||||||
|
tags: '[' + asset.tags.join(', ') + ']'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const parser = ParserBuilder.build<AssetReporter>('csv');
|
||||||
|
|
||||||
|
const file = await parser.parse(formatData);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `dadosfera_assets_${timestamp}.csv`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getOneDataAsset(data: {
|
async getOneDataAsset(data: {
|
||||||
id: string;
|
id: string;
|
||||||
customer_id: string;
|
customer_id: string;
|
||||||
@@ -257,15 +351,44 @@ class CatalogService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDataDocs(id: string, metadata: Metadata) {
|
async getDataDocs(id: string, metadata: Metadata) {
|
||||||
const { documentation } = await lastValueFrom(
|
let documentation: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await lastValueFrom(
|
||||||
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
||||||
);
|
);
|
||||||
console.log(documentation);
|
|
||||||
|
const doc = response.documentation;
|
||||||
|
documentation = doc;
|
||||||
|
|
||||||
|
if (!documentation) {
|
||||||
|
this.logger.warn(`No documentation found for id: ${id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se a documentação é um JSON válido
|
||||||
|
if (documentation.trim().startsWith('{') || documentation.trim().startsWith('[')) {
|
||||||
const docs = JSON.parse(documentation);
|
const docs = JSON.parse(documentation);
|
||||||
return docs;
|
return docs;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Documentation is not JSON format, returning as raw text for id: ${id}`);
|
||||||
|
return { raw_documentation: documentation };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`getDataDocs failed for id ${id}: ${error.message}`);
|
||||||
|
|
||||||
|
// Se for erro de JSON parsing, tentar retornar a documentação como string
|
||||||
|
if (error.message.includes('JSON') || error.message.includes('parse')) {
|
||||||
|
this.logger.warn(`JSON parsing failed, returning raw documentation for id: ${id}`);
|
||||||
|
return { raw_documentation: documentation || 'No documentation available' };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDatasetPreview(id: string, metadata: Metadata) {
|
async getDatasetPreview(id: string, metadata: Metadata) {
|
||||||
|
try {
|
||||||
const { preview } = await lastValueFrom(
|
const { preview } = await lastValueFrom(
|
||||||
this.catalogReadService.GetDatasetPreview(
|
this.catalogReadService.GetDatasetPreview(
|
||||||
{ id, type: undefined },
|
{ id, type: undefined },
|
||||||
@@ -274,18 +397,34 @@ class CatalogService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
const result = JSON.parse(preview);
|
const result = JSON.parse(preview);
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`getDatasetPreview failed for id ${id}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDatasetColumnsMetadata(id: string, metadata: Metadata) {
|
async getDatasetColumnsMetadata(id: string, metadata: Metadata) {
|
||||||
|
try {
|
||||||
const { columns_metadata } = await lastValueFrom(
|
const { columns_metadata } = await lastValueFrom(
|
||||||
this.catalogReadService.GetDatasetColumnsMetadata(
|
this.catalogReadService.GetDatasetColumnsMetadata(
|
||||||
{ id, type: undefined },
|
{ id, type: undefined },
|
||||||
metadata,
|
metadata,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!columns_metadata || columns_metadata.length <= 2) {
|
||||||
|
this.logger.warn(`Empty or minimal response from gRPC: "${columns_metadata}"`);
|
||||||
|
throw new Error('Empty response from gRPC service');
|
||||||
|
}
|
||||||
|
|
||||||
const result = JSON.parse(columns_metadata);
|
const result = JSON.parse(columns_metadata);
|
||||||
return result;
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`getDatasetColumnsMetadata failed for id ${id}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async createDataDocs(body) {
|
async createDataDocs(body) {
|
||||||
const nimbusUrl = this._getNimbusUrl(body);
|
const nimbusUrl = this._getNimbusUrl(body);
|
||||||
@@ -296,6 +435,7 @@ class CatalogService implements OnModuleInit {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async findAllTags(data, metadata) {
|
async findAllTags(data, metadata) {
|
||||||
this.logger.info('CatalogService - findAllCustomerTags');
|
this.logger.info('CatalogService - findAllCustomerTags');
|
||||||
|
|
||||||
@@ -420,6 +560,630 @@ class CatalogService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
return res.dashboards;
|
return res.dashboards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createTableMetadata(body: any): Promise<number> {
|
||||||
|
const nimbusUrl = this._getNimbusUrl(body);
|
||||||
|
this.logger.info(`Nimbus URL: ${nimbusUrl}`, {...body.logMetadata});
|
||||||
|
|
||||||
|
const endpoint = `${nimbusUrl}/api/catalog/table-metadata/`;
|
||||||
|
|
||||||
|
this.logger.info(`Creating table metadata for table ${body.table_metadata.table_name}`, {...body.logMetadata});
|
||||||
|
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||||
|
this.logger.debug(`Payload: ${JSON.stringify(body.table_metadata)}`, {...body.logMetadata});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, status } = await axios.post(endpoint, {...body.table_metadata});
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Table metadata created successfully with status ${status} for table ${body.table_metadata.table_name}`,
|
||||||
|
{...body.logMetadata},
|
||||||
|
);
|
||||||
|
return data.id;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create table metadata for table ${body.table_metadata.table_name} failed with status ${
|
||||||
|
error.response?.status
|
||||||
|
} because of ${JSON.stringify(error.response?.data) || error.message}`, {...body.logMetadata});
|
||||||
|
throw new Error(error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createColumnMetadata(body: any): Promise<number[]> {
|
||||||
|
const nimbusUrl = this._getNimbusUrl(body);
|
||||||
|
this.logger.info(`Nimbus URL: ${nimbusUrl}`, body.logMetadata);
|
||||||
|
const endpoint = `${nimbusUrl}/api/catalog/column-metadata/`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.info(`Creating column metadata for table ${body.column_metadata.table_name}`, {...body.logMetadata});
|
||||||
|
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||||
|
this.logger.debug(`Payload: ${JSON.stringify(body.column_metadata)}`, {...body.logMetadata});
|
||||||
|
const { data, status } = await axios.post(endpoint, body.column_metadata);
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Column metadata created successfully with status ${status} for table ${body.column_metadata.table_name}`,
|
||||||
|
{...body.logMetadata},
|
||||||
|
);
|
||||||
|
return data.map((column) => column.id);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create column metadata failed with status for table ${body.column_metadata.table_name} ${
|
||||||
|
error.response?.status
|
||||||
|
} because of ${error.response?.data || error.message}`, {...body.logMetadata});
|
||||||
|
throw new Error(error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDataPreview(body: any): Promise<number> {
|
||||||
|
const nimbusUrl = this._getNimbusUrl(body);
|
||||||
|
this.logger.info(`Nimbus URL: ${nimbusUrl}`, {...body.logMetadata});
|
||||||
|
const endpoint = `${nimbusUrl}/api/catalog/data-preview/`;
|
||||||
|
|
||||||
|
this.logger.info(`Creating data preview for table ${body.data_preview.table_name}`, {...body.logMetadata});
|
||||||
|
this.logger.info(`Using endpoint: ${endpoint}`, {...body.logMetadata});
|
||||||
|
this.logger.debug(`Payload: ${JSON.stringify(body.data_preview)}`, {...body.logMetadata});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, status } = await axios.post(endpoint, body.data_preview);
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`Data preview created successfully with status ${status} for table ${body.data_preview.table_name}`,
|
||||||
|
{...body.logMetadata},
|
||||||
|
);
|
||||||
|
return data.id;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create data preview for table ${body.data_preview.table_name} failed with status ${
|
||||||
|
error.response?.status
|
||||||
|
} because of ${error.response?.data || error.message}`,
|
||||||
|
{...body.logMetadata},
|
||||||
|
);
|
||||||
|
throw new Error(error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catalogDatasetItem(table_metadata_id: number, metadata: Metadata) {
|
||||||
|
const customer_name_raw = metadata.get('customer_name');
|
||||||
|
|
||||||
|
const customer_name = customer_name_raw?.[0]?.toString();
|
||||||
|
if (!customer_name) {
|
||||||
|
throw new BadRequestException('Customer name not found in metadata');
|
||||||
|
}
|
||||||
|
const res = await lastValueFrom(
|
||||||
|
this.platformWriteService.CatalogDataAssets(
|
||||||
|
{
|
||||||
|
data_assets: [
|
||||||
|
{
|
||||||
|
data_asset_id: table_metadata_id.toString(),
|
||||||
|
customer_name: customer_name,
|
||||||
|
data_asset_type: 'dataset',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private capitalizeFirst(str: string): string {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async updateDataAssetDocumentation(nimbusId: string, documentation: string, metadata: Metadata): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!documentation) {
|
||||||
|
this.logger.warn(`Documentation is undefined for ${nimbusId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = {
|
||||||
|
documentation: documentation
|
||||||
|
};
|
||||||
|
|
||||||
|
await lastValueFrom(
|
||||||
|
this.catalogWriteService.UpdateDataAsset(
|
||||||
|
{ id: nimbusId, changes: JSON.stringify(changes) },
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info(`Documentation updated successfully for ${nimbusId}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error updating documentation for ${nimbusId}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAiDocumentation(dataAssetId: string, metadata: Metadata, user?: any): Promise<string> {
|
||||||
|
this.logger.info(`Generating AI documentation for data asset: ${dataAssetId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customer_id = metadata.get('customer_id')?.[0]?.toString();
|
||||||
|
|
||||||
|
// 1. Buscar metadados do data asset
|
||||||
|
const dataAsset = await this.getOneDataAsset({
|
||||||
|
id: dataAssetId,
|
||||||
|
customer_id,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dataAsset || !dataAsset.data_asset || !dataAsset.data_asset.nimbus_id) {
|
||||||
|
throw new Error("Nimbus ID not found in data asset metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
const nimbusId = dataAsset.data_asset.nimbus_id.toString();
|
||||||
|
|
||||||
|
// 2. Buscar informações das colunas
|
||||||
|
let columnsData = null;
|
||||||
|
try {
|
||||||
|
columnsData = await this.getDatasetColumnsMetadata(dataAssetId, metadata);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to get column metadata: ${error.message}`);
|
||||||
|
columnsData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.1. Fallback para dados das colunas
|
||||||
|
const dataAssetWithColumns = dataAsset as any;
|
||||||
|
if (!columnsData && dataAssetWithColumns.columns && Array.isArray(dataAssetWithColumns.columns)) {
|
||||||
|
columnsData = { columns: dataAssetWithColumns.columns };
|
||||||
|
} else if (!columnsData) {
|
||||||
|
const numColumns = parseInt(dataAsset.data_asset.num_columns) || 0;
|
||||||
|
if (numColumns > 0) {
|
||||||
|
columnsData = {
|
||||||
|
columns: Array.from({ length: numColumns }, (_, i) => ({
|
||||||
|
name: `COLUMN_${i + 1}`,
|
||||||
|
type: 'UNKNOWN',
|
||||||
|
description: 'Column information not available',
|
||||||
|
nullable: 'UNKNOWN'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Buscar preview dos dados (opcional)
|
||||||
|
let dataPreview = [];
|
||||||
|
try {
|
||||||
|
dataPreview = await this.getDatasetPreview(nimbusId, metadata);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to get data preview: ${error.message}`);
|
||||||
|
dataPreview = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Formatar dados e chamar Autodrive
|
||||||
|
const combinedText = this.formatDataForAI(dataAsset, dataPreview, columnsData);
|
||||||
|
const documentation = await this.callAutodriveForDocumentation(combinedText, user?.access_token);
|
||||||
|
|
||||||
|
// 5. Salvar a documentação
|
||||||
|
const createDataDocsPayload = {
|
||||||
|
table_id: dataAsset.data_asset.nimbus_id,
|
||||||
|
docs: documentation,
|
||||||
|
info: {
|
||||||
|
customer: user.customer_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.createDataDocs(createDataDocsPayload);
|
||||||
|
this.logger.info(`AI documentation generated and saved successfully for ${dataAssetId}, length: ${documentation.length} chars`);
|
||||||
|
|
||||||
|
return documentation;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error generating AI documentation for ${dataAssetId}: ${error.message}`);
|
||||||
|
|
||||||
|
let errorMessage = error.message;
|
||||||
|
if (error.response) {
|
||||||
|
errorMessage = `API Error ${error.response.status}: ${error.response.data?.detail || error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
`Failed to generate AI documentation: ${errorMessage}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDataForAI(dataAsset: any, dataPreview: any[], columnsData?: any): string {
|
||||||
|
let combinedText = '';
|
||||||
|
|
||||||
|
// Seguir exatamente o padrão do Python: chamar json_to_free_text para cada objeto separadamente
|
||||||
|
|
||||||
|
// 1. metadata_data (dataAsset)
|
||||||
|
combinedText += this.jsonToFreeText(dataAsset);
|
||||||
|
|
||||||
|
// 2. info_data (columns) - usar columnsData se disponível, senão tentar dataAsset.columns
|
||||||
|
if (columnsData) {
|
||||||
|
// Adicionar aviso se os dados das colunas são limitados
|
||||||
|
if (columnsData.columns && columnsData.columns.some((col: any) => col.name?.startsWith('COLUMN_'))) {
|
||||||
|
combinedText += 'WARNING: Column information is limited or unavailable. Generated column names are placeholders.\n\n';
|
||||||
|
}
|
||||||
|
combinedText += this.jsonToFreeText(columnsData);
|
||||||
|
} else if (dataAsset.columns && Array.isArray(dataAsset.columns)) {
|
||||||
|
const columnsDataFromAsset = { columns: dataAsset.columns };
|
||||||
|
combinedText += this.jsonToFreeText(columnsDataFromAsset);
|
||||||
|
} else {
|
||||||
|
// Se não há dados das colunas, informar explicitamente
|
||||||
|
combinedText += 'WARNING: No column metadata available. Cannot generate detailed table structure.\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. preview_data
|
||||||
|
if (dataPreview && dataPreview.length > 0) {
|
||||||
|
const previewData = { preview: dataPreview };
|
||||||
|
combinedText += this.jsonToFreeText(previewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private jsonToFreeText(jsonObj: any, indentLevel: number = 0): string {
|
||||||
|
if (!jsonObj || typeof jsonObj !== 'object') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
const indent = ' '.repeat(indentLevel);
|
||||||
|
|
||||||
|
// Processa primeiro os dados geográficos se existirem (igual ao Python)
|
||||||
|
const geoKeys = GEOGRAPHIC_KEYS;
|
||||||
|
|
||||||
|
// Adiciona uma seção especial para dados de preview se existirem (igual ao Python)
|
||||||
|
if ('preview' in jsonObj) {
|
||||||
|
text += "=== PREVIEW DATA START ===\n";
|
||||||
|
if (Array.isArray(jsonObj['preview'])) {
|
||||||
|
// Primeiro, vamos procurar por colunas geográficas
|
||||||
|
const geoColumns = [];
|
||||||
|
if (jsonObj['preview'].length > 0 && typeof jsonObj['preview'][0] === 'object') {
|
||||||
|
for (const key of Object.keys(jsonObj['preview'][0])) {
|
||||||
|
const keyLower = key.toLowerCase();
|
||||||
|
if (geoKeys.some(geoTerm => keyLower.includes(geoTerm))) {
|
||||||
|
geoColumns.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se encontramos colunas geográficas, vamos destacá-las
|
||||||
|
if (geoColumns.length > 0) {
|
||||||
|
text += "GEOGRAPHIC DATA FOUND IN COLUMNS:\n";
|
||||||
|
for (const col of geoColumns) {
|
||||||
|
text += `=== Column: ${col} ===\n`;
|
||||||
|
const values = jsonObj['preview'].map(row => String(row[col] || '')).filter(v => v);
|
||||||
|
text += "Values: " + values.join(", ") + "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agora processamos todos os dados normalmente
|
||||||
|
for (const row of jsonObj['preview']) {
|
||||||
|
text += this.jsonToFreeText(row, indentLevel + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text += this.jsonToFreeText(jsonObj['preview'], indentLevel + 1);
|
||||||
|
}
|
||||||
|
text += "=== PREVIEW DATA END ===\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processa o resto dos dados (igual ao Python)
|
||||||
|
for (const [key, value] of Object.entries(jsonObj)) {
|
||||||
|
if (key === 'preview') { // Skip preview here since we handled it above
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
text += `${indent}${this.capitalize(key)}:\n`;
|
||||||
|
for (const item of value) {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
text += this.jsonToFreeText(item, indentLevel + 1);
|
||||||
|
} else {
|
||||||
|
text += `${indent} - ${item}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text += `${indent}${this.capitalize(key)}:\n`;
|
||||||
|
text += this.jsonToFreeText(value, indentLevel + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Destaca campos geográficos
|
||||||
|
if (geoKeys.includes(key.toLowerCase() as any)) {
|
||||||
|
text += `${indent}!!! GEOGRAPHIC DATA !!! ${this.capitalize(key)}: ${value}\n`;
|
||||||
|
} else {
|
||||||
|
text += `${indent}${this.capitalize(key)}: ${value}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text += '\n';
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private capitalize(str: string): string {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private async callAutodriveForDocumentation(combinedText: string, userAccessToken?: string): Promise<string> {
|
||||||
|
const credentials = this.getAutodriveCredentials();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Criar dataset temporário com os dados reais
|
||||||
|
const datasetId = await this.createTemporaryDataset(combinedText, userAccessToken);
|
||||||
|
|
||||||
|
// Fazer pergunta ao dataset
|
||||||
|
const documentation = await this.askQuestionToDataset(datasetId, credentials);
|
||||||
|
|
||||||
|
return documentation;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error calling Autodrive API: ${error.message}`);
|
||||||
|
throw new Error(`Autodrive API call failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAutodriveCredentials(): AutodriveCredentials {
|
||||||
|
const fallbackCredentials = process.env.AUTODRIVE_KEY || process.env.AUTO_DRIVE_KEY;
|
||||||
|
|
||||||
|
// Verificar se as credenciais principais estão disponíveis
|
||||||
|
if (AUTODRIVE_CONSTANTS.USERNAME && AUTODRIVE_CONSTANTS.PASSWORD) {
|
||||||
|
return {
|
||||||
|
username: AUTODRIVE_CONSTANTS.USERNAME,
|
||||||
|
password: AUTODRIVE_CONSTANTS.PASSWORD,
|
||||||
|
baseUrl: AUTODRIVE_CONSTANTS.BASE_URL,
|
||||||
|
model: AUTODRIVE_CONSTANTS.DEFAULT_MODEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback para credenciais alternativas
|
||||||
|
if (fallbackCredentials) {
|
||||||
|
const cleanKey = fallbackCredentials.startsWith("'") && fallbackCredentials.endsWith("'")
|
||||||
|
? fallbackCredentials.slice(1, -1)
|
||||||
|
: fallbackCredentials;
|
||||||
|
return {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
baseUrl: AUTODRIVE_CONSTANTS.BASE_URL,
|
||||||
|
model: AUTODRIVE_CONSTANTS.DEFAULT_MODEL,
|
||||||
|
authHeader: `Basic ${cleanKey}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback de emergência removido por segurança
|
||||||
|
// Configure as variáveis de ambiente necessárias
|
||||||
|
|
||||||
|
throw new Error('No authentication credentials available. Please configure AUTODRIVE_USERNAME and AUTODRIVE_PASSWORD in your .env file');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async askQuestionToDataset(datasetId: string, credentials: AutodriveCredentials): Promise<string> {
|
||||||
|
const authHeader = credentials.authHeader || this.createAuthHeader(credentials);
|
||||||
|
|
||||||
|
const askPayload: AutodriveAskPayload = {
|
||||||
|
question: AI_DOCUMENTATION_PROMPT.trim(),
|
||||||
|
fetch_k: AI_DOCUMENTATION_CONFIG.FETCH_K,
|
||||||
|
k: AI_DOCUMENTATION_CONFIG.K,
|
||||||
|
model: credentials.model
|
||||||
|
};
|
||||||
|
|
||||||
|
const askHeaders = {
|
||||||
|
'Authorization': authHeader,
|
||||||
|
...AUTODRIVE_CONSTANTS.HEADERS
|
||||||
|
};
|
||||||
|
|
||||||
|
const askResponse = await axios.post<AutodriveAskResponse>(
|
||||||
|
`${credentials.baseUrl}/dataset/${datasetId}/ai_question`,
|
||||||
|
askPayload,
|
||||||
|
{
|
||||||
|
headers: askHeaders,
|
||||||
|
timeout: AUTODRIVE_CONSTANTS.ASK_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verificar se a resposta contém a documentação diretamente
|
||||||
|
if (askResponse.data?.answer) {
|
||||||
|
const documentation = askResponse.data.answer;
|
||||||
|
this.logger.info(`Documentation generated successfully - Length: ${documentation.length} characters`);
|
||||||
|
return documentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não contém a resposta diretamente, verificar se tem question_id para buscar
|
||||||
|
if (askResponse.data?.question_id) {
|
||||||
|
const questionId = askResponse.data.question_id;
|
||||||
|
|
||||||
|
// Buscar a resposta da pergunta
|
||||||
|
const answerResponse = await axios.get<AutodriveAnswerResponse>(
|
||||||
|
`${credentials.baseUrl}/dataset/${datasetId}/ai_question/${questionId}`,
|
||||||
|
{
|
||||||
|
headers: { 'Authorization': authHeader },
|
||||||
|
timeout: AUTODRIVE_CONSTANTS.ANSWER_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Se a resposta ainda está sendo processada, fazer polling contínuo
|
||||||
|
if (answerResponse.data?.status === 'started' || !answerResponse.data?.answer) {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 12;
|
||||||
|
const pollInterval = 10000;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pollResponse = await axios.get<AutodriveAnswerResponse>(
|
||||||
|
`${credentials.baseUrl}/dataset/${datasetId}/ai_question/${questionId}`,
|
||||||
|
{
|
||||||
|
headers: { 'Authorization': authHeader },
|
||||||
|
timeout: AUTODRIVE_CONSTANTS.ANSWER_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Se a resposta está pronta, retornar
|
||||||
|
if (pollResponse.data?.answer && pollResponse.data?.status !== 'started') {
|
||||||
|
const documentation = pollResponse.data.answer;
|
||||||
|
this.logger.info(`Documentation generated successfully after ${attempts} attempts - Length: ${documentation.length} characters`);
|
||||||
|
return documentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda está processando, continuar o loop
|
||||||
|
if (pollResponse.data?.status === 'started') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se falhou, mostrar erro e quebrar
|
||||||
|
if (pollResponse.data?.status === 'failed') {
|
||||||
|
throw new Error(`Answer processing failed: ${pollResponse.data.status_reason || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se houve erro ou status inesperado, quebrar o loop
|
||||||
|
break;
|
||||||
|
|
||||||
|
} catch (pollError) {
|
||||||
|
this.logger.error(`Polling attempt ${attempts} failed: ${pollError.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Answer still not ready after ${maxAttempts} polling attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answerResponse.data || !answerResponse.data.answer) {
|
||||||
|
throw new Error(`Answer response invalid: ${JSON.stringify(answerResponse.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentation = answerResponse.data.answer;
|
||||||
|
this.logger.info(`Documentation generated successfully - Length: ${documentation.length} characters`);
|
||||||
|
return documentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Autodrive API response invalid: ${JSON.stringify(askResponse.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTemporaryDataset(combinedText: string, userAccessToken?: string): Promise<string> {
|
||||||
|
if (!AUTODRIVE_CONSTANTS.BASE_URL) {
|
||||||
|
throw new Error('AUTODRIVE_BASE_URL not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!combinedText || combinedText.trim().length === 0) {
|
||||||
|
throw new Error('combinedText is empty - cannot create dataset');
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = this.getAutodriveCredentials();
|
||||||
|
const authHeader = credentials.authHeader || this.createAuthHeader(credentials);
|
||||||
|
const tempFilePath = await this.createTempFile(combinedText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResponse = await this.uploadDataset(tempFilePath, authHeader);
|
||||||
|
const datasetId = uploadResponse.dataset_id;
|
||||||
|
|
||||||
|
await this.waitForDatasetReady(datasetId, authHeader, credentials.baseUrl);
|
||||||
|
return datasetId;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error creating temporary dataset: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await this.cleanupTempFile(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createTempFile(combinedText: string): Promise<string> {
|
||||||
|
const tempFileName = `temp_data_${Date.now()}.txt`;
|
||||||
|
const tempFilePath = `/tmp/${tempFileName}`;
|
||||||
|
|
||||||
|
const fs = await import('fs');
|
||||||
|
fs.writeFileSync(tempFilePath, combinedText, 'utf8');
|
||||||
|
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadDataset(tempFilePath: string, authHeader: string): Promise<AutodriveUploadResponse> {
|
||||||
|
const tempFileName = tempFilePath.split('/').pop() || 'temp_data.txt';
|
||||||
|
const fs = await import('fs');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('files', fs.createReadStream(tempFilePath), tempFileName);
|
||||||
|
formData.append('name', tempFileName);
|
||||||
|
|
||||||
|
const uploadResponse = await axios.post<AutodriveUploadResponse>(
|
||||||
|
`${AUTODRIVE_CONSTANTS.BASE_URL}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': authHeader,
|
||||||
|
...formData.getHeaders(),
|
||||||
|
},
|
||||||
|
timeout: 30000, // 30 segundos
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uploadResponse.data?.dataset_id) {
|
||||||
|
throw new Error('No dataset_id in upload response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadResponse.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForDatasetReady(datasetId: string, authHeader: string, autodriveBaseUrl: string): Promise<void> {
|
||||||
|
const maxAttempts = 12;
|
||||||
|
const pollInterval = 10000; // 10 segundos
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
const statusResponse = await axios.get<DatasetStatusResponse>(
|
||||||
|
`${autodriveBaseUrl}/dataset/${datasetId}`,
|
||||||
|
{
|
||||||
|
headers: { 'Authorization': authHeader },
|
||||||
|
timeout: 30000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (statusResponse.data?.status === 'success') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusResponse.data?.status === 'failed') {
|
||||||
|
throw new Error(`Dataset processing failed: ${statusResponse.data.status_reason || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusResponse.data?.status === 'processing') {
|
||||||
|
if (attempt < maxAttempts) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt === maxAttempts) {
|
||||||
|
throw new Error(`Dataset still not ready after ${maxAttempts} attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (statusError) {
|
||||||
|
this.logger.error(`Status check error on attempt ${attempt}: ${statusError.message}`);
|
||||||
|
if (attempt === maxAttempts) {
|
||||||
|
throw statusError;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanupTempFile(tempFilePath: string | null): Promise<void> {
|
||||||
|
if (!tempFilePath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
if (fs.existsSync(tempFilePath)) {
|
||||||
|
fs.unlinkSync(tempFilePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to remove temporary file: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createAuthHeader(credentials: AutodriveCredentials): string {
|
||||||
|
return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CatalogService };
|
export { CatalogService };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Constantes relacionadas ao Autodrive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const AUTODRIVE_CONSTANTS = {
|
||||||
|
// URLs e endpoints (apenas do ENV)
|
||||||
|
BASE_URL: process.env.BASE_URL_AUTODRIVE || process.env.AUTODRIVE_BASE_URL,
|
||||||
|
|
||||||
|
// Credenciais (apenas do ENV, sem fallback para segurança)
|
||||||
|
USERNAME: process.env.AUTODRIVE_USERNAME,
|
||||||
|
PASSWORD: process.env.AUTODRIVE_PASSWORD,
|
||||||
|
|
||||||
|
// Modelo padrão
|
||||||
|
DEFAULT_MODEL: process.env.AUTODRIVE_MODEL || "gpt-4o",
|
||||||
|
|
||||||
|
// Timeouts (em milissegundos)
|
||||||
|
ASK_TIMEOUT: 120000,
|
||||||
|
ANSWER_TIMEOUT: 180000,
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
HEADERS: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chaves geográficas para detecção de dados de localização
|
||||||
|
*/
|
||||||
|
export const GEOGRAPHIC_KEYS = [
|
||||||
|
'country', 'countries', 'city', 'cities',
|
||||||
|
'region', 'regions', 'location', 'state',
|
||||||
|
'states', 'address'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Países comuns para detecção automática
|
||||||
|
*/
|
||||||
|
export const COMMON_COUNTRIES = [
|
||||||
|
'brazil', 'brasil', 'usa', 'united states',
|
||||||
|
'canada', 'mexico', 'argentina', 'chile',
|
||||||
|
'colombia'
|
||||||
|
] as const;
|
||||||
@@ -320,3 +320,11 @@ export class BatchRemoveRlsRulesRequest {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
id_rls?: string;
|
id_rls?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssetReporter = {
|
||||||
|
id: string;
|
||||||
|
display_name: string;
|
||||||
|
data_asset_type: string;
|
||||||
|
created_at: string;
|
||||||
|
tags: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export class PiiDto {
|
||||||
|
database_name: string;
|
||||||
|
table_schema: string;
|
||||||
|
table_name: string;
|
||||||
|
column_name: string;
|
||||||
|
data_type: string;
|
||||||
|
pii_rules: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
|
||||||
|
*/
|
||||||
|
export const AI_DOCUMENTATION_PROMPT = `crie uma documentação em Portugues, Ingles e Espanhol seguindo essas instruções
|
||||||
|
1. Persona: como profissional de governança e engenharia de dados
|
||||||
|
2. Tarefa: ao receber as informações da tabela criar uma documentação com o seguinte escopo
|
||||||
|
**A primeira linha do documento tem que conter a seguinte informação: ## Document languages: EN / BR / ES
|
||||||
|
**A segunda linha tem que obrigatoriamente conter a escrita Table: nome da tabela
|
||||||
|
**A terceira linha tem que obrigatoriamente conter a escrita Table Schema: nome do table schema
|
||||||
|
**DIRETRIZ CRUCIAL DE CONSISTÊNCIA E COMPLETUDE DE SCHEMA:**
|
||||||
|
**1. Fonte Exclusiva de Metadados:** O 'Table Schema' definido na linha acima é a ÚNICA fonte de verdade para o schema dos dados a serem documentados. TODAS as informações subsequentes, especialmente na seção 'Estrutura da Tabela' (incluindo a lista de colunas, seus nomes, tipos de dados, descrições e exemplos) DEVEM ser extraídas EXCLUSIVAMENTE de metadados que correspondem a ESTE 'Table Schema'. Se os dados de entrada que você recebeu contiverem informações para a mesma tabela ou colunas mas de schemas diferentes (ex: um schema 'bronze' e um 'silver'), você DEVE IGNORAR TOTALMENTE as informações dos schemas divergentes para esta tarefa de documentação e utilizar APENAS as do 'Table Schema' aqui especificado.
|
||||||
|
**2. Listagem Completa de Colunas:** Sua principal tarefa na seção 'Estrutura da Tabela' é identificar e listar TODAS as colunas que pertencem ao 'Table Schema' especificado. Verifique nos dados de entrada fornecidos se há uma indicação explícita do número total de colunas para esta tabela neste schema (por exemplo, um campo como 'Num_columns' ou similar nos metadados da tabela). Você deve se esforçar para listar exatamente essa quantidade de colunas. Se essa contagem não estiver disponível, liste todas as colunas que você puder identificar como pertencentes exclusivamente a este 'Table Schema'. A completude em relação ao schema especificado é essencial.
|
||||||
|
|
||||||
|
**Depois de "Estrutura da tablea", incluir a mensagem "Este documento foi gerado por IA", traduzida corretamente para cada idioma.**
|
||||||
|
**Obrigatoriamente:Após finalizar a versão em Inglês, começar a versão em Português** **Após finalizar a versão em Português, começar a versão em Espanhol** **Antes de começar cada versão, colocar um título como:** - \`## English Version\` (para inglês)
|
||||||
|
- \`## Versão em Português\` (para português)
|
||||||
|
- \`## Versión en Español\` (para espanhol)
|
||||||
|
- Descrição: fornece uma visão geral do ativo de dados,
|
||||||
|
destacando seu propósito e principal funcionalidade.
|
||||||
|
Esta sessão resume o conteúdo e o objetivo do ativo, ajudando os usuários a entender rapidamente o que o ativo representa
|
||||||
|
e como pode ser utilizado em suas análises e decisões.
|
||||||
|
- Sugestão de Domínio de Dados:
|
||||||
|
Analise cuidadosamente os dados da tabela e sugira o domínio mais apropriado. Inclua:
|
||||||
|
- Domínio Sugerido: [Nome do domínio]
|
||||||
|
- Motivo: [Explicação breve sobre porque a tabela pertence a este domínio]
|
||||||
|
- Observações: [Qualquer observação adicional relevante]
|
||||||
|
|
||||||
|
Exemplos de Domínios de Dados para referência:
|
||||||
|
- Financeiro: Dados sobre transações, receitas, despesas, etc.
|
||||||
|
- Recursos Humanos: Dados sobre funcionários, cargos, salários, etc.
|
||||||
|
- Produtos: Dados sobre produtos, categorias, preços, etc.
|
||||||
|
- Fornecedores: Dados sobre fornecedores, produtos fornecidos, localizações, etc.
|
||||||
|
- Marketing: Dados sobre campanhas, leads, conversões, etc.
|
||||||
|
- Vendas: Dados sobre vendas, clientes, produtos vendidos, etc.
|
||||||
|
- Operações: Dados sobre processos, logística, produção, etc.
|
||||||
|
- Clientes: Dados sobre clientes, interações, histórico, etc.
|
||||||
|
-Tags Sugeridas:
|
||||||
|
A IA deve gerar tags relevantes **com base nos dados da tabela**.
|
||||||
|
- **IMPORTANTE: Analise cuidadosamente os dados de preview da tabela (PREVIEW DATA) para encontrar países. Procure em todas as colunas por nomes de países, cidades ou regiões.**
|
||||||
|
- **Garanta que as tags estejam separadas por espaços vazios, todas na mesma linha, exemplo: #marketing #sales #australia #canada, limitar até 3 países que mais aparecem** - **Os países DEVEM ser extraídos dos dados de preview da tabela. Procure em colunas como City, Country, Region, Location, etc.** - Por que esta tabela é interessante:
|
||||||
|
Nesta sessão, é destacada a importância do ativo, explicando como ele pode ser útil para os usuários.
|
||||||
|
São abordadas as formas como o ativo pode melhorar a tomada de decisões, identificar padrões relevantes ou fornecer insights valiosos.
|
||||||
|
O objetivo é ressaltar a utilidade prática e o impacto positivo que o ativo pode ter em suas atividades.
|
||||||
|
- Análises potencialmente úteis feitas com esses dados:
|
||||||
|
Aqui são listadas algumas das análises que podem ser realizadas com o ativo de dados. Inclui sugestões de dashboards,
|
||||||
|
relatórios ou outros tipos de análises que aproveitam as informações fornecidas pelo ativo.
|
||||||
|
O objetivo é oferecer maneiras de utilizar os dados para obter insights valiosos e apoiar a tomada de decisões informadas.
|
||||||
|
- Links Úteis:
|
||||||
|
Os Links Úteis oferecem recursos adicionais relacionados ao ativo de dados, incluindo guias,
|
||||||
|
artigos ou outras fontes de informação que podem ajudar os usuários a compreender melhor o ativo e suas aplicações. Além disso,
|
||||||
|
inclui um link rápido dentro da Dadosfera para ativos relacionados diretamente com o ativo em questão, facilitando a navegação entre os ativos.
|
||||||
|
- Estrutura da Tabela:
|
||||||
|
A Estrutura da Tabela detalha TODAS as colunas e os dados disponíveis no ativo, conforme pertencentes ao 'Table Schema' principal definido no início deste documento.
|
||||||
|
**Instrução Detalhada para Estrutura da Tabela:**
|
||||||
|
Siga rigorosamente estes passos:
|
||||||
|
1. Identifique nos dados de entrada (metadados da tabela e das colunas) todas as colunas que pertencem EXCLUSIVAMENTE ao 'Table Schema' especificado no cabeçalho deste documento. Se houver uma contagem de colunas (ex: 'Num_columns') para este schema específico, assegure-se de listar essa quantidade.
|
||||||
|
2. Para CADA uma dessas colunas identificadas, formate a saída da seguinte maneira, **SEM utilizar NENHUM marcador de lista (como traços ou asteriscos) no início de cada entrada de coluna**. Cada coluna deve ser apresentada como um bloco de texto. Inclua uma linha em branco entre a documentação de cada coluna para separação visual.
|
||||||
|
- Apresente o NOME_DA_COLUNA em maiúsculas, seguido pelo (TIPO_DE_DADO_EXTRAÍDO_DOS_METADADOS_DO_SCHEMA_CORRETO) entre parênteses.
|
||||||
|
- O **NOME_DA_COLUNA (TIPO_DE_DADO_EXTRAÍDO_DOS_METADADOS_DO_SCHEMA_CORRETO)** deve estar na primeira linha do bloco da coluna e **inteiramente em negrito**.
|
||||||
|
- Na linha seguinte, a etiqueta "**Descrição:**" deve estar **em negrito**, seguida pelo texto da descrição da coluna.
|
||||||
|
- Na linha seguinte à descrição, a etiqueta "**Exemplo:**" deve estar **em negrito**, seguida pelo valor do exemplo. Se o exemplo for um valor literal ou código, formate-o entre crases (\`) se apropriado.
|
||||||
|
- Se houver informações adicionais relevantes (como "Valores Possíveis:", "Observações:", etc.), coloque a etiqueta correspondente **em negrito** em uma nova linha, seguida pelo seu texto.
|
||||||
|
|
||||||
|
Este documento foi gerado por IA.
|
||||||
|
|
||||||
|
NOME_COLUNA_1 (TIPO_DADO_SCHEMA_CORRETO_1):
|
||||||
|
Descrição: [Descrição da coluna 1, do schema correto]
|
||||||
|
Exemplo: \`[Exemplo de valor para coluna 1, do schema correto]\`
|
||||||
|
|
||||||
|
NOME_COLUNA_2 (TIPO_DADO_SCHEMA_CORRETO_2):
|
||||||
|
Descrição: [Descrição da coluna 2, do schema correto]
|
||||||
|
Exemplo: \`[Exemplo de valor para coluna 2, do schema correto]\`
|
||||||
|
|
||||||
|
(continue este formato com início de cada coluna para TODAS as colunas do 'Table Schema' especificado, garanta com que NUNCA tenha TRAÇO OU PONTO no inicio)
|
||||||
|
|
||||||
|
3. Contexto : O usuário ira cadastrar um ativo de dados na nossa plataforma e para ter um bom catalogo ele ira querer gerar a documentação padronizada mas explicativa e
|
||||||
|
automática
|
||||||
|
4. Restrições : A documentação deve seguir obrigatoriamente o mesmo padrão principalmente na parte de estrutura de dados
|
||||||
|
5. Objetivo: O principal objetivo é gerar uma documentação acessível, clara,
|
||||||
|
automática e padronizada para os usuários que desejem cadastrar um ativo de dados na plataforma`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configurações para a geração de documentação com IA
|
||||||
|
*/
|
||||||
|
export const AI_DOCUMENTATION_CONFIG = {
|
||||||
|
FETCH_K: 250,
|
||||||
|
K: 100,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Req,
|
||||||
|
UseFilters,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiHeaders,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import {
|
||||||
|
IColumnsMetadataResponse,
|
||||||
|
IDocsResponse,
|
||||||
|
IPreviewResponse,
|
||||||
|
|
||||||
|
} from '../dtos';
|
||||||
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
|
import { ShareService } from './share.service';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('Catalog')
|
||||||
|
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
|
||||||
|
@Controller('catalog/data-asset/share')
|
||||||
|
@UseFilters(new GrpcToHttpExceptionFilter())
|
||||||
|
export class ShareController {
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
private catalogShareService: ShareService,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
async getShareDataAsset(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Req() request: Request
|
||||||
|
) {
|
||||||
|
this.logger.info(`GET //:id`);
|
||||||
|
return await this.catalogShareService.getOneDataAssetPublic(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id/columns-metadata')
|
||||||
|
async getShareDataAssetColumnsMetadata(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Req() request: Request
|
||||||
|
): Promise<IColumnsMetadataResponse> {
|
||||||
|
this.logger.info(`GET /:id/columns-metadata`);
|
||||||
|
|
||||||
|
const columns_metadata =
|
||||||
|
await this.catalogShareService.getDatasetColumnsMetadata(id, request);
|
||||||
|
|
||||||
|
|
||||||
|
return { columns_metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id/preview')
|
||||||
|
async getShareDataAssetPreview(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Req() request: Request
|
||||||
|
): Promise<IPreviewResponse> {
|
||||||
|
this.logger.info(`GET /:id/preview`);
|
||||||
|
const preview = await this.catalogShareService.getDatasetPreview(id, request);
|
||||||
|
|
||||||
|
return { preview };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id/docs')
|
||||||
|
async getShareDataAssetDocs(
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Req() request: Request
|
||||||
|
): Promise<IDocsResponse> {
|
||||||
|
this.logger.info(`GET /:id/docs`);
|
||||||
|
const docs = await this.catalogShareService.getDataDocs(id, request);
|
||||||
|
|
||||||
|
return { docs };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { CatalogClientConfiguration } from "../catalog-client";
|
||||||
|
import { ClientsModule } from "@nestjs/microservices";
|
||||||
|
import { RolesModule } from "src/modules/roles/roles.module";
|
||||||
|
import { UsersModule } from "src/modules/users/users.module";
|
||||||
|
import { CustomersModule } from "src/modules/customers/customers.module";
|
||||||
|
import { ShareMetadataModule } from "src/modules/share-metadata/share-metadata.module";
|
||||||
|
import { ShareController } from "./share.controller";
|
||||||
|
import DadosferaLogger from "@dadosfera/dadosfera-logs";
|
||||||
|
import { ShareService } from "./share.service";
|
||||||
|
import { MixpanelModule } from "src/modules/mixpanel/mixpanel.module";
|
||||||
|
import { AuthModule } from "src/modules/auth/auth.module";
|
||||||
|
|
||||||
|
const client = new CatalogClientConfiguration();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ClientsModule.register([client.providerOptions]),
|
||||||
|
UsersModule,
|
||||||
|
RolesModule,
|
||||||
|
CustomersModule,
|
||||||
|
ShareMetadataModule,
|
||||||
|
MixpanelModule,
|
||||||
|
AuthModule
|
||||||
|
],
|
||||||
|
controllers: [ShareController],
|
||||||
|
providers: [ShareService, DadosferaLogger],
|
||||||
|
exports: [ShareModule],
|
||||||
|
})
|
||||||
|
export class ShareModule {}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import {
|
||||||
|
ProtoServices,
|
||||||
|
ReadService,
|
||||||
|
} from '@dadosfera/protospack-v2/dist/lib/Catalog';
|
||||||
|
import {
|
||||||
|
ForbiddenException,
|
||||||
|
Inject,
|
||||||
|
NotFoundException,
|
||||||
|
OnModuleInit,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CatalogClientConfiguration } from '../catalog-client';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { UsersService } from 'src/modules/users/users.service';
|
||||||
|
import { RolesService } from 'src/modules/roles/roles.service';
|
||||||
|
import { RequestUser } from 'src/decorators/user.decorator';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { ShareMetadataService } from 'src/modules/share-metadata/share-metadata.service';
|
||||||
|
import { isJWT } from 'class-validator';
|
||||||
|
import { MixpanelService } from 'src/modules/mixpanel/mixpanel.service';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { AuthClientService } from 'src/modules/auth/auth.service';
|
||||||
|
|
||||||
|
|
||||||
|
export class ShareService implements OnModuleInit {
|
||||||
|
catalogReadService: ReadService.CatalogReadServices;
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
@Inject(CatalogClientConfiguration.name)
|
||||||
|
private readonly grpcClient: ClientGrpc,
|
||||||
|
private readonly userService: UsersService,
|
||||||
|
private readonly roleService: RolesService,
|
||||||
|
private readonly shareMetadataService: ShareMetadataService,
|
||||||
|
private readonly mixpanelService: MixpanelService,
|
||||||
|
private authClient: AuthClientService,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.catalogReadService =
|
||||||
|
this.grpcClient.getService<ReadService.CatalogReadServices>(
|
||||||
|
ProtoServices.CatalogReadServices,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetColumnsMetadata(id: string, request: Request) {
|
||||||
|
const shareMetadata = await this.getShareMetadata(id, request);
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
customer_id: shareMetadata.customerId,
|
||||||
|
customer_name: shareMetadata.customerName,
|
||||||
|
});
|
||||||
|
const { columns_metadata } = await lastValueFrom(
|
||||||
|
this.catalogReadService.GetDatasetColumnsMetadata(
|
||||||
|
{ id: shareMetadata.assetId, type: undefined },
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const result = JSON.parse(columns_metadata);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDatasetPreview(id: string, request: Request) {
|
||||||
|
const shareMetadata = await this.getShareMetadata(id, request);
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
customer_id: shareMetadata.customerId,
|
||||||
|
customer_name: shareMetadata.customerName,
|
||||||
|
});
|
||||||
|
const { preview } = await lastValueFrom(
|
||||||
|
this.catalogReadService.GetDatasetPreview(
|
||||||
|
{ id: shareMetadata.assetId, type: undefined },
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const result = JSON.parse(preview);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOneDataAssetPublic(id: string, request: Request) {
|
||||||
|
this.logger.info("getOneDataAssetPublic: " + JSON.stringify({
|
||||||
|
id
|
||||||
|
}))
|
||||||
|
try {
|
||||||
|
const user = await this.getUserFromRequest(request);
|
||||||
|
|
||||||
|
const shareMetadata = await this.getShareMetadata(id, request);
|
||||||
|
|
||||||
|
const mixpanelTracker = {
|
||||||
|
asset: shareMetadata.assetId,
|
||||||
|
type: isJWT(id) ? 'assigned' : shareMetadata.type,
|
||||||
|
customer: shareMetadata.customerName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
await this.mixpanelService.track("share_page", user, request, mixpanelTracker);
|
||||||
|
} else {
|
||||||
|
await this.mixpanelService.trackShare(request, mixpanelTracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info("shareMetadata: " + JSON.stringify(shareMetadata))
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
customer_id: shareMetadata.customerId,
|
||||||
|
customer_name: shareMetadata.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data_asset } = await this.getOneDataAsset({
|
||||||
|
customer_id: shareMetadata.customerId,
|
||||||
|
id: shareMetadata.assetId,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
this.logger.info('found asset: ' + JSON.stringify(data_asset));
|
||||||
|
delete data_asset.p_roles;
|
||||||
|
delete data_asset.p_users;
|
||||||
|
data_asset.share_type = 'public';
|
||||||
|
if (data_asset.share_type !== 'public') throw new NotFoundException();
|
||||||
|
|
||||||
|
return { data_asset };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOneDataAsset(data: {
|
||||||
|
id: string;
|
||||||
|
customer_id: string;
|
||||||
|
metadata: Metadata;
|
||||||
|
}) {
|
||||||
|
const { customer_id, id, metadata } = data;
|
||||||
|
const { data_asset } = await lastValueFrom(
|
||||||
|
this.catalogReadService.GetOneDataAsset(
|
||||||
|
{ id, type: undefined },
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let asset = JSON.parse(data_asset);
|
||||||
|
asset = {
|
||||||
|
...asset,
|
||||||
|
p_roles: asset.roles,
|
||||||
|
p_users: asset.users,
|
||||||
|
};
|
||||||
|
asset = await this.getAssetsUsersAndRoles([asset], customer_id);
|
||||||
|
|
||||||
|
return { data_asset: asset[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataDocs(id: string, request: Request) {
|
||||||
|
const shareMetadata = await this.getShareMetadata(id, request);
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
customer_id: shareMetadata.customerId,
|
||||||
|
customer_name: shareMetadata.customerName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { documentation } = await lastValueFrom(
|
||||||
|
this.catalogReadService.GetDatasetDoc({ id, type: undefined }, metadata),
|
||||||
|
);
|
||||||
|
console.log(documentation);
|
||||||
|
const docs = JSON.parse(documentation);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAssetsUsersAndRoles(
|
||||||
|
data_assets: Array<any>,
|
||||||
|
customer_id: string,
|
||||||
|
) {
|
||||||
|
const { users: customer_users } =
|
||||||
|
await this.userService.findAllUsersByCustomerId(customer_id);
|
||||||
|
const { roles: customer_roles } = await this.roleService.roleSearch(
|
||||||
|
{},
|
||||||
|
{ customer_id },
|
||||||
|
);
|
||||||
|
return data_assets.map((data_asset) => {
|
||||||
|
const owner = customer_users.find(
|
||||||
|
(u) => u.id === data_asset.owner,
|
||||||
|
)?.username;
|
||||||
|
|
||||||
|
const roles = [];
|
||||||
|
const users = [];
|
||||||
|
for (const role_id of data_asset.roles) {
|
||||||
|
const role = customer_roles.find((r) => r.id === role_id);
|
||||||
|
if (role) roles.push({ id: role.id, name: role.name });
|
||||||
|
}
|
||||||
|
for (const user_id of data_asset.users) {
|
||||||
|
const user = customer_users.find((r) => r.id === user_id);
|
||||||
|
if (user) users.push({ id: user.id, username: user.username });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...data_asset,
|
||||||
|
roles,
|
||||||
|
users,
|
||||||
|
owner,
|
||||||
|
} as typeof data_asset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async getShareMetadata(id: string, request: Request) {
|
||||||
|
const metadata = PackTheMetadata({});
|
||||||
|
this.logger.info('GET share metadata')
|
||||||
|
const info = await this.shareMetadataService.get(id, metadata);
|
||||||
|
if (isJWT(id) && info ){
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.getUserFromRequest(request);
|
||||||
|
|
||||||
|
if (info.type === 'private') {
|
||||||
|
if (!user) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'You do not have permission to access this data asset.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_data_manager = user.permissions.includes(
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER.seqid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const is_get = user.permissions.includes(
|
||||||
|
PERMISSIONS_GROUPS.CATALOG.permissions.GET.seqid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_data_manager || is_get) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'You do not have permission to access this data asset.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUserFromRequest(request: Request): Promise<RequestUser | null> {
|
||||||
|
const accessToken = request.get('Authorization');
|
||||||
|
if (accessToken) {
|
||||||
|
const accessTokenDecoded: any = jwt.decode(accessToken, {
|
||||||
|
complete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { kid } = accessTokenDecoded.header;
|
||||||
|
|
||||||
|
const { keys } = await this.authClient.getPublicKeys();
|
||||||
|
|
||||||
|
const pemValue = keys.find((key) => key.kid === kid);
|
||||||
|
|
||||||
|
if (!pemValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(accessToken, pemValue.pem);
|
||||||
|
const accessTokenPayload = accessTokenDecoded.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
user_id: accessTokenPayload.user_id,
|
||||||
|
username: accessTokenPayload.username,
|
||||||
|
permissions: accessTokenPayload.permissions,
|
||||||
|
customer_id: accessTokenPayload.customer_id,
|
||||||
|
customer_name: accessTokenPayload.customer_name,
|
||||||
|
customer_tier: accessTokenPayload.customer_tier,
|
||||||
|
customer_modules: accessTokenPayload.customer_modules,
|
||||||
|
access_token: accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Tipos relacionados à geração de documentação com IA
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AutodriveCredentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
baseUrl: string;
|
||||||
|
model: string;
|
||||||
|
authHeader?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutodriveAskPayload {
|
||||||
|
question: string;
|
||||||
|
fetch_k: number;
|
||||||
|
k: number;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutodriveAskResponse {
|
||||||
|
answer?: string;
|
||||||
|
question_id?: string;
|
||||||
|
dataset_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutodriveAnswerResponse {
|
||||||
|
status: 'started' | 'success' | 'failed';
|
||||||
|
answer?: string;
|
||||||
|
status_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutodriveUploadResponse {
|
||||||
|
dataset_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatasetStatusResponse {
|
||||||
|
status: 'processing' | 'success' | 'failed';
|
||||||
|
status_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnData {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
nullable?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnsMetadata {
|
||||||
|
columns: ColumnData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataPreview {
|
||||||
|
preview: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormattedDataForAI {
|
||||||
|
dataAsset: any;
|
||||||
|
dataPreview: any[];
|
||||||
|
columnsData?: ColumnsMetadata;
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
DatabaseConnectionPropertiesDto,
|
DatabaseConnectionPropertiesDto,
|
||||||
} from '../connection/dtos/connection';
|
} from '../connection/dtos/connection';
|
||||||
import { RequestUser } from 'src/decorators/user.decorator';
|
import { RequestUser } from 'src/decorators/user.decorator';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConnectionTestService {
|
export class ConnectionTestService {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
UpdateConnectionDto,
|
UpdateConnectionDto,
|
||||||
} from './dtos/connection';
|
} from './dtos/connection';
|
||||||
import { CreateConnectionDto } from './dtos/connection';
|
import { CreateConnectionDto } from './dtos/connection';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
import { Language } from 'src/decorators/language.decorator';
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
|
|||||||
@@ -7,22 +7,26 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Inject,
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
UseFilters,
|
UseFilters,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOkResponse, ApiProduces, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiProduces, ApiTags } from '@nestjs/swagger';
|
||||||
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
import {
|
import {
|
||||||
Authenticated,
|
Authenticated,
|
||||||
RequireAllPermissions,
|
RequireAllPermissions,
|
||||||
|
RequireModule,
|
||||||
|
RequireSomePermission,
|
||||||
} from 'src/decorators/authentication.decorator';
|
} from 'src/decorators/authentication.decorator';
|
||||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
import { CustomersService } from './customers.service';
|
import { CustomersService } from './customers.service';
|
||||||
import { CustomerLinkRequest, CustomerLinksResponse } from './dtos/customers';
|
import { CustomerLinkRequest, CustomerLinksResponse } from './dtos/customers';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
import type { StringValue } from 'ms';
|
import type { StringValue } from 'ms';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
import { EnforceMfa } from './dtos/enforce-mfa';
|
||||||
|
|
||||||
@ApiTags('Customers')
|
@ApiTags('Customers')
|
||||||
@Controller('customers')
|
@Controller('customers')
|
||||||
@@ -38,6 +42,15 @@ export class CustomersController {
|
|||||||
this.logger = dadosferaLogger.logger;
|
this.logger = dadosferaLogger.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/mfa')
|
||||||
|
@Authenticated()
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@RequireModule(DADOSFERA_MODULES_KEYS.DANGER_ZONE)
|
||||||
|
async enableMfaEnforce(@Param('id') id: string, @Body() data: EnforceMfa) {
|
||||||
|
this.logger.info('enableMfaEnforce', { id });
|
||||||
|
return await this.customersService.enableEnforceMfa(id, data.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id/links')
|
@Get(':id/links')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@ApiOkResponse({ type: CustomerLinksResponse })
|
@ApiOkResponse({ type: CustomerLinksResponse })
|
||||||
@@ -91,4 +104,36 @@ export class CustomersController {
|
|||||||
const metadata = PackTheMetadata(user);
|
const metadata = PackTheMetadata(user);
|
||||||
return this.customersService.getMonitoringDashboardUrl(metadata);
|
return this.customersService.getMonitoringDashboardUrl(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('logs-dashboard')
|
||||||
|
@Authenticated()
|
||||||
|
@RequireAllPermissions(
|
||||||
|
PERMISSIONS_GROUPS.USERS.permissions.ADMIN,
|
||||||
|
)
|
||||||
|
@RequireModule(DADOSFERA_MODULES_KEYS.LOG_DASHBOARD)
|
||||||
|
async getCustomerMixPanelLogsDashboard(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
): Promise<{ url: string }> {
|
||||||
|
this.logger.info('getLogsDashboardUrl');
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
const result = await this.customersService.getLogsDashboardUrl(metadata);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('access-dashboard')
|
||||||
|
@Authenticated()
|
||||||
|
@RequireAllPermissions(
|
||||||
|
PERMISSIONS_GROUPS.USERS.permissions.ADMIN,
|
||||||
|
)
|
||||||
|
@RequireModule(DADOSFERA_MODULES_KEYS.ACCESS_DASHBOARD)
|
||||||
|
async getAccessDashboard(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
): Promise<{ url: string }> {
|
||||||
|
this.logger.info('getAccessDashboard');
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
const result = await this.customersService.getAccessDashboardUrl(user.customer_name, metadata);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
|
ForbiddenException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
||||||
@@ -33,6 +34,7 @@ import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
|||||||
// This function will accept any string, which may result in a bug.
|
// This function will accept any string, which may result in a bug.
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomersService implements OnModuleInit {
|
export class CustomersService implements OnModuleInit {
|
||||||
|
|
||||||
private customerService: CustomersProtoService;
|
private customerService: CustomersProtoService;
|
||||||
private logger: DadosferaLogger;
|
private logger: DadosferaLogger;
|
||||||
private pipelineReadService: ReadService.PipelineV2ReadService;
|
private pipelineReadService: ReadService.PipelineV2ReadService;
|
||||||
@@ -57,6 +59,14 @@ export class CustomersService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCustomer(customerId: string) {
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.customerService.CustomerFindOneById({
|
||||||
|
id: customerId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async getLinks(customerId: string) {
|
async getLinks(customerId: string) {
|
||||||
try {
|
try {
|
||||||
const result = await lastValueFrom(
|
const result = await lastValueFrom(
|
||||||
@@ -139,13 +149,14 @@ export class CustomersService implements OnModuleInit {
|
|||||||
// const decoded = jwt.decode(jwt_token, { complete: true });
|
// const decoded = jwt.decode(jwt_token, { complete: true });
|
||||||
return jwt_token;
|
return jwt_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMonitoringDashboardUrl(metadata: Metadata) {
|
async getMonitoringDashboardUrl(metadata: Metadata) {
|
||||||
logger.info('CustomersService - getMonitoringDashboardUrl');
|
logger.info('CustomersService - getMonitoringDashboardUrl');
|
||||||
|
|
||||||
const res = await lastValueFrom(
|
const res = await lastValueFrom(
|
||||||
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||||
{
|
{
|
||||||
dashboard_id: '45',
|
dashboard_id: '98',
|
||||||
exp: '15m',
|
exp: '15m',
|
||||||
metabase_customer_name: 'dadosferatech',
|
metabase_customer_name: 'dadosferatech',
|
||||||
},
|
},
|
||||||
@@ -156,4 +167,60 @@ export class CustomersService implements OnModuleInit {
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLogsDashboardUrl(metadata: Metadata) {
|
||||||
|
logger.info('CustomersService - getMixPanelLogsDashboardUrl');
|
||||||
|
|
||||||
|
const res = await lastValueFrom(
|
||||||
|
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||||
|
{
|
||||||
|
dashboard_id: '103',
|
||||||
|
exp: '15m',
|
||||||
|
metabase_customer_name: 'dadosferatech',
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
logger.info('Done');
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessDashboardUrl(customerName: string, metadata: Metadata) {
|
||||||
|
/*
|
||||||
|
* TODO(Refactor): dar um jeito de exibir o dash da sbm diferente dos outros customer
|
||||||
|
* pois o signicado de department para sbm significa as instituições do usuários
|
||||||
|
*/
|
||||||
|
if (customerName !== 'sbmoffshorecom') {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
logger.info('CustomersService - getAccessDashboardUrl');
|
||||||
|
|
||||||
|
const res = await this.getDashboardUrl('105', metadata);
|
||||||
|
logger.info('Done');
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDashboardUrl(dashboardId: string, metadata: Metadata) {
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||||
|
{
|
||||||
|
dashboard_id: dashboardId,
|
||||||
|
exp: '15m',
|
||||||
|
metabase_customer_name: 'dadosferatech',
|
||||||
|
},
|
||||||
|
metadata
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableEnforceMfa(id: string, enabled: boolean) {
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.customerService.CustomerUpdateEnforceMfa({
|
||||||
|
customerId: id,
|
||||||
|
enforceMfa: enabled
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class EnforceMfa {
|
||||||
|
@ApiProperty()
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class CreateIdentityProvider {
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
clientId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
clientSecret: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
issuerUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
permissions: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class IdentityProviderResponse {
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
clientId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
issueUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
permissions: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IdentityProviderListResponse {
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
providers: IdentityProviderResponse[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export class SsoSignInDto {
|
||||||
|
readonly nonce: string;
|
||||||
|
readonly codeVerifier: string;
|
||||||
|
readonly state: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly clientId: string;
|
||||||
|
readonly clientSecret: string;
|
||||||
|
readonly issuerUrl: string;
|
||||||
|
readonly redirectUrls: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Redirect,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { IdentityProviderService } from './identity-provider.service';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
import { ApiOkResponse } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
CreateIdentityProvider,
|
||||||
|
IdentityProviderListResponse,
|
||||||
|
IdentityProviderResponse,
|
||||||
|
} from './dto/identity-provider.dto';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import ErrorCodes from 'src/utils/errorCodes';
|
||||||
|
import {
|
||||||
|
Authenticated,
|
||||||
|
RequireModule,
|
||||||
|
RequireSomePermission,
|
||||||
|
} from 'src/decorators/authentication.decorator';
|
||||||
|
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
|
|
||||||
|
@Controller('identity-providers')
|
||||||
|
export class IdentityProviderController {
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
private identityProviderService: IdentityProviderService,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOkResponse({ type: IdentityProviderResponse })
|
||||||
|
@Authenticated()
|
||||||
|
@RequireModule('sso')
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
async addIdentityProvider(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() body: CreateIdentityProvider,
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
) {
|
||||||
|
this.logger.info('POST /identity-providers');
|
||||||
|
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
...user,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.identityProviderService.create(body, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOkResponse({ type: IdentityProviderListResponse })
|
||||||
|
@Authenticated()
|
||||||
|
@RequireModule('sso')
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
async getProviders(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
) {
|
||||||
|
this.logger.info('GET identity-providers');
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
...user,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.identityProviderService.getList(metadata);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@RequireModule('sso')
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
async deleteIdentityProvider(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
this.logger.info('DELETE /identity-providers');
|
||||||
|
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
...user,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.identityProviderService.deleteIdentityProvider(
|
||||||
|
id,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Authenticated()
|
||||||
|
@RequireModule('sso')
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
async updateIdentityProviders(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: CreateIdentityProvider,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
this.logger.info('PUT /identity-providers');
|
||||||
|
|
||||||
|
const metadata = PackTheMetadata({
|
||||||
|
...user,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.identityProviderService.updateIdentityProviders(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/callback')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async callbackIdp(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Body()
|
||||||
|
body: {
|
||||||
|
state: string;
|
||||||
|
code: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.logger.info('GET /identity-providers/callback');
|
||||||
|
const { code, state } = body;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
this.logger.error('No code received from IDP');
|
||||||
|
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
this.logger.error('No state received from IDP');
|
||||||
|
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const origin = req.headers['origin'] as string;
|
||||||
|
this.logger.info('Header Origin: ' + origin);
|
||||||
|
|
||||||
|
const lang =
|
||||||
|
language.substring(0, 2) + language.substring(2).toUpperCase();
|
||||||
|
const callbackUrl =
|
||||||
|
process.env.ENV !== 'prd'
|
||||||
|
? `${origin}/auth/callback`
|
||||||
|
: `${origin}/${lang}/auth/callback`;
|
||||||
|
|
||||||
|
this.logger.info('Callback URL: ' + callbackUrl);
|
||||||
|
return await this.identityProviderService.getTokenByIdp(
|
||||||
|
code,
|
||||||
|
state,
|
||||||
|
callbackUrl,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/links')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async providerLinks(@Req() req: Request) {
|
||||||
|
this.logger.info('GET /identity-providers/links');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontDomain = req.headers['origin'] as string;
|
||||||
|
this.logger.info('Header Origin: ' + frontDomain);
|
||||||
|
|
||||||
|
if (!frontDomain) {
|
||||||
|
this.logger.info('Not found front domain');
|
||||||
|
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
await this.identityProviderService.identityProvidersLinksPerDomain(
|
||||||
|
frontDomain,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Redirect()
|
||||||
|
async loginIdp(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Req() req: Request,
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
) {
|
||||||
|
this.logger.info('GET /identity-providers/:id');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontDomain =
|
||||||
|
(req.headers['origin'] as string) || (req.headers['referer'] as string);
|
||||||
|
this.logger.info(`Front domain: ${frontDomain}`);
|
||||||
|
const host =
|
||||||
|
frontDomain.lastIndexOf('/') !== -1
|
||||||
|
? frontDomain.substring(0, frontDomain.lastIndexOf('/'))
|
||||||
|
: frontDomain;
|
||||||
|
|
||||||
|
const lang =
|
||||||
|
language.substring(0, 2) + language.substring(2).toUpperCase();
|
||||||
|
const callbackUrl =
|
||||||
|
process.env.ENV !== 'prd'
|
||||||
|
? `${host}/auth/callback`
|
||||||
|
: `${host}/${lang}/auth/callback`;
|
||||||
|
|
||||||
|
this.logger.info('Callback URL: ' + callbackUrl);
|
||||||
|
const redirectUrl =
|
||||||
|
await this.identityProviderService.loginIdentityProvider(
|
||||||
|
id,
|
||||||
|
callbackUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info(`Redirecting to: ${redirectUrl}`);
|
||||||
|
return {
|
||||||
|
url: redirectUrl,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { IdentityProviderController } from './identity-provider.controller';
|
||||||
|
import { IdentityProviderService } from './identity-provider.service';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { ServicesModule } from 'src/services/service.module';
|
||||||
|
|
||||||
|
const client = new DucClient();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ClientsModule.register([client.providerOptions]), ServicesModule],
|
||||||
|
controllers: [IdentityProviderController],
|
||||||
|
providers: [IdentityProviderService, DadosferaLogger],
|
||||||
|
})
|
||||||
|
export class IdentityProviderModule {}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
OnModuleInit,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
|
import { IdentityProviderProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { IdentityProviderRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { Issuer, generators } from 'openid-client';
|
||||||
|
import { SsoSignInDto } from './dto/sso-signin.dto';
|
||||||
|
import { CacheService } from 'src/services/cache.service';
|
||||||
|
import { CreateIdentityProvider } from './dto/identity-provider.dto';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IdentityProviderService implements OnModuleInit {
|
||||||
|
private logger: DadosferaLogger;
|
||||||
|
private identityProviderService: IdentityProviderProtoService;
|
||||||
|
constructor(
|
||||||
|
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||||
|
private readonly cacheService: CacheService<SsoSignInDto>,
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
private dadosferaLoggger: DadosferaLogger
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLoggger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.identityProviderService =
|
||||||
|
this.grpcClient.getService<IdentityProviderProtoService>(
|
||||||
|
ProtoServices.IdentityProviderProtoService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body: IdentityProviderRequest, metadata: Metadata) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC Create")
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.Create(body, metadata),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getList(metadata: Metadata) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC GetList")
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.GetList({}, metadata),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginIdentityProvider(id: string, callbackUrl: string) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC FindIdentityProvider with: " + id);
|
||||||
|
const idp = await lastValueFrom(
|
||||||
|
this.identityProviderService.FindIdentityProvider({ id }),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info("Discovery issueURL: " + idp.issuerUrl)
|
||||||
|
const issuer = await Issuer.discover(idp.issuerUrl);
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: idp.clientId,
|
||||||
|
client_secret: idp.clientSecret,
|
||||||
|
redirect_uris: idp.redirectUrls,
|
||||||
|
response_types: ['code'],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info("Generate Challenge")
|
||||||
|
const code_verifier: string = generators.codeVerifier();
|
||||||
|
const code_challenge: string = generators.codeChallenge(code_verifier);
|
||||||
|
|
||||||
|
this.logger.info("Generate State")
|
||||||
|
const state = generators.state();
|
||||||
|
|
||||||
|
this.logger.info("Generate Nonce")
|
||||||
|
const nonce = generators.nonce();
|
||||||
|
|
||||||
|
// Using state because it is returned in the callback
|
||||||
|
// and we can use it to retrieve the code_verifier and nonce
|
||||||
|
this.logger.info("Save Login parameters in redis")
|
||||||
|
await this.cacheService.set(state, {
|
||||||
|
codeVerifier: code_verifier,
|
||||||
|
nonce,
|
||||||
|
id: idp.id,
|
||||||
|
state,
|
||||||
|
clientId: idp.clientId,
|
||||||
|
clientSecret: idp.clientSecret,
|
||||||
|
issuerUrl: idp.issuerUrl,
|
||||||
|
redirectUrls: idp.redirectUrls,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info("Generate Authorization URL")
|
||||||
|
const url = client.authorizationUrl({
|
||||||
|
scope: 'openid email',
|
||||||
|
response_type: 'code',
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
state,
|
||||||
|
nonce,
|
||||||
|
redirect_uri: callbackUrl,
|
||||||
|
});
|
||||||
|
const idpUrl = url + '&identity_provider=' + idp.name;
|
||||||
|
this.logger.info(idpUrl)
|
||||||
|
return idpUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokenByIdp(code: string, state: string, callbackUrl: string) {
|
||||||
|
this.logger.info("Get login parameters in redis")
|
||||||
|
const ssoSign = await this.cacheService.get(state);
|
||||||
|
|
||||||
|
if (!ssoSign) {
|
||||||
|
this.logger.info("Login Parameters Not Found")
|
||||||
|
throw new BadRequestException('SSO sign-in is expired or not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info("Discovery Issue URL: " + ssoSign.issuerUrl)
|
||||||
|
const issuer = await Issuer.discover(ssoSign.issuerUrl);
|
||||||
|
const client = new issuer.Client({
|
||||||
|
client_id: ssoSign.clientId,
|
||||||
|
client_secret: ssoSign.clientSecret,
|
||||||
|
redirect_uris: ssoSign.redirectUrls,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = client.callbackParams(
|
||||||
|
`${callbackUrl}?code=${code}&state=${state}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.logger.info("Get Token Set");
|
||||||
|
const tokenSet = await client.callback(callbackUrl, params, {
|
||||||
|
nonce: ssoSign.nonce,
|
||||||
|
code_verifier: ssoSign.codeVerifier,
|
||||||
|
state: ssoSign.state
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info("Delete parameters in redis");
|
||||||
|
await this.cacheService.delete(ssoSign.state);
|
||||||
|
|
||||||
|
this.logger.info("Call IdentityProvider GRPC SignInUser");
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.SignInUser({
|
||||||
|
accessToken: tokenSet.access_token,
|
||||||
|
idToken: tokenSet.id_token,
|
||||||
|
refreshToken: tokenSet.refresh_token,
|
||||||
|
id: ssoSign.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteIdentityProvider(id: string, metadata: Metadata) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC Delete with: " + id)
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.DeleteIdentityProvider({ id }, metadata),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIdentityProviders(
|
||||||
|
id: string,
|
||||||
|
body: CreateIdentityProvider,
|
||||||
|
metadata: Metadata,
|
||||||
|
) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC Update with: " + id)
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.UpdateIdentityProvider(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
...body,
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async identityProvidersLinksPerDomain(frontDomain: string) {
|
||||||
|
this.logger.info("Call IdentityProvider GRPC LinksPerDomain with: " + frontDomain)
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.identityProviderService.GetProviderLinksFromDomain({ frontDomain }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,93 +1,29 @@
|
|||||||
import { Body, Controller, Inject, Param, Post } from '@nestjs/common';
|
import { Body, Controller, Inject, Param, Post, Req } from '@nestjs/common';
|
||||||
import { init } from 'mixpanel';
|
import { init } from 'mixpanel';
|
||||||
import { Authenticated } from 'src/decorators/authentication.decorator';
|
import { Authenticated } from 'src/decorators/authentication.decorator';
|
||||||
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { MixpanelService } from './mixpanel.service';
|
||||||
|
|
||||||
@ApiInternalOnlyController()
|
@ApiInternalOnlyController()
|
||||||
@Authenticated()
|
|
||||||
@Controller('trackEvent')
|
@Controller('trackEvent')
|
||||||
export class MixpanelController {
|
export class MixpanelController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('MIXPANEL_TOKEN')
|
private mixpanelService: MixpanelService
|
||||||
private readonly mixpanelToken: string,
|
|
||||||
) {}
|
) {}
|
||||||
@Post(':id')
|
@Post(':id')
|
||||||
async trackEvent(@Param('id') id, @Body() body, @User() user: RequestUser) {
|
async trackEvent(
|
||||||
|
@Param('id') id,
|
||||||
|
@Body() body,
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Req() request
|
||||||
|
) {
|
||||||
delete body.info;
|
delete body.info;
|
||||||
const mixpanel = init(this.mixpanelToken);
|
|
||||||
|
|
||||||
const separator = user.username.includes('-') ? '-' : '.';
|
await this.mixpanelService.track(id, user, request, body)
|
||||||
const removeValues = [
|
|
||||||
'.dadosferatech.dadosfera',
|
|
||||||
'.demo.dadosfera',
|
|
||||||
'.dadosferademo',
|
|
||||||
'.dadosferarh.dadosfera',
|
|
||||||
'.dadosferatech.dadosfera2',
|
|
||||||
'.dadosferatech.dadosfera',
|
|
||||||
'.dadosfera.fin',
|
|
||||||
'.dadosferafin.dadosfera',
|
|
||||||
'.praxio.dadosfera',
|
|
||||||
'.dadosfera.tech',
|
|
||||||
'.treinamentos@dadosfera.ai',
|
|
||||||
'.dadosfera2',
|
|
||||||
'.treinamentosfera',
|
|
||||||
'.dadosfera',
|
|
||||||
];
|
|
||||||
|
|
||||||
let username = user.username;
|
|
||||||
|
|
||||||
removeValues.forEach((value) => {
|
|
||||||
username = username.replace(value, '');
|
|
||||||
});
|
|
||||||
|
|
||||||
username = username.split('@')?.[0];
|
|
||||||
username = username.split('+')?.[0];
|
|
||||||
|
|
||||||
let firstName = username
|
|
||||||
.substring(0, username.indexOf(separator))
|
|
||||||
.replace('dadosfera', '');
|
|
||||||
let lastName = username
|
|
||||||
.substring(username.lastIndexOf(separator) + 1)
|
|
||||||
.replace('dadosfera', '');
|
|
||||||
|
|
||||||
if (!firstName) {
|
|
||||||
firstName = lastName;
|
|
||||||
lastName = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
firstName = this.capitalize(firstName);
|
|
||||||
lastName = this.capitalize(lastName);
|
|
||||||
|
|
||||||
await mixpanel.people.set(user.username, {
|
|
||||||
$first_name: firstName,
|
|
||||||
$last_name: lastName,
|
|
||||||
$name: this.getFullName(firstName, lastName),
|
|
||||||
$email: user.username.includes('@')
|
|
||||||
? user.username
|
|
||||||
: user.username + '@dadosfera.ai',
|
|
||||||
customer_name: user.customer_name,
|
|
||||||
});
|
|
||||||
|
|
||||||
await mixpanel.track(id, {
|
|
||||||
distinct_id: user.username,
|
|
||||||
customer: user.customer_name,
|
|
||||||
env: process.env.ENV,
|
|
||||||
...body,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { id, body, user: user.username };
|
return { id, body, user: user.username };
|
||||||
}
|
}
|
||||||
|
|
||||||
capitalize(sentence: string): string {
|
|
||||||
if (!sentence) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return sentence[0].toUpperCase() + sentence.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFullName(firstName: string, lastName: string) {
|
|
||||||
return `${firstName}${lastName ? ' ' + lastName : ''}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { getSecretFromSecretsManager } from 'src/utils/SecretManager';
|
import { getSecretFromSecretsManager } from 'src/utils/SecretManager';
|
||||||
import { MixpanelController } from './mixpanel.controller';
|
import { MixpanelController } from './mixpanel.controller';
|
||||||
|
import { MixpanelService } from './mixpanel.service';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [MixpanelController],
|
controllers: [MixpanelController],
|
||||||
@@ -8,9 +10,12 @@ import { MixpanelController } from './mixpanel.controller';
|
|||||||
{
|
{
|
||||||
provide: 'MIXPANEL_TOKEN',
|
provide: 'MIXPANEL_TOKEN',
|
||||||
useValue: getSecretFromSecretsManager(
|
useValue: getSecretFromSecretsManager(
|
||||||
`${process.env.ENV}/root/mixpanel_token`,
|
`prd/root/mixpanel_token`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
MixpanelService,
|
||||||
|
DadosferaLogger
|
||||||
],
|
],
|
||||||
|
exports: [MixpanelService]
|
||||||
})
|
})
|
||||||
export class MixpanelModule {}
|
export class MixpanelModule {}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import mixpanel, { init } from 'mixpanel';
|
||||||
|
import { RequestUser } from 'src/decorators/user.decorator';
|
||||||
|
|
||||||
|
export class MixpanelService {
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('MIXPANEL_TOKEN')
|
||||||
|
private readonly mixpanelToken: string,
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async track(eventName: string, user: RequestUser, request: Request, body: any) {
|
||||||
|
this.logger.info("track: " + JSON.stringify({
|
||||||
|
eventName,
|
||||||
|
...body
|
||||||
|
}))
|
||||||
|
const mixpanel = init(this.mixpanelToken);
|
||||||
|
await this.setPeople(user, mixpanel);
|
||||||
|
|
||||||
|
await mixpanel.track(eventName, {
|
||||||
|
distinct_id: user.username,
|
||||||
|
customer: user.customer_name,
|
||||||
|
env: process.env.ENV,
|
||||||
|
$ip: request.ip,
|
||||||
|
$os: request.headers['sec-ch-ua-platform'] || '',
|
||||||
|
$browser: request.headers['user-agent'],
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackShare(request: Request, body: any) {
|
||||||
|
this.logger.info("trackShare: " + JSON.stringify(body))
|
||||||
|
const mixpanel = init(this.mixpanelToken);
|
||||||
|
|
||||||
|
await mixpanel.track("share_page", {
|
||||||
|
env: process.env.ENV,
|
||||||
|
$ip: request.ip,
|
||||||
|
$os: request.headers['sec-ch-ua-platform'] || '',
|
||||||
|
$browser: request.headers['user-agent'],
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setPeople(user: RequestUser, mixpanel: mixpanel.Mixpanel) {
|
||||||
|
const separator = user.username.includes('-') ? '-' : '.';
|
||||||
|
const removeValues = [
|
||||||
|
'.dadosferatech.dadosfera',
|
||||||
|
'.demo.dadosfera',
|
||||||
|
'.dadosferademo',
|
||||||
|
'.dadosferarh.dadosfera',
|
||||||
|
'.dadosferatech.dadosfera2',
|
||||||
|
'.dadosferatech.dadosfera',
|
||||||
|
'.dadosfera.fin',
|
||||||
|
'.dadosferafin.dadosfera',
|
||||||
|
'.praxio.dadosfera',
|
||||||
|
'.dadosfera.tech',
|
||||||
|
'.treinamentos@dadosfera.ai',
|
||||||
|
'.dadosfera2',
|
||||||
|
'.treinamentosfera',
|
||||||
|
'.dadosfera',
|
||||||
|
];
|
||||||
|
|
||||||
|
let username = user.username;
|
||||||
|
|
||||||
|
removeValues.forEach((value) => {
|
||||||
|
username = username.replace(value, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
username = username.split('@')?.[0];
|
||||||
|
username = username.split('+')?.[0];
|
||||||
|
|
||||||
|
let firstName = username
|
||||||
|
.substring(0, username.indexOf(separator))
|
||||||
|
.replace('dadosfera', '');
|
||||||
|
let lastName = username
|
||||||
|
.substring(username.lastIndexOf(separator) + 1)
|
||||||
|
.replace('dadosfera', '');
|
||||||
|
|
||||||
|
if (!firstName) {
|
||||||
|
firstName = lastName;
|
||||||
|
lastName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
firstName = this.capitalize(firstName);
|
||||||
|
lastName = this.capitalize(lastName);
|
||||||
|
|
||||||
|
await mixpanel.people.set(user.username, {
|
||||||
|
$first_name: firstName,
|
||||||
|
$last_name: lastName,
|
||||||
|
$name: this.getFullName(firstName, lastName),
|
||||||
|
$email: user.username.includes('@')
|
||||||
|
? user.username
|
||||||
|
: user.username + '@dadosfera.ai',
|
||||||
|
customer_name: user.customer_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private capitalize(sentence: string): string {
|
||||||
|
if (!sentence) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return sentence[0].toUpperCase() + sentence.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFullName(firstName: string, lastName: string) {
|
||||||
|
return `${firstName}${lastName ? ' ' + lastName : ''}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class NetworkPoliciesDTO {
|
||||||
|
@ApiProperty()
|
||||||
|
policies: string []
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||||
|
import { ApiOkResponse } from '@nestjs/swagger';
|
||||||
|
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
||||||
|
import {
|
||||||
|
Authenticated,
|
||||||
|
RequireAllPermissions,
|
||||||
|
} from 'src/decorators/authentication.decorator';
|
||||||
|
import { NetworkPoliciesDTO } from './dto/network-policy.dto';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { NetworkPolicyService } from './network-policy.service';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
|
||||||
|
@Controller('network-policy')
|
||||||
|
export class NetworkPolicyController {
|
||||||
|
constructor(private networkPolicyService: NetworkPolicyService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authenticated()
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getNetworks(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
) {
|
||||||
|
return await this.networkPolicyService.getByCustomer(
|
||||||
|
user.customer_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authenticated()
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@ApiOkResponse()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
async applyNetworkPolicies(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() data: NetworkPoliciesDTO,
|
||||||
|
) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
return await this.networkPolicyService.apply(
|
||||||
|
data.policies,
|
||||||
|
user.customer_id,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@Authenticated()
|
||||||
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
@ApiOkResponse()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async removeNetworkPolicies(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Body() data: NetworkPoliciesDTO,
|
||||||
|
) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
|
||||||
|
return await this.networkPolicyService.delete(
|
||||||
|
data.policies,
|
||||||
|
user.customer_id,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { NetworkPolicyController } from './network-policy.controller';
|
||||||
|
import { NetworkPolicyService } from './network-policy.service';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
|
const ducClient = new DucClient();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ClientsModule.register([
|
||||||
|
ducClient.providerOptions
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [NetworkPolicyController],
|
||||||
|
providers: [NetworkPolicyService, DadosferaLogger]
|
||||||
|
})
|
||||||
|
export class NetworkPolicyModule {}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { CustomersProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { NetworkPoliciesDTO } from './dto/network-policy.dto';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NetworkPolicyService {
|
||||||
|
private customerService: CustomersProtoService;
|
||||||
|
private logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.customerService = this.grpcClient.getService<CustomersProtoService>(
|
||||||
|
ProtoServices.CustomersProtoService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByCustomer(id: string): Promise<NetworkPoliciesDTO> {
|
||||||
|
const {
|
||||||
|
customer
|
||||||
|
} = await lastValueFrom(this.customerService.CustomerFindOneById({
|
||||||
|
id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
policies: customer.networkPolicies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async apply(
|
||||||
|
networkPolicies: string[],
|
||||||
|
customerId: string,
|
||||||
|
metadata: Metadata,
|
||||||
|
) {
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.customerService.CustomerCreateNetworkPolicy(
|
||||||
|
{
|
||||||
|
customerId,
|
||||||
|
networkPolicies,
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
networkPolicies: string[],
|
||||||
|
customerId: string,
|
||||||
|
metadata: Metadata,
|
||||||
|
) {
|
||||||
|
return await lastValueFrom(
|
||||||
|
this.customerService.CustomerRemoveNetworkPolicy(
|
||||||
|
{
|
||||||
|
customerId,
|
||||||
|
networkPolicies,
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { ConnectionClientService } from '../connection/client.service';
|
import { ConnectionClientService } from '../connection/client.service';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import DadosferaLogger from '@dadosfera/dadosfera-logs/dist';
|
import DadosferaLogger from '@dadosfera/dadosfera-logs/dist';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
|
||||||
@ApiTags('oauth')
|
@ApiTags('oauth')
|
||||||
@Controller('oauth')
|
@Controller('oauth')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
import { Body, Controller, Header, HttpCode, Inject, Param, Post, Query, Req, UseFilters, UseGuards } from '@nestjs/common';
|
import { Body, Controller, ForbiddenException, Header, Headers, HttpCode, HttpException, Inject, Param, Post, Query, Req, Res, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||||
import { ApiCreatedResponse, ApiHeaders, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiCreatedResponse, ApiHeaders, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
||||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
@@ -8,9 +8,8 @@ import { UsersService } from '../users/users.service';
|
|||||||
import { Language } from 'src/decorators/language.decorator';
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
import { OpenDataService } from './open-data.service';
|
import { OpenDataService } from './open-data.service';
|
||||||
import { CreateUserOpenDataDTO, WordpressForm } from './dto/wordpres-form';
|
import { CreateUserOpenDataDTO, WordpressForm } from './dto/wordpres-form';
|
||||||
import { CORSGuard, SetOrigin } from 'src/decorators/set-origin.decorator';
|
|
||||||
import { Metadata } from '@grpc/grpc-js';
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import { request } from 'http';
|
import { request } from 'http';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ export class OpenDataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("/sharing-ocean-data")
|
@Post("/sharing-ocean-data")
|
||||||
// @SetOrigin('devsbm.dadosfera.io')
|
|
||||||
// @UseGuards(CORSGuard)
|
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Header('content-type', 'application/json')
|
@Header('content-type', 'application/json')
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@@ -43,23 +40,20 @@ export class OpenDataController {
|
|||||||
language: string,
|
language: string,
|
||||||
@Req()
|
@Req()
|
||||||
request: Request,
|
request: Request,
|
||||||
|
@Headers('origin')
|
||||||
|
origin: string
|
||||||
) {
|
) {
|
||||||
this.logger.info('createUser for open data'
|
this.logger.info('createUser for open data' + JSON.stringify(request.headers));
|
||||||
+ JSON.stringify({
|
|
||||||
origin: request.headers.origin,
|
|
||||||
language,
|
|
||||||
body
|
|
||||||
}));
|
|
||||||
|
|
||||||
// "401573bb-334f-44b2-b30e-88d4cea31ae9"
|
// const corslist = ["https://devsbm.dadosfera.io", "https://sharingoceandata.com"];
|
||||||
// const OPENDATA_PUBLIC_USERS_GROUP_ID = process.env.OPEN_GROUP_ID;
|
// if (!corslist.includes(origin)) {
|
||||||
// ""f239718a-a271-4ef9-ae7e-02a2f0f3aa6e""
|
// this.logger.info('block request by cors list: '+ origin);
|
||||||
// const OPENDATA_CUSTOMER_ID = process.env.OPEN_CUSTOMER_ID;
|
// throw new ForbiddenException();
|
||||||
const OPENDATA_CUSTOMER_ID = "b3e3dfe5-b992-4586-a73c-c0b0c00f615d";
|
// }
|
||||||
|
|
||||||
this.logger.info("OPENDATA_CUSTOMER_ID: " + process.env.OPEN_CUSTOMER_ID)
|
const OPENDATA_CUSTOMER_ID = process.env.OPEN_CUSTOMER_ID;
|
||||||
this.logger.info("OPEN_GROUP_ID: " + process.env.OPEN_GROUP_ID)
|
const OPENDATA_GROUP_ID = process.env.OPEN_GROUP_ID;
|
||||||
const roles = ["e3f98a2f-7748-4981-8505-7695c8ca8218"];
|
const roles = [process.env.OPEN_GROUP_ID];
|
||||||
const metadata = PackTheMetadata({
|
const metadata = PackTheMetadata({
|
||||||
language: language || 'en-us'
|
language: language || 'en-us'
|
||||||
});
|
});
|
||||||
@@ -77,7 +71,6 @@ export class OpenDataController {
|
|||||||
this.logger.error('user data ' + e.message);
|
this.logger.error('user data ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const user: CreateUserOpenDataDTO = {
|
const user: CreateUserOpenDataDTO = {
|
||||||
email: data["email"],
|
email: data["email"],
|
||||||
enquiryType: data["enquiry_type"],
|
enquiryType: data["enquiry_type"],
|
||||||
@@ -85,11 +78,11 @@ export class OpenDataController {
|
|||||||
lastName: data["last_name"],
|
lastName: data["last_name"],
|
||||||
organization: data["organization"]
|
organization: data["organization"]
|
||||||
}
|
}
|
||||||
this.logger.info('user request' + JSON.stringify({ user, roles, customer: OPENDATA_CUSTOMER_ID }));
|
this.logger.info(`user request to group ${OPENDATA_CUSTOMER_ID} with role ${OPENDATA_GROUP_ID}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.openDataService.createUser(OPENDATA_CUSTOMER_ID, user, roles, metadata);
|
const id = await this.openDataService.createUser(OPENDATA_CUSTOMER_ID, user, roles, metadata);
|
||||||
this.logger.info('user created with sucessfull data');
|
this.logger.info('user created with id: '+ id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export class OpenDataService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await lastValueFrom(
|
const { user } = await lastValueFrom(
|
||||||
this.usersClientService.SimpleUserCreate(body, metadata),
|
this.usersClientService.SimpleUserCreate(body, metadata),
|
||||||
);
|
);
|
||||||
return "User created";
|
return user.id;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { PipelinesService } from './pipelines.service';
|
|||||||
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
|
||||||
import { Messages } from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
|
import { Messages } from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
|
||||||
import { PipelinesService as OldPipelineService } from 'src/modules/pipelines/pipelines.service';
|
import { PipelinesService as OldPipelineService } from 'src/modules/pipelines/pipelines.service';
|
||||||
import {
|
import {
|
||||||
@@ -205,13 +205,10 @@ export class PipelinesController {
|
|||||||
async getPipelineStatus(@Body() body, @Param('id') id: string) {
|
async getPipelineStatus(@Body() body, @Param('id') id: string) {
|
||||||
body.id = id;
|
body.id = id;
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(`/pipeline/${id} - ON GET PIPELINE STATUS ROUTE`, {
|
||||||
process.env.DEV_URL + `/pipeline/${id} - ON GET PIPELINE STATUS ROUTE`,
|
|
||||||
{
|
|
||||||
user: body.info.user_id,
|
user: body.info.user_id,
|
||||||
customer: body.info.customer,
|
customer: body.info.customer,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const response = await this.oldPipelinesService.getPipelineStatus(body);
|
const response = await this.oldPipelinesService.getPipelineStatus(body);
|
||||||
|
|
||||||
@@ -256,6 +253,7 @@ export class PipelinesController {
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export class PipelinesService implements OnModuleInit {
|
|||||||
const res = await lastValueFrom(
|
const res = await lastValueFrom(
|
||||||
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
this.pipelineReadService.PipelineV2GetDashboardUrl(
|
||||||
{
|
{
|
||||||
dashboard_id: '83',
|
dashboard_id: '95',
|
||||||
exp: '15m',
|
exp: '15m',
|
||||||
metabase_customer_name: 'dadosferatech',
|
metabase_customer_name: 'dadosferatech',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export class CreateShareMetadataDto {
|
||||||
|
assetId: string;
|
||||||
|
proposedId: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateShareMetadataDto {
|
||||||
|
shareId: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Controller, Get, Body, Param, Put, Patch } from '@nestjs/common';
|
||||||
|
import { ShareMetadataService } from './share-metadata.service';
|
||||||
|
import { Authenticated } from 'src/decorators/authentication.decorator';
|
||||||
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
|
import { CreateShareMetadataDto, UpdateShareMetadataDto } from './dto/create-share-metadata.dto';
|
||||||
|
|
||||||
|
@Controller('share')
|
||||||
|
@Authenticated()
|
||||||
|
export class ShareMetadataController {
|
||||||
|
constructor(private readonly shareMetadataService: ShareMetadataService) {}
|
||||||
|
|
||||||
|
@Put('/')
|
||||||
|
findOrcreate(@Body() data: CreateShareMetadataDto, @User() user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
return this.shareMetadataService.create(data, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('/')
|
||||||
|
updateShareType(@Body() data: UpdateShareMetadataDto, @User() user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
return this.shareMetadataService.updateShare(data, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
get(@Param('id') id: string, @User() user: RequestUser) {
|
||||||
|
const metadata = PackTheMetadata(user);
|
||||||
|
return this.shareMetadataService.get(id, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ShareMetadataService } from './share-metadata.service';
|
||||||
|
import { ShareMetadataController } from './share-metadata.controller';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
|
||||||
|
const client = new DucClient();
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ClientsModule.register([client.providerOptions])],
|
||||||
|
controllers: [ShareMetadataController],
|
||||||
|
providers: [ShareMetadataService, DadosferaLogger],
|
||||||
|
exports: [ShareMetadataService]
|
||||||
|
})
|
||||||
|
export class ShareMetadataModule {}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { ClientGrpc } from '@nestjs/microservices';
|
||||||
|
import { DucClient } from '../duc/client.config';
|
||||||
|
import DadosferaLogger from '@dadosfera/dadosfera-logs';
|
||||||
|
import { ShareMetadataProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
|
||||||
|
import { CreateShareMetadataDto, UpdateShareMetadataDto } from './dto/create-share-metadata.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShareMetadataService implements OnModuleInit {
|
||||||
|
ducService: ShareMetadataProtoService;
|
||||||
|
logger: DadosferaLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DadosferaLogger)
|
||||||
|
dadosferaLogger: DadosferaLogger,
|
||||||
|
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
|
||||||
|
) {
|
||||||
|
this.logger = dadosferaLogger.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.ducService =this.grpcClient.getService<ShareMetadataProtoService>(
|
||||||
|
ProtoServices.ShareMetadataProtoService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(createShareDto: CreateShareMetadataDto, metadata: Metadata) {
|
||||||
|
return lastValueFrom(
|
||||||
|
this.ducService.FindOrCreateShareMetadata(
|
||||||
|
{
|
||||||
|
assetId: createShareDto.assetId,
|
||||||
|
proposedId: createShareDto.proposedId,
|
||||||
|
type: createShareDto.type
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShare(updateShare: UpdateShareMetadataDto, metadata: Metadata) {
|
||||||
|
return lastValueFrom(
|
||||||
|
this.ducService.ChangeShareMetadataType(
|
||||||
|
updateShare,
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string, metadata: Metadata) {
|
||||||
|
return lastValueFrom(
|
||||||
|
this.ducService.GetShareMetadata(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,9 +27,9 @@ export class CustomerTheme implements Theme {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
textColor: string;
|
textColor: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
logoWhite: string;
|
logo: string;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
logoBlack: string;
|
logoLogin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerThemeResponse {
|
export class CustomerThemeResponse {
|
||||||
|
|||||||
@@ -59,22 +59,22 @@ export class ThemeController {
|
|||||||
id,
|
id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const logoWhite = files.find(file => file.fieldname === 'logoWhite');
|
const logo = files.find(file => file.fieldname === 'logo');
|
||||||
const logoBlack = files.find(file => file.fieldname === 'logoBlack');
|
const logoLogin = files.find(file => file.fieldname === 'logoLogin');
|
||||||
|
|
||||||
this.validFileSize(logoWhite);
|
this.validFileSize(logo);
|
||||||
this.validFileSize(logoBlack);
|
this.validFileSize(logoLogin);
|
||||||
this.validMimeType(logoWhite);
|
this.validMimeType(logo);
|
||||||
this.validMimeType(logoBlack);
|
this.validMimeType(logoLogin);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const theme = await this.themeService.createThemeByCustomer(id, {
|
const theme = await this.themeService.createThemeByCustomer(id, {
|
||||||
...data,
|
...data,
|
||||||
logoWhite,
|
logo,
|
||||||
logoBlack
|
logoLogin
|
||||||
});
|
});
|
||||||
this.logger.info('saveCustomertheme' + JSON.stringify(theme));
|
this.logger.info('saveCustomertheme' + JSON.stringify(theme));
|
||||||
return { theme: theme };
|
return theme;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
|
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
|
||||||
this.logger.error('Error - saveCustomertheme - Expect CUSTOMER.NOT_FOUND');
|
this.logger.error('Error - saveCustomertheme - Expect CUSTOMER.NOT_FOUND');
|
||||||
@@ -98,7 +98,7 @@ export class ThemeController {
|
|||||||
const mimeTypesValid = ['image/jpeg', 'image/jpg', 'image/png'];
|
const mimeTypesValid = ['image/jpeg', 'image/jpg', 'image/png'];
|
||||||
|
|
||||||
if (file && !mimeTypesValid.includes(file.mimetype)) {
|
if (file && !mimeTypesValid.includes(file.mimetype)) {
|
||||||
throw new HttpException(`O Arquivo ${file.fieldname} deve ser jpeg, jpg ou png`, HttpStatus.BAD_REQUEST);
|
throw new HttpException(`O Arquivo ${file.fieldname} deve ser jpeg, jpg, ou png`, HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +109,10 @@ export class ThemeController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.themeService.getThemeByCustomer(id);
|
const data = await this.themeService.getThemeByCustomer(id);
|
||||||
this.logger.info('Success - getCustomerTheme with id'+ id);
|
this.logger.info('Success - getCustomerTheme'+ JSON.stringify(data));
|
||||||
return data;
|
if (data?.theme) return data;
|
||||||
|
|
||||||
|
return { theme: null };
|
||||||
}catch (err) {
|
}catch (err) {
|
||||||
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
|
if (err.details === ErrorCodes.CUSTOMER.NOT_FOUND) {
|
||||||
this.logger.error('Error - getCustomerTheme - Expect CUSTOMER.NOT_FOUND');
|
this.logger.error('Error - getCustomerTheme - Expect CUSTOMER.NOT_FOUND');
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ import { resolve } from 'path';
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
type Files = {
|
type Files = {
|
||||||
logoWhite: Express.Multer.File,
|
logo: Express.Multer.File,
|
||||||
logoBlack: Express.Multer.File,
|
logoLogin: Express.Multer.File,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function will accept any string, which may result in a bug.
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ThemeService implements OnModuleInit {
|
export class ThemeService implements OnModuleInit {
|
||||||
private themeService: ThemeProtoService;
|
private themeService: ThemeProtoService;
|
||||||
@@ -63,12 +62,12 @@ export class ThemeService implements OnModuleInit {
|
|||||||
chunk: Buffer.alloc(0)
|
chunk: Buffer.alloc(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
if(theme.logoBlack) {
|
if(theme.logo) {
|
||||||
await this.sendFile(theme.logoBlack, customerThemeRequest$);
|
await this.sendFile(theme.logo, customerThemeRequest$);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(theme.logoWhite) {
|
if(theme.logoLogin) {
|
||||||
await this.sendFile(theme.logoWhite, customerThemeRequest$);
|
await this.sendFile(theme.logoLogin, customerThemeRequest$);
|
||||||
}
|
}
|
||||||
customerThemeRequest$.complete();
|
customerThemeRequest$.complete();
|
||||||
|
|
||||||
@@ -95,18 +94,20 @@ export class ThemeService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendFile(file: Express.Multer.File, stream$: ReplaySubject<ThemeRequest>) {
|
private async sendFile(file: Express.Multer.File, stream$: ReplaySubject<ThemeRequest>) {
|
||||||
const bufferStream = new Readable({
|
const chunkSize = 4 * 1024 * 1024;
|
||||||
highWaterMark: 1024 * 1024, // 1 MB por chunk
|
const bufferStream = new CustomBufferStream(file.buffer, chunkSize);
|
||||||
read() {}
|
const parseMimitypeForExtension = {
|
||||||
});
|
'image/jpeg': '.jpeg',
|
||||||
bufferStream.push(file.buffer);
|
'image/jpg': '.jpg',
|
||||||
bufferStream.push(null);
|
'image/png': '.png',
|
||||||
|
'image/svg+xml': '.svg',
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = parseMimitypeForExtension[file.mimetype];
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
bufferStream.on('data', (chunk) => {
|
bufferStream.on('data', (chunk) => {
|
||||||
console.log('enviando chunk', file.fieldname)
|
const filename = file.fieldname.concat(extension);
|
||||||
const mimetype = file.mimetype.split('/')[1]; // example image/jpeg
|
|
||||||
const filename = file.fieldname.concat(".", mimetype);
|
|
||||||
stream$.next({
|
stream$.next({
|
||||||
customerId: '',
|
customerId: '',
|
||||||
displayName: '',
|
displayName: '',
|
||||||
@@ -119,15 +120,39 @@ export class ThemeService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bufferStream.on('end', () => {
|
bufferStream.on('end', () => {
|
||||||
console.log('terminou de enviar')
|
|
||||||
resolve(file.filename)
|
resolve(file.filename)
|
||||||
});
|
});
|
||||||
|
|
||||||
bufferStream.on('error', (err) => {
|
bufferStream.on('error', (err) => {
|
||||||
console.error('Erro no stream:', err);
|
|
||||||
reject(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,3 +174,14 @@ export class GetAllDepartmentsRes {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
departments: string[];
|
departments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface UserReporter {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
mfaStatus: string;
|
||||||
|
status: string;
|
||||||
|
lastLogin: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import { RolesModule } from '../roles/roles.module';
|
|||||||
import { PermissionsModule } from '../permissions/permissions.module';
|
import { PermissionsModule } from '../permissions/permissions.module';
|
||||||
|
|
||||||
// const client = new DucClient();
|
// const client = new DucClient();
|
||||||
|
jest.mock('puppeteer', () => ({
|
||||||
|
launch: jest.fn().mockResolvedValue({
|
||||||
|
newPage: jest.fn().mockResolvedValue({
|
||||||
|
goto: jest.fn(),
|
||||||
|
evaluate: jest.fn(),
|
||||||
|
close: jest.fn()
|
||||||
|
}),
|
||||||
|
close: jest.fn()
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
info: (...args) => args,
|
info: (...args) => args,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
|
Res,
|
||||||
UseFilters,
|
UseFilters,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
@@ -24,12 +25,13 @@ import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
|
|||||||
import {
|
import {
|
||||||
Authenticated,
|
Authenticated,
|
||||||
RequireAllPermissions,
|
RequireAllPermissions,
|
||||||
|
RequireSomePermission,
|
||||||
} from 'src/decorators/authentication.decorator';
|
} from 'src/decorators/authentication.decorator';
|
||||||
import { Language } from 'src/decorators/language.decorator';
|
import { Language } from 'src/decorators/language.decorator';
|
||||||
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
|
||||||
import { RequestUser, User } from 'src/decorators/user.decorator';
|
import { RequestUser, User } from 'src/decorators/user.decorator';
|
||||||
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
|
||||||
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
|
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
|
||||||
import ErrorBuilder from 'src/utils/ErrorBuilder';
|
import ErrorBuilder from 'src/utils/ErrorBuilder';
|
||||||
import ErrorCodes from 'src/utils/errorCodes';
|
import ErrorCodes from 'src/utils/errorCodes';
|
||||||
import { LanguageEnum } from 'src/utils/languages.enum';
|
import { LanguageEnum } from 'src/utils/languages.enum';
|
||||||
@@ -52,6 +54,7 @@ import {
|
|||||||
UpdateUserRes,
|
UpdateUserRes,
|
||||||
} from './dtos/entities';
|
} from './dtos/entities';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
@ApiInternalOnlyController()
|
@ApiInternalOnlyController()
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
@@ -80,6 +83,26 @@ export class UsersController {
|
|||||||
return await this.userService.findAllUsersByCustomerId(user.customer_id);
|
return await this.userService.findAllUsersByCustomerId(user.customer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/download')
|
||||||
|
@RequireSomePermission(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
|
async downloadUsersInCsv(
|
||||||
|
@User() user: RequestUser,
|
||||||
|
@Language() language: LanguageEnum,
|
||||||
|
@Res() res: Response
|
||||||
|
) {
|
||||||
|
this.logger.info('downloadUsersInCsv');
|
||||||
|
this.userService.setLanguage(language);
|
||||||
|
const {
|
||||||
|
file,
|
||||||
|
filename
|
||||||
|
} = await this.userService.downloadUsersInCsv(user.customer_id);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
res.end(file);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('hierarchies')
|
@Get('hierarchies')
|
||||||
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
@RequireAllPermissions(PERMISSIONS_GROUPS.USERS.permissions.ADMIN)
|
||||||
@ApiOkResponse({ type: GetAllHierarchiesRes })
|
@ApiOkResponse({ type: GetAllHierarchiesRes })
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ import { PermissionsModule } from '../permissions/permissions.module';
|
|||||||
|
|
||||||
// const client = new DucClient();
|
// const client = new DucClient();
|
||||||
|
|
||||||
|
jest.mock('puppeteer', () => ({
|
||||||
|
launch: jest.fn().mockResolvedValue({
|
||||||
|
newPage: jest.fn().mockResolvedValue({
|
||||||
|
goto: jest.fn(),
|
||||||
|
evaluate: jest.fn(),
|
||||||
|
close: jest.fn()
|
||||||
|
}),
|
||||||
|
close: jest.fn()
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
info: (...args) => args,
|
info: (...args) => args,
|
||||||
error: (...args) => args,
|
error: (...args) => args,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
IUserByCustomer,
|
IUserByCustomer,
|
||||||
SetUserRolesReq,
|
SetUserRolesReq,
|
||||||
UpdateUserReq,
|
UpdateUserReq,
|
||||||
|
UserReporter,
|
||||||
} from './dtos/entities';
|
} from './dtos/entities';
|
||||||
import { RolesService } from '../roles/roles.service';
|
import { RolesService } from '../roles/roles.service';
|
||||||
import { HIERARCHIES } from './hierarchies';
|
import { HIERARCHIES } from './hierarchies';
|
||||||
@@ -29,6 +30,7 @@ import { UserByCustomer } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces
|
|||||||
import { EnrichErrorCode } from 'src/utils/ErrorBuilder';
|
import { EnrichErrorCode } from 'src/utils/ErrorBuilder';
|
||||||
import { DucClient } from '../duc/client.config';
|
import { DucClient } from '../duc/client.config';
|
||||||
import { Metadata } from '@grpc/grpc-js';
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { ParserBuilder } from 'src/utils/FileParser/parser.builder';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService implements OnModuleInit {
|
export class UsersService implements OnModuleInit {
|
||||||
@@ -78,6 +80,34 @@ export class UsersService implements OnModuleInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadUsersInCsv(customerId: string) {
|
||||||
|
const { users } = await lastValueFrom(
|
||||||
|
this.usersClientService.UserFindAllByCustomerId({ customerId }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatUsers: UserReporter[] = users.map(user => ({
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
email: user.email,
|
||||||
|
lastLogin: user.lastLogin,
|
||||||
|
mfaStatus: user.mfaStatus,
|
||||||
|
name: user.name,
|
||||||
|
status: user.status,
|
||||||
|
updatedAt: user.updatedAt
|
||||||
|
}))
|
||||||
|
|
||||||
|
const parser = ParserBuilder.build<UserReporter>('csv');
|
||||||
|
|
||||||
|
const file = await parser.parse(formatUsers);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `dadosfera_users_${timestamp}.csv`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async findOneById(id: string): Promise<{ user: IUserByCustomer }> {
|
async findOneById(id: string): Promise<{ user: IUserByCustomer }> {
|
||||||
const { user } = await lastValueFrom(
|
const { user } = await lastValueFrom(
|
||||||
this.usersClientService.UserFindOneById({ id }),
|
this.usersClientService.UserFindOneById({ id }),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Cache } from 'cache-manager';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService<T> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(CACHE_MANAGER)
|
||||||
|
private readonly cacheManager: Cache,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async get(key: string): Promise<T | null> {
|
||||||
|
return await this.cacheManager.get<T>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: T): Promise<void> {
|
||||||
|
await this.cacheManager.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
await this.cacheManager.del(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CacheModule, Module } from '@nestjs/common';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
CacheModule.registerAsync({
|
||||||
|
useFactory: async () => {
|
||||||
|
const baseRedisConfig = {
|
||||||
|
ttl: 5 * 1000 * 60, // 5 minute
|
||||||
|
host: process.env.REDIS_HOST,
|
||||||
|
port: process.env.REDIS_PORT && Number(process.env.REDIS_PORT),
|
||||||
|
db: process.env.REDIS_DATABASE && Number(process.env.REDIS_DATABASE),
|
||||||
|
keyPrefix: 'maestro:sso',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.ENV !== 'local') {
|
||||||
|
baseRedisConfig['tls'] = {
|
||||||
|
servername: process.env.REDIS_HOST,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
store: await redisStore(baseRedisConfig),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [CacheService],
|
||||||
|
exports: [CacheService],
|
||||||
|
})
|
||||||
|
export class ServicesModule {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user