feat: add admin REST API layer for frontend management panel
Implement JWT-protected /admin/* routes on the relay server: - Auth: login, logout, me, setup/status, setup/init (first-time config) - Dashboard: stats overview (connections, devices, cookies, domains) - Cookies: paginated list with domain/search filter, detail, delete, batch delete - Devices: list with online status, revoke - Settings: get/update (sync interval, max devices, theme) Uses scrypt for password hashing and jsonwebtoken for JWT. Adds listAll/revoke to DeviceRegistry, getAll/getById/deleteById to CookieBlobStore, disconnect to ConnectionManager. Updates frontend to use /admin/* endpoints. All 38 existing tests pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "/api",
|
||||
baseURL: "/admin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
const isAuthenticated = computed(() => !!token.value);
|
||||
|
||||
async function login(username: string, password: string): Promise<void> {
|
||||
const { data } = await api.post("/auth/login", { username, password });
|
||||
const { data } = await api.post("/auth/login", { username, password }, { baseURL: "/admin" });
|
||||
token.value = data.token;
|
||||
localStorage.setItem("cb_admin_token", data.token);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ export const useCookiesStore = defineStore("cookies", () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const params = domain ? { domain } : {};
|
||||
const params: Record<string, string> = { limit: "200" };
|
||||
if (domain) params.domain = domain;
|
||||
const { data } = await api.get("/cookies", { params });
|
||||
cookies.value = data.cookies;
|
||||
cookies.value = data.items ?? data.cookies ?? [];
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to fetch cookies";
|
||||
} finally {
|
||||
@@ -42,7 +43,13 @@ export const useCookiesStore = defineStore("cookies", () => {
|
||||
cookieName: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
await api.delete("/cookies", { data: { domain, cookieName, path } });
|
||||
// Find the cookie ID first, then delete by ID
|
||||
const cookie = cookies.value.find(
|
||||
(c) => c.domain === domain && c.cookieName === cookieName && c.path === path,
|
||||
);
|
||||
if (cookie) {
|
||||
await api.delete(`/cookies/${cookie.id}`);
|
||||
}
|
||||
cookies.value = cookies.value.filter(
|
||||
(c) =>
|
||||
!(c.domain === domain && c.cookieName === cookieName && c.path === path),
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useDevicesStore = defineStore("devices", () => {
|
||||
}
|
||||
|
||||
async function revokeDevice(deviceId: string): Promise<void> {
|
||||
await api.delete(`/devices/${deviceId}`);
|
||||
await api.post(`/devices/${deviceId}/revoke`);
|
||||
devices.value = devices.value.filter((d) => d.deviceId !== deviceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import api from "@/api/client";
|
||||
import type { HealthStatus } from "@/types/api";
|
||||
|
||||
const health = ref<HealthStatus | null>(null);
|
||||
interface DashboardData {
|
||||
connections: number;
|
||||
totalDevices: number;
|
||||
onlineDevices: number;
|
||||
totalCookies: number;
|
||||
uniqueDomains: number;
|
||||
}
|
||||
|
||||
const dashboard = ref<DashboardData | null>(null);
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await api.get("/health", { baseURL: "" });
|
||||
health.value = data;
|
||||
const { data } = await api.get("/dashboard");
|
||||
dashboard.value = data;
|
||||
} catch {
|
||||
// Server might be down
|
||||
} finally {
|
||||
@@ -23,26 +30,49 @@ onMounted(async () => {
|
||||
<h2 class="text-2xl font-semibold text-gray-900">Dashboard</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">CookieBridge relay server overview</p>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Server Status -->
|
||||
<div class="rounded-xl bg-white p-6 ring-1 ring-gray-200">
|
||||
<p class="text-sm font-medium text-gray-500">Server Status</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
class="inline-block h-2.5 w-2.5 rounded-full"
|
||||
:class="health?.status === 'ok' ? 'bg-green-500' : 'bg-red-500'"
|
||||
:class="dashboard ? 'bg-green-500' : 'bg-red-500'"
|
||||
/>
|
||||
<span class="text-lg font-semibold text-gray-900">
|
||||
{{ loading ? "Checking..." : health?.status === "ok" ? "Online" : "Offline" }}
|
||||
{{ loading ? "Checking..." : dashboard ? "Online" : "Offline" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Devices -->
|
||||
<div class="rounded-xl bg-white p-6 ring-1 ring-gray-200">
|
||||
<p class="text-sm font-medium text-gray-500">Devices</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-gray-900">
|
||||
{{ dashboard?.onlineDevices ?? "—" }}
|
||||
<span class="text-base font-normal text-gray-400">
|
||||
/ {{ dashboard?.totalDevices ?? "—" }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">online / total</p>
|
||||
</div>
|
||||
|
||||
<!-- Cookies -->
|
||||
<div class="rounded-xl bg-white p-6 ring-1 ring-gray-200">
|
||||
<p class="text-sm font-medium text-gray-500">Cookies</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-gray-900">
|
||||
{{ dashboard?.totalCookies ?? "—" }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
across {{ dashboard?.uniqueDomains ?? "—" }} domains
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Connections -->
|
||||
<div class="rounded-xl bg-white p-6 ring-1 ring-gray-200">
|
||||
<p class="text-sm font-medium text-gray-500">Active Connections</p>
|
||||
<p class="text-sm font-medium text-gray-500">WebSocket Connections</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-gray-900">
|
||||
{{ health?.connections ?? "—" }}
|
||||
{{ dashboard?.connections ?? "—" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ export default defineConfig({
|
||||
target: "http://localhost:8100",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/admin": {
|
||||
target: "http://localhost:8100",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/ws": {
|
||||
target: "ws://localhost:8100",
|
||||
ws: true,
|
||||
|
||||
Reference in New Issue
Block a user