Merge gitea/main into github/main
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled

Brings 9 Coolify deploy / Traefik routing / SPA fallback / CSP fix
commits from Gitea origin/main into GitHub main, keeping both
histories. Operational fixes preserved alongside the GitHub-side
work (Sentry, translation pipeline, deploy script refinements).

# Conflicts:
#	apps/api/src/app.module.ts
#	apps/api/src/common/filters/http-exception.filter.ts
#	apps/api/src/vehicles/vehicles.service.ts
#	pnpm-lock.yaml
This commit is contained in:
Semih
2026-05-10 23:38:43 +00:00
8 changed files with 280 additions and 3 deletions

16
.dockerignore Normal file
View File

@@ -0,0 +1,16 @@
node_modules
**/node_modules
**/dist
.git
.gitignore
*.md
docs
scripts
docker
.env*
.claude
logs
*.log
.turbo
coverage
.vscode

69
Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# ── Stage 1: Base ──────────────────────────────────────
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate
WORKDIR /app
# ── Stage 2: Dependencies ─────────────────────────────
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/api/package.json apps/api/package.json
COPY apps/web/package.json apps/web/package.json
COPY packages/shared/package.json packages/shared/package.json
COPY packages/config/package.json packages/config/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN pnpm install --frozen-lockfile
# ── Stage 3: Build ─────────────────────────────────────
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
COPY --from=deps /app/packages/config/node_modules ./packages/config/node_modules
COPY --from=deps /app/packages/ui/node_modules ./packages/ui/node_modules
COPY . .
# Vite env vars (baked at build time)
ARG VITE_FARO_ENABLED=false
ARG VITE_FARO_COLLECTOR_URL=
ARG VITE_POSTHOG_KEY=
RUN pnpm build
# ── Stage 4: Production ───────────────────────────────
FROM node:22-alpine AS production
# Chromium for Playwright (EMEX/PartsCatalogs)
RUN apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
WORKDIR /app
# Copy built artifacts
COPY --from=build /app/apps/api/dist ./apps/api/dist
COPY --from=build /app/apps/web/dist ./apps/web/dist
COPY --from=build /app/packages/shared/dist ./packages/shared/dist
COPY --from=build /app/packages/config/dist ./packages/config/dist
# Copy package files for production install
COPY --from=build /app/package.json ./
COPY --from=build /app/pnpm-lock.yaml ./
COPY --from=build /app/pnpm-workspace.yaml ./
COPY --from=build /app/apps/api/package.json ./apps/api/
COPY --from=build /app/apps/web/package.json ./apps/web/
COPY --from=build /app/packages/shared/package.json ./packages/shared/
COPY --from=build /app/packages/config/package.json ./packages/config/
COPY --from=build /app/packages/ui/package.json ./packages/ui/
# NestJS CLI config (needed for serve-static)
COPY --from=build /app/apps/api/nest-cli.json ./apps/api/
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate \
&& pnpm install --frozen-lockfile --prod \
&& pnpm store prune
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "--enable-source-maps", "apps/api/dist/main.js"]

View File

@@ -26,6 +26,7 @@
"@nestjs/core": "^10.4.0",
"@nestjs/platform-express": "^10.4.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/serve-static": "^5.0.4",
"@nestjs/swagger": "^8.1.0",
"@nestjs/throttler": "^6.3.0",
"@opentelemetry/api": "^1.9.0",

View File

@@ -1,7 +1,8 @@
import { resolve } from "node:path";
import { join, resolve } from "node:path";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { ServeStaticModule } from "@nestjs/serve-static";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { SentryModule } from "@sentry/nestjs/setup";
import { AdminModule } from "./admin/admin.module";
@@ -47,6 +48,10 @@ import { VehiclesModule } from "./vehicles/vehicles.module";
load: [configuration],
validate,
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, "..", "..", "web", "dist"),
exclude: ["/api/(.*)"],
}),
ThrottlerModule.forRoot([
{
name: "default",

View File

@@ -6,17 +6,20 @@ import {
HttpStatus,
Logger,
} from "@nestjs/common";
import { join } from "node:path";
import { SpanStatusCode, trace } from "@opentelemetry/api";
import { SentryExceptionCaptured } from "@sentry/nestjs";
import { Response } from "express";
import { Request, Response } from "express";
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger("ExceptionFilter");
private readonly indexPath = join(__dirname, "..", "..", "..", "..", "web", "dist", "index.html");
@SentryExceptionCaptured()
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
@@ -39,6 +42,11 @@ export class HttpExceptionFilter implements ExceptionFilter {
this.logger.error(`Unhandled error: ${exception.message}`, exception.stack);
}
// SPA fallback: serve index.html for non-API GET 404s
if (status === HttpStatus.NOT_FOUND && request.method === "GET" && !request.path.startsWith("/api")) {
return response.sendFile(this.indexPath);
}
// Record error on active OTel span
const span = trace.getActiveSpan();
if (span) {

View File

@@ -18,7 +18,22 @@ async function bootstrap() {
app.setGlobalPrefix("api");
// Security headers
app.use(helmet());
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "https:", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://storage.sase.tr"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://storage.sase.tr"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
},
},
}),
);
app.enableCors({
origin: corsOrigins,

133
docker-compose.coolify.yml Normal file
View File

@@ -0,0 +1,133 @@
services:
api:
build:
context: .
dockerfile: Dockerfile
args:
- VITE_FARO_ENABLED=${VITE_FARO_ENABLED:-false}
- VITE_FARO_COLLECTOR_URL=${VITE_FARO_COLLECTOR_URL:-}
- VITE_POSTHOG_KEY=${VITE_POSTHOG_KEY:-}
environment:
- NODE_ENV=production
- PORT=4000
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=sase-redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BETTER_AUTH_URL=${BETTER_AUTH_URL}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
- MINIO_ENDPOINT=http://minio-global:9000
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-sase-schemas}
- MINIO_PUBLIC_URL=${MINIO_PUBLIC_URL}
- MINIO_USE_SSL=false
- CORS_ORIGIN=${CORS_ORIGIN}
- IYZICO_API_KEY=${IYZICO_API_KEY:-}
- IYZICO_SECRET_KEY=${IYZICO_SECRET_KEY:-}
- IYZICO_BASE_URL=${IYZICO_BASE_URL:-}
- PL24_BASE_URL=${PL24_BASE_URL:-https://www.partslink24.com}
- PL24_COMPANY_CODE=${PL24_COMPANY_CODE:-}
- PL24_USERNAME=${PL24_USERNAME:-}
- PL24_PASSWORD=${PL24_PASSWORD:-}
- PL24_COMPANY_CODE_2=${PL24_COMPANY_CODE_2:-}
- PL24_USERNAME_2=${PL24_USERNAME_2:-}
- PL24_PASSWORD_2=${PL24_PASSWORD_2:-}
- PL24_PROXY_DE=${PL24_PROXY_DE:-}
- EMEX_USERNAME=${EMEX_USERNAME:-}
- EMEX_PASSWORD=${EMEX_PASSWORD:-}
- PCAT_USE_PROXY=${PCAT_USE_PROXY:-true}
- PCAT_PROXY_HOST=${PCAT_PROXY_HOST:-gw.dataimpulse.com}
- PCAT_PROXY_USER=${PCAT_PROXY_USER:-}
- PCAT_PROXY_PASS=${PCAT_PROXY_PASS:-}
- POSTAL_API_URL=${POSTAL_API_URL:-}
- POSTAL_API_KEY=${POSTAL_API_KEY:-}
- POSTAL_FROM_ADDRESS=${POSTAL_FROM_ADDRESS:-noreply@sase.tr}
- POSTAL_FROM_NAME=${POSTAL_FROM_NAME:-Sase.tr}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-sase-api}
- OTEL_TRACE_SAMPLE_RATE=${OTEL_TRACE_SAMPLE_RATE:-1.0}
- ML_PREDICTION_ENABLED=${ML_PREDICTION_ENABLED:-false}
depends_on:
sase-redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4000/api/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
labels:
- "traefik.docker.network=coolify"
expose:
- "4000"
networks:
- default
- coolify
worker:
build:
context: .
dockerfile: Dockerfile
command: ["node", "--enable-source-maps", "apps/api/dist/worker.js"]
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=sase-redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD}
- MINIO_ENDPOINT=http://minio-global:9000
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-sase-schemas}
- MINIO_PUBLIC_URL=${MINIO_PUBLIC_URL}
- MINIO_USE_SSL=false
- PL24_BASE_URL=${PL24_BASE_URL:-https://www.partslink24.com}
- PL24_COMPANY_CODE=${PL24_COMPANY_CODE:-}
- PL24_USERNAME=${PL24_USERNAME:-}
- PL24_PASSWORD=${PL24_PASSWORD:-}
- PL24_COMPANY_CODE_2=${PL24_COMPANY_CODE_2:-}
- PL24_USERNAME_2=${PL24_USERNAME_2:-}
- PL24_PASSWORD_2=${PL24_PASSWORD_2:-}
- PL24_PROXY_DE=${PL24_PROXY_DE:-}
- EMEX_USERNAME=${EMEX_USERNAME:-}
- EMEX_PASSWORD=${EMEX_PASSWORD:-}
- PCAT_USE_PROXY=${PCAT_USE_PROXY:-true}
- PCAT_PROXY_HOST=${PCAT_PROXY_HOST:-gw.dataimpulse.com}
- PCAT_PROXY_USER=${PCAT_PROXY_USER:-}
- PCAT_PROXY_PASS=${PCAT_PROXY_PASS:-}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
- OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-sase-worker}
- OTEL_TRACE_SAMPLE_RATE=${OTEL_TRACE_SAMPLE_RATE:-1.0}
depends_on:
sase-redis:
condition: service_healthy
networks:
- default
- coolify
sase-redis:
image: redis:7.4-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- default
volumes:
redis_data:
networks:
coolify:
external: true

30
pnpm-lock.yaml generated
View File

@@ -42,6 +42,9 @@ importers:
'@nestjs/schedule':
specifier: ^4.1.0
version: 4.1.2(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
'@nestjs/serve-static':
specifier: ^5.0.4
version: 5.0.5(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
'@nestjs/swagger':
specifier: ^8.1.0
version: 8.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)
@@ -1629,6 +1632,22 @@ packages:
peerDependencies:
typescript: '>=4.8.2'
'@nestjs/serve-static@5.0.5':
resolution: {integrity: sha512-AhYx3N9aMwR2cb0w5Nlb5nHNYiAcF74ea/D/xna+PxlXwjmwGN/PpC/5fuMtOwmPBMgOTxNPOnB8C9LDZBSgyw==}
peerDependencies:
'@fastify/static': ^8.0.4 || ^9.0.0
'@nestjs/common': ^11.0.2
'@nestjs/core': ^11.0.2
express: ^5.0.1
fastify: ^5.2.1
peerDependenciesMeta:
'@fastify/static':
optional: true
express:
optional: true
fastify:
optional: true
'@nestjs/swagger@8.1.1':
resolution: {integrity: sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==}
peerDependencies:
@@ -5023,6 +5042,9 @@ packages:
path-to-regexp@3.3.0:
resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
path-to-regexp@8.4.2:
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -7364,6 +7386,12 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@nestjs/serve-static@5.0.5(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)':
dependencies:
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
path-to-regexp: 8.4.2
'@nestjs/swagger@8.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)':
dependencies:
'@microsoft/tsdoc': 0.15.1
@@ -11055,6 +11083,8 @@ snapshots:
path-to-regexp@3.3.0: {}
path-to-regexp@8.4.2: {}
path-type@4.0.0: {}
pathe@2.0.3: {}