feat: no-JS support for all core UI — registration, settings, monitor CRUD, logout
This commit is contained in:
+24
-24
@@ -93,22 +93,22 @@ export const account = new Elysia({ prefix: "/account" })
|
||||
set.redirect = "/dashboard";
|
||||
}, { detail: { hide: true } })
|
||||
|
||||
.post("/register", async ({ body, cookie, request, error }) => {
|
||||
.post("/register", async ({ body, cookie, request, set, error }) => {
|
||||
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
||||
if (!checkAuthRateLimit(ip, 5)) return error(429, { error: "Too many registrations. Try again later." });
|
||||
|
||||
const key = generateKey();
|
||||
const emailHash = body.email ? hashEmail(body.email) : null;
|
||||
const emailHash = (body as any).email ? hashEmail((body as any).email) : null;
|
||||
await sql`INSERT INTO accounts (key, email_hash) VALUES (${key}, ${emailHash})`;
|
||||
cookie.pingql_key.set({ value: key, ...COOKIE_OPTS });
|
||||
return {
|
||||
key,
|
||||
...(body.email ? { email_registered: true } : { email_registered: false }),
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.Optional(t.String({ format: "email", description: "Optional. Used for account recovery only." })),
|
||||
}),
|
||||
|
||||
// Form submission → redirect to welcome page showing the key
|
||||
if ((body as any)._form) {
|
||||
set.redirect = `/dashboard/welcome?key=${encodeURIComponent(key)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
return { key, email_registered: !!emailHash };
|
||||
})
|
||||
|
||||
.use(requireAuth)
|
||||
@@ -124,31 +124,31 @@ export const account = new Elysia({ prefix: "/account" })
|
||||
};
|
||||
})
|
||||
|
||||
.post("/email", async ({ accountId, body }) => {
|
||||
const emailHash = body.email ? hashEmail(body.email) : null;
|
||||
.post("/email", async ({ accountId, body, set }) => {
|
||||
const emailHash = (body as any).email ? hashEmail((body as any).email) : null;
|
||||
await sql`UPDATE accounts SET email_hash = ${emailHash} WHERE id = ${accountId}`;
|
||||
if ((body as any)._form) { set.redirect = "/dashboard/settings"; return; }
|
||||
return { ok: true };
|
||||
}, {
|
||||
body: t.Object({
|
||||
email: t.Optional(t.Nullable(t.String({ description: "Email for account recovery only." }))),
|
||||
}),
|
||||
})
|
||||
|
||||
.post("/reset-key", async ({ accountId, cookie }) => {
|
||||
.post("/reset-key", async ({ accountId, cookie, body, set }) => {
|
||||
const key = generateKey();
|
||||
await sql`UPDATE accounts SET key = ${key} WHERE id = ${accountId}`;
|
||||
cookie.pingql_key.set({ value: key, ...COOKIE_OPTS });
|
||||
if ((body as any)?._form) { set.redirect = "/dashboard/settings"; return; }
|
||||
return { key, message: "Primary key rotated. Your old key is now invalid." };
|
||||
})
|
||||
|
||||
.post("/keys", async ({ accountId, body }) => {
|
||||
.post("/keys", async ({ accountId, body, set }) => {
|
||||
const key = generateKey();
|
||||
const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${body.label}) RETURNING id`;
|
||||
return { key, id: created.id, label: body.label };
|
||||
}, {
|
||||
body: t.Object({
|
||||
label: t.String({ description: "A name for this key, e.g. 'ci-pipeline' or 'mobile-app'" }),
|
||||
}),
|
||||
const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${(body as any).label}) RETURNING id`;
|
||||
if ((body as any)._form) { set.redirect = "/dashboard/settings"; return; }
|
||||
return { key, id: created.id, label: (body as any).label };
|
||||
})
|
||||
|
||||
.post("/keys/:id/delete", async ({ accountId, params, set }) => {
|
||||
await sql`DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId}`;
|
||||
set.redirect = "/dashboard/settings";
|
||||
})
|
||||
|
||||
.delete("/keys/:id", async ({ accountId, params, error }) => {
|
||||
|
||||
@@ -151,6 +151,13 @@ export const dashboard = new Elysia()
|
||||
return redirect("/dashboard");
|
||||
})
|
||||
|
||||
// Welcome page — shows new account key after registration (no-JS flow)
|
||||
.get("/dashboard/welcome", async ({ cookie, headers, query }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
return html("welcome", { key: query.key || cookie?.pingql_key?.value || "" });
|
||||
})
|
||||
|
||||
// Home — SSR monitor list
|
||||
.get("/dashboard/home", async ({ cookie, headers }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
@@ -325,6 +332,77 @@ export const dashboard = new Elysia()
|
||||
});
|
||||
})
|
||||
|
||||
// ── Form-based monitor actions (no-JS support) ─────────────────────
|
||||
|
||||
// Create monitor via form POST
|
||||
.post("/dashboard/monitors/new", async ({ cookie, headers, body, set }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
|
||||
const b = body as any;
|
||||
const regions = Array.isArray(b.regions) ? b.regions : (b.regions ? [b.regions] : []);
|
||||
const query = b.query ? (typeof b.query === "string" ? JSON.parse(b.query) : b.query) : undefined;
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
// Collect header_key[]/header_value[] pairs
|
||||
const hKeys = Array.isArray(b.header_key) ? b.header_key : (b.header_key ? [b.header_key] : []);
|
||||
const hVals = Array.isArray(b.header_value) ? b.header_value : (b.header_value ? [b.header_value] : []);
|
||||
for (let i = 0; i < hKeys.length; i++) {
|
||||
if (hKeys[i]?.trim()) requestHeaders[hKeys[i].trim()] = hVals[i] || "";
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
await fetch(`${apiUrl}/monitors/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
||||
body: JSON.stringify({
|
||||
name: b.name,
|
||||
url: b.url,
|
||||
method: b.method || "GET",
|
||||
interval_s: Number(b.interval_s) || 30,
|
||||
timeout_ms: Number(b.timeout_ms) || 10000,
|
||||
regions,
|
||||
request_headers: Object.keys(requestHeaders).length ? requestHeaders : null,
|
||||
request_body: b.request_body || null,
|
||||
query,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
|
||||
set.redirect = "/dashboard/home";
|
||||
})
|
||||
|
||||
// Delete monitor via form POST
|
||||
.post("/dashboard/monitors/:id/delete", async ({ cookie, headers, params, set }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
await fetch(`${apiUrl}/monitors/${params.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Authorization": `Bearer ${key}` },
|
||||
});
|
||||
|
||||
set.redirect = "/dashboard/home";
|
||||
})
|
||||
|
||||
// Toggle monitor via form POST
|
||||
.post("/dashboard/monitors/:id/toggle", async ({ cookie, headers, params, set }) => {
|
||||
const resolved = await getAccountId(cookie, headers);
|
||||
if (!resolved?.accountId) return redirect("/dashboard");
|
||||
|
||||
const apiUrl = process.env.API_URL || "https://api.pingql.com";
|
||||
const key = cookie?.pingql_key?.value;
|
||||
await fetch(`${apiUrl}/monitors/${params.id}/toggle`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${key}` },
|
||||
});
|
||||
|
||||
set.redirect = `/dashboard/monitors/${params.id}`;
|
||||
})
|
||||
|
||||
// Docs
|
||||
.get("/docs", () => html("docs", {}))
|
||||
.get("/privacy", () => html("privacy", {}))
|
||||
|
||||
Reference in New Issue
Block a user