harden browser routes with csrf checks and lock internal/dev endpoints
This commit is contained in:
@@ -76,7 +76,7 @@ function createApp(options = {}) {
|
||||
betterAuthBasePath: "/api/auth",
|
||||
xOAuthClientId: "x-client-id",
|
||||
xOAuthClientSecret: "x-client-secret",
|
||||
internalApiToken: "",
|
||||
internalApiToken: "internal-token",
|
||||
convexDeploymentUrl: "",
|
||||
convexAuthToken: "",
|
||||
convexStateQuery: "state:getLatestSnapshot",
|
||||
@@ -381,6 +381,7 @@ test("/api/x/mentions returns upstream mentions when configured", async () => {
|
||||
const response = await call(app, {
|
||||
method: "GET",
|
||||
path: "/api/x/mentions",
|
||||
headers: { "x-internal-token": "internal-token" },
|
||||
query: { sinceId: "100" },
|
||||
});
|
||||
|
||||
@@ -390,6 +391,61 @@ test("/api/x/mentions returns upstream mentions when configured", async () => {
|
||||
assert.equal(body.mentions[0].id, "m1");
|
||||
});
|
||||
|
||||
test("/api/x/mentions requires internal token", async () => {
|
||||
const app = createApp({
|
||||
xAdapter: {
|
||||
isConfigured() {
|
||||
return true;
|
||||
},
|
||||
async listMentions() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await call(app, {
|
||||
method: "GET",
|
||||
path: "/api/x/mentions",
|
||||
query: { sinceId: "1" },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
});
|
||||
|
||||
test("cross-site browser posts are blocked", async () => {
|
||||
const app = createApp();
|
||||
const response = await call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/topup",
|
||||
headers: {
|
||||
cookie: "xartaudio_user=alice",
|
||||
origin: "https://evil.example",
|
||||
"sec-fetch-site": "cross-site",
|
||||
},
|
||||
body: "amount=5",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
assert.match(response.body, /csrf_blocked|invalid_origin/);
|
||||
});
|
||||
|
||||
test("dev dashboard routes can be disabled", async () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
enableDevRoutes: false,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/topup",
|
||||
headers: { cookie: "xartaudio_user=alice" },
|
||||
body: "amount=5",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
});
|
||||
|
||||
test("simulate mention schedules background audio generation when service is configured", async () => {
|
||||
const queued = [];
|
||||
const app = createApp({
|
||||
@@ -573,7 +629,11 @@ test("internal retention endpoint prunes stale content and assets", async () =>
|
||||
});
|
||||
|
||||
test("internal endpoints are disabled when no token configured", async () => {
|
||||
const app = createApp();
|
||||
const app = createApp({
|
||||
config: {
|
||||
internalApiToken: "",
|
||||
},
|
||||
});
|
||||
const response = await call(app, {
|
||||
method: "POST",
|
||||
path: "/internal/retention/run",
|
||||
|
||||
@@ -59,6 +59,7 @@ test("config uses defaults when env is missing", () => {
|
||||
assert.equal(config.minioUseSSL, true);
|
||||
assert.equal(config.rateLimits.webhookPerMinute, 120);
|
||||
assert.equal(config.allowInMemoryStateFallback, true);
|
||||
assert.equal(config.enableDevRoutes, true);
|
||||
assert.equal(config.abuse.maxJobsPerUserPerDay, 0);
|
||||
assert.equal(config.abuse.cooldownSec, 0);
|
||||
assert.deepEqual(config.abuse.denyUserIds, []);
|
||||
@@ -117,6 +118,7 @@ test("config reads convex/qwen/minio overrides", () => {
|
||||
assert.equal(config.abuse.cooldownSec, 120);
|
||||
assert.deepEqual(config.abuse.denyUserIds, ["u1", "u2"]);
|
||||
assert.equal(config.allowInMemoryStateFallback, false);
|
||||
assert.equal(config.enableDevRoutes, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,6 +45,18 @@ test("app page renders stats and forms", () => {
|
||||
assert.match(html, /Hello/);
|
||||
});
|
||||
|
||||
test("app page can hide developer actions", () => {
|
||||
const html = renderAppPage({
|
||||
userId: "u1",
|
||||
summary: { balance: 4, totalJobs: 2, totalCreditsSpent: 2 },
|
||||
jobs: [],
|
||||
showDeveloperActions: false,
|
||||
});
|
||||
|
||||
assert.doesNotMatch(html, /Developer Actions/);
|
||||
assert.doesNotMatch(html, /\/app\/actions\/topup/);
|
||||
});
|
||||
|
||||
test("audio page shows unlock action when payment is required", () => {
|
||||
const html = renderAudioPage({
|
||||
audio: { id: "1", storageKey: "audio/1.mp3", articleTitle: "A", durationSec: 30 },
|
||||
|
||||
Reference in New Issue
Block a user