diff --git a/apps/web/src/routes/auth.ts b/apps/web/src/routes/auth.ts index 35bedbc..04985b6 100644 --- a/apps/web/src/routes/auth.ts +++ b/apps/web/src/routes/auth.ts @@ -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 }) => { diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index e24dc0a..a18c968 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -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 = {}; + // 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", {})) diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index 296e485..9917662 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -29,8 +29,12 @@

<%= m.url %>

- - +
+ +
+
+ +
diff --git a/apps/web/src/views/login.ejs b/apps/web/src/views/login.ejs index b243686..d3b7f41 100644 --- a/apps/web/src/views/login.ejs +++ b/apps/web/src/views/login.ejs @@ -33,10 +33,13 @@

No account?

- +
+ + +
@@ -102,8 +105,9 @@ } catch { showError('Connection error'); } }); - // Register - document.getElementById('register-btn').addEventListener('click', async () => { + // Register — JS-enhanced (overrides form POST for inline key display) + document.getElementById('register-form').addEventListener('submit', async (e) => { + e.preventDefault(); try { const res = await fetch('/account/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), diff --git a/apps/web/src/views/partials/monitor-form-js.ejs b/apps/web/src/views/partials/monitor-form-js.ejs index 33d4aae..2084c99 100644 --- a/apps/web/src/views/partials/monitor-form-js.ejs +++ b/apps/web/src/views/partials/monitor-form-js.ejs @@ -21,8 +21,8 @@ const bg = container.closest('form').querySelector('input')?.className.match(/bg-gray-\d+/)?.[0] || 'bg-gray-900'; const border = container.closest('form').querySelector('input')?.className.match(/border-gray-\d+/)?.[0] || 'border-gray-800'; row.innerHTML = ` - - + + `; container.appendChild(row); diff --git a/apps/web/src/views/partials/monitor-form.ejs b/apps/web/src/views/partials/monitor-form.ejs index ffbc40a..07860a4 100644 --- a/apps/web/src/views/partials/monitor-form.ejs +++ b/apps/web/src/views/partials/monitor-form.ejs @@ -17,23 +17,23 @@ const bodyHidden = ['GET','HEAD','OPTIONS'].includes(curMethod); %> -
+
-
- -
@@ -48,8 +48,8 @@ <% if (monitor.request_headers && typeof monitor.request_headers === 'object') { Object.entries(monitor.request_headers).forEach(function([k, v]) { %>
- - + +
<% }) } %> @@ -59,14 +59,14 @@
-
- <% timeouts.forEach(function([val, label]) { %> @@ -93,7 +93,7 @@
<% regions.forEach(function([val, label]) { %> <% }) %>
diff --git a/apps/web/src/views/partials/nav.ejs b/apps/web/src/views/partials/nav.ejs index ff33875..f1cc99e 100644 --- a/apps/web/src/views/partials/nav.ejs +++ b/apps/web/src/views/partials/nav.ejs @@ -11,6 +11,6 @@ <% } else { %> Settings <% } %> - + Logout
diff --git a/apps/web/src/views/settings.ejs b/apps/web/src/views/settings.ejs index 00e7860..a365493 100644 --- a/apps/web/src/views/settings.ejs +++ b/apps/web/src/views/settings.ejs @@ -89,7 +89,10 @@
<%= it.loginKey %> - + + + +
<% } %> @@ -112,13 +115,18 @@

Recovery Email

Used for account recovery only. Stored as a one-way hash — we can't read it.

-
- - - -
+
+ +
+ + + <% if (hasEmail) { %> + + <% } %> +
+
<% } %> @@ -133,19 +141,19 @@

Sub-Keys

Create separate keys for different apps, scripts, or teammates.

- + - - +
@@ -156,7 +164,10 @@

<%= k.label %>

- +
+ + +
<%= k.key %> @@ -174,31 +185,7 @@ <%~ include('./partials/foot') %> diff --git a/apps/web/src/views/welcome.ejs b/apps/web/src/views/welcome.ejs new file mode 100644 index 0000000..2c4d2ac --- /dev/null +++ b/apps/web/src/views/welcome.ejs @@ -0,0 +1,35 @@ +<%~ include('./partials/head', { title: 'Account Created' }) %> + + +
+
+

PingQL

+
+ +
+
+
+
+

Account created

+

Save your key — it's how you access your account

+
+
+ + + <%= it.key %> + +
+
+ + + +
+ + Skip +
+
+
+
+
+