fix(csp,faro): allow t.sase.tr in CSP and proxy /collect/ to Grafana Cloud

CSP was blocking PostHog scripts/connections (t.sase.tr) and the theme-FOUC
inline script in index.html. Faro /collect/ requests were aborting because
the bare-metal nginx route disappeared during the Coolify migration.

- Add https://t.sase.tr to scriptSrc + connectSrc
- Add sha256 hash for the theme-FOUC inline script in index.html
- Exclude /collect/* from the /api global prefix
- Add FaroCollectController that forwards POST /collect/:id to
  faro-collector-prod-eu-west-2.grafana.net (overridable via FARO_UPSTREAM)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fusion
2026-05-14 10:08:18 +00:00
parent 5bd5325367
commit 36d041d514
4 changed files with 48 additions and 3 deletions

View File

@@ -33,6 +33,7 @@ import { RedisModule } from "./redis/redis.module";
import { ReferralsModule } from "./referrals/referrals.module";
import { StorageModule } from "./storage/storage.module";
import { SubscriptionsModule } from "./subscriptions/subscriptions.module";
import { TelemetryModule } from "./telemetry/telemetry.module";
import { TranslationsModule } from "./translations/translations.module";
import { UsersModule } from "./users/users.module";
import { VehiclesModule } from "./vehicles/vehicles.module";
@@ -83,6 +84,7 @@ import { VehiclesModule } from "./vehicles/vehicles.module";
CatalogModule,
ChangelogModule,
PostHogModule,
TelemetryModule,
],
controllers: [HealthController],
providers: [

View File

@@ -16,7 +16,7 @@ async function bootstrap() {
const port = configService.get<number>("port", 4000);
const corsOrigins = configService.get<string[]>("cors.origin", ["http://localhost:3000"]);
app.setGlobalPrefix("api");
app.setGlobalPrefix("api", { exclude: ["/collect/(.*)"] });
// Security headers
app.use(
@@ -24,11 +24,15 @@ async function bootstrap() {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
scriptSrc: [
"'self'",
"https://t.sase.tr",
"'sha256-T5FzBQBMINFjZ4WLy58SeZ+J7xXzjnQEGlg618CQnhA='",
],
styleSrc: ["'self'", "https:", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://storage.sase.tr"],
fontSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://storage.sase.tr"],
connectSrc: ["'self'", "https://storage.sase.tr", "https://t.sase.tr"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
},

View File

@@ -0,0 +1,32 @@
import { Controller, Logger, Param, Post, Req, Res } from "@nestjs/common";
import type { Request, Response } from "express";
import { Public } from "../common/decorators/public.decorator";
const DEFAULT_FARO_UPSTREAM = "https://faro-collector-prod-eu-west-2.grafana.net";
@Controller("collect")
export class FaroCollectController {
private readonly logger = new Logger(FaroCollectController.name);
private readonly upstream = process.env.FARO_UPSTREAM ?? DEFAULT_FARO_UPSTREAM;
@Public()
@Post(":id")
async forward(@Param("id") id: string, @Req() req: Request, @Res() res: Response): Promise<void> {
const rawBody = (req as Request & { rawBody?: Buffer }).rawBody;
const body = rawBody ?? Buffer.from(JSON.stringify(req.body ?? {}));
try {
const upstream = await fetch(`${this.upstream}/collect/${encodeURIComponent(id)}`, {
method: "POST",
headers: {
"content-type": req.headers["content-type"] ?? "application/json",
"user-agent": req.headers["user-agent"] ?? "sase-faro-proxy",
},
body,
});
res.status(upstream.status).end();
} catch (err) {
this.logger.warn(`Faro forward failed: ${(err as Error).message}`);
res.status(502).end();
}
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { FaroCollectController } from "./faro-collect.controller";
@Module({
controllers: [FaroCollectController],
})
export class TelemetryModule {}