157 lines
5.2 KiB
Vue
157 lines
5.2 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h2>Permissions</h2>
|
|
<span class="subtitle">Bitmask permission flags assigned to users via their permissions mask</span>
|
|
</div>
|
|
<NychButton @click="showCreate = true" label="+ New Permission" />
|
|
</div>
|
|
|
|
<div class="table-card">
|
|
<div class="table-card-header">
|
|
<span class="count">{{ permissions.length }} {{ permissions.length === 1 ? 'permission' : 'permissions' }}</span>
|
|
</div>
|
|
<div v-if="loading" class="loading-overlay">
|
|
<div class="loading-spinner"></div>
|
|
<span>Loading…</span>
|
|
</div>
|
|
<template v-else-if="permissions.length"><div class="table-scroll"><table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Bit Value</th>
|
|
<th>Description</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="p in permissions" :key="p.id">
|
|
<td class="id-cell">{{ p.id }}</td>
|
|
<td><code>{{ p.name }}</code></td>
|
|
<td><span class="bit-badge">{{ p.bit_value }}</span></td>
|
|
<td class="desc-cell">{{ p.description ?? '—' }}</td>
|
|
<td class="actions-cell">
|
|
<NychButton size="small" @click="openEdit(p)" label="Edit" />
|
|
<NychButton size="small" severity="danger" @click="deletePermission(p.id)" label="Delete" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table></div></template>
|
|
<div class="empty-state" v-else>
|
|
<span class="empty-icon">⬡</span>
|
|
<span class="empty-label">No permissions defined</span>
|
|
<span class="empty-hint">Define bitmask flags to control fine-grained API access.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<NychDialog v-model:visible="showEdit" :header="`Edit — ${editForm.name}`" :modal="true" :draggable="false" style="width: min(600px, 95vw)">
|
|
<form @submit.prevent="submitEdit" class="dialog-form">
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<NychInputText v-model="editForm.name" fluid />
|
|
</div>
|
|
<div class="field">
|
|
<label>Description</label>
|
|
<NychInputText v-model="editForm.description" fluid />
|
|
</div>
|
|
<NychButton type="submit" fluid label="Save Changes" />
|
|
</form>
|
|
</NychDialog>
|
|
|
|
<NychDialog v-model:visible="showCreate" header="New Permission" :modal="true" :draggable="false" style="width: min(600px, 95vw)">
|
|
<form @submit.prevent="submitCreate" class="dialog-form">
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<NychInputText v-model="form.name" placeholder="PERMISSION_NAME" fluid />
|
|
</div>
|
|
<div class="field">
|
|
<label>Bit value</label>
|
|
<NychInputText v-model="form.bit_value" placeholder="64" fluid />
|
|
<p class="hint">Must be a power of 2 not already in use (1, 2, 4, 8, 16, 32, 64 …)</p>
|
|
</div>
|
|
<div class="field">
|
|
<label>Description</label>
|
|
<NychInputText v-model="form.description" placeholder="What this permission grants" fluid />
|
|
</div>
|
|
<NychButton type="submit" fluid label="Create Permission" />
|
|
</form>
|
|
</NychDialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useAuthStore } from '../../stores/auth'
|
|
|
|
const auth = useAuthStore()
|
|
const permissions = ref<any[]>([])
|
|
const showCreate = ref(false)
|
|
const showEdit = ref(false)
|
|
const editId = ref<number | null>(null)
|
|
const editForm = ref({ name: '', description: '' })
|
|
const form = ref({ name: '', bit_value: '', description: '' })
|
|
const loading = ref(false)
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const res = await fetch('/api/permissions', { headers: auth.authHeaders() })
|
|
permissions.value = await res.json()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function submitCreate() {
|
|
await fetch('/api/permissions', {
|
|
method: 'POST',
|
|
headers: { ...auth.authHeaders(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(form.value),
|
|
})
|
|
showCreate.value = false
|
|
form.value = { name: '', bit_value: '', description: '' }
|
|
load()
|
|
}
|
|
|
|
function openEdit(p: any) {
|
|
editId.value = p.id
|
|
editForm.value = { name: p.name, description: p.description ?? '' }
|
|
showEdit.value = true
|
|
}
|
|
|
|
async function submitEdit() {
|
|
await fetch(`/api/permissions/${editId.value}`, {
|
|
method: 'PUT',
|
|
headers: { ...auth.authHeaders(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(editForm.value),
|
|
})
|
|
showEdit.value = false
|
|
load()
|
|
}
|
|
|
|
async function deletePermission(id: number) {
|
|
if (!confirm('Delete this permission?')) return
|
|
await fetch(`/api/permissions/${id}`, { method: 'DELETE', headers: auth.authHeaders() })
|
|
load()
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.id-cell { color: var(--text-dim); font-family: var(--font-mono); font-size: 0.8rem; }
|
|
.desc-cell { color: var(--text-muted); font-size: 0.85rem; }
|
|
|
|
.bit-badge {
|
|
display: inline-block;
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border-lo);
|
|
color: var(--text-label);
|
|
}
|
|
</style>
|