mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
SaaS Consolidation (#6384)
Co-authored-by: ConnorYoh <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
parent
089de247b4
commit
9d081d1792
@@ -1904,22 +1904,6 @@ secureWorkflowDesc = "Asegura documentos PDF eliminando contenido potencialmente
|
||||
[autoRename]
|
||||
description = "Esta herramienta renombrará automáticamente los archivos PDF en función de su contenido. Analiza el documento para encontrar el título más adecuado a partir del texto."
|
||||
|
||||
[pdfCommentAgent]
|
||||
submit = "Generar comentarios"
|
||||
prompt.label = "¿Sobre qué debería comentar la IA?"
|
||||
prompt.placeholder = "p. ej., Señala fechas ambiguas y sugiere aclaraciones"
|
||||
|
||||
[pdfCommentAgent.settings]
|
||||
title = "Instrucciones de comentarios"
|
||||
|
||||
[pdfCommentAgent.results]
|
||||
title = "PDF con comentarios"
|
||||
|
||||
[pdfCommentAgent.error]
|
||||
failed = "No se pudieron generar los comentarios"
|
||||
emptyPrompt = "Describe sobre qué debería comentar la IA"
|
||||
tooLong = "El prompt es demasiado largo (máximo {{max}} caracteres)"
|
||||
|
||||
[autoSizeSplitPDF]
|
||||
tags = "pdf,dividir,documento,organización"
|
||||
|
||||
@@ -4141,11 +4125,6 @@ desc = "Renombra automáticamente un archivo PDF basándose en su encabezado det
|
||||
tags = "auto-detectar,basado-en-encabezado,organizar,reetiquetar"
|
||||
title = "Renombrar Automáticamente Archivo PDF"
|
||||
|
||||
[home.pdfCommentAgent]
|
||||
desc = "Pide a la IA que anote un PDF con comentarios de notas adhesivas según tu indicación"
|
||||
tags = "IA,agente,comentario,anotar,nota adhesiva,revisión,feedback,notas"
|
||||
title = "Añadir comentarios con IA"
|
||||
|
||||
[home.autoSizeSplitPDF]
|
||||
desc = "Divide un solo PDF en múltiples documentos según su tamaño, número de páginas, o número de documento"
|
||||
tags = "auto,dividir,tamaño"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Live saas-backend smoke. Hits the actual Spring Boot saas backend at a configured port and
|
||||
* verifies the fixes landed in this branch.
|
||||
*
|
||||
* Skipped automatically if the backend isn't reachable — CI doesn't boot one. To run locally:
|
||||
* STIRLING_FLAVOR=saas ./gradlew :stirling-pdf:bootRun --args="--server.port=18083 --spring.profiles.include=dev"
|
||||
* STIRLING_SAAS_URL=http://localhost:18083 npx playwright test --project=stubbed saas-backend-smoke
|
||||
*/
|
||||
import { test, expect, request } from "@playwright/test";
|
||||
|
||||
const SAAS_URL = process.env.STIRLING_SAAS_URL ?? "http://localhost:18083";
|
||||
|
||||
test.describe("SaaS backend smoke", () => {
|
||||
test.beforeAll(async () => {
|
||||
const ctx = await request.newContext();
|
||||
try {
|
||||
const r = await ctx.get(`${SAAS_URL}/actuator/health`, { timeout: 2000 });
|
||||
if (r.status() !== 200) {
|
||||
test.skip(true, `saas backend at ${SAAS_URL} returned ${r.status()}`);
|
||||
}
|
||||
} catch (err) {
|
||||
test.skip(true, `saas backend at ${SAAS_URL} unreachable: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("health endpoint returns UP", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const r = await ctx.get(`${SAAS_URL}/actuator/health`);
|
||||
expect(r.status()).toBe(200);
|
||||
const body = await r.json();
|
||||
expect(body.status).toBe("UP");
|
||||
});
|
||||
|
||||
test("public endpoints return 200 / protected return 401", async () => {
|
||||
const ctx = await request.newContext();
|
||||
expect((await ctx.get(`${SAAS_URL}/api/v1/info/status`)).status()).toBe(
|
||||
200,
|
||||
);
|
||||
expect(
|
||||
(await ctx.get(`${SAAS_URL}/api/v1/config/app-config`)).status(),
|
||||
).toBe(200);
|
||||
expect((await ctx.get(`${SAAS_URL}/api/v1/credits`)).status()).toBe(401);
|
||||
});
|
||||
|
||||
test("user-role webhook endpoints reject unauthenticated (regression #3)", async () => {
|
||||
const ctx = await request.newContext();
|
||||
for (const path of [
|
||||
"/api/v1/user-role/upgrade?supabaseId=foo",
|
||||
"/api/v1/user-role/downgrade?supabaseId=foo",
|
||||
"/api/v1/user-role/enable-metered-billing?supabaseId=foo",
|
||||
"/api/v1/user-role/disable-metered-billing?supabaseId=foo",
|
||||
]) {
|
||||
const r = await ctx.post(`${SAAS_URL}${path}`);
|
||||
expect(r.status(), `${path}: expected 401, got ${r.status()}`).toBe(401);
|
||||
expect(
|
||||
r.status(),
|
||||
`${path}: must not 500 — would indicate hasRole prefix bug returned`,
|
||||
).not.toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
test("CORS preflight rejects unknown origin (regression #11)", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
Origin: "https://attacker.example.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
});
|
||||
expect(r.status()).toBe(403);
|
||||
expect(r.headers()["access-control-allow-origin"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("CORS preflight rejects tenant wildcard origin (regression #11)", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
Origin: "https://abandoned-tenant.ssl.stirlingpdf.cloud",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
});
|
||||
expect(r.status()).toBe(403);
|
||||
expect(r.headers()["access-control-allow-origin"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("CORS preflight accepts allowed origin", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
Origin: "https://app.stirling.com",
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
});
|
||||
expect(r.status()).toBe(200);
|
||||
expect(r.headers()["access-control-allow-origin"]).toBe(
|
||||
"https://app.stirling.com",
|
||||
);
|
||||
expect(r.headers()["access-control-allow-credentials"]).toBe("true");
|
||||
});
|
||||
|
||||
test("backend HTML loads in a real browser (no boot-time console errors)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") errors.push(`console: ${msg.text()}`);
|
||||
});
|
||||
|
||||
const resp = await page.goto(`${SAAS_URL}/`, { waitUntil: "load" });
|
||||
expect(resp?.status()).toBe(200);
|
||||
|
||||
await page.waitForTimeout(750);
|
||||
|
||||
const real = errors.filter(
|
||||
(e) => !/Failed to load resource.*401|\/api\/v1\/credits/i.test(e),
|
||||
);
|
||||
expect(real, real.join("\n")).toEqual([]);
|
||||
});
|
||||
});
|
||||
+5
@@ -1,3 +1,8 @@
|
||||
// Supabase client. Relocated out of frontend/src/core/services/ during the SaaS<->OSS
|
||||
// consolidation so the Supabase SDK never reaches the OSS core bundle. Lives in
|
||||
// :proprietary because licensing/checkout/billing flows in proprietary mode use it; the
|
||||
// :saas mode bundle picks it up via the existing @app/* path mapping (saas to proprietary
|
||||
// to core).
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
+18
-11
@@ -26,16 +26,23 @@ export default defineConfig(async ({ mode }) => {
|
||||
// `VITE_` prefix.
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
|
||||
// Resolve the effective build mode.
|
||||
// Explicit --mode flags take precedence; otherwise default to proprietary
|
||||
// unless DISABLE_ADDITIONAL_FEATURES=true, in which case default to core.
|
||||
const effectiveMode: BuildMode = (VALID_MODES as readonly string[]).includes(
|
||||
mode,
|
||||
)
|
||||
// Effective mode: --mode > STIRLING_FLAVOR > ENABLE_SAAS > DISABLE_ADDITIONAL_FEATURES > proprietary.
|
||||
const explicitMode = (VALID_MODES as readonly string[]).includes(mode)
|
||||
? (mode as BuildMode)
|
||||
: process.env.DISABLE_ADDITIONAL_FEATURES === "true"
|
||||
? "core"
|
||||
: "proprietary";
|
||||
: null;
|
||||
const flavor = (process.env.STIRLING_FLAVOR ?? "").toLowerCase();
|
||||
const flavorMode: BuildMode | null =
|
||||
flavor === "core" || flavor === "proprietary" || flavor === "saas"
|
||||
? (flavor as BuildMode)
|
||||
: null;
|
||||
const effectiveMode: BuildMode =
|
||||
explicitMode ??
|
||||
flavorMode ??
|
||||
(process.env.ENABLE_SAAS === "true"
|
||||
? "saas"
|
||||
: process.env.DISABLE_ADDITIONAL_FEATURES === "true"
|
||||
? "core"
|
||||
: "proprietary");
|
||||
|
||||
const tsconfigProject = TSCONFIG_MAP[effectiveMode];
|
||||
|
||||
@@ -96,14 +103,14 @@ export default defineConfig(async ({ mode }) => {
|
||||
dest: "vendor/jscanify",
|
||||
},
|
||||
{
|
||||
// pdfjs-dist CMap data for CJK / non-latin glyph mapping — required
|
||||
// pdfjs-dist CMap data for CJK / non-latin glyph mapping. Required
|
||||
// when rendering PDFs inside workers where the default DOM fetch paths
|
||||
// aren't available.
|
||||
src: "node_modules/pdfjs-dist/cmaps/*",
|
||||
dest: "pdfjs/cmaps",
|
||||
},
|
||||
{
|
||||
// pdfjs-dist standard font data (Helvetica/Times/etc.) — needed so
|
||||
// pdfjs-dist standard font data (Helvetica/Times/etc.) needed so
|
||||
// workers can substitute non-embedded base 14 fonts without DOM access.
|
||||
src: "node_modules/pdfjs-dist/standard_fonts/*",
|
||||
dest: "pdfjs/standard_fonts",
|
||||
|
||||
Reference in New Issue
Block a user