Files
webui4frpc/web/src/views/LoginView.vue
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

156 lines
4.0 KiB
Vue

<!--
LoginView session-cookie sign-in page. Shown by App.vue when /me is 401
(no session cookie). Submitting POST /api/manager/login sets the HttpOnly
session cookie; on success auth.login() flips isAuthed and App drops into
the shell. There is no browser Basic-Auth prompt anymore (the 401 response
omits WWW-Authenticate), so credentials are never cached by the browser and
logout works.
-->
<template>
<div class="login-wrap">
<form class="login-card" @submit.prevent="submit">
<div class="lc-brand">
<img class="lc-logo" src="/favicon.svg" alt="webui4frpc" />
<b>webui4frpc</b>
<small>令牌环 · 转发编排</small>
</div>
<label class="lc-field">
<span>用户名</span>
<input v-model="username" autocomplete="username" placeholder="用户名" required />
</label>
<label class="lc-field">
<span>密码</span>
<input v-model="password" type="password" autocomplete="current-password" placeholder="••••••••" required />
</label>
<p v-if="error" class="lc-error">{{ error }}</p>
<button class="lc-btn" type="submit" :disabled="busy">
{{ busy ? '登录中…' : '登 录' }}
</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { login } from '../auth'
const username = ref('')
const password = ref('')
const busy = ref(false)
const error = ref('')
async function submit() {
busy.value = true
error.value = ''
try {
await login(username.value, password.value)
} catch {
error.value = '登录失败:用户名或密码错误'
} finally {
busy.value = false
}
}
</script>
<style scoped>
.login-wrap {
height: 100%;
display: grid;
place-items: center;
padding: 24px;
font-family: var(--w4f-font);
}
.login-card {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
gap: 14px;
padding: 32px 28px 28px;
border-radius: var(--radius-lg);
background: var(--w4f-card);
border: 1px solid var(--w4f-line);
box-shadow: var(--w4f-sh-lg);
backdrop-filter: blur(calc(var(--w4f-glass) * 0.8)) saturate(1.3);
-webkit-backdrop-filter: blur(calc(var(--w4f-glass) * 0.8)) saturate(1.3);
}
@media (prefers-reduced-motion: reduce) {
.login-card { backdrop-filter: none; -webkit-backdrop-filter: none; }
}
.lc-brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding-bottom: 8px;
}
.lc-logo {
width: 56px;
height: 56px;
border-radius: 16px;
object-fit: contain;
box-shadow: 0 8px 22px var(--w4f-glow);
}
.lc-brand b { font-size: 19px; letter-spacing: 0.3px; }
.lc-brand small { font-size: 12px; color: var(--w4f-muted); }
.lc-field {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: var(--w4f-fg-2);
}
.lc-field input {
padding: 9px 12px;
border-radius: 10px;
border: 1px solid var(--w4f-line-strong);
background: var(--w4f-card-solid);
color: var(--w4f-fg);
font-size: 13.5px;
font-family: inherit;
transition: border-color 0.15s var(--w4f-ease), box-shadow 0.15s var(--w4f-ease);
}
.lc-field input:focus {
outline: none;
border-color: var(--w4f-primary);
box-shadow: 0 0 0 3px var(--w4f-glow-soft);
}
.lc-error {
margin: 0;
font-size: 12.5px;
color: var(--w4f-danger);
background: var(--w4f-danger-50);
border-radius: 8px;
padding: 8px 10px;
}
.lc-btn {
margin-top: 4px;
padding: 11px 0;
border: none;
border-radius: 12px;
cursor: pointer;
font-family: inherit;
font-size: 14px;
font-weight: 700;
letter-spacing: 2px;
color: #fff;
background: linear-gradient(135deg, var(--w4f-primary), var(--w4f-secondary));
box-shadow: 0 6px 18px var(--w4f-glow);
transition: transform 0.15s var(--w4f-ease-spring), box-shadow 0.15s var(--w4f-ease-spring);
}
.lc-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 10px 24px var(--w4f-glow);
}
.lc-btn:disabled { opacity: 0.65; cursor: default; }
</style>