chore: format auth code and update runtime
This commit is contained in:
+2
-1
@@ -16,7 +16,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
expose: ["8090"]
|
# Nginx Proxy Manager routes https://roast.srmr.xyz to this host port.
|
||||||
|
ports: ["8090:8090"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
Generated
+20
-20
@@ -8,7 +8,7 @@
|
|||||||
"name": "roast-planner-webapp",
|
"name": "roast-planner-webapp",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.82.1",
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"pg": "^8.22.0"
|
"pg": "^8.22.0"
|
||||||
@@ -19,15 +19,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent": {
|
"node_modules/@earendil-works/pi-coding-agent": {
|
||||||
"version": "0.82.1",
|
"version": "0.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.82.1.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.83.0.tgz",
|
||||||
"integrity": "sha512-zbkAhoIuDPMF3pKuja0ajZabrMWU29FUMV9A/XMXT/XC1yXs5xt6t6t13GogQFsDrDqbFP4DkZQO1w8rWRAzYA==",
|
"integrity": "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==",
|
||||||
"hasShrinkwrap": true,
|
"hasShrinkwrap": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.82.1",
|
"@earendil-works/pi-agent-core": "^0.83.0",
|
||||||
"@earendil-works/pi-ai": "^0.82.1",
|
"@earendil-works/pi-ai": "^0.83.0",
|
||||||
"@earendil-works/pi-tui": "^0.82.1",
|
"@earendil-works/pi-tui": "^0.83.0",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"semver": "7.8.0",
|
"semver": "7.8.0",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.3.7",
|
||||||
"undici": "8.5.0",
|
"undici": "8.5.0",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
},
|
},
|
||||||
@@ -490,14 +490,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": {
|
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": {
|
||||||
"version": "0.82.1",
|
"version": "0.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.82.1.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.82.1",
|
"@earendil-works/pi-ai": "^0.83.0",
|
||||||
"diff": "8.0.4",
|
"diff": "8.0.4",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"typebox": "1.1.38",
|
"typebox": "1.3.7",
|
||||||
"yaml": "2.9.0"
|
"yaml": "2.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -505,8 +505,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": {
|
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": {
|
||||||
"version": "0.82.1",
|
"version": "0.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.82.1.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "0.91.1",
|
"@anthropic-ai/sdk": "0.91.1",
|
||||||
@@ -519,7 +519,7 @@
|
|||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"openai": "6.26.0",
|
"openai": "6.26.0",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"typebox": "1.1.38"
|
"typebox": "1.3.7"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"pi-ai": "dist/cli.js"
|
"pi-ai": "dist/cli.js"
|
||||||
@@ -529,8 +529,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": {
|
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": {
|
||||||
"version": "0.82.1",
|
"version": "0.83.0",
|
||||||
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.82.1.tgz",
|
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-east-asian-width": "1.6.0",
|
"get-east-asian-width": "1.6.0",
|
||||||
@@ -1724,9 +1724,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": {
|
"node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": {
|
||||||
"version": "1.1.38",
|
"version": "1.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz",
|
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz",
|
||||||
"integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==",
|
"integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@earendil-works/pi-coding-agent/node_modules/undici": {
|
"node_modules/@earendil-works/pi-coding-agent/node_modules/undici": {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.82.1",
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"pg": "^8.22.0"
|
"pg": "^8.22.0"
|
||||||
|
|||||||
+3
-1
@@ -37,7 +37,9 @@ document.querySelector("#save").addEventListener("click", async () => {
|
|||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-csrf-token": csrf(),
|
"x-csrf-token": csrf(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ enabled: document.querySelector("#signup").checked }),
|
body: JSON.stringify({
|
||||||
|
enabled: document.querySelector("#signup").checked,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
await load();
|
await load();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ form.addEventListener("submit", (event) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submit("/api/auth/login");
|
submit("/api/auth/login");
|
||||||
});
|
});
|
||||||
document.querySelector("#signup").addEventListener("click", () =>
|
document
|
||||||
submit("/api/auth/signup"),
|
.querySelector("#signup")
|
||||||
);
|
.addEventListener("click", () => submit("/api/auth/signup"));
|
||||||
|
|||||||
+6
-2
@@ -409,7 +409,8 @@ function autosave() {
|
|||||||
clearTimeout(autosave._t);
|
clearTimeout(autosave._t);
|
||||||
autosave._t = setTimeout(() => {
|
autosave._t = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state.plan));
|
if (storageKey)
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(state.plan));
|
||||||
// Local storage is namespaced per authenticated account; the account copy is authoritative when online.
|
// Local storage is namespaced per authenticated account; the account copy is authoritative when online.
|
||||||
if (navigator.onLine && csrfToken()) {
|
if (navigator.onLine && csrfToken()) {
|
||||||
const method = remotePlanId ? "PUT" : "POST";
|
const method = remotePlanId ? "PUT" : "POST";
|
||||||
@@ -455,7 +456,10 @@ function loadFromStorage(userId) {
|
|||||||
// A shared browser must never retain a previous account's local-only draft.
|
// A shared browser must never retain a previous account's local-only draft.
|
||||||
for (let i = localStorage.length - 1; i >= 0; i--) {
|
for (let i = localStorage.length - 1; i >= 0; i--) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if ((key?.startsWith(`${STORAGE_PREFIX}:`) && key !== storageKey) || key === "roastPlannerPlan.v1")
|
if (
|
||||||
|
(key?.startsWith(`${STORAGE_PREFIX}:`) && key !== storageKey) ||
|
||||||
|
key === "roastPlannerPlan.v1"
|
||||||
|
)
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
const raw = localStorage.getItem(storageKey);
|
const raw = localStorage.getItem(storageKey);
|
||||||
|
|||||||
+32
-25
@@ -28,7 +28,14 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
const buckets = new Map();
|
const buckets = new Map();
|
||||||
const MAX_RATE_BUCKETS = 10_000;
|
const MAX_RATE_BUCKETS = 10_000;
|
||||||
const rateLimit = (name, max, windowMs) => {
|
const rateLimit = (name, max, windowMs) => {
|
||||||
if (!Number.isInteger(max) || max < 1 || max > 1_000 || !Number.isInteger(windowMs) || windowMs < 1_000 || windowMs > 3_600_000)
|
if (
|
||||||
|
!Number.isInteger(max) ||
|
||||||
|
max < 1 ||
|
||||||
|
max > 1_000 ||
|
||||||
|
!Number.isInteger(windowMs) ||
|
||||||
|
windowMs < 1_000 ||
|
||||||
|
windowMs > 3_600_000
|
||||||
|
)
|
||||||
throw new Error("Invalid rate-limit configuration");
|
throw new Error("Invalid rate-limit configuration");
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
const key = `${name}:${req.ip}`;
|
const key = `${name}:${req.ip}`;
|
||||||
@@ -53,7 +60,11 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
// identifies its proxy. A numeric hop count is unsafe when the topology changes.
|
// identifies its proxy. A numeric hop count is unsafe when the topology changes.
|
||||||
app.set("trust proxy", env.TRUST_PROXY || false);
|
app.set("trust proxy", env.TRUST_PROXY || false);
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path.startsWith("/api/") || req.path === "/app" || req.path === "/admin")
|
if (
|
||||||
|
req.path.startsWith("/api/") ||
|
||||||
|
req.path === "/app" ||
|
||||||
|
req.path === "/admin"
|
||||||
|
)
|
||||||
res.set("Cache-Control", "no-store, private");
|
res.set("Cache-Control", "no-store, private");
|
||||||
res.set({
|
res.set({
|
||||||
"X-Content-Type-Options": "nosniff",
|
"X-Content-Type-Options": "nosniff",
|
||||||
@@ -146,14 +157,12 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
const email = emailOf(req.body.email),
|
const email = emailOf(req.body.email),
|
||||||
password = req.body.password;
|
password = req.body.password;
|
||||||
if (!/^\S+@\S+\.\S+$/.test(email) || !PASSWORD_OK(password))
|
if (!/^\S+@\S+\.\S+$/.test(email) || !PASSWORD_OK(password))
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
ok: false,
|
||||||
.json({
|
code: "invalid_credentials",
|
||||||
ok: false,
|
error:
|
||||||
code: "invalid_credentials",
|
"Use a valid email and a password of at least 12 characters.",
|
||||||
error:
|
});
|
||||||
"Use a valid email and a password of at least 12 characters.",
|
|
||||||
});
|
|
||||||
const setting = await db.query(
|
const setting = await db.query(
|
||||||
"SELECT value FROM app_settings WHERE key='signup_enabled'",
|
"SELECT value FROM app_settings WHERE key='signup_enabled'",
|
||||||
);
|
);
|
||||||
@@ -168,13 +177,11 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
).rows[0];
|
).rows[0];
|
||||||
const s = await createSession(user);
|
const s = await createSession(user);
|
||||||
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
|
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
|
||||||
res
|
res.status(201).json({
|
||||||
.status(201)
|
ok: true,
|
||||||
.json({
|
user: { email: user.email, role: user.role },
|
||||||
ok: true,
|
csrfToken: s.csrfToken,
|
||||||
user: { email: user.email, role: user.role },
|
});
|
||||||
csrfToken: s.csrfToken,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === "23505")
|
if (e.code === "23505")
|
||||||
return res.status(409).json({ ok: false, code: "email_exists" });
|
return res.status(409).json({ ok: false, code: "email_exists" });
|
||||||
@@ -196,7 +203,9 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
return res.status(409).json({ ok: false, code: "bootstrap_used" });
|
return res.status(409).json({ ok: false, code: "bootstrap_used" });
|
||||||
const bootstrap = String(req.body.setupToken || "");
|
const bootstrap = String(req.body.setupToken || "");
|
||||||
if (!env.BOOTSTRAP_SETUP_TOKEN)
|
if (!env.BOOTSTRAP_SETUP_TOKEN)
|
||||||
return res.status(503).json({ ok: false, code: "bootstrap_unavailable" });
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ ok: false, code: "bootstrap_unavailable" });
|
||||||
if (
|
if (
|
||||||
bootstrap.length !== env.BOOTSTRAP_SETUP_TOKEN.length ||
|
bootstrap.length !== env.BOOTSTRAP_SETUP_TOKEN.length ||
|
||||||
!crypto.timingSafeEqual(
|
!crypto.timingSafeEqual(
|
||||||
@@ -222,13 +231,11 @@ export function createApp({ db, root, env = process.env } = {}) {
|
|||||||
).rows[0];
|
).rows[0];
|
||||||
const s = await createSession(user);
|
const s = await createSession(user);
|
||||||
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
|
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
|
||||||
res
|
res.status(201).json({
|
||||||
.status(201)
|
ok: true,
|
||||||
.json({
|
user: { email: user.email, role: user.role },
|
||||||
ok: true,
|
csrfToken: s.csrfToken,
|
||||||
user: { email: user.email, role: user.role },
|
});
|
||||||
csrfToken: s.csrfToken,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-18
@@ -20,11 +20,16 @@ async function setup() {
|
|||||||
});
|
});
|
||||||
const pg = mem.adapters.createPg();
|
const pg = mem.adapters.createPg();
|
||||||
const db = new pg.Pool();
|
const db = new pg.Pool();
|
||||||
await db.query(`CREATE TABLE users(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),email text UNIQUE NOT NULL,password_hash text NOT NULL,role text NOT NULL DEFAULT 'user',created_at timestamptz DEFAULT now()); CREATE TABLE sessions(token_hash text PRIMARY KEY,user_id uuid NOT NULL REFERENCES users(id),csrf_hash text NOT NULL,expires_at timestamptz NOT NULL,created_at timestamptz DEFAULT now()); CREATE TABLE roast_plans(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),user_id uuid NOT NULL REFERENCES users(id),plan jsonb NOT NULL,created_at timestamptz DEFAULT now(),updated_at timestamptz DEFAULT now()); CREATE TABLE app_settings(key text PRIMARY KEY,value text NOT NULL); INSERT INTO app_settings VALUES('signup_enabled','true')`);
|
await db.query(
|
||||||
|
`CREATE TABLE users(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),email text UNIQUE NOT NULL,password_hash text NOT NULL,role text NOT NULL DEFAULT 'user',created_at timestamptz DEFAULT now()); CREATE TABLE sessions(token_hash text PRIMARY KEY,user_id uuid NOT NULL REFERENCES users(id),csrf_hash text NOT NULL,expires_at timestamptz NOT NULL,created_at timestamptz DEFAULT now()); CREATE TABLE roast_plans(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),user_id uuid NOT NULL REFERENCES users(id),plan jsonb NOT NULL,created_at timestamptz DEFAULT now(),updated_at timestamptz DEFAULT now()); CREATE TABLE app_settings(key text PRIMARY KEY,value text NOT NULL); INSERT INTO app_settings VALUES('signup_enabled','true')`,
|
||||||
|
);
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
db,
|
db,
|
||||||
root,
|
root,
|
||||||
env: { NODE_ENV: "test", BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token" },
|
env: {
|
||||||
|
NODE_ENV: "test",
|
||||||
|
BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return { db, app, agent: request.agent(app) };
|
return { db, app, agent: request.agent(app) };
|
||||||
}
|
}
|
||||||
@@ -43,23 +48,38 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
|
|||||||
|
|
||||||
const landing = await anonymous.get("/");
|
const landing = await anonymous.get("/");
|
||||||
assert.equal(landing.status, 200);
|
assert.equal(landing.status, 200);
|
||||||
assert.match(landing.headers["content-security-policy"], /default-src 'self'/);
|
assert.match(
|
||||||
|
landing.headers["content-security-policy"],
|
||||||
|
/default-src 'self'/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(
|
||||||
landing.headers["content-security-policy"],
|
landing.headers["content-security-policy"],
|
||||||
/(?:default-src|script-src)[^;]*unsafe-inline/,
|
/(?:default-src|script-src)[^;]*unsafe-inline/,
|
||||||
);
|
);
|
||||||
assert.match(landing.text, /<script type="module" src="\/js\/landing\.js"><\/script>/);
|
assert.match(
|
||||||
|
landing.text,
|
||||||
|
/<script type="module" src="\/js\/landing\.js"><\/script>/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(landing.text, /<script type="module">/);
|
assert.doesNotMatch(landing.text, /<script type="module">/);
|
||||||
const adminHtml = await anonymous.get("/admin");
|
const adminHtml = await anonymous.get("/admin");
|
||||||
assert.equal(adminHtml.status, 401);
|
assert.equal(adminHtml.status, 401);
|
||||||
assert.equal(adminHtml.headers["cache-control"], "no-store, private");
|
assert.equal(adminHtml.headers["cache-control"], "no-store, private");
|
||||||
assert.equal((await anonymous.get("/api/plans")).status, 401);
|
assert.equal((await anonymous.get("/api/plans")).status, 401);
|
||||||
assert.equal((await anonymous.get("/api/plans")).headers["cache-control"], "no-store, private");
|
assert.equal(
|
||||||
assert.match((await anonymous.get("/js/admin.js")).text, /async function load/);
|
(await anonymous.get("/api/plans")).headers["cache-control"],
|
||||||
|
"no-store, private",
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
(await anonymous.get("/js/admin.js")).text,
|
||||||
|
/async function load/,
|
||||||
|
);
|
||||||
const mainScript = await anonymous.get("/js/main.js");
|
const mainScript = await anonymous.get("/js/main.js");
|
||||||
assert.match(mainScript.text, /roastPlannerPlan\.v2/);
|
assert.match(mainScript.text, /roastPlannerPlan\.v2/);
|
||||||
assert.match(mainScript.text, /localStorage\.removeItem\(key\)/);
|
assert.match(mainScript.text, /localStorage\.removeItem\(key\)/);
|
||||||
assert.match((await anonymous.get("/sw.js")).text, /Never serve authenticated/);
|
assert.match(
|
||||||
|
(await anonymous.get("/sw.js")).text,
|
||||||
|
/Never serve authenticated/,
|
||||||
|
);
|
||||||
|
|
||||||
const one = await signup(first, "[email protected]");
|
const one = await signup(first, "[email protected]");
|
||||||
const two = await signup(second, "[email protected]");
|
const two = await signup(second, "[email protected]");
|
||||||
@@ -68,9 +88,12 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
|
|||||||
const plan = await first
|
const plan = await first
|
||||||
.post("/api/plans")
|
.post("/api/plans")
|
||||||
.set("x-csrf-token", one.csrf)
|
.set("x-csrf-token", one.csrf)
|
||||||
.send({ plan: { fields: { "0.1": "Private" } } });
|
.send({ plan: { fields: { 0.1: "Private" } } });
|
||||||
assert.equal(plan.status, 201);
|
assert.equal(plan.status, 201);
|
||||||
assert.equal((await first.get("/api/plans")).headers["cache-control"], "no-store, private");
|
assert.equal(
|
||||||
|
(await first.get("/api/plans")).headers["cache-control"],
|
||||||
|
"no-store, private",
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(
|
(
|
||||||
await second
|
await second
|
||||||
@@ -84,13 +107,22 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
|
|||||||
|
|
||||||
const login = request.agent(app);
|
const login = request.agent(app);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await login.post("/api/auth/login").send({ email: "[email protected]", password })).status,
|
(
|
||||||
|
await login
|
||||||
|
.post("/api/auth/login")
|
||||||
|
.send({ email: "[email protected]", password })
|
||||||
|
).status,
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
assert.equal((await login.get("/api/auth/me")).status, 200);
|
assert.equal((await login.get("/api/auth/me")).status, 200);
|
||||||
const loginCsrf = (await login.post("/api/auth/login").send({ email: "[email protected]", password })).body.csrfToken;
|
const loginCsrf = (
|
||||||
|
await login
|
||||||
|
.post("/api/auth/login")
|
||||||
|
.send({ email: "[email protected]", password })
|
||||||
|
).body.csrfToken;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf)).status,
|
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf))
|
||||||
|
.status,
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
assert.equal((await login.get("/api/auth/me")).status, 401);
|
assert.equal((await login.get("/api/auth/me")).status, 401);
|
||||||
@@ -113,10 +145,17 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
|
|||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await anonymous.post("/api/auth/signup").send({ email: "[email protected]", password })).status,
|
(
|
||||||
|
await anonymous
|
||||||
|
.post("/api/auth/signup")
|
||||||
|
.send({ email: "[email protected]", password })
|
||||||
|
).status,
|
||||||
403,
|
403,
|
||||||
);
|
);
|
||||||
assert.equal((await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count, 3);
|
assert.equal(
|
||||||
|
(await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count,
|
||||||
|
3,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
|
test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
|
||||||
@@ -142,10 +181,10 @@ test("bootstrap token is optional after first setup and unavailable before setup
|
|||||||
).status,
|
).status,
|
||||||
503,
|
503,
|
||||||
);
|
);
|
||||||
await db.query("INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')", [
|
await db.query(
|
||||||
"[email protected]",
|
"INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')",
|
||||||
"not-used-in-this-test",
|
["[email protected]", "not-used-in-this-test"],
|
||||||
]);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(
|
(
|
||||||
await request(noTokenApp).post("/api/auth/bootstrap").send({
|
await request(noTokenApp).post("/api/auth/bootstrap").send({
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ test("Docker image starts against PostgreSQL and applies migrations", async (t)
|
|||||||
t.skip("Docker daemon is unavailable");
|
t.skip("Docker daemon is unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const temp = await mkdtemp(path.join(os.tmpdir(), "roast-planner-container-"));
|
const temp = await mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "roast-planner-container-"),
|
||||||
|
);
|
||||||
const agentDir = path.join(temp, "pi-agent");
|
const agentDir = path.join(temp, "pi-agent");
|
||||||
await mkdir(agentDir);
|
await mkdir(agentDir);
|
||||||
const envFile = path.join(temp, "compose.env");
|
const envFile = path.join(temp, "compose.env");
|
||||||
@@ -66,7 +68,11 @@ test("Docker image starts against PostgreSQL and applies migrations", async (t)
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.equal(lastError, null, "application never became ready in its container");
|
assert.equal(
|
||||||
|
lastError,
|
||||||
|
null,
|
||||||
|
"application never became ready in its container",
|
||||||
|
);
|
||||||
const migration = await compose([
|
const migration = await compose([
|
||||||
"exec",
|
"exec",
|
||||||
"-T",
|
"-T",
|
||||||
|
|||||||
Reference in New Issue
Block a user