diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6da5b80d..22d4d247 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,8 @@ on: push: branches: - master + - development + - experimental jobs: packages: @@ -18,8 +20,23 @@ jobs: run: | git config --global user.email "devops@brightsec.com" git config --global user.name "Bright Security" + + - name: Change name to development + if: ${{ github.ref == 'refs/heads/development' }} + run: | + sed -i 's/brokencrystals/brokencrystals-dev/g' ./charts/brokencrystals/Chart.yaml + sed -i 's/brkn/brkn-dev/g' ./charts/brokencrystals/Chart.yaml + + - name: Change values to development + if: ${{ github.ref == 'refs/heads/development' }} + run: | + sed -i 's/^ main:.*/ main: development/' ./charts/brokencrystals/values.yaml + sed -i 's/^ client:.*/ client: development/' ./charts/brokencrystals/values.yaml + - name: Release packages uses: helm/chart-releaser-action@v1.5.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_SKIP_EXISTING: true + + diff --git a/README.md b/README.md index 97e37d3b..07806bd5 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,26 @@ There are specific endpoints for each cloud provider as well - `/api/file/google * **Version Control System** - The client_s build process copies SVN, GIT, and Mercurial source control directories to the client application root and they are accessible under Nginx root. * **XML External Entity (XXE)** - The endpoint, POST /api/metadata, receives URL-encoded XML data in the _xml_ query parameter, processes it with enabled external entities (using libxmnl library) and returns the serialized DOM. Additionally, for a request that tries to load file:///etc/passwd as an entity, the endpoint returns a mocked up content of the file. +Additionally, the endpoint PUT /api/users/one/{email}/photo accepts SVG images, which are proccessed with libxml library and stored on the server, as well as sent back to the client. * **JavaScript Vulnerabilities Scanning** - Index.html includes an older version of the jQuery library with known vulnerabilities. * **AO1 Vertical access controls** - The page /dashboard can be reached despite the rights of user. + +* **Broken Function Level Authorization** - The endpoint DELETE `/users/one/:id/photo?isAdmin=` can be used to delete any user's profile photo by enumerating the user IDs and setting the `isAdmin` query parameter to true, as there is no validation of it's value on the server side. + +* **IFrame Injection** - The `/testimonials` page a URL parameter `videosrc` which directly controls the src attribute of the IFrame at the bottom of this page. Similarly, the home page takes a URL param `maptitle` which directly controls the `title` attribute of the IFrame at the CONTACT section of this page. + +* **Excessive Data Exposure** - The `/api/users/one/:email` is supposed to expose only basic user information required to be displayed on the UI, but it also returns the user's phone number which is unnecessary information. + +* **Business Constraint Bypass** - The `/api/products/latest` endpoint supports a `limit` parameter, which by default is set to 3. The `/api/products` endpoint is a password protected endpoint which returns all of the products, yet if you change the `limit` param of `/api/products/latest` to be high enough you could get the same results without the need to be authenticated. + +* **ID Enumeration** - There are a few ID Enumeration vulnerabilities: + 1. The endpoint DELETE `/users/one/:id/photo?isAdmin=` which is used to delete a user's profile picture is vulnerable to ID Enumeration together with [Broken Function Level Authorization](#broken-function-level-authorization). + 2. The `/users/id/:id` endpoint returns user info by ID, it doesn't require neither authentication nor authorization. + +* **XPATH Injection** - The `/api/partners/*` endpoint contains the following XPATH injection vulnerabilities: + 1. The endpoint GET `/api/partners/partnerLogin` is supposed to login with the user's credentials in order to obtain account info. It's vulnerable to an XPATH injection using boolean based payloads. When exploited it'll retrieve data about other users as well. You can use `' or '1'='1` in the password field to exploit the EP. + 2. The endpoint GET `/api/partners/searchPartners` is supposed to search partners' names by a given keyword. It's vulnerable to an XPATH injection using string detection payloads. When exploited, it can grant access to sensitive information like passwords and even lead to full data leak. You can use `')] | //password%00//` or `')] | //* | a[('` to exploit the EP. + 3. The endpoint GET `/api/partners/query` is a raw XPATH injection endpoint. You can put whatever you like there. It is not referenced in the frontend, but it is an exposed API endpoint. + 4. Note: All endpoints are vulnerable to error based payloads. diff --git a/charts/brokencrystals/Chart.yaml b/charts/brokencrystals/Chart.yaml index 22bf0f98..153dc938 100644 --- a/charts/brokencrystals/Chart.yaml +++ b/charts/brokencrystals/Chart.yaml @@ -4,7 +4,7 @@ description: | Benchmark application that uses modern technologies and implements a set of common security vulnerabilities type: application -version: 0.0.29 +version: 0.0.46 keywords: - brokencrystals - brkn diff --git a/charts/brokencrystals/templates/_helpers.tpl b/charts/brokencrystals/templates/_helpers.tpl index e66810ae..cd31aba8 100644 --- a/charts/brokencrystals/templates/_helpers.tpl +++ b/charts/brokencrystals/templates/_helpers.tpl @@ -12,13 +12,13 @@ If release name contains chart name it will be used as a full name. */}} {{- define "brokencrystals.fullname" -}} {{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- .Values.fullnameOverride | trunc 50 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- .Release.Name | trunc 50 | trimSuffix "-" }} {{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- printf "%s-%s" .Release.Name $name | trunc 50 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} diff --git a/charts/brokencrystals/templates/bc-postgres-deployment.yaml b/charts/brokencrystals/templates/bc-postgres-deployment.yaml deleted file mode 100644 index 748a3231..00000000 --- a/charts/brokencrystals/templates/bc-postgres-deployment.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "brokencrystals.fullname" . }}-postgres-prod - namespace: {{ .Release.Namespace }} - labels: - app: {{ include "brokencrystals.fullname" . }}-postgres-prod -spec: - selector: - matchLabels: - app: {{ include "brokencrystals.fullname" . }}-postgres-prod - template: - metadata: - labels: - app: {{ include "brokencrystals.fullname" . }}-postgres-prod - spec: - containers: - - name: {{ include "brokencrystals.fullname" . }}-postgres-prod - image: postgres - livenessProbe: - tcpSocket: - port: 5432 - initialDelaySeconds: 60 - periodSeconds: 30 - env: - - name: POSTGRES_DB - value: "bc" - - name: POSTGRES_USER - value: "bc" - - name: POSTGRES_PASSWORD - value: "bc" - resources: - requests: - cpu: 200m - memory: 100Mi - volumeMounts: - - name: {{ include "brokencrystals.fullname" . }}-postgres - mountPath: /docker-entrypoint-initdb.d/pg.sql - subPath: pg.sql - readOnly: true - volumes: - - name: {{ include "brokencrystals.fullname" . }}-postgres - configMap: - name: {{ include "brokencrystals.fullname" . }}-postgres - - ---- -kind: Service -apiVersion: v1 -metadata: - name: {{ include "brokencrystals.fullname" . }}-postgres-prod-service - namespace: {{ .Release.Namespace }} -spec: - selector: - app: {{ include "brokencrystals.fullname" . }}-postgres-prod - ports: - - name: postgres - port: 5432 - protocol: TCP - targetPort: 5432 - diff --git a/charts/brokencrystals/templates/bc-prod-deployment.yaml b/charts/brokencrystals/templates/bc-prod-deployment.yaml deleted file mode 100644 index 8d1f2368..00000000 --- a/charts/brokencrystals/templates/bc-prod-deployment.yaml +++ /dev/null @@ -1,86 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "brokencrystals.fullname" . }}-nodejs-prod - namespace: {{ .Release.Namespace }} - labels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-prod -spec: - selector: - matchLabels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-prod - template: - metadata: - labels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-prod - spec: - containers: - - name: {{ include "brokencrystals.fullname" . }}-nodejs-prod-app - image: brightsec/brokencrystals:{{ .Values.images.main }} -# command: ["/bin/sleep"] -# args: ["10000"] - env: - - name: URL - value: "https://{{ .Values.ingress.url }}" - # value: "https://brokencrystals.com" - - name: DATABASE_HOST - value: "{{ include "brokencrystals.fullname" . }}-postgres-prod-service" - - name: DATABASE_SCHEMA - value: "bc" - - name: DATABASE_USER - value: "bc" - - name: DATABASE_PASSWORD - value: "bc" - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_DEBUG - value: "true" - - name: AWS_BUCKET - value: "https://neuralegion-open-bucket.s3.amazonaws.com" - - name: GOOGLE_MAPS_API - value: "AIzaSyD2wIxpYCuNI0Zjt8kChs2hLTS5abVQfRQ" - - name: JWT_PRIVATE_KEY_LOCATION - value: "config/keys/jwtRS256.key" - - name: JWT_PUBLIC_KEY_LOCATION - value: "config/keys/jwtRS256.key.pub.pem" - - name: JWT_SECRET_KEY - value: "1234" - - name: JWK_PRIVATE_KEY_LOCATION - value: "config/keys/jwk.key.pem" - - name: JWK_PUBLIC_KEY_LOCATION - value: "config/keys/jwk.pub.key.pem" - - name: JWK_PUBLIC_JSON - value: "config/keys/jwk.pub.json" - - name: JKU_URL - value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/jku.json" - - name: X5U_URL - value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/x509.crt" - resources: - requests: - cpu: 1800m - memory: 1024Mi - livenessProbe: - httpGet: - path: /api/config - port: 3000 - scheme: HTTP - initialDelaySeconds: 120 - periodSeconds: 30 - ---- -kind: Service -apiVersion: v1 -metadata: -# name: bc-nodejs-prod-service - name: {{ include "brokencrystals.fullname" . }}-nodejs - namespace: {{ .Release.Namespace }} -spec: - selector: - app: {{ include "brokencrystals.fullname" . }}-nodejs-prod - ports: - - name: http - port: 3000 - protocol: TCP - targetPort: 3000 - diff --git a/charts/brokencrystals/templates/bc-prod-proxy-deployment.yaml b/charts/brokencrystals/templates/bc-prod-proxy-deployment.yaml deleted file mode 100644 index 46731ef7..00000000 --- a/charts/brokencrystals/templates/bc-prod-proxy-deployment.yaml +++ /dev/null @@ -1,91 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod - namespace: {{ .Release.Namespace }} - labels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod -spec: - selector: - matchLabels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod - template: - metadata: - labels: - app: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod - spec: - containers: - - name: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod-app - image: brightsec/brokencrystals-proxy-http:{{ .Values.images.client }} - env: - - name: URL - value: "https://{{ .Values.ingress.url }}" - - name: DATABASE_HOST - value: "{{ include "brokencrystals.fullname" . }}-postgres-prod-service" - - name: DATABASE_SCHEMA - value: "bc" - - name: DATABASE_USER - value: "bc" - - name: DATABASE_PASSWORD - value: "bc" - - name: DATABASE_PORT - value: "5432" - - name: DATABASE_DEBUG - value: "true" - - name: AWS_BUCKET - value: "https://neuralegion-open-bucket.s3.amazonaws.com" - - name: GOOGLE_MAPS_API - value: "AIzaSyD2wIxpYCuNI0Zjt8kChs2hLTS5abVQfRQ" - - name: JWT_PRIVATE_KEY_LOCATION - value: "config/keys/jwtRS256.key" - - name: JWT_PUBLIC_KEY_LOCATION - value: "config/keys/jwtRS256.key.pub.pem" - - name: JWT_SECRET_KEY - value: "1234" - - name: JWK_PRIVATE_KEY_LOCATION - value: "config/keys/jwk.key.pem" - - name: JWK_PUBLIC_KEY_LOCATION - value: "config/keys/jwk.pub.key.pem" - - name: JWK_PUBLIC_JSON - value: "config/keys/jwk.pub.json" - - name: JKU_URL - value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/jku.json" - - name: X5U_URL - value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/x509.crt" - volumeMounts: - - name: {{ include "brokencrystals.fullname" . }}-nginx-proxy - mountPath: /etc/nginx/conf.d/default.conf - subPath: default.conf - readOnly: true - resources: - requests: - cpu: 500m - memory: 50Mi - livenessProbe: - httpGet: - path: / - port: 80 - scheme: HTTP - initialDelaySeconds: 120 - periodSeconds: 30 - volumes: - - name: {{ include "brokencrystals.fullname" . }}-nginx-proxy - configMap: - name: {{ include "brokencrystals.fullname" . }}-nginx-proxy - ---- -kind: Service -apiVersion: v1 -metadata: - name: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod-service - namespace: {{ .Release.Namespace }} -spec: - selector: - app: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod - ports: - - name: http - port: 80 - protocol: TCP - targetPort: 80 - diff --git a/charts/brokencrystals/templates/config-keycloak-postgres.yaml b/charts/brokencrystals/templates/config-keycloak-postgres.yaml new file mode 100644 index 00000000..b0323d1a --- /dev/null +++ b/charts/brokencrystals/templates/config-keycloak-postgres.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres + namespace: {{ .Release.Namespace }} +data: + postgresql.conf.sample: | + listen_addresses = '*' + port = 5433 diff --git a/charts/brokencrystals/templates/config.yaml b/charts/brokencrystals/templates/config-postgres.yaml similarity index 86% rename from charts/brokencrystals/templates/config.yaml rename to charts/brokencrystals/templates/config-postgres.yaml index a8dee576..79ec3e9e 100644 --- a/charts/brokencrystals/templates/config.yaml +++ b/charts/brokencrystals/templates/config-postgres.yaml @@ -7,17 +7,13 @@ data: pg.sql: | set names 'utf8'; set session_replication_role = 'replica'; - - create table "user" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "email" varchar(255) not null, "password" varchar(255) not null, "first_name" varchar(255) not null, "last_name" varchar(255) not null, "is_admin" bool not null, "photo" bytea null, "company" varchar(255) not null, "card_number" varchar(255) not null, "phone_number" varchar(255) not null); - + create table "user" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "email" varchar(255) not null, "password" varchar(255) not null, "first_name" varchar(255) not null, "last_name" varchar(255) not null, "is_admin" bool not null, "photo" bytea null, "company" varchar(255) not null, "card_number" varchar(255) not null, "phone_number" varchar(255) not null, "is_basic" bool not null); create table "testimonial" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "name" varchar(255) not null, "title" varchar(255) not null, "message" varchar(255) not null); - create table "product" ("id" serial primary key, "created_at" timestamptz(0) not null default now(), "category" varchar(255) not null, "photo_url" varchar(255) not null, "name" varchar(255) not null, "description" varchar(255) null, "views_count" int DEFAULT 0); set session_replication_role = 'origin'; --password is admin - INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number) VALUES (now(), now(), 'admin', '$2b$10$BBJjmVNNdyEgv7pV/zQR9u/ssIuwZsdDJbowW/Dgp28uws3GmO0Ky', 'admin', 'admin', true, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890'); - INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number) VALUES (now(), now(), 'user', '$2b$10$edsq4aqzAHnrJu68t8GS2.v0Z7hJSstAo7wBBDmmbpjYGxMMTYpVi', 'user', 'user', false, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890'); - + INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number, is_basic) VALUES (now(), now(), 'admin', '$2b$10$BBJjmVNNdyEgv7pV/zQR9u/ssIuwZsdDJbowW/Dgp28uws3GmO0Ky', 'admin', 'admin', true, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890', true); + INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number, is_basic) VALUES (now(), now(), 'user', '$2b$10$edsq4aqzAHnrJu68t8GS2.v0Z7hJSstAo7wBBDmmbpjYGxMMTYpVi', 'user', 'user', false, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890', true); --insert default products into the table INSERT INTO "product" ("category", "photo_url", "name", "description") VALUES ('Healing', '/api/file?path=config/products/crystals/amethyst.jpg&type=image/jpg', 'Amethyst', 'a violet variety of quartz'); INSERT INTO "product" ("category", "photo_url", "name", "description") VALUES ('Gemstones', '/api/file?path=config/products/crystals/ruby.jpg&type=image/jpg', 'Ruby', 'an intense heart crystal'); diff --git a/charts/brokencrystals/templates/nginx-proxy-config.yaml b/charts/brokencrystals/templates/config-proxy.yaml similarity index 73% rename from charts/brokencrystals/templates/nginx-proxy-config.yaml rename to charts/brokencrystals/templates/config-proxy.yaml index c4eed89b..e7d488b3 100644 --- a/charts/brokencrystals/templates/nginx-proxy-config.yaml +++ b/charts/brokencrystals/templates/config-proxy.yaml @@ -23,24 +23,24 @@ data: } location /api { - proxy_pass http://{{ include "brokencrystals.fullname" . }}-nodejs:3000; + proxy_pass http://127.0.0.1:3000; } location /swagger { - proxy_pass http://{{ include "brokencrystals.fullname" . }}-nodejs:3000; + proxy_pass http://127.0.0.1:3000; } location /graphiql { - proxy_pass http://{{ include "brokencrystals.fullname" . }}-nodejs:3000; + proxy_pass http://127.0.0.1:3000; } location /graphql { - proxy_pass http://{{ include "brokencrystals.fullname" . }}-nodejs:3000; + proxy_pass http://127.0.0.1:3000; } location /put.raw { rewrite put.raw /api/file/raw?path=./gil.txt break; - proxy_pass http://{{ include "brokencrystals.fullname" . }}-nodejs:3000; + proxy_pass http://127.0.0.1:3000; } location ~* ^/(config\.js|config\.json|\.htaccess|\.env|\.nginx\.conf|\.robots\.txt)$ { diff --git a/charts/brokencrystals/templates/deployment.yaml b/charts/brokencrystals/templates/deployment.yaml new file mode 100644 index 00000000..26d9f56b --- /dev/null +++ b/charts/brokencrystals/templates/deployment.yaml @@ -0,0 +1,239 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Release.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + selector: + matchLabels: + app: {{ .Release.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/instance: {{ .Release.Name }} + app: {{ .Release.Name }} + spec: + hostAliases: + - ip: "127.0.0.1" + hostnames: + - "postgres" + - "keycloak-postgres" + - "keycloak" + - "nodejs" + - "proxy" + containers: + - name: postgres + image: postgres + livenessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 60 + periodSeconds: 30 + env: + - name: POSTGRES_DB + value: "bc" + - name: POSTGRES_USER + value: "bc" + - name: POSTGRES_PASSWORD + value: "bc" + resources: + requests: + cpu: 200m + memory: 100Mi + volumeMounts: + - name: {{ include "brokencrystals.fullname" . }}-postgres + mountPath: /docker-entrypoint-initdb.d/pg.sql + subPath: pg.sql + readOnly: true + + - name: keycloak-postgres + image: postgres:12.2-alpine + ports: + - containerPort: 5433 + livenessProbe: + tcpSocket: + port: 5433 + initialDelaySeconds: 60 + periodSeconds: 30 + env: + - name: POSTGRES_DB + value: "keycloak" + - name: POSTGRES_USER + value: "keycloak" + - name: POSTGRES_PASSWORD + value: "password" + resources: + requests: + cpu: 100m + memory: 50Mi + volumeMounts: + - name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres + mountPath: /usr/local/share/postgresql/postgresql.conf.sample + subPath: postgresql.conf.sample + readOnly: true + + - name: keycloak + image: jboss/keycloak:latest + resources: + requests: + cpu: 100m + memory: 500Mi + livenessProbe: + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 30 + env: + - name: DB_VENDOR + value: "POSTGRES" + - name: DB_ADDR + value: "keycloak-postgres" + - name: DB_PORT + value: "5433" + - name: DB_DATABASE + value: "keycloak" + - name: DB_SCHEMA + value: "public" + - name: DB_PASSWORD + value: "password" + - name: KEYCLOAK_USER + value: "admin" + - name: KEYCLOAK_PASSWORD + value: "Pa55w0rd" + - name: KEYCLOAK_IMPORT + value: "/opt/jboss/keycloak/imports/realm-export.json -Dkeycloak.profile.feature.upload_scripts=enabled" + - name: PROXY_ADDRESS_FORWARDING + value: "true" + - name: KEYCLOAK_FRONTEND_URL + value: "https://auth{{ .Values.ingress.authlevel }}{{ .Values.ingress.url }}/auth/" + volumeMounts: + - name: {{ include "brokencrystals.fullname" . }}-keycloak + mountPath: /opt/jboss/keycloak/imports/realm-export.json + subPath: realm-export.json + readOnly: true + + - name: nodejs + image: brightsec/brokencrystals:{{ .Values.images.main }} + env: + - name: URL + value: "https://{{ .Values.ingress.url }}" + - name: DATABASE_HOST + value: "postgres" + - name: DATABASE_SCHEMA + value: "bc" + - name: DATABASE_USER + value: "bc" + - name: DATABASE_PASSWORD + value: "bc" + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_DEBUG + value: "true" + - name: AWS_BUCKET + value: "https://neuralegion-open-bucket.s3.amazonaws.com" + - name: GOOGLE_MAPS_API + value: "AIzaSyD2wIxpYCuNI0Zjt8kChs2hLTS5abVQfRQ" + - name: JWT_PRIVATE_KEY_LOCATION + value: "config/keys/jwtRS256.key" + - name: JWT_PUBLIC_KEY_LOCATION + value: "config/keys/jwtRS256.key.pub.pem" + - name: JWT_SECRET_KEY + value: "1234" + - name: JWK_PRIVATE_KEY_LOCATION + value: "config/keys/jwk.key.pem" + - name: JWK_PUBLIC_KEY_LOCATION + value: "config/keys/jwk.pub.key.pem" + - name: JWK_PUBLIC_JSON + value: "config/keys/jwk.pub.json" + - name: JKU_URL + value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/jku.json" + - name: X5U_URL + value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/x509.crt" + resources: + requests: + cpu: 900m + memory: 1024Mi + limits: + memory: 15G + livenessProbe: + httpGet: + path: /api/config + port: 3000 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 30 + + - name: proxy + image: brightsec/brokencrystals-proxy-http:{{ .Values.images.client }} + env: + - name: URL + value: "https://{{ .Values.ingress.url }}" + - name: DATABASE_HOST + value: "postgres" + - name: DATABASE_SCHEMA + value: "bc" + - name: DATABASE_USER + value: "bc" + - name: DATABASE_PASSWORD + value: "bc" + - name: DATABASE_PORT + value: "5432" + - name: DATABASE_DEBUG + value: "true" + - name: AWS_BUCKET + value: "https://neuralegion-open-bucket.s3.amazonaws.com" + - name: GOOGLE_MAPS_API + value: "AIzaSyD2wIxpYCuNI0Zjt8kChs2hLTS5abVQfRQ" + - name: JWT_PRIVATE_KEY_LOCATION + value: "config/keys/jwtRS256.key" + - name: JWT_PUBLIC_KEY_LOCATION + value: "config/keys/jwtRS256.key.pub.pem" + - name: JWT_SECRET_KEY + value: "1234" + - name: JWK_PRIVATE_KEY_LOCATION + value: "config/keys/jwk.key.pem" + - name: JWK_PUBLIC_KEY_LOCATION + value: "config/keys/jwk.pub.key.pem" + - name: JWK_PUBLIC_JSON + value: "config/keys/jwk.pub.json" + - name: JKU_URL + value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/jku.json" + - name: X5U_URL + value: "https://raw.githubusercontent.com/NeuraLegion/brokencrystals/development/config/keys/x509.crt" + volumeMounts: + - name: {{ include "brokencrystals.fullname" . }}-nginx-proxy + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + readOnly: true + resources: + requests: + cpu: 500m + memory: 50Mi + livenessProbe: + httpGet: + path: / + port: 80 + scheme: HTTP + initialDelaySeconds: 120 + periodSeconds: 30 + restartPolicy: Always + + volumes: + - name: {{ include "brokencrystals.fullname" . }}-postgres + configMap: + name: {{ include "brokencrystals.fullname" . }}-postgres + - name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres + configMap: + name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres + - name: {{ include "brokencrystals.fullname" . }}-keycloak + configMap: + name: {{ include "brokencrystals.fullname" . }}-keycloak + - name: {{ include "brokencrystals.fullname" . }}-nginx-proxy + configMap: + name: {{ include "brokencrystals.fullname" . }}-nginx-proxy diff --git a/charts/brokencrystals/templates/ingress.yaml b/charts/brokencrystals/templates/ingress.yaml index 23488593..21fd918b 100644 --- a/charts/brokencrystals/templates/ingress.yaml +++ b/charts/brokencrystals/templates/ingress.yaml @@ -2,7 +2,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "brokencrystals.fullname" . }}-prod + name: {{ include "brokencrystals.fullname" . }} namespace: {{ .Release.Namespace }} annotations: kubernetes.io/ingress.class: nginx @@ -15,7 +15,7 @@ spec: tls: - hosts: - {{ .Values.ingress.url }} - secretName: {{ if eq .Values.ingress.cert "" }}{{ include "brokencrystals.fullname" . }}-brokencrystals-prod-secret{{ else }}{{ .Values.ingress.cert }}{{ end }} + secretName: {{ if eq .Values.ingress.cert "" }}{{ include "brokencrystals.fullname" . }}-brokencrystals-secret{{ else }}{{ .Values.ingress.cert }}{{ end }} rules: - host: {{ .Values.ingress.url }} http: @@ -24,7 +24,7 @@ spec: pathType: Prefix backend: service: - name: {{ include "brokencrystals.fullname" . }}-nodejs-proxy-prod-service + name: {{ .Release.Name }} port: number: 80 @@ -32,7 +32,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "brokencrystals.fullname" . }}-prod-keycloak + name: {{ include "brokencrystals.fullname" . }}-keycloak namespace: {{ .Release.Namespace }} annotations: kubernetes.io/ingress.class: nginx @@ -45,7 +45,7 @@ spec: tls: - hosts: - auth{{ .Values.ingress.authlevel }}{{ .Values.ingress.url }} - secretName: {{ if eq .Values.ingress.cert "" }}{{ include "brokencrystals.fullname" . }}-brokencrystals-prod-keycloak-secret{{ else }}{{ .Values.ingress.cert }}{{ end }} + secretName: {{ if eq .Values.ingress.cert "" }}{{ include "brokencrystals.fullname" . }}-brokencrystals-keycloak-secret{{ else }}{{ .Values.ingress.cert }}{{ end }} rules: - host: auth{{ .Values.ingress.authlevel }}{{ .Values.ingress.url }} http: @@ -54,6 +54,6 @@ spec: pathType: Prefix backend: service: - name: {{ include "brokencrystals.fullname" . }}-keycloak-prod-service + name: {{ .Release.Name }}-keycloak port: - number: 8080 \ No newline at end of file + number: 8080 diff --git a/charts/brokencrystals/templates/keycloak-deployment.yaml b/charts/brokencrystals/templates/keycloak-deployment.yaml deleted file mode 100644 index f5dcbe52..00000000 --- a/charts/brokencrystals/templates/keycloak-deployment.yaml +++ /dev/null @@ -1,79 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "brokencrystals.fullname" . }}-keycloak - namespace: {{ .Release.Namespace }} - labels: - app: {{ include "brokencrystals.fullname" . }}-keycloak -spec: - selector: - matchLabels: - app: {{ include "brokencrystals.fullname" . }}-keycloak - template: - metadata: - labels: - app: {{ include "brokencrystals.fullname" . }}-keycloak - spec: - containers: - - name: {{ include "brokencrystals.fullname" . }}-keycloak - image: jboss/keycloak:latest - resources: - requests: - cpu: 100m - memory: 500Mi - livenessProbe: - httpGet: - path: / - port: 8080 - scheme: HTTP - initialDelaySeconds: 120 - periodSeconds: 30 - - env: - - name: DB_VENDOR - value: "POSTGRES" - - name: DB_ADDR - value: "{{ include "brokencrystals.fullname" . }}-keycloak-postgres" - - name: DB_DATABASE - value: "keycloak" - - name: DB_SCHEMA - value: "public" - - name: DB_PASSWORD - value: "password" - - name: KEYCLOAK_USER - value: "admin" - - name: KEYCLOAK_PASSWORD - value: "Pa55w0rd" - - name: KEYCLOAK_IMPORT - value: "/opt/jboss/keycloak/imports/realm-export.json -Dkeycloak.profile.feature.upload_scripts=enabled" - - name: PROXY_ADDRESS_FORWARDING - value: "true" - - name: KEYCLOAK_FRONTEND_URL - value: "https://auth{{ .Values.ingress.authlevel }}{{ .Values.ingress.url }}/auth/" - - volumeMounts: - - name: {{ include "brokencrystals.fullname" . }}-keycloak - mountPath: /opt/jboss/keycloak/imports/realm-export.json - subPath: realm-export.json - readOnly: true - - volumes: - - name: {{ include "brokencrystals.fullname" . }}-keycloak - configMap: - name: {{ include "brokencrystals.fullname" . }}-keycloak ---- -kind: Service -apiVersion: v1 -metadata: - name: {{ include "brokencrystals.fullname" . }}-keycloak - namespace: {{ .Release.Namespace }} -spec: - selector: - app: {{ include "brokencrystals.fullname" . }}-keycloak - ports: - - name: http - port: 8080 - protocol: TCP - targetPort: 8080 - diff --git a/charts/brokencrystals/templates/keycloak-postgres-deployment.yaml b/charts/brokencrystals/templates/keycloak-postgres-deployment.yaml deleted file mode 100644 index d4f79422..00000000 --- a/charts/brokencrystals/templates/keycloak-postgres-deployment.yaml +++ /dev/null @@ -1,53 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - namespace: {{ .Release.Namespace }} - labels: - app: {{ include "brokencrystals.fullname" . }}-keycloak-postgres -spec: - selector: - matchLabels: - app: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - template: - metadata: - labels: - app: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - spec: - containers: - - name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - image: postgres:12.2-alpine - livenessProbe: - tcpSocket: - port: 5432 - initialDelaySeconds: 60 - periodSeconds: 30 - env: - - name: POSTGRES_DB - value: "keycloak" - - name: POSTGRES_USER - value: "keycloak" - - name: POSTGRES_PASSWORD - value: "password" - resources: - requests: - cpu: 100m - memory: 50Mi - restartPolicy: Always - ---- -kind: Service -apiVersion: v1 -metadata: - name: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - namespace: {{ .Release.Namespace }} -spec: - selector: - app: {{ include "brokencrystals.fullname" . }}-keycloak-postgres - ports: - - name: postgres-keycloak - port: 5432 - protocol: TCP - targetPort: 5432 - diff --git a/charts/brokencrystals/templates/service.yaml b/charts/brokencrystals/templates/service.yaml new file mode 100644 index 00000000..31dbe8ec --- /dev/null +++ b/charts/brokencrystals/templates/service.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} +spec: + selector: + app: {{ .Release.Name }} + ports: + - protocol: TCP + port: 80 + targetPort: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-keycloak +spec: + selector: + app: {{ .Release.Name }} + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + \ No newline at end of file diff --git a/charts/brokencrystals/values.yaml b/charts/brokencrystals/values.yaml index 56815b01..cf35515a 100644 --- a/charts/brokencrystals/values.yaml +++ b/charts/brokencrystals/values.yaml @@ -1,7 +1,7 @@ ingress: - url: brokencrystals.dev.vuln.nexploit.app + url: k3s.brokencrystals.nexploit.app cert: "" authlevel: "." images: - main: master - client: master + main: latest + client: latest diff --git a/package-lock.json b/package-lock.json index 25b6536a..1e3b4d21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2197,6 +2197,12 @@ "@sinonjs/commons": "^1.7.0" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, "@ts-morph/common": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.18.1.tgz", @@ -3935,23 +3941,6 @@ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, "csv": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.3.tgz", @@ -3986,51 +3975,6 @@ "assert-plus": "^1.0.0" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "dependencies": { - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - } - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4457,15 +4401,14 @@ "integrity": "sha512-WLLmvdG72Z0pCq8XUBd03GEJlAiMceXFanjdQeEzeSiuV1ZgrJqbkU7ZEe/hu0OsBlg5wLlySEeOvfzcGoO8mg==" }, "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "requires": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { @@ -4475,51 +4418,12 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } } } }, @@ -5560,17 +5464,6 @@ } } }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -6114,15 +6007,6 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6969,6 +6853,206 @@ "jest-mock": "^26.6.2", "jest-util": "^26.6.2", "jsdom": "^16.4.0" + }, + "dependencies": { + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + } } }, "jest-environment-node": { @@ -7525,116 +7609,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8893,9 +8867,9 @@ "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" }, "nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", "dev": true }, "oauth-sign": { @@ -9197,12 +9171,6 @@ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==" }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -10222,15 +10190,6 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, "schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", @@ -11868,15 +11827,6 @@ "browser-process-hrtime": "^1.0.0" } }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -12007,26 +11957,6 @@ } } }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -12129,18 +12059,22 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==" }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" + }, + "xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 041e7253..9922d11a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,9 @@ "rimraf": "^3.0.2", "rxjs": "^7.8.0", "stream-buffers": "^3.0.2", - "ts-morph": "^17.0.1" + "ts-morph": "^17.0.1", + "xmldom": "^0.6.0", + "xpath": "0.0.34" }, "devDependencies": { "@mikro-orm/cli": "^4.5.10", diff --git a/pg.sql b/pg.sql index a8473569..1095537e 100644 --- a/pg.sql +++ b/pg.sql @@ -1,7 +1,7 @@ set names 'utf8'; set session_replication_role = 'replica'; -create table "user" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "email" varchar(255) not null, "password" varchar(255) not null, "first_name" varchar(255) not null, "last_name" varchar(255) not null, "is_admin" bool not null, "photo" bytea null, "company" varchar(255) not null, "card_number" varchar(255) not null, "phone_number" varchar(255) not null); +create table "user" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "email" varchar(255) not null, "password" varchar(255) not null, "first_name" varchar(255) not null, "last_name" varchar(255) not null, "is_admin" bool not null, "photo" bytea null, "company" varchar(255) not null, "card_number" varchar(255) not null, "phone_number" varchar(255) not null, "is_basic" bool not null); create table "testimonial" ("id" serial primary key, "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "name" varchar(255) not null, "title" varchar(255) not null, "message" varchar(255) not null); @@ -9,8 +9,8 @@ create table "product" ("id" serial primary key, "created_at" timestamptz(0) not set session_replication_role = 'origin'; --password is admin -INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number) VALUES (now(), now(), 'admin', '$2b$10$BBJjmVNNdyEgv7pV/zQR9u/ssIuwZsdDJbowW/Dgp28uws3GmO0Ky', 'admin', 'admin', true, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890'); -INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number) VALUES (now(), now(), 'user', '$2b$10$edsq4aqzAHnrJu68t8GS2.v0Z7hJSstAo7wBBDmmbpjYGxMMTYpVi', 'user', 'user', false, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890'); +INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number, is_basic) VALUES (now(), now(), 'admin', '$2b$10$BBJjmVNNdyEgv7pV/zQR9u/ssIuwZsdDJbowW/Dgp28uws3GmO0Ky', 'admin', 'admin', true, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890', true); +INSERT INTO "user" (created_at, updated_at, email, password, first_name, last_name, is_admin, photo, company, card_number, phone_number, is_basic) VALUES (now(), now(), 'user', '$2b$10$edsq4aqzAHnrJu68t8GS2.v0Z7hJSstAo7wBBDmmbpjYGxMMTYpVi', 'user', 'user', false, null, 'Brightsec', '1234 5678 9012 3456', '+1 234 567 890', true); --insert default products into the table INSERT INTO "product" ("category", "photo_url", "name", "description") VALUES ('Healing', '/api/file?path=config/products/crystals/amethyst.jpg&type=image/jpg', 'Amethyst', 'a violet variety of quartz'); diff --git a/public/package-lock.json b/public/package-lock.json index f940e41a..92097f77 100644 --- a/public/package-lock.json +++ b/public/package-lock.json @@ -2588,6 +2588,11 @@ "@babel/runtime": "^7.12.5" } }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2913,6 +2918,12 @@ } } }, + "@types/xmldom": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.34.tgz", + "integrity": "sha512-7eZFfxI9XHYjJJuugddV6N5YNeXgQE1lArWOcd1eCOKWb/FGs5SIjacSYuEJuwhsGS3gy4RuZ5EUIcqYscuPDA==", + "dev": true + }, "@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -3199,9 +3210,9 @@ "dev": true }, "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, "accepts": { @@ -3258,15 +3269,6 @@ "regex-parser": "^2.2.11" } }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -5714,23 +5716,6 @@ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, "csstype": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", @@ -5848,17 +5833,6 @@ "assert-plus": "^1.0.0" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - } - }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -5887,9 +5861,9 @@ "dev": true }, "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, "decode-uri-component": { @@ -6668,15 +6642,14 @@ "dev": true }, "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "requires": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { @@ -6685,45 +6658,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } } } }, @@ -7899,6 +7833,16 @@ } } }, + "file-type": { + "version": "16.5.3", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz", + "integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==", + "requires": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -8736,15 +8680,6 @@ "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", "dev": true }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, "html-entities": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", @@ -8915,17 +8850,6 @@ "requires-port": "^1.0.0" } }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, "http-proxy-middleware": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", @@ -9060,16 +8984,6 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -9106,8 +9020,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "iferr": { "version": "0.1.5", @@ -9221,8 +9134,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.7", @@ -10214,6 +10126,21 @@ "@types/yargs-parser": "*" } }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -10223,6 +10150,205 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true } } }, @@ -11387,77 +11513,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "dev": true - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12685,9 +12740,9 @@ "dev": true }, "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", "dev": true }, "object-assign": { @@ -13068,12 +13123,6 @@ "lines-and-columns": "^1.1.6" } }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13182,6 +13231,11 @@ "sha.js": "^2.4.8" } }, + "peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -15187,6 +15241,26 @@ } } }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "requires": { + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -16141,15 +16215,6 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, "scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -17077,7 +17142,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" }, @@ -17085,8 +17149,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -17169,6 +17232,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + } + }, "style-loader": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", @@ -17714,6 +17786,15 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -17724,15 +17805,6 @@ "punycode": "^2.1.1" } }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, "tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -18122,8 +18194,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -18233,15 +18304,6 @@ "browser-process-hrtime": "^1.0.0" } }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, "wait-on": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", @@ -18537,12 +18599,6 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", "integrity": "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==" }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, "webpack": { "version": "4.44.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", @@ -19425,38 +19481,12 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, "whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", "dev": true }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -19787,24 +19817,17 @@ "typedarray-to-buffer": "^3.1.5" } }, - "ws": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", - "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/public/package.json b/public/package.json index 52f5c23b..f8079d21 100644 --- a/public/package.json +++ b/public/package.json @@ -6,13 +6,15 @@ "dependencies": { "axios": "^0.21.4", "dangerously-set-html-content": "^1.0.8", + "file-type": "^16.5.3", "get-browser-fingerprint": "^2.0.1", "history": "^4.10.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-owl-carousel": "^2.3.3", "react-router-dom": "^5.2.0", - "web-vitals": "^0.2.4" + "web-vitals": "^0.2.4", + "xmldom": "^0.6.0" }, "scripts": { "cypress:run": "cypress open --browser chrome", @@ -48,15 +50,16 @@ "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.6", + "@types/xmldom": "^0.1.34", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", "cross-env": "^7.0.3", "cypress": "^6.4.0", "eslint": "^7.20.0", - "jest": "26.6.0", "eslint-config-prettier": "^7.2.0", - "start-server-and-test": "^1.12.0", + "jest": "26.6.0", "react-scripts": "4.0.3", + "start-server-and-test": "^1.12.0", "typescript": "4.0.5" } } diff --git a/public/public/assets/css/style.css b/public/public/assets/css/style.css index 80f51fe3..30ade4f2 100644 --- a/public/public/assets/css/style.css +++ b/public/public/assets/css/style.css @@ -848,6 +848,42 @@ section { min-height: 400px; } +/*-------------------------------------------------------------- +# Partners +--------------------------------------------------------------*/ +.partners .partner-item { + box-sizing: content-box; +} + +.partner-item { + font-style: italic; + margin: 0 15px 0 15px; + padding: 15px 20px 35px 25px; + background: #fff; + border-radius: 6px; + z-index: 1; + box-shadow: 0 0px 20px 0 rgba(0, 0, 0, 0.1); +} + +.partner-img { + border: 6px solid #fff; + + max-width: 350px; + width: 230px; + + height: 100%; + vertical-align: middle; +} + +.partner-name { + color: #4c99df; + font-weight: normal; + text-align: center; + + margin-bottom: 5px; + margin-top: 5px; +} + /*-------------------------------------------------------------- # Testimonials --------------------------------------------------------------*/ diff --git a/public/public/assets/img/partners/gus-fring.jpg b/public/public/assets/img/partners/gus-fring.jpg new file mode 100644 index 00000000..45c1eff1 Binary files /dev/null and b/public/public/assets/img/partners/gus-fring.jpg differ diff --git a/public/public/assets/img/partners/jesse-pinkman.jpg b/public/public/assets/img/partners/jesse-pinkman.jpg new file mode 100644 index 00000000..692da177 Binary files /dev/null and b/public/public/assets/img/partners/jesse-pinkman.jpg differ diff --git a/public/public/assets/img/partners/michael-ehrmantraut.jpg b/public/public/assets/img/partners/michael-ehrmantraut.jpg new file mode 100644 index 00000000..580f4e5f Binary files /dev/null and b/public/public/assets/img/partners/michael-ehrmantraut.jpg differ diff --git a/public/public/assets/img/partners/walter-white.jpg b/public/public/assets/img/partners/walter-white.jpg new file mode 100644 index 00000000..71c65cac Binary files /dev/null and b/public/public/assets/img/partners/walter-white.jpg differ diff --git a/public/src/api/ApiUrl.ts b/public/src/api/ApiUrl.ts index eebb2d50..4050fc1b 100644 --- a/public/src/api/ApiUrl.ts +++ b/public/src/api/ApiUrl.ts @@ -9,5 +9,7 @@ export enum ApiUrl { Goto = '/api/goto', Render = '/api/render', Spawn = '/api/spawn', - File = '/api/file' + File = '/api/file', + NestedJson = '/api/nestedJson', + Partners = '/api/partners' } diff --git a/public/src/api/httpClient.ts b/public/src/api/httpClient.ts index b82e082f..619b0595 100644 --- a/public/src/api/httpClient.ts +++ b/public/src/api/httpClient.ts @@ -103,6 +103,13 @@ export function getUserData( }); } +export function getUserDataById(id: string): Promise { + return makeApiRequest({ + url: `${ApiUrl.Users}/id/${id}`, + method: 'get' + }); +} + export function getLdap(ldapProfileLink: string): Promise { return makeApiRequest({ url: `${ApiUrl.Users}/ldap?query=${encodeURIComponent(ldapProfileLink)}`, @@ -141,8 +148,7 @@ export function postMetadata(): Promise { url: `${ApiUrl.Metadata}`, method: 'post', headers: { 'content-type': 'text/xml' }, - data: - ' ]>' + data: ' ]>' }); } @@ -166,6 +172,20 @@ export function getUserPhoto(email: string): Promise { }); } +export function removeUserPhotoById( + id: string, + isAdmin: boolean +): Promise { + return makeApiRequest({ + url: `${ApiUrl.Users}/one/${id}/photo?isAdmin=${isAdmin}`, + method: 'delete', + headers: { + authorization: + sessionStorage.getItem('token') || localStorage.getItem('token') + } + }); +} + export function getAdminStatus(email: string): Promise { return makeApiRequest({ url: `${ApiUrl.Users}/one/${email}/adminpermission`, @@ -261,3 +281,31 @@ export function viewProduct(productName: string): Promise { } }); } + +export function getNestedJson(jsonNestingLevel: number = 1): Promise { + return makeApiRequest({ + url: `${ApiUrl.NestedJson}?depth=${jsonNestingLevel}`, + method: 'get' + }); +} + +export function queryPartnersRaw(xpath: string): Promise { + return makeApiRequest({ + url: `${ApiUrl.Partners}/query?xpath=${xpath}`, + method: 'get' + }); +} + +export function partnerLogin(username: string, password: string): Promise { + return makeApiRequest({ + url: `${ApiUrl.Partners}/partnerLogin?username=${username}&password=${password}`, + method: 'get' + }); +} + +export function searchPartners(keyword: string): Promise { + return makeApiRequest({ + url: `${ApiUrl.Partners}/searchPartners?keyword=${keyword}`, + method: 'get' + }); +} diff --git a/public/src/interfaces/Partner.ts b/public/src/interfaces/Partner.ts new file mode 100644 index 00000000..bf9be060 --- /dev/null +++ b/public/src/interfaces/Partner.ts @@ -0,0 +1,4 @@ +export interface Partner { + name: string; + photoUrl: string; +} diff --git a/public/src/pages/auth/Login/Login.tsx b/public/src/pages/auth/Login/Login.tsx index 3ec75961..0b2218a7 100644 --- a/public/src/pages/auth/Login/Login.tsx +++ b/public/src/pages/auth/Login/Login.tsx @@ -83,12 +83,13 @@ export const Login: FC = () => { sessionStorage.setItem('email', email); return getUserData(email); }) - .then((userData: UserData) => + .then((userData: UserData) => { sessionStorage.setItem( 'userName', `${userData.firstName} ${userData.lastName}` - ) - ); + ); + sessionStorage.setItem('user_id', userData.id); + }); }; const sendLdap = () => { diff --git a/public/src/pages/main/Contact.tsx b/public/src/pages/main/Contact.tsx index 1701ec2a..d26f3c9c 100644 --- a/public/src/pages/main/Contact.tsx +++ b/public/src/pages/main/Contact.tsx @@ -1,6 +1,15 @@ -import React, { FC } from 'react'; +import React, { useEffect } from 'react'; + +export const Contact = (props: { mapTitle: string | null }) => { + useEffect(() => { + const mapElement = document.getElementById('about-map'); + if (mapElement) { + mapElement.outerHTML = ``; + } + }, []); -export const Contact: FC = () => { return (
@@ -44,6 +53,7 @@ export const Contact: FC = () => {
`; + } + }, []); + return ( - <> +
{props.preview ||
}
@@ -85,6 +105,7 @@ export const Marketplace: FC = (props: Props) => {
)}
+
@@ -119,7 +140,22 @@ export const Marketplace: FC = (props: Props) => {
- + {props.preview || ( +
+
+ +
+
+ )} + ); }; diff --git a/public/src/pages/marketplace/Partners/Partners.tsx b/public/src/pages/marketplace/Partners/Partners.tsx new file mode 100644 index 00000000..9fab3605 --- /dev/null +++ b/public/src/pages/marketplace/Partners/Partners.tsx @@ -0,0 +1,97 @@ +import React, { FC, useEffect, useState } from 'react'; +import { DOMParser } from 'xmldom'; + +import OwlCarousel from 'react-owl-carousel'; +import 'owl.carousel/dist/assets/owl.carousel.css'; +import 'owl.carousel/dist/assets/owl.theme.default.css'; + +import { searchPartners, partnerLogin } from '../../../api/httpClient'; +import { Partner } from '../../../interfaces/Partner'; + +export const Partners: FC = () => { + const PARTNER_DEFAULT_USERNAME = 'walter100'; + const PARTNER_DEFAULT_PASSWORD = 'Heisenberg123'; + + const [partners, setPartners] = useState>([]); + + const fetchPartners = () => { + // XPATH injection string detection + searchPartners('').then((data) => { + const partnersList: Array = []; + + const xmlDoc = new DOMParser().parseFromString(data, 'text/xml'); + + if (!xmlDoc) { + partnersList.push({ name: 'Failed loading name', photoUrl: '' }); + setPartners(partnersList); + return; + } + + const partnerNameTags = xmlDoc.getElementsByTagName('name'); + + for (const nameTag of Array.from(partnerNameTags)) { + const name = nameTag.textContent || 'Error in loading name'; + const photoUrl = `assets/img/partners/${name + .toLowerCase() + .replace(' ', '-')}.jpg`; + partnersList.push({ name: name, photoUrl: photoUrl }); + } + + setPartners(partnersList); + }); + }; + + const loginPartner = () => { + // XPATH injection boolean detection + partnerLogin(PARTNER_DEFAULT_USERNAME, PARTNER_DEFAULT_PASSWORD).then( + (data) => { + const xmlDoc = new DOMParser().parseFromString(data, 'text/xml'); + + if (!xmlDoc) { + console.log(`Partner login as '${PARTNER_DEFAULT_USERNAME}' failed`); + return; + } + + console.log(`Partner login as '${PARTNER_DEFAULT_USERNAME}' succeded!`); + } + ); + }; + + useEffect(() => { + fetchPartners(); + loginPartner(); + }, [partners.length]); + + const generatePartnerItem = (partner: Partner, idx: number) => ( +
+

{partner.name}

+ +
+ ); + + return ( +
+
+
+

Our Partners

+
+ +
+ {partners ? ( + + {partners.map((partner, idx) => + generatePartnerItem(partner, idx) + )} + + ) : null} +
+
+
+ ); +}; + +export default Partners; diff --git a/src/api/app.model.ts b/src/api/app.model.ts new file mode 100644 index 00000000..4f5562ef --- /dev/null +++ b/src/api/app.model.ts @@ -0,0 +1,4 @@ +import { ObjectType } from '@nestjs/graphql'; + +@ObjectType({ description: 'app ' }) +export class App {} diff --git a/src/app.controller.swagger.desc.ts b/src/app.controller.swagger.desc.ts index bdbef2c3..b265f085 100644 --- a/src/app.controller.swagger.desc.ts +++ b/src/app.controller.swagger.desc.ts @@ -1,11 +1,15 @@ -export const SWAGGER_DESC_RENDER_REQUEST = `Template for rendering by doT. Expects plain text as request body`; +export const API_DESC_RENDER_REQUEST = `Template for rendering by doT. Expects plain text as request body`; -export const SWAGGER_DESC_REDIRECT_REQUEST = `Redirects the user to the provided url`; +export const API_DESC_REDIRECT_REQUEST = `Redirects the user to the provided url`; -export const SWAGGER_DESC_XML_METADATA = `Receives client's metadata in XML format. Returns the passed XML`; +export const API_DESC_XML_METADATA = `Receives client's metadata in XML format. Returns the passed XML`; -export const SWAGGER_DESC_OPTIONS_REQUEST = `Returns the list of supported operations`; +export const API_DESC_OPTIONS_REQUEST = `Returns the list of supported operations`; -export const SWAGGER_DESC_LAUNCH_COMMAND = `Launches system command on server`; +export const API_DESC_LAUNCH_COMMAND = `Launches system command on server`; -export const SWAGGER_DESC_CONFIG_SERVER = `Returns server configuration to the client`; +export const API_DESC_CONFIG_SERVER = `Returns server configuration to the client`; + +export const SWAGGER_DESC_SECRETS = `Returns server secrets. Shhhh 🤫`; + +export const SWAGGER_DESC_NESTED_JSON = `Returns a nested JSON response with configurable depth`; diff --git a/src/app.controller.ts b/src/app.controller.ts index 2eea5a34..dde32d17 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -5,6 +5,7 @@ import { Get, Header, HttpException, + InternalServerErrorException, Logger, Options, Param, @@ -14,8 +15,10 @@ import { SerializeOptions, UseGuards, UseInterceptors, + ParseIntPipe, + DefaultValuePipe, + HttpStatus, } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { ApiBody, ApiConsumes, @@ -25,26 +28,26 @@ import { ApiOkResponse, ApiOperation, ApiProduces, + ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { spawn } from 'child_process'; import * as dotT from 'dot'; import { parseXml } from 'libxmljs'; import { AppConfig } from './app.config.api'; -import { AppModuleConfigProperties } from './app.module.config.properties'; -import { OrmModuleConfigProperties } from './orm/orm.module.config.properties'; import { - SWAGGER_DESC_CONFIG_SERVER, - SWAGGER_DESC_LAUNCH_COMMAND, - SWAGGER_DESC_OPTIONS_REQUEST, - SWAGGER_DESC_REDIRECT_REQUEST, - SWAGGER_DESC_RENDER_REQUEST, - SWAGGER_DESC_XML_METADATA, + API_DESC_CONFIG_SERVER, + API_DESC_LAUNCH_COMMAND, + API_DESC_OPTIONS_REQUEST, + API_DESC_REDIRECT_REQUEST, + API_DESC_RENDER_REQUEST, + API_DESC_XML_METADATA, + SWAGGER_DESC_SECRETS, + SWAGGER_DESC_NESTED_JSON, } from './app.controller.swagger.desc'; -import { UsersService } from './users/users.service'; import { AuthGuard } from './auth/auth.guard'; import { JwtType } from './auth/jwt/jwt.type.decorator'; import { JwtProcessorType } from './auth/auth.service'; +import { AppService } from './app.service'; import { BASIC_USER_INFO, UserDto } from './users/api/UserDto'; import { SWAGGER_DESC_FIND_USER } from './users/users.controller.swagger.desc'; @@ -53,16 +56,13 @@ import { SWAGGER_DESC_FIND_USER } from './users/users.controller.swagger.desc'; export class AppController { private readonly logger = new Logger(AppController.name); - constructor( - private readonly configService: ConfigService, - private readonly userService: UsersService, - ) {} + constructor(private readonly appService: AppService) { } @Post('render') @ApiProduces('text/plain') @ApiConsumes('text/plain') @ApiOperation({ - description: SWAGGER_DESC_RENDER_REQUEST, + description: API_DESC_RENDER_REQUEST, }) @ApiBody({ description: 'Write your text here' }) @ApiCreatedResponse({ @@ -78,8 +78,9 @@ export class AppController { } @Get('goto') + @ApiQuery({ name: 'url', example: 'https://google.com', required: true }) @ApiOperation({ - description: SWAGGER_DESC_REDIRECT_REQUEST, + description: API_DESC_REDIRECT_REQUEST, }) @ApiOkResponse({ description: 'Redirected', @@ -90,8 +91,19 @@ export class AppController { } @Post('metadata') + @ApiProduces('text/plain') + @ApiConsumes('text/plain') + @ApiBody({ + type: String, + examples: { + xml_doc: { + summary: 'XML doc', + value: ``, + }, + }, + }) @ApiOperation({ - description: SWAGGER_DESC_XML_METADATA, + description: API_DESC_XML_METADATA, }) @ApiInternalServerErrorResponse({ description: 'Invalid data', @@ -117,7 +129,7 @@ export class AppController { @Options() @ApiOperation({ - description: SWAGGER_DESC_OPTIONS_REQUEST, + description: API_DESC_OPTIONS_REQUEST, }) @Header('allow', 'OPTIONS, GET, HEAD, POST') async getTestOptions(): Promise { @@ -125,8 +137,9 @@ export class AppController { } @Get('spawn') + @ApiQuery({ name: 'command', example: 'ls -la', required: true }) @ApiOperation({ - description: SWAGGER_DESC_LAUNCH_COMMAND, + description: API_DESC_LAUNCH_COMMAND, }) @ApiOkResponse({ type: String, @@ -137,38 +150,21 @@ export class AppController { properties: { location: { type: 'string' } }, }, }) - async launchCommand(@Query('command') command: string): Promise { + async getCommandResult(@Query('command') command: string): Promise { this.logger.debug(`launch ${command} command`); - - return new Promise((res, rej) => { - try { - const [exec, ...args] = command.split(' '); - const ps = spawn(exec, args); - - ps.stdout.on('data', (data: Buffer) => { - this.logger.debug(`stdout: ${data}`); - res(data.toString('ascii')); - }); - - ps.stderr.on('data', (data: Buffer) => { - this.logger.debug(`stderr: ${data}`); - res(data.toString('ascii')); - }); - - ps.on('error', (err) => rej(err.message)); - - ps.on('close', (code) => - this.logger.debug(`child process exited with code ${code}`), - ); - } catch (err) { - rej(err.message); - } - }); + try { + return await this.appService.launchCommand(command); + } catch (err) { + throw new InternalServerErrorException({ + error: err.message || err, + location: __filename, + }); + } } @Get('/config') @ApiOperation({ - description: SWAGGER_DESC_CONFIG_SERVER, + description: API_DESC_CONFIG_SERVER, }) @ApiOkResponse({ type: AppConfig, @@ -176,33 +172,44 @@ export class AppController { }) getConfig(): AppConfig { this.logger.debug('Called getConfig'); - const dbSchema = this.configService.get( - OrmModuleConfigProperties.ENV_DATABASE_SCHEMA, - ); - const dbHost = this.configService.get( - OrmModuleConfigProperties.ENV_DATABASE_HOST, - ); - const dbPort = this.configService.get( - OrmModuleConfigProperties.ENV_DATABASE_PORT, - ); - const dbUser = this.configService.get( - OrmModuleConfigProperties.ENV_DATABASE_USER, - ); - const dbPwd = this.configService.get( - OrmModuleConfigProperties.ENV_DATABASE_PASSWORD, - ); - return { - awsBucket: this.configService.get( - AppModuleConfigProperties.ENV_AWS_BUCKET, - ), - sql: `postgres://${dbUser}:${dbPwd}@${dbHost}:${dbPort}/${dbSchema} `, - googlemaps: this.configService.get( - AppModuleConfigProperties.ENV_GOOGLE_MAPS, - ), + const config = this.appService.getConfig(); + return config; + } + + @Get('/secrets') + @ApiOperation({ + description: SWAGGER_DESC_SECRETS, + }) + @ApiOkResponse({ + type: Object, + status: 200, + }) + getSecrets(): Object { + const secrets = { + codeclimate: + 'CODECLIMATE_REPO_TOKEN=62864c476ade6ab9d10d0ce0901ae2c211924852a28c5f960ae5165c1fdfec73', + facebook: + 'EAACEdEose0cBAHyDF5HI5o2auPWv3lPP3zNYuWWpjMrSaIhtSvX73lsLOcas5k8GhC5HgOXnbF3rXRTczOpsbNb54CQL8LcQEMhZAWAJzI0AzmL23hZByFAia5avB6Q4Xv4u2QVoAdH0mcJhYTFRpyJKIAyDKUEBzz0GgZDZD', + google_b64: 'QUl6YhT6QXlEQnbTr2dSdEI1W7yL2mFCX3c4PPP5NlpkWE65NkZV', + google_oauth: + '188968487735-c7hh7k87juef6vv84697sinju2bet7gn.apps.googleusercontent.com', + google_oauth_token: + 'ya29.a0TgU6SMDItdQQ9J7j3FVgJuByTTevl0FThTEkBs4pA4-9tFREyf2cfcL-_JU6Trg1O0NWwQKie4uGTrs35kmKlxohWgcAl8cg9DTxRx-UXFS-S1VYPLVtQLGYyNTfGp054Ad3ej73-FIHz3RZY43lcKSorbZEY4BI', + heroku: + 'herokudev.staging.endosome.975138 pid=48751 request_id=0e9a8698-a4d2-4925-a1a5-113234af5f60', + hockey_app: 'HockeySDK: 203d3af93f4a218bfb528de08ae5d30ff65e1cf', + outlook: + 'https://outlook.office.com/webhook/7dd49fc6-1975-443d-806c-08ebe8f81146@a532313f-11ec-43a2-9a7a-d2e27f4f3478/IncomingWebhook/8436f62b50ab41b3b93ba1c0a50a0b88/eff4cd58-1bb8-4899-94de-795f656b4a18', + paypal: + 'access_token$production$x0lb4r69dvmmnufd$3ea7cb281754b7da7dac131ef5783321', + slack: + 'xoxo-175588824543-175748345725-176608801663-826315f84e553d482bb7e73e8322sdf3', }; + return secrets; } @Get('/v1/userinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseInterceptors(ClassSerializerInterceptor) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ @@ -224,14 +231,14 @@ export class AppController { }) async getUserInfo(@Param('email') email: string): Promise { try { - this.logger.debug(`Find a user by email: ${email}`); - return new UserDto(await this.userService.findByEmail(email)); + return await this.appService.getUserInfo(email); } catch (err) { throw new HttpException(err.message, err.status); } } @Get('/v2/userinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @UseInterceptors(ClassSerializerInterceptor) @@ -255,10 +262,32 @@ export class AppController { }) async getUserInfoV2(@Param('email') email: string): Promise { try { - this.logger.debug(`Find a user by email: ${email}`); - return new UserDto(await this.userService.findByEmail(email)); + return await this.appService.getUserInfo(email); } catch (err) { throw new HttpException(err.message, err.status); } } + + @Get('nestedJson') + @ApiOperation({ + description: SWAGGER_DESC_NESTED_JSON, + }) + @Header('content-type', 'application/json') + async getNestedJson(@Query('depth', new DefaultValuePipe(1), new ParseIntPipe({ errorHttpStatusCode: HttpStatus.BAD_REQUEST })) depth: number): Promise { + if (depth < 1) { + throw new HttpException("JSON nesting depth is invalid", HttpStatus.BAD_REQUEST); + } + + this.logger.debug(`Creating a JSON with a nesting depth of ${depth}`); + + var tmpObj: object = {}; + var jsonObj: object = { "0": "Leaf" }; + for (let i = 1; i < depth; i++) { + tmpObj = {}; + tmpObj[i.toString()] = Object.assign({}, jsonObj); + jsonObj = Object.assign({}, tmpObj); + } + + return JSON.stringify(jsonObj); + } } diff --git a/src/app.module.ts b/src/app.module.ts index e852ad5d..0410cfbd 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,12 +7,16 @@ import { SubscriptionsModule } from './subscriptions/subscriptions.module'; import { TestimonialsModule } from './testimonials/testimonials.module'; import { ProductsModule } from './products/products.module'; import { OrmModule } from './orm/orm.module'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { HttpClientService } from './httpclient/httpclient.service'; import { HttpClientModule as HttpClientModule } from './httpclient/httpclient.module'; import { TraceMiddleware } from './components/trace.middleware'; import { GraphQLModule } from '@nestjs/graphql'; import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius'; +import { AppService } from './app.service'; +import { UsersService } from './users/users.service'; +import { AppResolver } from './app.resolver'; +import { PartnersModule } from './partners/partners.module'; @Module({ imports: [ @@ -32,9 +36,16 @@ import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius'; graphiql: true, autoSchemaFile: true, }), + PartnersModule, ], controllers: [AppController], - providers: [HttpClientService], + providers: [ + HttpClientService, + AppService, + UsersService, + ConfigService, + AppResolver, + ], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/app.resolver.ts b/src/app.resolver.ts new file mode 100644 index 00000000..8d2e8a63 --- /dev/null +++ b/src/app.resolver.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException, Logger } from '@nestjs/common'; +import { Query, Resolver, Args } from '@nestjs/graphql'; +import { AppService } from './app.service'; +import { API_DESC_LAUNCH_COMMAND } from './app.controller.swagger.desc'; +import { App } from './api/app.model'; + +@Resolver(App) +export class AppResolver { + private readonly logger = new Logger(AppResolver.name); + + constructor(private readonly appService: AppService) {} + + @Query(() => String, { + description: API_DESC_LAUNCH_COMMAND, + }) + async getCommandResult(@Args('command') command: string): Promise { + this.logger.debug(`launch ${command} command`); + try { + return await this.appService.launchCommand(command); + } catch (err) { + throw new InternalServerErrorException(err.message); + } + } +} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100644 index 00000000..633f272c --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,85 @@ +import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { spawn } from 'child_process'; +import { ConfigService } from '@nestjs/config'; +import { UsersService } from './users/users.service'; +import { AppModuleConfigProperties } from './app.module.config.properties'; +import { OrmModuleConfigProperties } from './orm/orm.module.config.properties'; +import { AppConfig } from './app.config.api'; +import { UserDto } from './users/api/UserDto'; + +@Injectable() +export class AppService { + private readonly logger = new Logger(AppService.name); + + constructor( + private readonly configService: ConfigService, + private readonly userService: UsersService, + ) {} + + async launchCommand(command: string): Promise { + this.logger.debug(`launch ${command} command`); + + return new Promise((res, rej) => { + try { + const [exec, ...args] = command.split(' '); + const ps = spawn(exec, args); + + ps.stdout.on('data', (data: Buffer) => { + this.logger.debug(`stdout: ${data}`); + res(data.toString('ascii')); + }); + + ps.stderr.on('data', (data: Buffer) => { + this.logger.debug(`stderr: ${data}`); + res(data.toString('ascii')); + }); + + ps.on('error', (err) => rej(err.message)); + + ps.on('close', (code) => + this.logger.debug(`child process exited with code ${code}`), + ); + } catch (err) { + rej(err.message); + } + }); + } + + getConfig(): AppConfig { + this.logger.debug('Called getConfig'); + const dbSchema = this.configService.get( + OrmModuleConfigProperties.ENV_DATABASE_SCHEMA, + ), + dbHost = this.configService.get( + OrmModuleConfigProperties.ENV_DATABASE_HOST, + ), + dbPort = this.configService.get( + OrmModuleConfigProperties.ENV_DATABASE_PORT, + ), + dbUser = this.configService.get( + OrmModuleConfigProperties.ENV_DATABASE_USER, + ), + dbPwd = this.configService.get( + OrmModuleConfigProperties.ENV_DATABASE_PASSWORD, + ); + + return { + awsBucket: this.configService.get( + AppModuleConfigProperties.ENV_AWS_BUCKET, + ), + sql: `postgres://${dbUser}:${dbPwd}@${dbHost}:${dbPort}/${dbSchema} `, + googlemaps: this.configService.get( + AppModuleConfigProperties.ENV_GOOGLE_MAPS, + ), + }; + } + + async getUserInfo(email: string): Promise { + try { + this.logger.debug(`Find a user by email: ${email}`); + return new UserDto(await this.userService.findByEmail(email)); + } catch (err) { + throw new HttpException(err.message, err.status); + } + } +} diff --git a/src/auth/api/login.request.ts b/src/auth/api/login.request.ts index 314f35a7..cf45ad27 100644 --- a/src/auth/api/login.request.ts +++ b/src/auth/api/login.request.ts @@ -9,14 +9,15 @@ export enum FormMode { } export class LoginRequest { - @ApiProperty() + @ApiProperty({ example: 'john', required: true }) user: string; - @ApiProperty() + @ApiProperty({ example: 'Pa55w0rd', required: true }) password: string; @ApiProperty({ enum: FormMode, + required: true, }) op?: string; diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ef96048f..aec83b01 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -2,6 +2,7 @@ import { BadRequestException, Body, Controller, + ForbiddenException, Get, HttpStatus, InternalServerErrorException, @@ -131,7 +132,7 @@ export class AuthController { if (req.op === FormMode.OIDC) { loginData = await this.loginOidc(req); } else { - loginData = await this.login(req); + loginData = await this.loginBasic(req); } const { token, ...loginResponse } = loginData; @@ -264,7 +265,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithKIDSqlJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -324,7 +325,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithKIDSqlJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -384,7 +385,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithJKUJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -444,7 +445,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithJWKJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -504,7 +505,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithX5CJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -564,7 +565,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithX5UJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'Authorization', @@ -624,7 +625,7 @@ export class AuthController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { this.logger.debug('Call loginWithHMACJwt'); - const profile = await this.login(req); + const profile = await this.loginBasic(req); res.header( 'authorization', @@ -690,7 +691,7 @@ export class AuthController { } } - private async login(req: LoginRequest): Promise { + private async loginBasic(req: LoginRequest): Promise { let user: User; try { @@ -709,6 +710,13 @@ export class AuthController { }); } + if (!user.isBasic) { + throw new ForbiddenException({ + error: 'Invalid authentication method for this user', + location: __filename, + }); + } + const token = await this.authService.createToken( { user: user.email, diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index a23ec0a3..de6f7071 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -14,6 +14,7 @@ import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; @Injectable() export class AuthGuard implements CanActivate { private static readonly AUTH_HEADER = 'authorization'; + private static readonly BEARER_PREFIX = 'bearer'; private readonly logger = new Logger(AuthGuard.name); constructor( @@ -25,8 +26,13 @@ export class AuthGuard implements CanActivate { try { this.logger.debug('Called canActivate'); const request = this.getRequest(context); + const token = this.extractToken(request); - return !!(await this.verifyToken(request, context)); + if (!token) { + return false; + } + + return await this.verifyToken(token, context); } catch (err) { this.logger.debug(`Failed to validate token: ${err.message}`); throw new UnauthorizedException({ @@ -36,16 +42,7 @@ export class AuthGuard implements CanActivate { } } - private getRequest(context: ExecutionContext): FastifyRequest { - return context.getType() === 'graphql' - ? GqlExecutionContext.create(context).getContext().req - : context.switchToHttp().getRequest(); - } - - private async verifyToken( - request: FastifyRequest, - context: ExecutionContext, - ): Promise { + private extractToken(request: FastifyRequest): string | undefined { let token = request.headers[AuthGuard.AUTH_HEADER]; if (!token?.length) { @@ -53,31 +50,38 @@ export class AuthGuard implements CanActivate { } if (this.checkIsBearer(token)) { - token = token.substring(7); + token = token.substring(AuthGuard.BEARER_PREFIX.length).trim(); } - if (!token?.length) { - return false; - } + return token?.length ? token : undefined; + } + + private getRequest(context: ExecutionContext): FastifyRequest { + return context.getType() === 'graphql' + ? GqlExecutionContext.create(context).getContext().req + : context.switchToHttp().getRequest(); + } + private async verifyToken( + token: string, + context: ExecutionContext, + ): Promise { const processorType = this.reflector.get( JwTypeMetadataField, context.getHandler(), ); - return this.authService.validateToken( - token, - processorType ?? JwtProcessorType.BEARER, - ); + try { + return await this.authService.validateToken(token, processorType); + } catch (err) { + return this.authService.validateToken(token, JwtProcessorType.BEARER); + } } private checkIsBearer(bearer: string): boolean { - if (!bearer || bearer.length < 10) { - return false; - } - - const prefix = bearer.substring(0, 7).toLowerCase(); - - return prefix === 'bearer '; + return ( + !!bearer && + bearer.toLowerCase().startsWith(AuthGuard.BEARER_PREFIX.toLowerCase()) + ); } } diff --git a/src/components/global-exception.filter.ts b/src/components/global-exception.filter.ts index dafe1079..67383644 100644 --- a/src/components/global-exception.filter.ts +++ b/src/components/global-exception.filter.ts @@ -33,10 +33,10 @@ export class GlobalExceptionFilter extends BaseExceptionFilter { this.applicationRef || (this.httpAdapterHost && this.httpAdapterHost.httpAdapter); - return this.handleUnknownError( - unprocessableException, - host, - applicationRef, + return applicationRef.reply( + host.getArgByIndex(1), + unprocessableException.getResponse(), + unprocessableException.getStatus(), ); } } diff --git a/src/file/file.controller.ts b/src/file/file.controller.ts index 9ca0cb04..d63e3622 100644 --- a/src/file/file.controller.ts +++ b/src/file/file.controller.ts @@ -12,10 +12,12 @@ import { Res, } from '@nestjs/common'; import { + ApiHeader, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { W_OK } from 'constants'; @@ -49,17 +51,24 @@ export class FileController { } } - private async loadCPFile(cpBaseUrl: string, path: string){ - if (!path.startsWith(cpBaseUrl)){ - throw new BadRequestException(`Invalid paramater 'path' ${path}`) + private async loadCPFile(cpBaseUrl: string, path: string) { + if (!path.startsWith(cpBaseUrl)) { + throw new BadRequestException(`Invalid paramater 'path' ${path}`); } - + const file: Stream = await this.fileService.getFile(path); return file; } @Get() + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -81,15 +90,21 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.fileService.getFile(path); - const type = this.getContentType(contentType, acceptHeader) + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/google') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -111,14 +126,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.GOOGLE, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.GOOGLE, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/aws') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -140,14 +165,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.AWS, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.AWS, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/azure') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -169,14 +204,24 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.AZURE, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.AZURE, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Get('/digital_ocean') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) + @ApiQuery({ name: 'type', example: 'image/jpg', required: true }) + @ApiHeader({ name: 'accept', example: 'image/jpg', required: true }) @ApiOkResponse({ description: 'File read successfully', }) @@ -198,14 +243,22 @@ export class FileController { @Res({ passthrough: true }) res: FastifyReply, @Headers('accept') acceptHeader: string, ) { - const file: Stream = await this.loadCPFile(CloudProvidersMetaData.DIGITAL_OCEAN, path); - const type = this.getContentType(contentType, acceptHeader) + const file: Stream = await this.loadCPFile( + CloudProvidersMetaData.DIGITAL_OCEAN, + path, + ); + const type = this.getContentType(contentType, acceptHeader); res.type(type); return file; } @Delete() + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/some_file.jpg', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_DELETE_FILE, }) @@ -226,6 +279,11 @@ export class FileController { } @Put('raw') + @ApiQuery({ + name: 'path', + example: 'some/path/to/file.png', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_SAVE_RAW_CONTENT, }) @@ -247,6 +305,11 @@ export class FileController { } @Get('raw') + @ApiQuery({ + name: 'path', + example: 'config/products/crystals/amethyst.jpg', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_READ_FILE_ON_SERVER, }) diff --git a/src/keycloak/keycloak.service.ts b/src/keycloak/keycloak.service.ts index 77610625..a5df0fab 100644 --- a/src/keycloak/keycloak.service.ts +++ b/src/keycloak/keycloak.service.ts @@ -250,8 +250,9 @@ export class KeyCloakService implements OnModuleInit { ); } - return new Map( + const jwks = new Map( data.keys.map((key: JWK & { kid: string }) => [key.kid, jwkToPem(key)]), ); + return jwks; } } diff --git a/src/main.ts b/src/main.ts index acf355dd..34f2117e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -81,6 +81,8 @@ async function bootstrap() { * [Products](#/Products%20controller) — operations with products + * [Partners](#/Partners%20controller) — operations with partners + `, ) diff --git a/src/model/user.entity.ts b/src/model/user.entity.ts index 6fb035ad..5853f812 100644 --- a/src/model/user.entity.ts +++ b/src/model/user.entity.ts @@ -33,4 +33,7 @@ export class User extends Base { @Property() phoneNumber: string; + + @Property() + isBasic: boolean; } diff --git a/src/partners/partners.controller.swagger.desc.ts b/src/partners/partners.controller.swagger.desc.ts new file mode 100644 index 00000000..1e8a1c26 --- /dev/null +++ b/src/partners/partners.controller.swagger.desc.ts @@ -0,0 +1,5 @@ +export const API_DESC_QUERY_PARTNERS_RAW = `Query Broken Crystal's partners using a raw XPATH query`; + +export const API_DESC_PARTNERS_LOGIN = `Login as one of Broken Crystal's partners using a username and a password`; + +export const API_DESC_SEARCH_PARTNERS_NAMES = `Search Broken Crystal's partners' names using a keyword`; diff --git a/src/partners/partners.controller.ts b/src/partners/partners.controller.ts new file mode 100644 index 00000000..1981f0ee --- /dev/null +++ b/src/partners/partners.controller.ts @@ -0,0 +1,131 @@ +import { + Controller, + Get, + Header, + HttpException, + HttpStatus, + Logger, + Query, +} from '@nestjs/common'; +import { + ApiOkResponse, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; +import { + API_DESC_QUERY_PARTNERS_RAW, + API_DESC_PARTNERS_LOGIN, + API_DESC_SEARCH_PARTNERS_NAMES +} from './partners.controller.swagger.desc'; +import { + PartnersService, +} from './partners.service'; + + +@Controller('/api/partners') +@ApiTags('Partners controller') +export class PartnersController { + private readonly logger = new Logger(PartnersController.name); + + constructor(private readonly partnersService: PartnersService) { } + + // **** This is a general XPATH injection EP - Will accept anything **** + @Get('query') + @ApiQuery({ + name: 'xpath', + type: 'string', + example: '/partners/partner/name', + required: true, + }) + @Header('content-type', 'text/xml') + @ApiOperation({ + description: API_DESC_QUERY_PARTNERS_RAW, + }) + @ApiOkResponse({ + type: String, + }) + async queryPartnersRaw(@Query('xpath') xpath: string): Promise { + this.logger.debug(`Getting partners with xpath expression "${xpath}"`); + + try { + return this.partnersService.getPartnersProperties(xpath); + } catch (err) { + throw new HttpException(`Failed to load XML using XPATH. Details: ${err}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + // **** This is a boolean based XPATH injection EP **** + @Get('partnerLogin') + @ApiQuery({ + name: 'username', + type: 'string', + example: 'walter100', + required: true, + }) + @ApiQuery({ + name: 'password', + type: 'string', + example: 'Heisenberg123', + required: true, + }) + @Header('content-type', 'text/xml') + @ApiOperation({ + description: API_DESC_PARTNERS_LOGIN, + }) + @ApiOkResponse({ + type: String, + }) + async partnerLogin(@Query('username') username: string, @Query('password') password: string): Promise { + this.logger.debug(`Trying to login partner with username ${username} using password ${password}`); + + try { + let xpath = `//partners/partner[username/text()='${username}' and password/text()='${password}']/*` + let xmlStr = this.partnersService.getPartnersProperties(xpath); + + // Check if account's data contains any information - If not, the login failed! + if (!(xmlStr && xmlStr.includes('password') && xmlStr.includes('wealth'))) { + throw new Error("Login attempt failed!"); + } + + return xmlStr; + } catch (err) { + let strErr = err.toString(); + if (strErr.includes('Unterminated string literal')) { + err = 'Error in XPath expression' + } + throw new HttpException(`Access denied to partner's account. ${err}`, HttpStatus.FORBIDDEN); + } + } + + + // **** This is a string based XPATH injection EP **** + @Get('searchPartners') + @ApiQuery({ + name: 'keyword', + type: 'string', + example: 'Walter', + required: true, + }) + @Header('content-type', 'text/xml') + @ApiOperation({ + description: API_DESC_SEARCH_PARTNERS_NAMES, + }) + @ApiOkResponse({ + type: String, + }) + async searchPartners(@Query('keyword') keyword: string): Promise { + this.logger.debug(`Searching partner names by the keyword "${keyword}"`); + + try { + let xpath = `//partners/partner/name[contains(., '${keyword}')]` + return this.partnersService.getPartnersProperties(xpath); + } catch (err) { + let strErr = err.toString(); + if (strErr.includes('XPath parse error') || strErr.includes('Unterminated string literal')) { + err = 'Error in XPath expression' + } + throw new HttpException(`Couldn't find partners. ${err}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file diff --git a/src/partners/partners.module.ts b/src/partners/partners.module.ts new file mode 100644 index 00000000..fd867ab4 --- /dev/null +++ b/src/partners/partners.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PartnersService } from './partners.service'; +import { PartnersController } from './partners.controller'; + +@Module({ + providers: [PartnersService], + controllers: [PartnersController], + exports: [PartnersService], +}) +export class PartnersModule { } diff --git a/src/partners/partners.service.spec.ts b/src/partners/partners.service.spec.ts new file mode 100644 index 00000000..fcf5393b --- /dev/null +++ b/src/partners/partners.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PartnersService } from './partners.service'; + +describe('PartnersService', () => { + let service: PartnersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PartnersService], + }).compile(); + + service = module.get(PartnersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/partners/partners.service.ts b/src/partners/partners.service.ts new file mode 100644 index 00000000..cb4f4fea --- /dev/null +++ b/src/partners/partners.service.ts @@ -0,0 +1,83 @@ +import { Injectable, Logger } from '@nestjs/common'; + +const xpath = require('xpath'); +const dom = require('xmldom').DOMParser; + + +@Injectable() +export class PartnersService { + private readonly logger = new Logger(PartnersService.name); + + private readonly XML_HEADER = ''; + private readonly XML_AUTHORS_STR: string = ` + ${this.XML_HEADER} + + + Walter White + 50 + Chemistry Teacher + + walter100 + Heisenberg123 + 15M USD + + + + Jesse Pinkman + 25 + Professional Product Distributer + + dapinkman69 + Yoyo1! + 5M USD + + + + Michael Ehrmantraut + 65 + Personal Security Agent + + _safetyman_ + LittleKid777 + 50M USD + + + + Gus Fring + 52 + Restaurant Chain Owner + + ChickMan + GoodChicken4U + Too much USD + + + `; + + private getPartnersXMLObj(): object { + let partnersXMLObj = new dom().parseFromString(this.XML_AUTHORS_STR, 'text/xml'); + return partnersXMLObj; + } + + private selectPartnerPropertiesByXPATH(xpathExpression: string): Array { + let partnersXMLObj = this.getPartnersXMLObj(); + return xpath.select(xpathExpression, partnersXMLObj); + } + + private getFormattedXMLOutput(xmlNodes): string { + return `${this.XML_HEADER}\n\n${xmlNodes.join('\n')}\n`; + } + + getPartnersProperties(xpathExpression: string): string { + let xmlNodes = this.selectPartnerPropertiesByXPATH(xpathExpression); + + if (!Array.isArray(xmlNodes)) { + this.logger.debug(`xmlNodes's type wasn't 'Array', and it's value was: ${xmlNodes}`) + xmlNodes = Array(); + } else { + this.logger.debug(`Raw xpath xmlNodes value is: ${xmlNodes}`); + } + + return this.getFormattedXMLOutput(xmlNodes); + } +} diff --git a/src/products/api/ProductDto.ts b/src/products/api/ProductDto.ts index db459399..d5dc4ba4 100644 --- a/src/products/api/ProductDto.ts +++ b/src/products/api/ProductDto.ts @@ -1,19 +1,23 @@ import { ApiProperty } from '@nestjs/swagger'; export class ProductDto { - @ApiProperty() + @ApiProperty({ example: 'Amethyst', required: true }) name: string; - @ApiProperty() + @ApiProperty({ example: 'Healing', required: true }) category: string; - @ApiProperty() + @ApiProperty({ + default: + '/api/file?path=config/products/crystals/amethyst.jpg&type=image/jpg', + required: true, + }) photoUrl: string; - @ApiProperty() + @ApiProperty({ example: 'a violet variety of quartz', required: true }) description: string; - @ApiProperty() + @ApiProperty({ example: 1, required: true }) viewsCount: number; constructor(params: { diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index b95c7bfe..f407ba82 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -6,6 +6,8 @@ import { UseGuards, Headers, InternalServerErrorException, + Query, + BadRequestException, } from '@nestjs/common'; import { ApiOperation, @@ -13,6 +15,8 @@ import { ApiTags, ApiForbiddenResponse, ApiInternalServerErrorResponse, + ApiHeader, + ApiQuery, } from '@nestjs/swagger'; import { AuthGuard } from '../auth/auth.guard'; import { JwtProcessorType } from '../auth/auth.service'; @@ -60,6 +64,7 @@ export class ProductsController { } @Get('latest') + @ApiQuery({ name: 'limit', example: 3, required: false }) @ApiOperation({ description: API_DESC_GET_LATEST_PRODUCTS, }) @@ -67,13 +72,22 @@ export class ProductsController { type: ProductDto, isArray: true, }) - async getLatestProducts(): Promise { + async getLatestProducts( + @Query('limit') limit: number, + ): Promise { this.logger.debug('Get latest products.'); - const products = await this.productsService.findLatest(3); + if (limit && isNaN(limit)) { + throw new BadRequestException('Limit must be a number'); + } + if (limit && limit < 0) { + throw new BadRequestException('Limit must be positive'); + } + const products = await this.productsService.findLatest(limit || 3); return products.map((p: Product) => new ProductDto(p)); } @Get('views') + @ApiHeader({ name: 'x-product-name', example: 'Amethyst' }) @ApiOperation({ description: API_DESC_GET_VIEW_PRODUCT, }) diff --git a/src/subscriptions/subscriptions.controller.ts b/src/subscriptions/subscriptions.controller.ts index 8269ad4e..24e68312 100644 --- a/src/subscriptions/subscriptions.controller.ts +++ b/src/subscriptions/subscriptions.controller.ts @@ -1,5 +1,10 @@ import { Controller, Logger, Post, Query } from '@nestjs/common'; -import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { + ApiCreatedResponse, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; import { SWAGGER_DESC_CREATE_SUBSCRIPTION } from './subscriptions.controller.swagger.desc'; @Controller('/api/subscriptions') @@ -8,6 +13,11 @@ export class SubscriptionsController { private readonly logger = new Logger(SubscriptionsController.name); @Post() + @ApiQuery({ + name: 'email', + example: 'john.doe@example.com', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_CREATE_SUBSCRIPTION, }) diff --git a/src/testimonials/api/TestimonialDto.ts b/src/testimonials/api/TestimonialDto.ts index 89d3cf73..981312c7 100644 --- a/src/testimonials/api/TestimonialDto.ts +++ b/src/testimonials/api/TestimonialDto.ts @@ -2,13 +2,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Testimonial } from '../../model/testimonial.entity'; export class TestimonialDto { - @ApiProperty() + @ApiProperty({ example: 'John', required: true }) name: string; - @ApiProperty() + @ApiProperty({ example: 'Doe', required: true }) title: string; - @ApiProperty() + @ApiProperty({ example: "I've broken all the crystals", required: true }) message: string; public static covertToApi(t: Testimonial): TestimonialDto { diff --git a/src/testimonials/testimonials.controller.ts b/src/testimonials/testimonials.controller.ts index f28f7ea2..90749ce0 100644 --- a/src/testimonials/testimonials.controller.ts +++ b/src/testimonials/testimonials.controller.ts @@ -9,9 +9,11 @@ import { UseGuards, } from '@nestjs/common'; import { + ApiBody, ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { AuthGuard } from '../auth/auth.guard'; @@ -34,6 +36,9 @@ export class TestimonialsController { constructor(private readonly testimonialsService: TestimonialsService) {} @Post() + @ApiBody({ + type: TestimonialDto, + }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -81,6 +86,11 @@ export class TestimonialsController { } @Get('count') + @ApiQuery({ + name: 'query', + example: 'select count(*) as count from testimonial', + required: true, + }) @Header('content-type', 'text/html') @ApiOperation({ description: API_DESC_GET_TESTIMONIALS_ON_SQL_QUERY, diff --git a/src/users/api/CreateUserRequest.ts b/src/users/api/CreateUserRequest.ts index 366d9bb0..d650493a 100644 --- a/src/users/api/CreateUserRequest.ts +++ b/src/users/api/CreateUserRequest.ts @@ -1,6 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { UserDto } from './UserDto'; +export enum SignupMode { + BASIC = 'basic', + OIDC = 'oidc', +} + export class CreateUserRequest extends UserDto { @ApiProperty() company: string; @@ -10,5 +15,5 @@ export class CreateUserRequest extends UserDto { phoneNumber: string; @ApiProperty() password: string; - op: string; + op: SignupMode; } diff --git a/src/users/api/UserDto.ts b/src/users/api/UserDto.ts index 9221731e..398c75d5 100644 --- a/src/users/api/UserDto.ts +++ b/src/users/api/UserDto.ts @@ -6,25 +6,31 @@ export const FULL_USER_INFO = 'fullUserInfo'; export class UserDto { @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'john.doe@examle.com', required: true }) email: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'John', required: true }) firstName: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'Doe', required: true }) lastName: string; @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) - @ApiProperty() + @ApiProperty({ example: 'Bright Security', required: true }) company: string; + @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) + @ApiProperty({ example: 1 }) + id: number; + @Expose({ groups: [FULL_USER_INFO] }) + @ApiProperty({ example: '4263982640269299' }) cardNumber: string; - @Expose({ groups: [FULL_USER_INFO] }) + @Expose({ groups: [BASIC_USER_INFO, FULL_USER_INFO] }) + @ApiProperty({ example: '12065550100' }) phoneNumber: string; @Exclude() @@ -33,12 +39,9 @@ export class UserDto { @Exclude() @ApiHideProperty() + @ApiProperty({ example: 'Pa55w0rd' }) password?: string; - @Exclude() - @ApiHideProperty() - id: number; - @Exclude() photo: Buffer; @@ -48,11 +51,10 @@ export class UserDto { @Expose({ groups: [FULL_USER_INFO] }) createdAt: Date; - constructor( - params: { - [P in keyof UserDto]: UserDto[P]; - }, - ) { + @Exclude() + isBasic: boolean; + + constructor(params: UserDto) { Object.assign(this, params); } } diff --git a/src/users/users.controller.swagger.desc.ts b/src/users/users.controller.swagger.desc.ts index be9e9d11..92227445 100644 --- a/src/users/users.controller.swagger.desc.ts +++ b/src/users/users.controller.swagger.desc.ts @@ -8,6 +8,8 @@ export const SWAGGER_DESC_FIND_USERS = `Returns users info contains searched sub export const SWAGGER_DESC_PHOTO_USER_BY_EMAIL = `Returns user profile photo`; +export const SWAGGER_DESC_DELETE_PHOTO_USER_BY_ID = `Deletes user profile photo by user's ID`; + export const SWAGGER_DESC_LDAP_SEARCH = `Performs LDAP search for user details`; export const SWAGGER_DESC_CREATE_BASIC_USER = `Creates BASIC user in PostGreSQL db`; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d01b0a8c..4d3944ce 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -2,6 +2,7 @@ import { Body, ClassSerializerInterceptor, Controller, + Delete, ForbiddenException, Get, Header, @@ -18,6 +19,7 @@ import { Req, Res, SerializeOptions, + UnauthorizedException, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -29,13 +31,14 @@ import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, + ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { CreateUserRequest } from './api/CreateUserRequest'; +import { CreateUserRequest, SignupMode } from './api/CreateUserRequest'; import { UserDto } from './api/UserDto'; import { LdapQueryHandler } from './ldap.query.handler'; import { UsersService } from './users.service'; -import { Readable } from 'stream'; import { User } from '../model/user.entity'; import { AuthGuard } from '../auth/auth.guard'; import { JwtType } from '../auth/jwt/jwt.type.decorator'; @@ -55,10 +58,12 @@ import { SWAGGER_DESC_ADMIN_RIGHTS, SWAGGER_DESC_FIND_USERS, SWAGGER_DESC_FIND_FULL_USER_INFO, + SWAGGER_DESC_DELETE_PHOTO_USER_BY_ID, } from './users.controller.swagger.desc'; import { AdminGuard } from './users.guard'; import { PermissionDto } from './api/PermissionDto'; import { BASIC_USER_INFO, FULL_USER_INFO } from './api/UserDto'; +import { parseXml } from 'libxmljs'; @Controller('/api/users') @UseInterceptors(ClassSerializerInterceptor) @@ -82,6 +87,7 @@ export class UsersController { } @Get('/one/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USER, @@ -110,6 +116,7 @@ export class UsersController { } @Get('/id/:id') + @ApiQuery({ name: 'id', example: 1, required: true }) @SerializeOptions({ groups: [BASIC_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USER, @@ -138,6 +145,7 @@ export class UsersController { } @Get('/fullinfo/:email') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @SerializeOptions({ groups: [FULL_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_FULL_USER_INFO, @@ -166,6 +174,7 @@ export class UsersController { } @Get('/search/:name') + @ApiQuery({ name: 'name', example: 'john', required: true }) @SerializeOptions({ groups: [FULL_USER_INFO] }) @ApiOperation({ description: SWAGGER_DESC_FIND_USERS, @@ -185,6 +194,7 @@ export class UsersController { } @Get('/one/:email/photo') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -218,14 +228,7 @@ export class UsersController { } try { - const readable = new Readable({ - read() { - this.push(user.photo); - this.push(null); - }, - }); - res.type('image/png'); - return readable; + return user.photo; } catch (err) { throw new InternalServerErrorException({ error: err.message, @@ -234,7 +237,55 @@ export class UsersController { } } + @Delete('/one/:id/photo') + @ApiQuery({ name: 'id', example: 1, required: true }) + @UseGuards(AuthGuard) + @JwtType(JwtProcessorType.RSA) + @ApiOperation({ + description: SWAGGER_DESC_DELETE_PHOTO_USER_BY_ID, + }) + @ApiOkResponse({ + description: 'Deletes user profile photo', + }) + @ApiNoContentResponse({ + description: 'Returns empty content if there was no user profile photo', + }) + @ApiForbiddenResponse({ + description: 'Returns when user is not authenticated', + }) + @ApiUnauthorizedResponse({ + description: 'Returns when isAdmin is false', + }) + async deleteUserPhotoById( + @Param('id') id: number, + @Query('isAdmin') isAdminParam: string, + @Res({ passthrough: true }) res: FastifyReply, + ) { + isAdminParam = isAdminParam.toLowerCase(); + const isAdmin = + isAdminParam === 'true' || isAdminParam === '1' ? true : false; + if (!isAdmin) { + throw new UnauthorizedException(); + } + + const user = await this.usersService.findById(id); + if (!user) { + throw new NotFoundException({ + error: 'Could not file user', + location: __filename, + }); + } + + await this.usersService.deletePhoto(id); + } + @Get('/ldap') + @ApiQuery({ + name: 'query', + example: + '(&(objectClass=person)(objectClass=user)(email=john.doe@example.com))', + required: true, + }) @ApiOperation({ description: SWAGGER_DESC_LDAP_SEARCH, }) @@ -295,17 +346,18 @@ export class UsersController { try { this.logger.debug(`Create a basic user: ${user}`); - const userExists = await this.usersService.findByEmail(user.email); + const userExists = await this.doesUserExist(user); if (userExists) { - throw new HttpException('User already exists', 409); + throw new HttpException('User already exists', HttpStatus.CONFLICT); } + + return new UserDto( + await this.usersService.createUser(user, user.op === SignupMode.BASIC), + ); } catch (err) { - if (err.status === 404) { - return new UserDto(await this.usersService.createUser(user)); - } throw new HttpException( err.message ?? 'Something went wrong', - err.status ?? 500, + err.status ?? HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -330,7 +382,13 @@ export class UsersController { try { this.logger.debug(`Create a OIDC user: ${user}`); - return new UserDto( + const userExists = await this.doesUserExist(user); + + if (userExists) { + throw new HttpException('User already exists', HttpStatus.CONFLICT); + } + + const keycloakUser = new UserDto( await this.keyCloakService.registerUser({ email: user.email, firstName: user.firstName, @@ -338,6 +396,10 @@ export class UsersController { password: user.password, }), ); + + this.createUser(user); + + return keycloakUser; } catch (err) { throw new HttpException( err.response.data ?? 'Something went wrong', @@ -347,6 +409,7 @@ export class UsersController { } @Put('/one/:email/info') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -389,6 +452,7 @@ export class UsersController { } @Get('/one/:email/info') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -432,6 +496,7 @@ export class UsersController { } @Get('/one/:email/adminpermission') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard, AdminGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -455,6 +520,7 @@ export class UsersController { } @Put('/one/:email/photo') + @ApiQuery({ name: 'email', example: 'john.doe@example.com', required: true }) @UseGuards(AuthGuard) @JwtType(JwtProcessorType.RSA) @ApiOperation({ @@ -467,7 +533,27 @@ export class UsersController { async uploadFile(@Param('email') email: string, @Req() req: FastifyRequest) { try { const file = await req.file(); - await this.usersService.updatePhoto(email, await file.toBuffer()); + const file_name = file.filename; + const file_buffer = await file.toBuffer(); + + if (file_name.endsWith('.svg')) { + const xml = file_buffer.toString(); + const xmlDoc = parseXml(xml, { + dtdload: true, + noent: true, + doctype: true, + dtdvalid: true, + errors: true, + recover: true, + }); + await this.usersService.updatePhoto( + email, + Buffer.from(xmlDoc.toString(), 'utf8'), + ); + return xmlDoc.toString(true); + } else { + await this.usersService.updatePhoto(email, file_buffer); + } } catch (err) { throw new InternalServerErrorException({ error: err.message, @@ -484,4 +570,21 @@ export class UsersController { ).toString(), ).user; } + + private async doesUserExist(user: UserDto): Promise { + try { + const userExists = await this.usersService.findByEmail(user.email); + if (userExists) { + return true; + } + } catch (err) { + if (err.status === HttpStatus.NOT_FOUND) { + return false; + } + throw new HttpException( + err.message ?? 'Something went wrong', + err.status ?? HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 1934e688..62fbe581 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -23,7 +23,7 @@ export class UsersService { private readonly usersRepository: EntityRepository, ) {} - async createUser(user: UserDto): Promise { + async createUser(user: UserDto, isBasicUser: boolean = true): Promise { this.log.debug(`Called createUser`); const u = new User(); @@ -35,6 +35,7 @@ export class UsersService { u.cardNumber = user.cardNumber; u.phoneNumber = user.phoneNumber; u.password = await hashPassword(user.password); + u.isBasic = isBasicUser; await this.usersRepository.persistAndFlush(u); this.log.debug(`Saved new user`); @@ -55,6 +56,18 @@ export class UsersService { return user; } + async deletePhoto(id: number): Promise { + this.log.debug(`deletePhoto for user with id ${id}`); + const user = await this.findById(id); + if (!user) { + throw new NotFoundError('Could not find user'); + } + delete user.photo; + + await this.usersRepository.persistAndFlush(user); + return user; + } + async updateUserInfo(oldUser: User, newData: UserDto): Promise { this.log.debug(`updateUserInfo ${oldUser.email}`); const newUser = oldUser; @@ -93,7 +106,8 @@ export class UsersService { async searchByName(query: string, limit?: number): Promise { this.log.debug(`Called searchUsersByName`); - return this.usersRepository.find({ + return this.usersRepository.find( + { firstName: { $like: query + '%' }, }, limit ? { limit } : {},