refactor(ui): simplify role management UI and auth flow

- Replace permission selector dialog with inline add form (scope/id/level)
    - Replace per-user role API calls with batch PUT /api/roles/{id}/users
    - Add isLogin() async method with lazy _ensureAuth initialization
    - Clean up login page CSS: replace hardcoded colors with CSS variables
    - Add Chrome autofill style override for dark theme support
    - Use @submit.prevent instead of manual e.preventDefault()
    - Remove redundant inline comments from script sections
master
veypi 3 weeks ago
parent ac7a8d2108
commit adf0cd36ca

@ -13,14 +13,13 @@
display: flex;
justify-content: center;
align-items: center;
font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
}
h1 {
font-weight: bold;
margin: 0;
margin-bottom: 15px;
color: var(--color-text, #1f2937);
color: var(--text-color);
}
p {
@ -29,33 +28,32 @@
line-height: 20px;
letter-spacing: 0.5px;
margin: 20px 0 30px;
color: var(--color-text, #1f2937);
color: var(--text-color);
}
span {
font-size: 12px;
margin: 15px 0;
color: var(--color-text-light, #6b7280);
}
a {
color: var(--color-primary, #4f46e5);
color: var(--color-primary);
font-size: 14px;
text-decoration: none;
margin: 15px 0;
}
button {
border-radius: var(--border-radius, 8px);
border: 1px solid var(--color-primary, #4f46e5);
background-color: var(--color-primary, #4f46e5);
color: #ffffff;
border-radius: var(--radius-lg);
border: 1px solid var(--color-primary);
background-color: var(--color-primary);
color: var(--color-primary-text);
font-size: 12px;
font-weight: bold;
padding: 12px 45px;
letter-spacing: 1px;
text-transform: uppercase;
transition: transform 80ms ease-in, background-color 0.2s;
transition: transform 80ms ease-in, background-color var(--transition-fast);
cursor: pointer;
overflow: hidden;
}
@ -70,8 +68,8 @@
button.ghost {
background-color: transparent;
border-color: #ffffff;
color: #ffffff;
border-color: #fff;
color: #fff;
}
button.ghost:hover {
@ -79,35 +77,33 @@
}
button:disabled {
background-color: var(--color-border, #d1d5db);
border-color: var(--color-border, #d1d5db);
background-color: var(--text-color-disabled);
border-color: var(--text-color-disabled);
cursor: not-allowed;
}
/* Code input row */
.code-input-row {
display: flex;
gap: 8px;
gap: var(--spacing-sm);
align-items: center;
}
.code-input-row input {
flex: 1;
margin: 8px 0;
margin: var(--spacing-sm) 0;
}
button.send-code-btn {
width: auto;
min-width: 90px;
padding: 10px 14px;
margin: 8px 0;
margin: var(--spacing-sm) 0;
white-space: nowrap;
text-transform: none;
font-size: 13px;
letter-spacing: 0;
}
/* Loading spinner */
.loading-spinner {
position: absolute;
top: 50%;
@ -116,7 +112,7 @@
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid #ffffff;
border-top: 2px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@ -136,7 +132,7 @@
}
form {
background-color: var(--bg-color-secondary, #ffffff);
background-color: var(--bg-color-secondary);
display: flex;
align-items: center;
justify-content: center;
@ -147,32 +143,40 @@
}
input {
background-color: var(--bg-color-secondary, #ffffff);
border: 1px solid var(--color-border, #d1d5db);
background-color: var(--bg-color-secondary);
border: 1px solid var(--border-color);
width: 100%;
padding: 10px 12px;
margin: 8px 0;
border-radius: var(--border-radius, 8px);
color: var(--color-text, #1f2937);
transition: border-color 0.2s, box-shadow 0.2s;
margin: var(--spacing-sm) 0;
border-radius: var(--radius-lg);
color: var(--text-color);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
font-size: 14px;
}
input:focus {
outline: none;
border-color: var(--color-primary, #4f46e5);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 85%);
}
input::placeholder {
color: var(--color-text-light, #9ca3af);
color: var(--text-color-disabled);
}
/* Chrome 自动填充覆盖 */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px var(--bg-color-secondary) inset !important;
-webkit-text-fill-color: var(--text-color) !important;
}
.container {
position: relative;
background-color: var(--bg-color-secondary, #ffffff);
background-color: var(--bg-color-secondary);
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.1);
box-shadow: var(--shadow-lg);
overflow: hidden;
width: 768px;
max-width: 100%;
@ -227,12 +231,8 @@
.overlay {
position: relative;
background: var(--color-primary, #4f46e5);
background: linear-gradient(to right, var(--color-secondary, #7c3aed), var(--color-primary, #4f46e5));
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #ffffff;
background: linear-gradient(to right, var(--color-secondary), var(--color-primary));
color: var(--color-primary-text);
left: -100%;
height: 100%;
width: 200%;
@ -281,7 +281,7 @@
}
.social-container a {
border: 1px solid #dddddd;
border: 1px solid var(--border-color);
border-radius: 50%;
display: inline-flex;
justify-content: center;
@ -290,13 +290,13 @@
height: 40px;
width: 40px;
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-fast);
overflow: hidden;
font-size: 14px;
}
.social-container a:hover {
background-color: #f3f4f6;
background-color: var(--bg-color-tertiary);
border-color: var(--color-primary);
color: var(--color-primary);
}
@ -311,16 +311,16 @@
}
.error-message {
color: var(--color-danger, #ef4444);
color: var(--color-danger);
font-size: 12px;
margin-top: 8px;
margin-top: var(--spacing-sm);
min-height: 18px;
}
.login-tab {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--border-color);
width: 100%;
}
@ -329,15 +329,15 @@
cursor: pointer;
flex: 1;
text-align: center;
color: #666;
color: var(--text-color-secondary);
font-size: 13px;
border-bottom: 2px solid transparent;
transition: all 0.3s;
transition: all var(--transition-base);
}
.tab-item.active {
color: var(--color-primary, #4f46e5);
border-bottom-color: var(--color-primary, #4f46e5);
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: 600;
}
@ -350,7 +350,6 @@
margin-top: 10px;
}
/* Responsive */
@media (max-width: 768px) {
.container {
width: 100%;
@ -389,7 +388,7 @@
display: block !important;
margin-top: 20px;
font-size: 14px;
color: var(--color-primary, #4f46e5);
color: var(--color-primary);
cursor: pointer;
}
}
@ -406,7 +405,7 @@
<div class="container" :class="{ 'right-panel-active': isSignUp }">
<!-- Register Form -->
<div class="form-container sign-up-container">
<form @submit="handleSignUp">
<form @submit.prevent="handleSignUp">
<h1>{{ $t('auth.create_account') }}</h1>
<div class="social-container">
<a v-for="p in providers" @click="handleOAuth(p.code)" :title="p.name" class="social-btn">
@ -452,7 +451,7 @@
<!-- Login Form -->
<div class="form-container sign-in-container">
<form @submit="handleSignIn">
<form @submit.prevent="handleSignIn">
<h1>{{ $t('auth.login') }}</h1>
<div class="social-container">
<a v-for="p in providers" @click="handleOAuth(p.code)" :title="p.name" class="social-btn">
@ -529,7 +528,6 @@
</body>
<script setup>
// Reactive data - use = for reactive state (NO let/const/var)
isSignUp = false;
loginType = 'username';
redirect = $router.query.redirect || '/';
@ -564,7 +562,6 @@
signUpLoading = false;
providers = [];
// Switch to sign up panel
switchToSignUp = () => {
if (signInLoading || signUpLoading) return;
isSignUp = true;
@ -572,7 +569,6 @@
signInError = '';
};
// Switch to sign in panel
switchToSignIn = () => {
if (signInLoading || signUpLoading) return;
isSignUp = false;
@ -580,7 +576,6 @@
signInError = '';
};
// Switch login type
switchLoginType = (type) => {
if (signInLoading) return;
loginType = type;
@ -591,7 +586,6 @@
signInForm.code = '';
};
// Handle OAuth login
handleOAuth = async (provider) => {
try {
const params = new URLSearchParams({provider, redirect});
@ -609,7 +603,6 @@
}
};
// Load configuration
loadConfig = async () => {
try {
const res = await fetch('/api/info');
@ -639,7 +632,7 @@
console.error('Failed to load config:', e);
}
};
// Send verification code
sendCode = async () => {
if (countDown > 0 || sendCodeLoading) return;
@ -685,16 +678,13 @@
}
};
// Handle Sign In
handleSignIn = async (e) => {
e.preventDefault();
if (signInLoading) return;
signInError = '';
signInLoading = true;
try {
// Code login
if (loginType === 'code') {
if (!signInForm.target || !signInForm.code) {
throw new Error($t('auth.target_and_code_required'));
@ -721,7 +711,6 @@
throw new Error('Login failed');
}
} else {
// Username/Password login
if (!signInForm.username || !signInForm.password) {
throw new Error($t('auth.fill_all_fields'));
}
@ -741,14 +730,11 @@
}
};
// Handle Sign Up
handleSignUp = async (e) => {
e.preventDefault();
if (signUpLoading) return;
signUpError = '';
// Validation
if (!signUpForm.username || !signUpForm.password || !signUpForm.confirmPassword) {
signUpError = $t('auth.fill_all_fields');
return;
@ -772,7 +758,6 @@
signUpLoading = true;
try {
// Use vbase.request for consistent API handling
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
@ -804,7 +789,6 @@
}
};
// Initialize
loadConfig();
</script>

@ -322,8 +322,7 @@
<v-btn icon size="sm" variant="outline" :click="() => openDetailModal(r)" :title="$t('common.detail')">
<i class="fas fa-eye"></i>
</v-btn>
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(r)" :title="$t('common.edit')"
:disabled="r.is_system">
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(r)" :title="$t('common.edit')">
<i class="fas fa-edit"></i>
</v-btn>
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteRole(r)"
@ -372,10 +371,33 @@
<span style="color: var(--text-color-secondary); font-size: var(--font-size-sm);">
{{ $t('role.permissions_desc') }}
</span>
<v-btn size="sm" color="primary" :click="openPermissionSelector" :disabled="currentRole?.is_system">
<v-btn size="sm" color="primary" @click="showPermForm = true">
<i class="fas fa-plus"></i> {{ $t('role.add_permission') }}
</v-btn>
</div>
<!-- Add Permission Inline Form -->
<div v-if="showPermForm"
style="display: flex; gap: 8px; align-items: flex-end; padding: 12px; background: var(--bg-color-tertiary); border-radius: var(--radius-md); flex-wrap: wrap;">
<div style="display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px;">
<label style="font-size: 12px; color: var(--text-color-secondary);">Scope</label>
<v-input type="text" v:value="newPerm.scope" placeholder="vb" style="margin: 0;" />
</div>
<div style="display: flex; flex-direction: column; gap: 4px; flex: 2; min-width: 180px;">
<label style="font-size: 12px; color: var(--text-color-secondary);">Permission ID</label>
<v-input type="text" v:value="newPerm.permission_id" placeholder="resource:instance:*" style="margin: 0;" />
</div>
<div style="display: flex; flex-direction: column; gap: 4px; width: 100px;">
<label style="font-size: 12px; color: var(--text-color-secondary);">Level</label>
<v-input v:value="newPerm.level" type='select' :opts="{options: permission_levels}" style="margin: 0;">
</v-input>
</div>
<v-btn size="sm" color="primary" :click="addPermission" :disabled="permAdding">
{{ permAdding ? '...' : $t('common.save') }}
</v-btn>
<v-btn size="sm" variant="outline" @click="showPermForm = false">{{ $t('common.cancel') }}</v-btn>
</div>
<div class="permission-list">
<div v-for="p in rolePermissions" class="permission-item">
<div class="permission-info">
@ -385,12 +407,12 @@
<div class="permission-level">
<span :class="'level-badge level-' + p.level">{{ formatLevel(p.level) }}</span>
</div>
<v-btn v-if="!currentRole?.is_system" icon size="xs" color="danger" variant="ghost"
:click="() => removePermission(p)" :title="$t('common.remove')">
<v-btn icon size="xs" color="danger" variant="ghost" :click="() => removePermission(p)"
:title="$t('common.remove')">
<i class="fas fa-times"></i>
</v-btn>
</div>
<div v-if="rolePermissions.length === 0" class="empty-state">
<div v-if="rolePermissions.length === 0 && !showPermForm" class="empty-state">
<i class="fas fa-shield-alt" style="font-size: 48px; color: var(--text-color-disabled);"></i>
<p>{{ $t('role.no_permissions') }}</p>
</div>
@ -404,7 +426,7 @@
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
<input type="text" v:value="userSearchQuery" :placeholder="$t('role.search_users')">
</div>
<v-btn size="sm" color="primary" :click="openUserSelector" :disabled="currentRole?.is_system">
<v-btn size="sm" color="primary" :click="openUserSelector">
<i class="fas fa-plus"></i> {{ $t('role.add_user') }}
</v-btn>
</div>
@ -432,32 +454,6 @@
</div>
</v-dialog>
<!-- Permission Selector Dialog -->
<v-dialog v:visible="showPermissionSelector" width="600px" :title="$t('role.select_permissions')">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="search-box">
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
<input type="text" v:value="permissionSearchQuery" :placeholder="$t('role.search_permissions')">
</div>
<div class="available-permission-list">
<div v-for="p in filteredAvailablePermissions" class="available-permission-item">
<input type="checkbox" :checked="isPermissionSelected(p.id)" @change="togglePermission(p.id)">
<div class="permission-info">
<div class="permission-id">{{ p.permission_id }}</div>
<div class="permission-scope">{{ p.scope }}</div>
</div>
</div>
<div v-if="filteredAvailablePermissions.length === 0" class="empty-state">
<p>{{ $t('role.no_available_permissions') }}</p>
</div>
</div>
</div>
<div vslot="footer">
<v-btn variant="outline" :click="closePermissionSelector">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary" :click="saveRolePermissions">{{ $t('common.save') }}</v-btn>
</div>
</v-dialog>
<!-- User Selector Dialog -->
<v-dialog v:visible="showUserSelector" width="600px" :title="$t('role.select_users')">
<div style="display: flex; flex-direction: column; gap: 16px;">
@ -498,6 +494,13 @@
name: "",
description: ""
};
permission_levels = [
{value: 1, label: $t('permission.level.create') || 'Create'},
{value: 2, label: $t('permission.level.read') || 'Read'},
{value: 4, label: $t('permission.level.write') || 'Write'},
{value: 6, label: $t('permission.level.rw') || 'Read+Write'},
{value: 7, label: $t('permission.level.admin') || 'Admin'},
]
// Detail modal data
showDetailModal = false;
@ -506,10 +509,9 @@
// Permissions data
rolePermissions = [];
showPermissionSelector = false;
permissionSearchQuery = "";
availablePermissions = [];
selectedPermissionIds = [];
showPermForm = false;
permAdding = false;
newPerm = {scope: 'vb', permission_id: '', level: 7};
// Users data
roleUsers = [];
@ -635,94 +637,27 @@
const err = await res.json();
throw new Error(err.message);
}
rolePermissions = res || [];
rolePermissions = await res.json() || [];
} catch (e) {
$message.error(e.message);
}
};
openPermissionSelector = async () => {
selectedPermissionIds = rolePermissions.map(p => p.id);
showPermissionSelector = true;
await loadAvailablePermissions();
};
closePermissionSelector = () => {
showPermissionSelector = false;
permissionSearchQuery = "";
selectedPermissionIds = [];
};
loadAvailablePermissions = async () => {
// Get all permissions from user's own permissions or a dedicated endpoint
// For now, we'll use a mock approach - in real implementation,
// you might need a /api/permissions endpoint to list all available permissions
try {
// This is a placeholder - adjust based on your actual API
const res = await fetch('/api/auth/me');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const me = await res.json();
if (me.permissions) {
// Extract unique permission IDs
const uniquePerms = [];
const seen = new Set();
me.permissions.forEach(p => {
if (!seen.has(p.permission_id)) {
seen.add(p.permission_id);
uniquePerms.push({
id: p.permission_id,
permission_id: p.permission_id,
scope: p.scope,
level: p.level
});
}
});
availablePermissions = uniquePerms;
}
} catch (e) {
// Fallback: use current role permissions as base
availablePermissions = rolePermissions.map(p => ({
id: p.id,
permission_id: p.permission_id,
scope: p.scope,
level: p.level
}));
}
};
filteredAvailablePermissions = () => {
if (!permissionSearchQuery) return availablePermissions;
const query = permissionSearchQuery.toLowerCase();
return availablePermissions.filter(p =>
p.permission_id.toLowerCase().includes(query) ||
p.scope.toLowerCase().includes(query)
);
};
isPermissionSelected = (id) => {
return selectedPermissionIds.includes(id);
};
togglePermission = (id) => {
const idx = selectedPermissionIds.indexOf(id);
if (idx > -1) {
selectedPermissionIds.splice(idx, 1);
} else {
selectedPermissionIds.push(id);
addPermission = async () => {
if (!currentRole || !newPerm.permission_id || !newPerm.scope) {
$message.error('Scope and Permission ID are required');
return;
}
// Trigger reactivity
selectedPermissionIds = [...selectedPermissionIds];
};
saveRolePermissions = async () => {
if (!currentRole) return;
permAdding = true;
try {
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: selectedPermissionIds
method: 'PUT', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
permissions: [{
scope: newPerm.scope,
permission_id: newPerm.permission_id,
level: parseInt(newPerm.level)
}]
})
});
if (res.status !== 200) {
@ -730,23 +665,24 @@
throw new Error(err.message);
}
$message.success($t('role.permissions_updated'));
closePermissionSelector();
showPermForm = false;
newPerm = {scope: 'vb', permission_id: '', level: 7};
loadRolePermissions();
} catch (e) {
$message.error(e.message);
} finally {
permAdding = false;
}
};
removePermission = async (p) => {
if (!currentRole || currentRole.is_system) return;
if (!currentRole) return;
try {
await $message.confirm($t('role.remove_permission_confirm'));
const newPermissions = rolePermissions
.filter(rp => rp.id !== p.id)
.map(rp => rp.id);
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: newPermissions
method: 'PUT', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
remove: [p.id]
})
});
if (res.status !== 200) {
@ -761,48 +697,21 @@
};
formatLevel = (level) => {
const levels = {
1: $t('permission.level.create') || 'Create',
2: $t('permission.level.read') || 'Read',
4: $t('permission.level.write') || 'Write',
6: $t('permission.level.rw') || 'Read+Write',
7: $t('permission.level.admin') || 'Admin'
};
return levels[level] || `Level ${level}`;
const item = permission_levels.find(l => l.value === level);
return item ? item.label : 'Level ' + level;
};
// User functions
loadRoleUsers = async () => {
if (!currentRole) return;
try {
// Get users who have this role
// We need to fetch users and filter by role
const params = new URLSearchParams({page_size: 1000});
let res = await fetch('/api/users?' + params);
const res = await fetch(`/api/roles/${currentRole.id}/users?page=1&page_size=100`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
const allUsers = data.items || [];
// For each user, check if they have this role
const usersWithRole = [];
for (const user of allUsers) {
try {
res = await fetch(`/api/users/${user.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
if (userRoles.some(ur => ur.id === currentRole.id)) {
usersWithRole.push(user);
}
} catch (e) {
// Skip users we can't check
}
}
roleUsers = usersWithRole;
roleUsers = data.items || [];
} catch (e) {
$message.error(e.message);
}
@ -835,8 +744,8 @@
const params = new URLSearchParams({keyword: availableUserSearchQuery, limit: 50});
const res = await fetch('/api/auth/users?' + params);
const json_ = await res.json();
if (json_.code !== 200) throw new Error(json_.message);
availableUsers = json_.data?.items || [];
if (res.status !== 200) throw new Error(json_.message);
availableUsers = json_?.items || [];
} catch (e) {
$message.error(e.message);
}
@ -860,45 +769,15 @@
saveRoleUsers = async () => {
if (!currentRole) return;
try {
// Update each user's roles
const currentUserIds = roleUsers.map(u => u.id);
// Users to add
const toAdd = selectedUserIds.filter(id => !currentUserIds.includes(id));
// Users to remove
const toRemove = currentUserIds.filter(id => !selectedUserIds.includes(id));
let res = null
for (const userId of toAdd) {
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = [...userRoles.map(r => r.id), currentRole.id];
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
for (const userId of toRemove) {
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const res = await fetch(`/api/roles/${currentRole.id}/users`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_ids: selectedUserIds})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
$message.success($t('role.users_updated'));
closeUserSelector();
loadRoleUsers();
@ -911,14 +790,12 @@
if (!currentRole || currentRole.is_system) return;
try {
await $message.confirm($t('role.remove_user_confirm'));
let res = await fetch(`/api/users/${u.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
res = await fetch(`/api/users/${u.id}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
const newIds = roleUsers.filter(ru => ru.id !== u.id).map(ru => ru.id);
const res = await fetch(`/api/roles/${currentRole.id}/users`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_ids: newIds})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);

@ -39,17 +39,12 @@ export default ({ $mod }) => ({
routes: routes,
beforeEnter: async (to, from, next) => {
const isAuth = to.meta && to.meta.auth;
const roles = to.meta && to.meta.roles; // Array of required roles
const vbase = $mod.$auth
if (isAuth) {
if (!vbase.user) {
try {
await vbase.fetchUser();
} catch (e) {
vbase.logout('/login?redirect=' + encodeURIComponent(to.fullPath));
return false;
}
if (!(await vbase.isLogin())) {
vbase.logout();
return false;
}
// Permission Check

@ -42,11 +42,16 @@ class VBase {
this._loadingUserIDs = new Set();
this._resolvedUserIDs = new Set();
this._pendingUserFlush = null;
}
// 初始化鉴权:有用户缓存就验证 + 定时刷新
if (this._user) {
this._ensureAuth();
/** 异步判断是否已登录 */
async isLogin() {
if (!this._user) return false;
if (!this._initDone) {
this._initDone = true;
await this._ensureAuth();
}
return !!this._user;
}
async _ensureAuth() {

Loading…
Cancel
Save