mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
A re-verification of every draft and published advisory against develop found 9 fully closed and 8
with residuals. This closes the code-fixable ones. Three patterns cover almost all of them:
1. `{ id, ...body }` — the body wins. A path id spread BEFORE the body is overridden by a body `id`,
and save()/create() with an existing PK is an UPDATE of that row: every authorization check ran
against the path id while the write hit the body id. On PUT /user/:id that is account takeover
(a PROFILE_EDIT employee posts {"id":"<SUPER_ADMIN>","hash":"..."}). The id is now pinned LAST in
all 25 controllers/handlers with that shape, UserService.updateProfile pins entity.id = id, and
TenantAwareCrudService.create/save/createMany/saveMany refuse an entity whose id already names a
row of ANOTHER tenant (or a tenant-less row) — the update-through-create endpoints had no other
ownership check. (GHSA-x4mv-fhwj-g3rp, GHSA-gwpq-mmw7-vx85)
2. A client-supplied value decides an authorization branch. The register handler gated "only a
SUPER_ADMIN may register a SUPER_ADMIN" on input.user.role.name, and POST /user had no gate at
all. Both now resolve EVERY role identifier (the flat roleId and the role relation — the relation
wins on persist) from the database in the caller's tenant and fail closed on an id that does not
resolve. (GHSA-hjcg-633x-qq74, GHSA-x4mv-fhwj-g3rp)
3. Only the root row is tenant-scoped. SharedEntity turned caller-supplied shareRules.relations
straight into TypeORM relations on a @Public() token route, so a share of an OWNED Organization
could pivot featureOrganizations -> feature -> featureOrganizations -> tenant -> organizations ->
employees -> user into every tenant. Relations are now validated against entity metadata (each hop
must exist AND target a tenant-scoped entity), depth-bounded, joined rows are scope-filtered,
tenant-less roots are refused, and create/update bodies are whitelisted. (GHSA-cx2q-xmh2-pc38,
GHSA-gpg5-qwjc-8hqh)
Also:
- /invite/accept mass-assignment: AuthService.register strips id/hash/emailVerifiedAt/emailToken/
code/codeExpireAt/refreshToken from input.user, honours createdByUserId only for the authenticated
caller, pins user.tenantId to the trusted tenant; invite accept pins the invited email.
(GHSA-929w-5p4w-cxjp)
- TimeOffStatusHandler used raw repositories with no tenant scope (an admin of tenant A could
approve/deny tenant B's requests); equipment-sharing deleted the request_approval row unscoped, and
its status change went through an update() that deletes and re-inserts — a { status }-only body
replaced the record with a stub. (GHSA-gwpq-mmw7-vx85)
- Hubstaff /refresh-token no longer returns the refresh token. (GHSA-3rqg-gpm9-gx84)
- Upload filters on the endpoints that had none: POST /import (archive allowlist), POST
/ai-chat/attachments and the 5 registry upload routes (script-capable-extension denylist).
(GHSA-p334-cm7f-php5)
- docker-compose defaults NODE_ENV to production so the insecure-secret guard actually fires (compose
`environment:` overrode .env.compose and the image ENV); render blueprints generate their secrets
instead of shipping secretKey/refreshSecretKey/gauzy, and the CORP policy is overridable
(CORP_POLICY) because API and webapp live on two different *.onrender.com sites.
(GHSA-chm8-2ggf-pgjq)
And a functional bug found on the way: POST /ai-chat/attachments was broken on the default LOCAL file
provider. It used Nest's @UploadedFile(), which hands over multer's diskStorage object — no `key` (only
core's @UploadedFileStorage() maps it through provider.mapUploadedFile, where LOCAL derives key from
path) — so the service threw 400 AFTER the bytes were written, leaving an orphan. It now uses the core
decorator, never puts a browser-renderable extension on the stored object name, and deletes the stored
object when the upload is rejected (service) or when sniffFile rejects it (docs chat-capture).
PUT /product-types/:id was likewise a silent 400 for every caller (a DTO instance was passed to
EntityManager.save, which resolves metadata from the constructor); it now saves with an explicit
entity target under the verified id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
146 lines
5.3 KiB
YAML
146 lines
5.3 KiB
YAML
include:
|
|
- ./docker-compose.infra.yml
|
|
|
|
services:
|
|
api:
|
|
container_name: api
|
|
image: ghcr.io/ever-co/gauzy-api:latest
|
|
environment:
|
|
API_HOST: ${API_HOST:-api}
|
|
API_PORT: ${API_PORT:-3000}
|
|
NODE_ENV: ${NODE_ENV:-production}
|
|
DB_HOST: db
|
|
API_BASE_URL: ${API_BASE_URL:-http://localhost:3000}
|
|
CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200}
|
|
CLOUD_PROVIDER: ${CLOUD_PROVIDER:-}
|
|
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-}
|
|
SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-}
|
|
SENTRY_PROFILING_ENABLED: ${SENTRY_PROFILING_ENABLED:-}
|
|
POSTHOG_KEY: ${POSTHOG_KEY:-}
|
|
POSTHOG_HOST: ${POSTHOG_HOST:-}
|
|
POSTHOG_ENABLED: ${POSTHOG_ENABLED:-}
|
|
POSTHOG_FLUSH_INTERVAL: ${POSTHOG_FLUSH_INTERVAL:-}
|
|
JITSU_SERVER_URL: ${JITSU_SERVER_URL:-}
|
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
|
|
OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
|
|
OTEL_ENABLED: ${OTEL_ENABLED:-}
|
|
OTEL_PROVIDER: ${OTEL_PROVIDER:-}
|
|
JITSU_SERVER_WRITE_KEY: ${JITSU_SERVER_WRITE_KEY:-}
|
|
GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-}
|
|
GAUZY_GITHUB_CLIENT_SECRET: ${GAUZY_GITHUB_CLIENT_SECRET:-}
|
|
GAUZY_GITHUB_WEBHOOK_URL: ${GAUZY_GITHUB_WEBHOOK_URL:-}
|
|
GAUZY_GITHUB_WEBHOOK_SECRET: ${GAUZY_GITHUB_WEBHOOK_SECRET:-}
|
|
GAUZY_GITHUB_APP_PRIVATE_KEY: ${GAUZY_GITHUB_APP_PRIVATE_KEY:-}
|
|
GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-}
|
|
GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-}
|
|
GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-}
|
|
GAUZY_GITHUB_OAUTH_CLIENT_ID: ${GAUZY_GITHUB_OAUTH_CLIENT_ID:-}
|
|
GAUZY_GITHUB_OAUTH_CLIENT_SECRET: ${GAUZY_GITHUB_OAUTH_CLIENT_SECRET:-}
|
|
GAUZY_GITHUB_OAUTH_CALLBACK_URL: ${GAUZY_GITHUB_OAUTH_CALLBACK_URL:-}
|
|
MAGIC_CODE_EXPIRATION_TIME: ${MAGIC_CODE_EXPIRATION_TIME:-}
|
|
APP_NAME: ${APP_NAME:-}
|
|
APP_LOGO: ${APP_LOGO:-}
|
|
APP_SIGNATURE: ${APP_SIGNATURE:-}
|
|
APP_LINK: ${APP_LINK:-}
|
|
APP_EMAIL_CONFIRMATION_URL: ${APP_EMAIL_CONFIRMATION_URL:-}
|
|
APP_MAGIC_SIGN_URL: ${APP_MAGIC_SIGN_URL:-}
|
|
COMPANY_LINK: ${COMPANY_LINK:-}
|
|
COMPANY_NAME: ${COMPANY_NAME:-}
|
|
|
|
env_file:
|
|
- .env.compose
|
|
entrypoint: './entrypoint.compose.sh'
|
|
command: ['node', 'main.js']
|
|
restart: on-failure
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
zipkin:
|
|
condition: service_started
|
|
redis:
|
|
condition: service_started
|
|
minio:
|
|
condition: service_healthy
|
|
minio_create_buckets:
|
|
condition: service_started
|
|
opensearch:
|
|
condition: service_started
|
|
cube:
|
|
condition: service_started
|
|
links:
|
|
- db:${DB_HOST:-db}
|
|
- cube:${CUBE_HOST:-cube}
|
|
- redis:${REDIS_HOST:-redis}
|
|
- minio:${MINIO_HOST:-minio}
|
|
- opensearch:${ES_HOST:-opensearch}
|
|
# volumes:
|
|
# - webapp_node_modules:/srv/gauzy/node_modules
|
|
# - api_node_modules:/srv/gauzy/apps/api/node_modules
|
|
ports:
|
|
- '3000:${API_PORT:-3000}'
|
|
networks:
|
|
- overlay
|
|
|
|
webapp:
|
|
container_name: webapp
|
|
image: ghcr.io/ever-co/gauzy-webapp:latest
|
|
environment:
|
|
WEB_HOST: ${WEB_HOST:-webapp}
|
|
WEB_PORT: ${WEB_PORT:-4200}
|
|
NODE_ENV: ${NODE_ENV:-production}
|
|
API_BASE_URL: ${API_BASE_URL:-http://localhost:3000}
|
|
CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200}
|
|
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0.1}
|
|
SENTRY_PROFILE_SAMPLE_RATE: ${SENTRY_PROFILE_SAMPLE_RATE:-1}
|
|
CHATWOOT_SDK_TOKEN: ${CHATWOOT_SDK_TOKEN:-}
|
|
CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-}
|
|
CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY:-}
|
|
GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY:-}
|
|
GOOGLE_PLACE_AUTOCOMPLETE: ${GOOGLE_PLACE_AUTOCOMPLETE:-false}
|
|
DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136}
|
|
DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736}
|
|
DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD}
|
|
GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-}
|
|
GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-}
|
|
GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-}
|
|
GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-}
|
|
GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-}
|
|
JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-}
|
|
JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-}
|
|
DEMO: ${DEMO:-false}
|
|
API_HOST: ${API_HOST:-api}
|
|
API_PORT: ${API_PORT:-3000}
|
|
entrypoint: './entrypoint.compose.sh'
|
|
command: ['nginx', '-g', 'daemon off;']
|
|
env_file:
|
|
- .env.compose
|
|
restart: on-failure
|
|
links:
|
|
- db:${DB_HOST:-db}
|
|
- api:${API_HOST:-api}
|
|
- cube:${CUBE_HOST:-cube}
|
|
- redis:${REDIS_HOST:-redis}
|
|
- minio:${MINIO_HOST:-minio}
|
|
- opensearch:${ES_HOST:-opensearch}
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_started
|
|
minio:
|
|
condition: service_healthy
|
|
minio_create_buckets:
|
|
condition: service_started
|
|
opensearch:
|
|
condition: service_started
|
|
api:
|
|
condition: service_started
|
|
# volumes:
|
|
# - webapp_node_modules:/srv/gauzy/node_modules
|
|
ports:
|
|
- '4200:${UI_PORT:-4200}'
|
|
networks:
|
|
- overlay
|