mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-03 15:43:59 +00:00
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
This commit is contained in:
155
web/src/views/LoginView.vue
Normal file
155
web/src/views/LoginView.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<!--
|
||||
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>
|
||||
Reference in New Issue
Block a user