Files
webui4frpc/web/src/auth.ts
jianf f29ec81e4a feat: session-cookie login/logout + three-tier roles (superadmin/admin/viewer) with read-only UI + remove pink theme
- cookie-based auth (/login /logout) replacing Basic Auth for UI, enabling logout
- roles: superadmin (account management only), admin (full except accounts), viewer/audit (read-only status+cluster, export logs)
- readonly accounts hide edit buttons (added remote node, group ops, canvas layout/save/import, cluster manage, install) instead of greying them
- auditors see status+cluster only; ordinary admins lose the accounts nav; last-admin guard covers superadmin
- remove pink theme entirely (switcher, [data-theme=pink], leftover localStorage), keep white/blue
2026-08-20 09:08:15 +08:00

65 lines
2.4 KiB
TypeScript

// Lightweight reactive auth principal for the SPA. Since the switch to
// session-cookie login (POST /api/manager/login), the SPA no longer relies on
// the browser's Basic Auth: on mount we resolve GET /me — a session cookie
// makes it 200 (signed in); otherwise it is 401 and the login page shows.
// logout() clears the cookie server-side, so sign-out actually sticks.
//
// We default to read until /me resolves so a viewer never sees a flash of
// admin controls. canWrite/isAdmin are memoized over the reactive level.
import { computed, ref } from 'vue'
import type { AccessLevel } from './types'
import { api } from './api'
export const authLevel = ref<AccessLevel>('read')
export const authName = ref('')
export const authReady = ref(false)
export const isAuthed = ref(false)
const RANK: Record<AccessLevel, number> = { read: 1, write: 2, admin: 3, superadmin: 4 }
export const canWrite = computed(() => RANK[authLevel.value] >= RANK.write)
// admin: full write access but cannot manage accounts (that needs superadmin).
export const isAdmin = computed(() => authLevel.value === 'admin')
// superadmin: the only tier that can manage accounts (users/apikeys routes).
export const isSuperAdmin = computed(() => authLevel.value === 'superadmin')
// login signs in via the session-cookie endpoint and mirrors the resolved
// principal. Call from the login page; on success App.vue drops into the shell.
export async function login(username: string, password: string): Promise<void> {
const me = await api.login(username, password)
authLevel.value = me.level
authName.value = me.name
isAuthed.value = true
authReady.value = true
}
// logout clears the UI session and returns to the login page.
export async function logout(): Promise<void> {
try {
await api.logout()
} catch {
// session cookie may already be gone; still reset local state
}
authLevel.value = 'read'
authName.value = ''
isAuthed.value = false
}
// fetchMe resolves the current principal. Called once on app mount; safe to
// call again after account changes that could affect the live session.
export async function fetchMe(): Promise<void> {
try {
const me = await api.me()
authLevel.value = me.level
authName.value = me.name
isAuthed.value = true
} catch {
// 401 (no/failed auth) — show the login page.
authLevel.value = 'read'
authName.value = ''
isAuthed.value = false
} finally {
authReady.value = true
}
}