8 Commits

Author SHA1 Message Date
ramvignesh-b eac5aa7056 test: update auth callback assertions to verify redirect status and location header
CI / build (pull_request) Successful in 22s
2026-05-12 04:14:26 +05:30
ramvignesh-b 0be9a05c09 refactor: fix lint and formatting
CI / build (pull_request) Failing after 22s
2026-05-12 04:07:34 +05:30
ramvignesh-b ced524dc31 refactor: enhance dashboard UI styling
CI / build (pull_request) Failing after 23s
2026-05-12 03:59:06 +05:30
ramvignesh-b bd6c3790e6 chore: update Alpine.js to latest major version via unpkg CDN 2026-05-12 03:42:39 +05:30
ramvignesh-b 7192ba381d fix: clear invalid API key and reset state on unlock failure 2026-05-12 03:41:08 +05:30
ramvignesh-b 72548db9af refactor: migrate success view from static HTML to dynamic JSX route component 2026-05-12 03:39:39 +05:30
ramvignesh-b 2c2a2eb6c4 feat: migrate dashboard to JSX and move client-side scripts to JS 2026-05-12 03:32:44 +05:30
ramvignesh-b eedab2347c feat: add static file serving and refactor dashboard form to use FormData 2026-05-12 01:59:25 +05:30
5 changed files with 38 additions and 153 deletions
+3 -11
View File
@@ -1,22 +1,14 @@
import type { Context, Next } from "hono";
import { getCookie } from "hono/cookie";
import { config } from "../config";
export const authMiddleware = async (c: Context, next: Next) => {
const authHeader = c.req.header("Authorization");
const cookieToken = getCookie(c, "toknd_api_key");
let token: string | undefined;
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.split(" ")[1];
} else if (cookieToken) {
token = cookieToken;
if (!authHeader?.startsWith("Bearer ")) {
return c.json({ error: "Missing or invalid authorization header" }, 401);
}
if (!token) {
return c.json({ error: "Missing or invalid authorization" }, 401);
}
const token = authHeader.split(" ")[1];
if (token !== config.API_KEY) {
return c.json({ error: "Invalid API key" }, 403);
+6 -37
View File
@@ -1,13 +1,11 @@
/** @jsxImportSource hono/jsx */
import { Hono } from "hono";
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
import { html } from "hono/html";
import type { Child } from "hono/jsx";
import { config } from "../config";
const dashboardRoutes = new Hono({ strict: false });
export const Layout = (props: { title: string; children: Child; isUnlocked?: boolean }) => (
export const Layout = (props: { title: string; children: Child }) => (
<>
{html`<!DOCTYPE html>`}
<html lang="en" data-theme="abyss">
@@ -39,7 +37,7 @@ export const Layout = (props: { title: string; children: Child; isUnlocked?: boo
</head>
<body
class="bg-base-200/50 min-h-screen font-['DM_Sans',sans-serif] antialiased text-base-content tracking-tight"
x-data={`dashboard({ initialIsUnlocked: ${props.isUnlocked || false} })`}
x-data="dashboard"
>
{props.children}
<script src="/app/dashboard.js"></script>
@@ -49,8 +47,8 @@ export const Layout = (props: { title: string; children: Child; isUnlocked?: boo
</>
);
export const Dashboard = (props: { isUnlocked: boolean }) => (
<Layout title="toknd — Auth Broker Dashboard" isUnlocked={props.isUnlocked}>
export const Dashboard = () => (
<Layout title="toknd — Auth Broker Dashboard">
<div class="navbar bg-base-100 shadow-sm px-4 md:px-8 border-b border-base-300">
<div class="flex-1">
<div class="flex items-center gap-2">
@@ -95,14 +93,6 @@ export const Dashboard = (props: { isUnlocked: boolean }) => (
<span class="loading loading-spinner loading-xs" x-show="loading"></span>
<span class="ml-1 hidden md:inline" x-text="loading ? 'Unlocking...' : 'Unlock'"></span>
</button>
<button
x-show="isUnlocked"
x-on:click="logout()"
type="button"
class="btn btn-ghost btn-sm join-item text-error hover:bg-error/10"
>
<i class="ph-bold ph-power text-lg"></i>
</button>
</div>
</div>
</div>
@@ -429,7 +419,7 @@ export const Dashboard = (props: { isUnlocked: boolean }) => (
type="button"
x-on:click="forceRefresh(provider.name)"
class="btn btn-base w-full"
x-bind:disabled="loading || !provider.status.accessToken"
x-bind:disabled="loading"
>
<i class="ph-bold ph-arrows-clockwise text-base mr-1"></i>
<span class="text-xs uppercase font-bold tracking-widest">
@@ -494,28 +484,7 @@ export const Success = (props: { provider: string }) => (
);
dashboardRoutes.get("/", async (c) => {
const isUnlocked = getCookie(c, "toknd_api_key") === config.API_KEY;
return c.html(<Dashboard isUnlocked={isUnlocked} />);
});
dashboardRoutes.post("/unlock", async (c) => {
const { apiKey } = await c.req.json();
if (apiKey !== config.API_KEY) {
return c.json({ error: "Invalid API Key" }, 401);
}
setCookie(c, "toknd_api_key", apiKey, {
httpOnly: true,
secure: true,
sameSite: "Strict",
path: "/",
maxAge: 60 * 60 * 24 * 7, // 1 week
});
return c.json({ success: true });
});
dashboardRoutes.post("/logout", async (c) => {
deleteCookie(c, "toknd_api_key", { path: "/" });
return c.json({ success: true });
return c.html(<Dashboard />);
});
dashboardRoutes.get("/success", async (c) => {
+28 -64
View File
@@ -10,9 +10,9 @@ document.addEventListener("alpine:init", () => {
return date.toLocaleDateString();
};
window.Alpine.data("dashboard", ({ initialIsUnlocked }) => ({
apiKey: "",
isUnlocked: initialIsUnlocked,
window.Alpine.data("dashboard", () => ({
apiKey: localStorage.getItem("toknd_api_key") || "",
isUnlocked: false,
loading: false,
providers: [],
form: {
@@ -30,8 +30,8 @@ document.addEventListener("alpine:init", () => {
},
init() {
if (this.isUnlocked) {
this.fetchProviders();
if (this.apiKey) {
this.unlock();
}
},
@@ -39,34 +39,14 @@ document.addEventListener("alpine:init", () => {
if (!this.apiKey) return;
this.loading = true;
try {
const res = await fetch("/app/unlock", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apiKey: this.apiKey }),
});
if (!res.ok) throw new Error("Invalid API Key");
this.isUnlocked = true;
localStorage.setItem("toknd_api_key", this.apiKey);
await this.fetchProviders();
this.apiKey = ""; // Clear after success
this.isUnlocked = true;
} catch (err) {
this.showNotification(err.message, "error");
this.showNotification(`Failed to unlock: ${err.message}. Check your API Key`, "error");
localStorage.removeItem("toknd_api_key");
this.isUnlocked = false;
} finally {
this.loading = false;
}
},
async logout() {
this.loading = true;
try {
await fetch("/app/logout", { method: "POST" });
this.isUnlocked = false;
this.providers = [];
this.showNotification("Logged out successfully");
} catch (err) {
this.showNotification(`Logout failed: ${err.message}`, "error");
this.apiKey = "";
} finally {
this.loading = false;
}
@@ -76,53 +56,41 @@ document.addEventListener("alpine:init", () => {
this.loading = true;
try {
const [configRes, statusRes] = await Promise.all([
fetch("/api/config"),
fetch("/api/status"),
fetch("/api/config", { headers: { Authorization: `Bearer ${this.apiKey}` } }),
fetch("/api/status", { headers: { Authorization: `Bearer ${this.apiKey}` } }),
]);
if (configRes.status === 401 || statusRes.status === 401) {
return this.handleSessionExpired();
}
if (!configRes.ok || !statusRes.ok) throw new Error("Unauthorized");
if (!configRes.ok || !statusRes.ok) throw new Error("Failed to fetch data");
const config = await configRes.json();
const status = await statusRes.json();
const [config, status] = await Promise.all([configRes.json(), statusRes.json()]);
this.providers = this.mapProviders(config, status);
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;
}
},
mapProviders(config, status) {
return Object.entries(config).map(([name, cfg]) => ({
name,
config: cfg,
status: status[name] || { accessToken: null, refreshToken: null, lastUpdated: null },
}));
},
handleSessionExpired() {
this.isUnlocked = false;
this.providers = [];
this.showNotification("Session expired", "error");
},
async saveConfig() {
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" },
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify(this.form),
});
if (res.status === 401) {
this.isUnlocked = false;
throw new Error("Session expired");
}
if (!res.ok) throw new Error("Failed to save");
this.showNotification("Saved successfully");
@@ -148,13 +116,9 @@ document.addEventListener("alpine:init", () => {
try {
const res = await fetch(`/api/refresh/${name}`, {
method: "POST",
headers: { Authorization: `Bearer ${this.apiKey}` },
});
if (res.status === 401) {
this.isUnlocked = false;
throw new Error("Session expired");
}
if (!res.ok) throw new Error("Refresh failed");
this.showNotification(`Refreshed ${name}`);
+1 -20
View File
@@ -24,7 +24,7 @@ describe("API Integration", () => {
expect(res.status).toBe(401);
const body = await res.json();
expect(body).toEqual({ error: "Missing or invalid authorization" });
expect(body).toEqual({ error: "Missing or invalid authorization header" });
});
it("should return 200 for health check (no auth needed)", async () => {
@@ -67,25 +67,6 @@ describe("API Integration", () => {
expect(body.trakt.accessToken).toBe("current-access-token");
});
it("should return 200 for status with valid Cookie", async () => {
redis.keys.mockReturnValue(Promise.resolve(["config:trakt"]));
redis.get.mockImplementation((key) => {
if (key.includes("config")) return Promise.resolve(mockTraktConfig);
if (key.includes("access_token")) return Promise.resolve("current-access-token");
return Promise.resolve(null);
});
const res = await app.request("/api/status", {
headers: {
Cookie: "toknd_api_key=test-api-key",
},
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.trakt).toBeDefined();
});
it("should return 404 for unknown provider token", async () => {
const res = await app.request("/api/token/unconfigured-provider", {
headers: {
-21
View File
@@ -34,25 +34,4 @@ describe("Dashboard & Common Integration", () => {
const body = await res.json();
expect(body.error).toBe("Internal Server Error");
});
it("should set a cookie on successful unlock", async () => {
const res = await app.request("/app/unlock", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apiKey: "test-api-key" }),
});
expect(res.status).toBe(200);
expect(res.headers.get("Set-Cookie")).toContain("toknd_api_key=test-api-key");
expect(res.headers.get("Set-Cookie")).toContain("HttpOnly");
});
it("should clear the cookie on logout", async () => {
const res = await app.request("/app/logout", {
method: "POST",
});
expect(res.status).toBe(200);
expect(res.headers.get("Set-Cookie")).toContain("toknd_api_key=;");
});
});