refactor(09-05): CODE-09 purge — Phase 9 files (WHAT comments removed)
Frontend: AdminLayout, AdminSidebar, AdminOverviewView, AdminUsersView, AdminQuotasView, AdminAiView, AdminAuditView, router/index.js, LoginView, tailwind.config.js, api/admin.js — WHAT labels stripped; WHY constraints preserved (D-16 anchors, security notes, api_key write-only invariant, router guard rationale, D-08/D-09/D-10 decision refs, fetchWithRetry reason). Backend: overview.py was already clean (no WHAT comments present). Frontend: 137 tests pass, build succeeds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3b639b7a72
commit
7ef65de046
@@ -1,10 +1,3 @@
|
|||||||
/**
|
|
||||||
* Admin API — user management, quota, AI config, audit log, daily exports.
|
|
||||||
*
|
|
||||||
* Consumers: AdminUsersView.vue, AdminQuotasView.vue, AdminAiView.vue,
|
|
||||||
* AdminAuditView.vue
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { request, fetchWithRetry } from './utils.js'
|
import { request, fetchWithRetry } from './utils.js'
|
||||||
|
|
||||||
export function adminListUsers() {
|
export function adminListUsers() {
|
||||||
@@ -104,17 +97,8 @@ export function adminListAuditLog({ start, end, user_handle, event_type, page =
|
|||||||
return request(`/api/admin/audit-log?${params}`)
|
return request(`/api/admin/audit-log?${params}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Unlike window.location.href, fetchWithRetry sends the Authorization Bearer header so
|
||||||
* Export the audit log as a CSV file using fetch + Blob URL.
|
// the endpoint can authenticate. Must NOT call res.json() — CSV is text/csv.
|
||||||
*
|
|
||||||
* Unlike window.location.href, this sends the Authorization Bearer header so
|
|
||||||
* the endpoint can authenticate the request (D-13, T-06.2-04-03).
|
|
||||||
*
|
|
||||||
* Refactored to use fetchWithRetry() — the retry boilerplate is consolidated
|
|
||||||
* in utils.js (CODE-08).
|
|
||||||
*
|
|
||||||
* Must NOT call res.json() — CSV is text/csv.
|
|
||||||
*/
|
|
||||||
export async function adminExportAuditLogCsv(params = {}) {
|
export async function adminExportAuditLogCsv(params = {}) {
|
||||||
const searchParams = new URLSearchParams({ format: 'csv' })
|
const searchParams = new URLSearchParams({ format: 'csv' })
|
||||||
if (params.start) searchParams.set('start', params.start)
|
if (params.start) searchParams.set('start', params.start)
|
||||||
@@ -145,14 +129,7 @@ export async function getAdminOverview() {
|
|||||||
return request('/api/admin/overview')
|
return request('/api/admin/overview')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Uses fetchWithRetry() to send the Authorization Bearer header (D-17, T-06.2-04-03).
|
||||||
* Download a specific Celery daily audit export file from MinIO using fetch + Blob URL.
|
|
||||||
*
|
|
||||||
* Uses fetchWithRetry() to send the Authorization Bearer header (D-17, T-06.2-04-03).
|
|
||||||
* Refactored to use fetchWithRetry() — retry logic consolidated in utils.js (CODE-08).
|
|
||||||
*
|
|
||||||
* @param {string} date — YYYY-MM-DD format date string
|
|
||||||
*/
|
|
||||||
export async function adminDownloadDailyExport(date) {
|
export async function adminDownloadDailyExport(date) {
|
||||||
const res = await fetchWithRetry(`/api/admin/audit-log/daily-exports/${date}`)
|
const res = await fetchWithRetry(`/api/admin/audit-log/daily-exports/${date}`)
|
||||||
if (!res.ok) throw new Error(`Download failed: ${res.status}`)
|
if (!res.ok) throw new Error(`Download failed: ${res.status}`)
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
|
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
|
||||||
<!-- Logo -->
|
|
||||||
<div class="px-6 py-5 border-b border-gray-100">
|
<div class="px-6 py-5 border-b border-gray-100">
|
||||||
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1>
|
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1>
|
||||||
<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>
|
<p class="text-xs text-indigo-500 font-semibold mt-0.5">Admin</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nav -->
|
|
||||||
<nav class="flex-1 px-3 py-4 overflow-y-auto">
|
<nav class="flex-1 px-3 py-4 overflow-y-auto">
|
||||||
<!-- Overview -->
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@@ -21,7 +18,6 @@
|
|||||||
Overview
|
Overview
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- Users -->
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/admin/users"
|
to="/admin/users"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@@ -34,7 +30,6 @@
|
|||||||
Users
|
Users
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- Quotas -->
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/admin/quotas"
|
to="/admin/quotas"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@@ -47,7 +42,6 @@
|
|||||||
Quotas
|
Quotas
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- AI Config -->
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/admin/ai"
|
to="/admin/ai"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@@ -60,7 +54,6 @@
|
|||||||
AI Config
|
AI Config
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- Audit Log -->
|
|
||||||
<router-link
|
<router-link
|
||||||
to="/admin/audit"
|
to="/admin/audit"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@@ -74,7 +67,6 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- User identity footer -->
|
|
||||||
<div class="px-3 py-4 border-t border-gray-100">
|
<div class="px-3 py-4 border-t border-gray-100">
|
||||||
<div v-if="authStore.user" class="flex items-center gap-3 px-4 py-3 border-t border-gray-100 mt-2 -mx-3">
|
<div v-if="authStore.user" class="flex items-center gap-3 px-4 py-3 border-t border-gray-100 mt-2 -mx-3">
|
||||||
<div class="bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0">
|
<div class="bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0">
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const routes = [
|
|||||||
{ path: '/document/:id', component: DocumentView },
|
{ path: '/document/:id', component: DocumentView },
|
||||||
{ path: '/settings', component: SettingsView },
|
{ path: '/settings', component: SettingsView },
|
||||||
|
|
||||||
// Phase 2 — public auth routes (no guard)
|
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
component: () => import('../views/auth/LoginView.vue'),
|
component: () => import('../views/auth/LoginView.vue'),
|
||||||
@@ -37,7 +36,6 @@ const routes = [
|
|||||||
meta: { public: true, layout: 'auth' },
|
meta: { public: true, layout: 'auth' },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Phase 2 — authenticated routes
|
|
||||||
{ path: '/account', redirect: '/settings' },
|
{ path: '/account', redirect: '/settings' },
|
||||||
|
|
||||||
// Admin panel — standalone route subtree with AdminLayout as the route component.
|
// Admin panel — standalone route subtree with AdminLayout as the route component.
|
||||||
@@ -56,7 +54,6 @@ const routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cloud storage overview and folder browser
|
|
||||||
{
|
{
|
||||||
path: '/cloud',
|
path: '/cloud',
|
||||||
name: 'cloud',
|
name: 'cloud',
|
||||||
@@ -70,7 +67,6 @@ const routes = [
|
|||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Phase 4 — folder and sharing routes
|
|
||||||
{
|
{
|
||||||
path: '/folders/:folderId',
|
path: '/folders/:folderId',
|
||||||
name: 'folder',
|
name: 'folder',
|
||||||
@@ -96,7 +92,6 @@ const router = createRouter({
|
|||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
// Step 1: ensure access token is present for non-public routes.
|
|
||||||
if (!to.meta.public && !authStore.accessToken) {
|
if (!to.meta.public && !authStore.accessToken) {
|
||||||
try {
|
try {
|
||||||
await authStore.refresh()
|
await authStore.refresh()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- ── System AI Providers (Global) ───────────────────────────────────── -->
|
|
||||||
<section class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
|
<section class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
<h2 class="text-sm font-semibold text-gray-900">System AI Providers (Global)</h2>
|
<h2 class="text-sm font-semibold text-gray-900">System AI Providers (Global)</h2>
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
|
||||||
<div v-if="loadingSystem" class="p-6 text-center">
|
<div v-if="loadingSystem" class="p-6 text-center">
|
||||||
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
||||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||||
@@ -17,19 +15,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System error banner -->
|
|
||||||
<div v-if="systemError" class="mx-6 mt-4 px-4 py-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
<div v-if="systemError" class="mx-6 mt-4 px-4 py-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||||
{{ systemError }}
|
{{ systemError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Provider accordion list -->
|
|
||||||
<div v-if="!loadingSystem" class="divide-y divide-gray-100">
|
<div v-if="!loadingSystem" class="divide-y divide-gray-100">
|
||||||
<div
|
<div
|
||||||
v-for="prov in systemProviders"
|
v-for="prov in systemProviders"
|
||||||
:key="prov.provider_id"
|
:key="prov.provider_id"
|
||||||
:class="['transition-colors', prov.is_active ? 'border-l-4 border-l-green-500' : '']"
|
:class="['transition-colors', prov.is_active ? 'border-l-4 border-l-green-500' : '']"
|
||||||
>
|
>
|
||||||
<!-- Accordion header -->
|
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center justify-between px-6 py-3 text-left hover:bg-gray-50 transition-colors"
|
class="w-full flex items-center justify-between px-6 py-3 text-left hover:bg-gray-50 transition-colors"
|
||||||
@click="toggleProvider(prov.provider_id)"
|
@click="toggleProvider(prov.provider_id)"
|
||||||
@@ -49,9 +44,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Accordion body -->
|
|
||||||
<div v-if="openProviderId === prov.provider_id" class="px-6 pb-5 pt-2 bg-gray-50 space-y-3">
|
<div v-if="openProviderId === prov.provider_id" class="px-6 pb-5 pt-2 bg-gray-50 space-y-3">
|
||||||
<!-- API Key -->
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">API Key</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">API Key</label>
|
||||||
<input
|
<input
|
||||||
@@ -63,7 +56,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Base URL -->
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Base URL</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">Base URL</label>
|
||||||
<input
|
<input
|
||||||
@@ -74,7 +66,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model name — searchable dropdown populated from provider's /models endpoint -->
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Model Name</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">Model Name</label>
|
||||||
<SearchableModelSelect
|
<SearchableModelSelect
|
||||||
@@ -84,7 +75,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Context chars -->
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Context Characters</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">Context Characters</label>
|
||||||
<input
|
<input
|
||||||
@@ -97,9 +87,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
|
||||||
<div class="flex items-center gap-2 pt-1">
|
<div class="flex items-center gap-2 pt-1">
|
||||||
<!-- Set Active -->
|
|
||||||
<button
|
<button
|
||||||
@click="saveSystemProvider(prov.provider_id, { activate: true })"
|
@click="saveSystemProvider(prov.provider_id, { activate: true })"
|
||||||
:disabled="savingProvider === prov.provider_id || prov.is_active"
|
:disabled="savingProvider === prov.provider_id || prov.is_active"
|
||||||
@@ -108,7 +96,6 @@
|
|||||||
Set Active
|
Set Active
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Save -->
|
|
||||||
<button
|
<button
|
||||||
@click="saveSystemProvider(prov.provider_id)"
|
@click="saveSystemProvider(prov.provider_id)"
|
||||||
:disabled="savingProvider === prov.provider_id"
|
:disabled="savingProvider === prov.provider_id"
|
||||||
@@ -118,7 +105,6 @@
|
|||||||
{{ savingProvider === prov.provider_id ? 'Saving…' : 'Save' }}
|
{{ savingProvider === prov.provider_id ? 'Saving…' : 'Save' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Test Connection -->
|
|
||||||
<button
|
<button
|
||||||
@click="runTestConnection(prov.provider_id)"
|
@click="runTestConnection(prov.provider_id)"
|
||||||
:disabled="testingProvider === prov.provider_id || savingProvider === prov.provider_id"
|
:disabled="testingProvider === prov.provider_id || savingProvider === prov.provider_id"
|
||||||
@@ -131,7 +117,6 @@
|
|||||||
{{ testingProvider === prov.provider_id ? 'Testing…' : 'Test Connection' }}
|
{{ testingProvider === prov.provider_id ? 'Testing…' : 'Test Connection' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Test result badge -->
|
|
||||||
<span
|
<span
|
||||||
v-if="testResults[prov.provider_id] === 'ok'"
|
v-if="testResults[prov.provider_id] === 'ok'"
|
||||||
class="text-xs bg-green-100 text-green-700 font-semibold px-2 py-1 rounded-full"
|
class="text-xs bg-green-100 text-green-700 font-semibold px-2 py-1 rounded-full"
|
||||||
@@ -148,7 +133,6 @@
|
|||||||
|
|
||||||
<!-- ── Per-user AI provider assignment (existing — DO NOT MODIFY) ─────── -->
|
<!-- ── Per-user AI provider assignment (existing — DO NOT MODIFY) ─────── -->
|
||||||
|
|
||||||
<!-- Loading state -->
|
|
||||||
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||||
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
||||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||||
@@ -156,13 +140,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div v-else-if="users.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
<div v-else-if="users.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
||||||
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI config table -->
|
|
||||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -216,7 +198,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error message -->
|
|
||||||
<p v-if="loadError" class="mt-3 text-sm text-red-600">{{ loadError }}</p>
|
<p v-if="loadError" class="mt-3 text-sm text-red-600">{{ loadError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -227,7 +208,6 @@ import * as api from '../../api/client.js'
|
|||||||
import { getAiConfig, saveAiConfig, testAiConnection } from '../../api/client.js'
|
import { getAiConfig, saveAiConfig, testAiConnection } from '../../api/client.js'
|
||||||
import SearchableModelSelect from '../../components/ui/SearchableModelSelect.vue'
|
import SearchableModelSelect from '../../components/ui/SearchableModelSelect.vue'
|
||||||
|
|
||||||
// ── Per-provider default lookup (mirrors backend PROVIDER_DEFAULTS) ───────────
|
|
||||||
|
|
||||||
const _PROVIDER_DEFAULTS = {
|
const _PROVIDER_DEFAULTS = {
|
||||||
openai: { base_url: null, model: 'gpt-4o', context_chars: 120000 },
|
openai: { base_url: null, model: 'gpt-4o', context_chars: 120000 },
|
||||||
@@ -246,7 +226,6 @@ function providerDefaultBaseUrl(pid) { return _PROVIDER_DEFAULTS[pid]?.base_url
|
|||||||
function providerDefaultModel(pid) { return _PROVIDER_DEFAULTS[pid]?.model || '' }
|
function providerDefaultModel(pid) { return _PROVIDER_DEFAULTS[pid]?.model || '' }
|
||||||
function providerDefaultContextChars(pid) { return _PROVIDER_DEFAULTS[pid]?.context_chars || 8000 }
|
function providerDefaultContextChars(pid) { return _PROVIDER_DEFAULTS[pid]?.context_chars || 8000 }
|
||||||
|
|
||||||
// ── System provider state ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const systemProviders = ref([])
|
const systemProviders = ref([])
|
||||||
const loadingSystem = ref(false)
|
const loadingSystem = ref(false)
|
||||||
@@ -263,7 +242,7 @@ function toggleProvider(pid) {
|
|||||||
|
|
||||||
function _initForm(prov) {
|
function _initForm(prov) {
|
||||||
formByProvider[prov.provider_id] = {
|
formByProvider[prov.provider_id] = {
|
||||||
api_key: '', // write-only: never pre-filled from server
|
api_key: '', // write-only: never pre-filled from server
|
||||||
base_url: prov.base_url || '',
|
base_url: prov.base_url || '',
|
||||||
model_name: prov.model_name || '',
|
model_name: prov.model_name || '',
|
||||||
context_chars: prov.context_chars || 0,
|
context_chars: prov.context_chars || 0,
|
||||||
@@ -291,7 +270,6 @@ async function saveSystemProvider(providerId, opts = {}) {
|
|||||||
const form = formByProvider[providerId] || {}
|
const form = formByProvider[providerId] || {}
|
||||||
const body = { provider_id: providerId }
|
const body = { provider_id: providerId }
|
||||||
|
|
||||||
// Only include fields the admin actually modified (api_key only if user typed something)
|
|
||||||
if (form.api_key !== '') body.api_key = form.api_key
|
if (form.api_key !== '') body.api_key = form.api_key
|
||||||
if (form.base_url !== '') body.base_url = form.base_url
|
if (form.base_url !== '') body.base_url = form.base_url
|
||||||
if (form.model_name !== '') body.model_name = form.model_name
|
if (form.model_name !== '') body.model_name = form.model_name
|
||||||
@@ -312,7 +290,6 @@ async function runTestConnection(providerId) {
|
|||||||
testingProvider.value = providerId
|
testingProvider.value = providerId
|
||||||
testResults[providerId] = null
|
testResults[providerId] = null
|
||||||
const form = formByProvider[providerId] || {}
|
const form = formByProvider[providerId] || {}
|
||||||
// Pass unsaved form values so admins can test credentials before saving
|
|
||||||
const overrides = {}
|
const overrides = {}
|
||||||
if (form.api_key) overrides.api_key = form.api_key
|
if (form.api_key) overrides.api_key = form.api_key
|
||||||
if (form.base_url) overrides.base_url = form.base_url
|
if (form.base_url) overrides.base_url = form.base_url
|
||||||
@@ -328,7 +305,6 @@ async function runTestConnection(providerId) {
|
|||||||
setTimeout(() => { testResults[providerId] = null }, 3000)
|
setTimeout(() => { testResults[providerId] = null }, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-user assignment state (existing — DO NOT MODIFY) ──────────────────────
|
|
||||||
|
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -336,7 +312,6 @@ const loadError = ref(null)
|
|||||||
const savingId = ref(null)
|
const savingId = ref(null)
|
||||||
const savedId = ref(null)
|
const savedId = ref(null)
|
||||||
|
|
||||||
// Per-user config state: { [userId]: { provider, model } }
|
|
||||||
const configs = reactive({})
|
const configs = reactive({})
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
@@ -367,7 +342,6 @@ async function saveConfig(userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Load system providers AND per-user list in parallel
|
|
||||||
loadSystemProviders()
|
loadSystemProviders()
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -375,7 +349,6 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
const data = await api.adminListUsers()
|
const data = await api.adminListUsers()
|
||||||
users.value = data.items || []
|
users.value = data.items || []
|
||||||
// Initialize per-user config state
|
|
||||||
for (const user of users.value) {
|
for (const user of users.value) {
|
||||||
configs[user.id] = {
|
configs[user.id] = {
|
||||||
provider: user.ai_provider || '',
|
provider: user.ai_provider || '',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Filter bar -->
|
|
||||||
<div class="flex flex-wrap gap-3 mb-4 items-end">
|
<div class="flex flex-wrap gap-3 mb-4 items-end">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-500 mb-1">From</label>
|
<label class="block text-xs font-semibold text-gray-500 mb-1">From</label>
|
||||||
@@ -76,7 +75,6 @@
|
|||||||
<p v-if="exportError" class="text-xs text-red-600 self-center">{{ exportError }}</p>
|
<p v-if="exportError" class="text-xs text-red-600 self-center">{{ exportError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading state -->
|
|
||||||
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||||
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
||||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||||
@@ -84,15 +82,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fetch error state -->
|
|
||||||
<p v-else-if="fetchError" class="text-xs text-red-600 mt-1">{{ fetchError }}</p>
|
<p v-else-if="fetchError" class="text-xs text-red-600 mt-1">{{ fetchError }}</p>
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div v-else-if="entries.length === 0" class="text-center py-12 text-gray-400 text-sm">
|
<div v-else-if="entries.length === 0" class="text-center py-12 text-gray-400 text-sm">
|
||||||
No audit log entries match the selected filters.
|
No audit log entries match the selected filters.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table -->
|
|
||||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||||
<table class="w-full text-sm border-collapse">
|
<table class="w-full text-sm border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -131,7 +126,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<div v-if="!loading && entries.length > 0" class="flex items-center justify-between mt-4">
|
<div v-if="!loading && entries.length > 0" class="flex items-center justify-between mt-4">
|
||||||
<button
|
<button
|
||||||
@click="prevPage"
|
@click="prevPage"
|
||||||
@@ -203,7 +197,7 @@ const fetchError = ref(null)
|
|||||||
const exportingCsv = ref(false)
|
const exportingCsv = ref(false)
|
||||||
const exportError = ref(null)
|
const exportError = ref(null)
|
||||||
|
|
||||||
// Daily exports state (D-17)
|
|
||||||
const dailyExports = ref([])
|
const dailyExports = ref([])
|
||||||
const loadingExports = ref(false)
|
const loadingExports = ref(false)
|
||||||
const selectedExportDate = ref('')
|
const selectedExportDate = ref('')
|
||||||
|
|||||||
@@ -2,42 +2,33 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>
|
<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>
|
||||||
|
|
||||||
<!-- Loading -->
|
|
||||||
<div v-if="loading" class="text-gray-500">Loading overview…</div>
|
<div v-if="loading" class="text-gray-500">Loading overview…</div>
|
||||||
|
|
||||||
<!-- Error -->
|
|
||||||
<div v-else-if="error" class="text-red-600">{{ error }}</div>
|
<div v-else-if="error" class="text-red-600">{{ error }}</div>
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<template v-else-if="overview">
|
<template v-else-if="overview">
|
||||||
<!-- Stat cards -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
<!-- Users -->
|
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Users</p>
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Users</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">{{ overview.user_count }}</p>
|
<p class="text-2xl font-bold text-gray-900">{{ overview.user_count }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Storage -->
|
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Storage</p>
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Storage</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">{{ formatSize(overview.total_storage_bytes) }}</p>
|
<p class="text-2xl font-bold text-gray-900">{{ formatSize(overview.total_storage_bytes) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processing -->
|
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Processing</p>
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Processing</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.processing ?? 0 }}</p>
|
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.processing ?? 0 }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ready -->
|
|
||||||
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
<div class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Ready</p>
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Ready</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.ready ?? 0 }}</p>
|
<p class="text-2xl font-bold text-gray-900">{{ overview.doc_status?.ready ?? 0 }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent audit table -->
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Recent Activity</h3>
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">Recent Activity</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Loading state -->
|
|
||||||
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||||
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
||||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||||
@@ -8,13 +7,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div v-else-if="rows.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
<div v-else-if="rows.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
||||||
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quotas table -->
|
|
||||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -83,7 +80,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error message -->
|
|
||||||
<p v-if="loadError" class="mt-3 text-sm text-red-600">{{ loadError }}</p>
|
<p v-if="loadError" class="mt-3 text-sm text-red-600">{{ loadError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -129,7 +125,6 @@ async function saveQuota(row) {
|
|||||||
savingId.value = row.id
|
savingId.value = row.id
|
||||||
try {
|
try {
|
||||||
const result = await api.adminUpdateQuota(row.id, newBytes)
|
const result = await api.adminUpdateQuota(row.id, newBytes)
|
||||||
// Update the row in place
|
|
||||||
const idx = rows.value.findIndex(r => r.id === row.id)
|
const idx = rows.value.findIndex(r => r.id === row.id)
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
rows.value[idx] = {
|
rows.value[idx] = {
|
||||||
@@ -139,7 +134,6 @@ async function saveQuota(row) {
|
|||||||
}
|
}
|
||||||
if (result.warning) {
|
if (result.warning) {
|
||||||
editWarning.value = true
|
editWarning.value = true
|
||||||
// Keep edit mode open briefly to show warning, then close
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editingId.value = null
|
editingId.value = null
|
||||||
editWarning.value = false
|
editWarning.value = false
|
||||||
@@ -161,11 +155,9 @@ onMounted(async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
loadError.value = null
|
loadError.value = null
|
||||||
try {
|
try {
|
||||||
// Load users first to get email addresses
|
|
||||||
const usersData = await api.adminListUsers()
|
const usersData = await api.adminListUsers()
|
||||||
const users = usersData.items || []
|
const users = usersData.items || []
|
||||||
|
|
||||||
// Fetch quotas for all users in parallel
|
|
||||||
const quotaResults = await Promise.allSettled(
|
const quotaResults = await Promise.allSettled(
|
||||||
users.map(u => api.adminGetUserQuota(u.id).then(q => ({ ...q, id: u.id, email: u.email })))
|
users.map(u => api.adminGetUserQuota(u.id).then(q => ({ ...q, id: u.id, email: u.email })))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Create user panel (inline above table) -->
|
|
||||||
<div v-if="showCreateForm" class="bg-white border border-gray-200 rounded-xl p-6 mb-4">
|
<div v-if="showCreateForm" class="bg-white border border-gray-200 rounded-xl p-6 mb-4">
|
||||||
<h3 class="text-sm font-semibold text-gray-900 mb-4">Create user</h3>
|
<h3 class="text-sm font-semibold text-gray-900 mb-4">Create user</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@@ -80,7 +79,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table header with Create user button -->
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<p class="text-sm text-gray-500">{{ users.length }} user{{ users.length !== 1 ? 's' : '' }}</p>
|
<p class="text-sm text-gray-500">{{ users.length }} user{{ users.length !== 1 ? 's' : '' }}</p>
|
||||||
<button
|
<button
|
||||||
@@ -92,7 +90,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading state -->
|
|
||||||
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
<div v-if="loading" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||||
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
||||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||||
@@ -100,13 +97,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div v-else-if="users.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
<div v-else-if="users.length === 0" class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||||
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
<h3 class="text-sm font-semibold text-gray-900 mb-1">No users yet</h3>
|
||||||
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
<p class="text-sm text-gray-500">Create the first user account to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Users table -->
|
|
||||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -261,7 +256,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action error message -->
|
|
||||||
<p v-if="actionError" class="mt-3 text-sm text-red-600">{{ actionError }}</p>
|
<p v-if="actionError" class="mt-3 text-sm text-red-600">{{ actionError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -297,13 +291,10 @@ function generateRandomPassword() {
|
|||||||
const special = '!@#$%^&*'
|
const special = '!@#$%^&*'
|
||||||
const charset = upper + lower + digits + special // 64 chars — 256 % 64 === 0, no modulo bias
|
const charset = upper + lower + digits + special // 64 chars — 256 % 64 === 0, no modulo bias
|
||||||
|
|
||||||
// Generate 16 random positions
|
|
||||||
const arr = new Uint8Array(16)
|
const arr = new Uint8Array(16)
|
||||||
crypto.getRandomValues(arr)
|
crypto.getRandomValues(arr)
|
||||||
const chars = Array.from(arr, byte => charset[byte % charset.length])
|
const chars = Array.from(arr, byte => charset[byte % charset.length])
|
||||||
|
|
||||||
// Inject one guaranteed character from each required class at random positions
|
|
||||||
// using four additional random bytes to pick the injection positions.
|
|
||||||
const posArr = new Uint8Array(8)
|
const posArr = new Uint8Array(8)
|
||||||
crypto.getRandomValues(posArr)
|
crypto.getRandomValues(posArr)
|
||||||
const required = [
|
const required = [
|
||||||
@@ -312,11 +303,9 @@ function generateRandomPassword() {
|
|||||||
digits[posArr[2] % digits.length],
|
digits[posArr[2] % digits.length],
|
||||||
special[posArr[3] % special.length],
|
special[posArr[3] % special.length],
|
||||||
]
|
]
|
||||||
// Place each required char at a distinct position (0..3) in the array
|
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
chars[i] = required[i]
|
chars[i] = required[i]
|
||||||
}
|
}
|
||||||
// Shuffle using Fisher-Yates with the last 4 random bytes as seeds
|
|
||||||
for (let i = chars.length - 1; i > 0; i--) {
|
for (let i = chars.length - 1; i > 0; i--) {
|
||||||
const j = posArr[4 + (i % 4)] % (i + 1)
|
const j = posArr[4 + (i % 4)] % (i + 1)
|
||||||
;[chars[i], chars[j]] = [chars[j], chars[i]]
|
;[chars[i], chars[j]] = [chars[j], chars[i]]
|
||||||
@@ -365,7 +354,6 @@ async function submitCreate() {
|
|||||||
password: newUser.password,
|
password: newUser.password,
|
||||||
role: newUser.role,
|
role: newUser.role,
|
||||||
})
|
})
|
||||||
// Prepend the new user with sensible defaults for display
|
|
||||||
users.value.unshift({
|
users.value.unshift({
|
||||||
id: created.id,
|
id: created.id,
|
||||||
handle: created.handle,
|
handle: created.handle,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8 w-full">
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8 w-full">
|
||||||
<!-- Step: password -->
|
|
||||||
<template v-if="step === 'password'">
|
<template v-if="step === 'password'">
|
||||||
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Sign in to DocuVault</h2>
|
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Sign in to DocuVault</h2>
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@
|
|||||||
<label for="remember-me" class="text-sm text-gray-600">Stay signed in for 30 days</label>
|
<label for="remember-me" class="text-sm text-gray-600">Stay signed in for 30 days</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form-level error -->
|
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="error"
|
||||||
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
||||||
@@ -70,7 +68,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Step: TOTP -->
|
|
||||||
<template v-else-if="step === 'totp'">
|
<template v-else-if="step === 'totp'">
|
||||||
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Two-factor authentication</h2>
|
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Two-factor authentication</h2>
|
||||||
<p class="text-sm text-gray-500 mb-6">Enter the 6-digit code from your authenticator app.</p>
|
<p class="text-sm text-gray-500 mb-6">Enter the 6-digit code from your authenticator app.</p>
|
||||||
@@ -88,7 +85,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form-level error -->
|
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="error"
|
||||||
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
||||||
@@ -127,7 +123,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Step: backup code -->
|
|
||||||
<template v-else-if="step === 'backup'">
|
<template v-else-if="step === 'backup'">
|
||||||
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Two-factor authentication</h2>
|
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Two-factor authentication</h2>
|
||||||
<p class="text-sm text-gray-500 mb-6">Enter a backup code.</p>
|
<p class="text-sm text-gray-500 mb-6">Enter a backup code.</p>
|
||||||
@@ -144,7 +139,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form-level error -->
|
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="error"
|
||||||
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
class="p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
|
||||||
@@ -195,7 +189,6 @@ const authStore = useAuthStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// Form state
|
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const totpInput = ref('')
|
const totpInput = ref('')
|
||||||
@@ -204,7 +197,6 @@ const loading = ref(false)
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const rememberMe = ref(false)
|
const rememberMe = ref(false)
|
||||||
|
|
||||||
// Step: 'password' | 'totp' | 'backup'
|
|
||||||
const step = ref('password')
|
const step = ref('password')
|
||||||
|
|
||||||
function resetToPassword() {
|
function resetToPassword() {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
import forms from '@tailwindcss/forms'
|
import forms from '@tailwindcss/forms'
|
||||||
export default {
|
export default {
|
||||||
content: ['./index.html', './src/**/*.{vue,js}'],
|
content: ['./index.html', './src/**/*.{vue,js}'],
|
||||||
|
|||||||
Reference in New Issue
Block a user