feat: scaffold Vue 3 + TypeScript + Vite frontend admin panel
Set up web/ directory with complete frontend scaffolding: - Vue 3 + TypeScript + Vite with Tailwind CSS v4 - Vue Router with auth guard (redirects to /login when unauthenticated) - Pinia stores: auth, cookies, devices, settings - Axios HTTP client with token interceptor - Views: Login, Dashboard, Cookies, Devices, Settings - Vite dev server proxy to relay API on port 8100 - Headless UI and Heroicons dependencies Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
22
web/src/stores/auth.ts
Normal file
22
web/src/stores/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import api from "@/api/client";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const token = ref<string | null>(localStorage.getItem("cb_admin_token"));
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value);
|
||||
|
||||
async function login(username: string, password: string): Promise<void> {
|
||||
const { data } = await api.post("/auth/login", { username, password });
|
||||
token.value = data.token;
|
||||
localStorage.setItem("cb_admin_token", data.token);
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
token.value = null;
|
||||
localStorage.removeItem("cb_admin_token");
|
||||
}
|
||||
|
||||
return { token, isAuthenticated, login, logout };
|
||||
});
|
||||
53
web/src/stores/cookies.ts
Normal file
53
web/src/stores/cookies.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import api from "@/api/client";
|
||||
import type { EncryptedCookieBlob } from "@/types/api";
|
||||
|
||||
export const useCookiesStore = defineStore("cookies", () => {
|
||||
const cookies = ref<EncryptedCookieBlob[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const domains = computed(() => {
|
||||
const set = new Set(cookies.value.map((c) => c.domain));
|
||||
return Array.from(set).sort();
|
||||
});
|
||||
|
||||
const byDomain = computed(() => {
|
||||
const map = new Map<string, EncryptedCookieBlob[]>();
|
||||
for (const cookie of cookies.value) {
|
||||
const list = map.get(cookie.domain) ?? [];
|
||||
list.push(cookie);
|
||||
map.set(cookie.domain, list);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
async function fetchCookies(domain?: string): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const params = domain ? { domain } : {};
|
||||
const { data } = await api.get("/cookies", { params });
|
||||
cookies.value = data.cookies;
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to fetch cookies";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCookie(
|
||||
domain: string,
|
||||
cookieName: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
await api.delete("/cookies", { data: { domain, cookieName, path } });
|
||||
cookies.value = cookies.value.filter(
|
||||
(c) =>
|
||||
!(c.domain === domain && c.cookieName === cookieName && c.path === path),
|
||||
);
|
||||
}
|
||||
|
||||
return { cookies, loading, error, domains, byDomain, fetchCookies, deleteCookie };
|
||||
});
|
||||
30
web/src/stores/devices.ts
Normal file
30
web/src/stores/devices.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import api from "@/api/client";
|
||||
import type { DeviceInfo } from "@/types/api";
|
||||
|
||||
export const useDevicesStore = defineStore("devices", () => {
|
||||
const devices = ref<DeviceInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function fetchDevices(): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data } = await api.get("/devices");
|
||||
devices.value = data.devices;
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to fetch devices";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeDevice(deviceId: string): Promise<void> {
|
||||
await api.delete(`/devices/${deviceId}`);
|
||||
devices.value = devices.value.filter((d) => d.deviceId !== deviceId);
|
||||
}
|
||||
|
||||
return { devices, loading, error, fetchDevices, revokeDevice };
|
||||
});
|
||||
41
web/src/stores/settings.ts
Normal file
41
web/src/stores/settings.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import api from "@/api/client";
|
||||
|
||||
export interface AppSettings {
|
||||
syncIntervalMs: number;
|
||||
maxDevices: number;
|
||||
autoSync: boolean;
|
||||
theme: "light" | "dark" | "system";
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
syncIntervalMs: 30_000,
|
||||
maxDevices: 10,
|
||||
autoSync: true,
|
||||
theme: "system",
|
||||
};
|
||||
|
||||
export const useSettingsStore = defineStore("settings", () => {
|
||||
const settings = ref<AppSettings>({ ...DEFAULT_SETTINGS });
|
||||
const loading = ref(false);
|
||||
|
||||
async function fetchSettings(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.get("/settings");
|
||||
settings.value = { ...DEFAULT_SETTINGS, ...data };
|
||||
} catch {
|
||||
// Use defaults if settings endpoint doesn't exist yet
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSettings(patch: Partial<AppSettings>): Promise<void> {
|
||||
const { data } = await api.patch("/settings", patch);
|
||||
settings.value = { ...settings.value, ...data };
|
||||
}
|
||||
|
||||
return { settings, loading, fetchSettings, updateSettings };
|
||||
});
|
||||
Reference in New Issue
Block a user