From 2c2a2eb6c46e11390ef2932613df54295e390505 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Tue, 12 May 2026 03:32:44 +0530 Subject: [PATCH] feat: migrate dashboard to JSX and move client-side scripts to JS --- src/index.ts | 2 +- src/routes/dashboard.ts | 17 -- src/routes/dashboard.tsx | 292 ++++++++++++++++++++ src/views/dashboard.html | 557 --------------------------------------- src/views/dashboard.js | 164 ++++++++++++ tsconfig.json | 2 + 6 files changed, 459 insertions(+), 575 deletions(-) delete mode 100644 src/routes/dashboard.ts create mode 100644 src/routes/dashboard.tsx delete mode 100644 src/views/dashboard.html create mode 100644 src/views/dashboard.js diff --git a/src/index.ts b/src/index.ts index 5ba9e88..d88247e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ app.use("*", prettyJSON()); app.get("/", (c) => c.redirect("/app")); -app.use("/app/*", serveStatic({ root: "./src/views" })); +app.get("/app/dashboard.js", serveStatic({ path: "./src/views/dashboard.js" })); app.route("/auth", authRoutes); app.route("/api/config", configRoutes); app.route("/api", apiRoutes); diff --git a/src/routes/dashboard.ts b/src/routes/dashboard.ts deleted file mode 100644 index 0c8c4aa..0000000 --- a/src/routes/dashboard.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import { Hono } from "hono"; - -const dashboardRoutes = new Hono({ strict: false }); - -dashboardRoutes.get("/", async (c) => { - try { - const htmlPath = join(process.cwd(), "src/views/dashboard.html"); - const html = await readFile(htmlPath, "utf-8"); - return c.html(html); - } catch (_error) { - return c.text("Error loading dashboard", 500); - } -}); - -export { dashboardRoutes }; diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx new file mode 100644 index 0000000..a204e7f --- /dev/null +++ b/src/routes/dashboard.tsx @@ -0,0 +1,292 @@ +/** @jsxImportSource hono/jsx */ +import { Hono } from "hono"; +import { html } from "hono/html"; +import type { Child } from "hono/jsx"; + +const dashboardRoutes = new Hono({ strict: false }); + +export const Layout = (props: { title: string; children: Child }) => ( + <> + {html``} + + + + + {props.title} + + + + + + + + + + {props.children} + + + + + +); + +export const Dashboard = () => ( + + + +
+
+
+
+
+
+

Configure Provider

+
+ +
+
+ + +
+ +
Credentials
+ +
+ + +
+
+ +
+ + +
+
+ +
Endpoints
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+ Must match provider's callback URL +
+
+
+ + +
+ +
+ +
+
+
+
+ +
+
+
+
+
+

Provider Registry

+
+ +
+ +
+
+ +
+ +
+
+ +

Enter Master API Key to access registry

+
+
+ +
+
+ +

No providers configured yet

+
+
+ +
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+); + +dashboardRoutes.get("/", async (c) => { + return c.html(); +}); + +export { dashboardRoutes }; diff --git a/src/views/dashboard.html b/src/views/dashboard.html deleted file mode 100644 index 716801d..0000000 --- a/src/views/dashboard.html +++ /dev/null @@ -1,557 +0,0 @@ - - - - - - - toknd — Auth Broker Dashboard - - - - - - - - - - - - - -
-
-
-
-
-
-

Configure Provider

-
- -
-
- - -
- -
Credentials -
- -
- - -
-
- -
- - -
-
- -
Endpoints -
- -
- - -
-
- - -
-
- -
- - -
- -
-
- - -
- -
- -
-
-
-
- -
-
-
-
-
-

Provider Registry

-
- -
-
- - - -
- -
-
- -

Enter Master API Key to access registry

-
-
-
-
-
-
-
-
- - - - - - - diff --git a/src/views/dashboard.js b/src/views/dashboard.js new file mode 100644 index 0000000..b62d12b --- /dev/null +++ b/src/views/dashboard.js @@ -0,0 +1,164 @@ +document.addEventListener("alpine:init", () => { + const formatTime = (timestamp) => { + if (!timestamp) return "Never"; + const date = new Date(timestamp); + const diff = Math.floor((Date.now() - date) / 1000); + + if (diff < 60) return "Just now"; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return date.toLocaleDateString(); + }; + + window.Alpine.data("dashboard", () => ({ + apiKey: localStorage.getItem("toknd_api_key") || "", + isUnlocked: false, + loading: false, + providers: [], + form: { + providerName: "", + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + scope: "public", + }, + notification: { + show: false, + message: "", + type: "success", + }, + + init() { + if (this.apiKey) { + this.unlock(); + } + }, + + async unlock() { + if (!this.apiKey) return; + this.loading = true; + try { + localStorage.setItem("toknd_api_key", this.apiKey); + await this.fetchProviders(); + this.isUnlocked = true; + } catch (err) { + this.showNotification(`Failed to unlock: ${err.message}`, "error"); + } finally { + this.loading = false; + } + }, + + async fetchProviders() { + this.loading = true; + try { + const [configRes, statusRes] = await Promise.all([ + fetch("/api/config", { headers: { Authorization: `Bearer ${this.apiKey}` } }), + fetch("/api/status", { headers: { Authorization: `Bearer ${this.apiKey}` } }), + ]); + + if (!configRes.ok || !statusRes.ok) throw new Error("Unauthorized"); + + const config = await configRes.json(); + const status = await statusRes.json(); + + this.providers = Object.entries(config).map(([name, cfg]) => ({ + name, + config: cfg, + status: status[name] || { accessToken: null, refreshToken: null, lastUpdated: null }, + })); + } catch (err) { + this.showNotification(err.message, "error"); + throw err; + } finally { + this.loading = false; + } + }, + + async saveConfig(event) { + if (event) event.preventDefault(); + this.loading = true; + try { + const res = await fetch(`/api/config/${this.form.providerName}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(this.form), + }); + + if (!res.ok) throw new Error("Failed to save"); + + this.showNotification("Saved successfully"); + await this.fetchProviders(); + + this.form = { + providerName: "", + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + scope: "public", + }; + } catch (err) { + this.showNotification(err.message, "error"); + } finally { + this.loading = false; + } + }, + + async forceRefresh(name) { + this.loading = true; + try { + const res = await fetch(`/api/refresh/${name}`, { + method: "POST", + headers: { Authorization: `Bearer ${this.apiKey}` }, + }); + + if (!res.ok) throw new Error("Refresh failed"); + + this.showNotification(`Refreshed ${name}`); + await this.fetchProviders(); + } catch (err) { + this.showNotification(err.message, "error"); + } finally { + this.loading = false; + } + }, + + editProvider(provider) { + this.form = { + providerName: provider.name, + clientId: provider.config.clientId, + clientSecret: provider.config.clientSecret, + authUrl: provider.config.authUrl, + tokenUrl: provider.config.tokenUrl, + scope: provider.config.scope, + }; + window.scrollTo({ top: 0, behavior: "smooth" }); + }, + + getRedirectUri() { + return `${window.location.origin}/auth/${this.form.providerName || "{provider}"}/callback`; + }, + + copyToClipboard(text) { + if (!text) return; + navigator.clipboard.writeText(text).then(() => { + this.showNotification("Copied"); + }); + }, + + showNotification(message, type = "success") { + this.notification = { show: true, message, type }; + setTimeout(() => { + this.notification.show = false; + }, 3000); + }, + + formatTime(timestamp) { + return formatTime(timestamp); + }, + })); +}); diff --git a/tsconfig.json b/tsconfig.json index 1fac6c7..8fdce01 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,8 @@ "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", "lib": ["ESNext"], "types": ["node", "bun-types"] },