mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-19 16:38:31 +00:00
- 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
156 lines
4.0 KiB
Vue
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>
|