feat(ops-web): add operations console frontend
Vue 3 + Ant Design Vue SPA scaffold for the internal ops console. Ships the auth, account list, and audit log views that consume the new ops-api endpoints, plus the nginx/vite/typecheck plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { http } from "@/lib/http";
|
||||
import {
|
||||
type OperatorView,
|
||||
clearStoredSession,
|
||||
readStoredSession,
|
||||
writeStoredSession,
|
||||
} from "@/lib/storage";
|
||||
|
||||
interface LoginResponse {
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
operator: OperatorView;
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("ops-auth", () => {
|
||||
const accessToken = ref<string | null>(null);
|
||||
const expiresAt = ref<number | null>(null);
|
||||
const operator = ref<OperatorView | null>(null);
|
||||
const initialized = ref(false);
|
||||
|
||||
function hydrate(): void {
|
||||
const stored = readStoredSession();
|
||||
accessToken.value = stored.accessToken;
|
||||
expiresAt.value = stored.expiresAt;
|
||||
operator.value = stored.operator;
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
async function login(username: string, password: string): Promise<void> {
|
||||
const result = await http.post<LoginResponse>("/auth/login", { username, password });
|
||||
accessToken.value = result.access_token;
|
||||
expiresAt.value = result.expires_at;
|
||||
operator.value = result.operator;
|
||||
writeStoredSession({
|
||||
accessToken: result.access_token,
|
||||
expiresAt: result.expires_at,
|
||||
operator: result.operator,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshSelf(): Promise<void> {
|
||||
if (!accessToken.value) return;
|
||||
try {
|
||||
const me = await http.get<OperatorView & { last_login_at: string | null }>("/auth/me");
|
||||
operator.value = {
|
||||
id: me.id,
|
||||
username: me.username,
|
||||
display_name: me.display_name,
|
||||
email: me.email,
|
||||
role: me.role,
|
||||
status: me.status,
|
||||
};
|
||||
writeStoredSession({
|
||||
accessToken: accessToken.value,
|
||||
expiresAt: expiresAt.value,
|
||||
operator: operator.value,
|
||||
});
|
||||
} catch {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
accessToken.value = null;
|
||||
expiresAt.value = null;
|
||||
operator.value = null;
|
||||
clearStoredSession();
|
||||
}
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
expiresAt,
|
||||
operator,
|
||||
initialized,
|
||||
isAuthenticated,
|
||||
hydrate,
|
||||
login,
|
||||
refreshSelf,
|
||||
logout,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
export const pinia = createPinia();
|
||||
Reference in New Issue
Block a user