Feature - add logs for admin user
This commit is contained in:
@@ -14,6 +14,7 @@ import ResetPasswordPage from './pages/ResetPasswordPage'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
||||
import TemplatesPage from './pages/TemplatesPage'
|
||||
import UsersPage from './pages/UsersPage'
|
||||
import LogsPage from './pages/LogsPage'
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth()
|
||||
@@ -43,6 +44,7 @@ function AppRoutes() {
|
||||
<Route path="/documents/:id" element={<ProtectedRoute><DocumentDetailPage /></ProtectedRoute>} />
|
||||
<Route path="/documents/:id/edit" element={<ProtectedRoute><EditDocumentPage /></ProtectedRoute>} />
|
||||
<Route path="/users" element={<ProtectedRoute><UsersPage /></ProtectedRoute>} />
|
||||
<Route path="/logs" element={<ProtectedRoute><LogsPage /></ProtectedRoute>} />
|
||||
</Routes>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AuditLog,
|
||||
DocumentTemplate,
|
||||
FilledDocument,
|
||||
PreviewResponse,
|
||||
@@ -164,6 +165,15 @@ export const api = {
|
||||
return request<void>(`/users/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
|
||||
async listLogs(params?: { limit?: number; offset?: number; action?: string }): Promise<AuditLog[]> {
|
||||
const search = new URLSearchParams()
|
||||
if (params?.limit != null) search.set('limit', String(params.limit))
|
||||
if (params?.offset != null) search.set('offset', String(params.offset))
|
||||
if (params?.action) search.set('action', params.action)
|
||||
const query = search.toString()
|
||||
return request<AuditLog[]>(`/logs${query ? `?${query}` : ''}`)
|
||||
},
|
||||
|
||||
async listTemplates(): Promise<TemplateListItem[]> {
|
||||
return request<TemplateListItem[]>('/templates')
|
||||
},
|
||||
|
||||
52
frontend/src/components/ConfirmDialog.tsx
Normal file
52
frontend/src/components/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
danger?: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function ConfirmDialog({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
danger = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="dialog-overlay" onClick={onCancel}>
|
||||
<div
|
||||
className="dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 id="confirm-dialog-title" style={{ marginTop: 0 }}>
|
||||
{title}
|
||||
</h2>
|
||||
<p style={{ color: 'var(--muted)', marginBottom: 24 }}>{message}</p>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${danger ? 'btn-danger' : 'btn-primary'}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,9 +21,14 @@ export default function Navbar() {
|
||||
Documents
|
||||
</Link>
|
||||
{user?.role === 'admin' && (
|
||||
<Link to="/users" className={isActive('/users') ? 'active' : ''}>
|
||||
Users
|
||||
</Link>
|
||||
<>
|
||||
<Link to="/users" className={isActive('/users') ? 'active' : ''}>
|
||||
Users
|
||||
</Link>
|
||||
<Link to="/logs" className={isActive('/logs') ? 'active' : ''}>
|
||||
Logs
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{user && (
|
||||
<>
|
||||
|
||||
@@ -378,3 +378,30 @@ textarea {
|
||||
background: #dcfce7;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.log-action {
|
||||
font-size: 0.85rem;
|
||||
background: #e8eef7;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { api, downloadWithAuth } from '../api'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { DocumentTemplate, FilledDocument } from '../types'
|
||||
|
||||
@@ -13,6 +14,7 @@ export default function DocumentDetailPage() {
|
||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
@@ -61,7 +63,7 @@ export default function DocumentDetailPage() {
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!document || !confirm(`Delete document "${document.name}"?`)) return
|
||||
if (!document) return
|
||||
try {
|
||||
await api.deleteDocument(document.id)
|
||||
navigate('/documents')
|
||||
@@ -78,6 +80,16 @@ export default function DocumentDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
title="Delete document"
|
||||
message={`Delete document "${document.name}"? This action cannot be undone.`}
|
||||
confirmLabel="Delete"
|
||||
danger
|
||||
onConfirm={() => void handleDelete()}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1>{document.name}</h1>
|
||||
@@ -105,7 +117,7 @@ export default function DocumentDetailPage() {
|
||||
</Link>
|
||||
)}
|
||||
{canManage && (
|
||||
<button className="btn btn-danger" onClick={handleDelete}>
|
||||
<button className="btn btn-danger" onClick={() => setShowDeleteConfirm(true)}>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { FilledDocument } from '../types'
|
||||
|
||||
@@ -9,6 +10,7 @@ export default function DocumentsPage() {
|
||||
const [documents, setDocuments] = useState<FilledDocument[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [deleteTarget, setDeleteTarget] = useState<FilledDocument | null>(null)
|
||||
|
||||
const load = () => {
|
||||
api.listDocuments()
|
||||
@@ -21,11 +23,12 @@ export default function DocumentsPage() {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
const handleDelete = async (doc: FilledDocument) => {
|
||||
if (!confirm(`Delete document "${doc.name}"?`)) return
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return
|
||||
try {
|
||||
await api.deleteDocument(doc.id)
|
||||
setDocuments((list) => list.filter((d) => d.id !== doc.id))
|
||||
await api.deleteDocument(deleteTarget.id)
|
||||
setDocuments((list) => list.filter((d) => d.id !== deleteTarget.id))
|
||||
setDeleteTarget(null)
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed')
|
||||
}
|
||||
@@ -35,6 +38,20 @@ export default function DocumentsPage() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<ConfirmDialog
|
||||
open={deleteTarget !== null}
|
||||
title="Delete document"
|
||||
message={
|
||||
deleteTarget
|
||||
? `Delete document "${deleteTarget.name}"? This action cannot be undone.`
|
||||
: ''
|
||||
}
|
||||
confirmLabel="Delete"
|
||||
danger
|
||||
onConfirm={() => void handleDelete()}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
<div className="page-header">
|
||||
<h1>Documents</h1>
|
||||
<Link to="/templates" className="btn btn-primary">
|
||||
@@ -79,7 +96,7 @@ export default function DocumentsPage() {
|
||||
{(isAdmin || d.owner_id === user?.id) && (
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDelete(d)}
|
||||
onClick={() => setDeleteTarget(d)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
135
frontend/src/pages/LogsPage.tsx
Normal file
135
frontend/src/pages/LogsPage.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { AuditLog } from '../types'
|
||||
|
||||
function formatAction(action: string): string {
|
||||
return action.replace(/\./g, ' · ').replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
function formatDetails(details: Record<string, unknown> | null): string {
|
||||
if (!details) return '—'
|
||||
const text = JSON.stringify(details, null, 0)
|
||||
return text.length > 120 ? `${text.slice(0, 120)}…` : text
|
||||
}
|
||||
|
||||
export default function LogsPage() {
|
||||
const { user: currentUser } = useAuth()
|
||||
const [logs, setLogs] = useState<AuditLog[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [actionFilter, setActionFilter] = useState('')
|
||||
|
||||
const load = async (action?: string) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
setLogs(await api.listLogs({ action: action || undefined }))
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load logs')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
const handleFilter = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
load(actionFilter.trim() || undefined)
|
||||
}
|
||||
|
||||
if (currentUser?.role !== 'admin') {
|
||||
return <Navigate to="/templates" replace />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="page-header">
|
||||
<h1>Activity Logs</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFilter} className="card" style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<div className="form-group" style={{ marginBottom: 0, flex: '1 1 240px' }}>
|
||||
<label>Filter by action</label>
|
||||
<input
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
placeholder="e.g. user.delete, template.create"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => {
|
||||
setActionFilter('')
|
||||
load()
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div className="card" style={{ overflowX: 'auto' }}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Resource</th>
|
||||
<th>Details</th>
|
||||
<th>IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ textAlign: 'center', color: 'var(--muted)' }}>
|
||||
No log entries found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
logs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ whiteSpace: 'nowrap' }}>
|
||||
{new Date(log.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td>{log.username || '—'}</td>
|
||||
<td>
|
||||
<code className="log-action">{formatAction(log.action)}</code>
|
||||
</td>
|
||||
<td>
|
||||
{log.resource_type
|
||||
? `${log.resource_type}${log.resource_id != null ? ` #${log.resource_id}` : ''}`
|
||||
: '—'}
|
||||
</td>
|
||||
<td>
|
||||
<span className="style-hint" title={JSON.stringify(log.details, null, 2)}>
|
||||
{formatDetails(log.details)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{log.ip_address || '—'}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { api, downloadWithAuth } from '../api'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { TemplateListItem } from '../types'
|
||||
|
||||
@@ -9,6 +10,7 @@ export default function TemplatesPage() {
|
||||
const [templates, setTemplates] = useState<TemplateListItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [deleteTarget, setDeleteTarget] = useState<TemplateListItem | null>(null)
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -24,11 +26,12 @@ export default function TemplatesPage() {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
const handleDelete = async (t: TemplateListItem) => {
|
||||
if (!confirm(`Delete template "${t.name}"?`)) return
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return
|
||||
try {
|
||||
await api.deleteTemplate(t.id)
|
||||
setTemplates((list) => list.filter((x) => x.id !== t.id))
|
||||
await api.deleteTemplate(deleteTarget.id)
|
||||
setTemplates((list) => list.filter((x) => x.id !== deleteTarget.id))
|
||||
setDeleteTarget(null)
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed')
|
||||
}
|
||||
@@ -65,6 +68,20 @@ export default function TemplatesPage() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<ConfirmDialog
|
||||
open={deleteTarget !== null}
|
||||
title="Delete template"
|
||||
message={
|
||||
deleteTarget
|
||||
? `Delete template "${deleteTarget.name}"? This action cannot be undone.`
|
||||
: ''
|
||||
}
|
||||
confirmLabel="Delete"
|
||||
danger
|
||||
onConfirm={() => void handleDelete()}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
<div className="page-header">
|
||||
<h1>Templates</h1>
|
||||
<Link to="/templates/new" className="btn btn-primary">
|
||||
@@ -142,7 +159,7 @@ export default function TemplatesPage() {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleDelete(t)}
|
||||
onClick={() => setDeleteTarget(t)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { User } from '../types'
|
||||
|
||||
type PendingConfirm = {
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel: string
|
||||
danger?: boolean
|
||||
onConfirm: () => Promise<void>
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const { user: currentUser } = useAuth()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
@@ -11,6 +20,7 @@ export default function UsersPage() {
|
||||
const [error, setError] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(null)
|
||||
const [form, setForm] = useState({
|
||||
email: '',
|
||||
username: '',
|
||||
@@ -63,8 +73,7 @@ export default function UsersPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const submitEdit = async () => {
|
||||
if (!editingUser) return
|
||||
setError('')
|
||||
try {
|
||||
@@ -82,22 +91,64 @@ export default function UsersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleActive = async (u: User) => {
|
||||
try {
|
||||
await api.updateUser(u.id, { is_active: !u.is_active })
|
||||
await load()
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Update failed')
|
||||
const handleEdit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!editingUser) return
|
||||
|
||||
if (editForm.is_active !== editingUser.is_active) {
|
||||
const activating = editForm.is_active
|
||||
setPendingConfirm({
|
||||
title: activating ? 'Activate user' : 'Deactivate user',
|
||||
message: activating
|
||||
? `Activate user "${editingUser.username}"? They will be able to sign in again.`
|
||||
: `Deactivate user "${editingUser.username}"? They will no longer be able to sign in.`,
|
||||
confirmLabel: activating ? 'Activate' : 'Deactivate',
|
||||
danger: !activating,
|
||||
onConfirm: submitEdit,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
void submitEdit()
|
||||
}
|
||||
|
||||
const handleDelete = async (u: User) => {
|
||||
if (!confirm(`Delete user "${u.username}"?`)) return
|
||||
const handleToggleActive = (u: User) => {
|
||||
const activating = !u.is_active
|
||||
setPendingConfirm({
|
||||
title: activating ? 'Activate user' : 'Deactivate user',
|
||||
message: activating
|
||||
? `Activate user "${u.username}"? They will be able to sign in again.`
|
||||
: `Deactivate user "${u.username}"? They will no longer be able to sign in.`,
|
||||
confirmLabel: activating ? 'Activate' : 'Deactivate',
|
||||
danger: !activating,
|
||||
onConfirm: async () => {
|
||||
await api.updateUser(u.id, { is_active: !u.is_active })
|
||||
await load()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (u: User) => {
|
||||
setPendingConfirm({
|
||||
title: 'Delete user',
|
||||
message: `Delete user "${u.username}"? This action cannot be undone.`,
|
||||
confirmLabel: 'Delete',
|
||||
danger: true,
|
||||
onConfirm: async () => {
|
||||
await api.deleteUser(u.id)
|
||||
await load()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!pendingConfirm) return
|
||||
const action = pendingConfirm.onConfirm
|
||||
setPendingConfirm(null)
|
||||
try {
|
||||
await api.deleteUser(u.id)
|
||||
await load()
|
||||
await action()
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : 'Delete failed')
|
||||
alert(err instanceof Error ? err.message : 'Action failed')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +158,16 @@ export default function UsersPage() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<ConfirmDialog
|
||||
open={pendingConfirm !== null}
|
||||
title={pendingConfirm?.title ?? ''}
|
||||
message={pendingConfirm?.message ?? ''}
|
||||
confirmLabel={pendingConfirm?.confirmLabel}
|
||||
danger={pendingConfirm?.danger}
|
||||
onConfirm={() => void handleConfirm()}
|
||||
onCancel={() => setPendingConfirm(null)}
|
||||
/>
|
||||
|
||||
<div className="page-header">
|
||||
<h1>User Management</h1>
|
||||
<button className="btn btn-primary" onClick={() => setShowForm(!showForm)}>
|
||||
|
||||
@@ -78,3 +78,15 @@ export interface PreviewResponse {
|
||||
html: string
|
||||
field_data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: number
|
||||
user_id: number | null
|
||||
username: string | null
|
||||
action: string
|
||||
resource_type: string | null
|
||||
resource_id: number | null
|
||||
details: Record<string, unknown> | null
|
||||
ip_address: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/api.ts","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/Navbar.tsx","./src/components/TableRowEditor.tsx","./src/components/VariableField.tsx","./src/context/AuthContext.tsx","./src/pages/ActivatePage.tsx","./src/pages/CreateTemplatePage.tsx","./src/pages/DocumentDetailPage.tsx","./src/pages/DocumentsPage.tsx","./src/pages/EditDocumentPage.tsx","./src/pages/FillTemplatePage.tsx","./src/pages/ForgotPasswordPage.tsx","./src/pages/LoginPage.tsx","./src/pages/RegisterPage.tsx","./src/pages/ResetPasswordPage.tsx","./src/pages/TemplateDetailPage.tsx","./src/pages/TemplatesPage.tsx","./src/pages/UsersPage.tsx"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/api.ts","./src/main.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/ConfirmDialog.tsx","./src/components/Navbar.tsx","./src/components/TableRowEditor.tsx","./src/components/VariableField.tsx","./src/context/AuthContext.tsx","./src/pages/ActivatePage.tsx","./src/pages/CreateTemplatePage.tsx","./src/pages/DocumentDetailPage.tsx","./src/pages/DocumentsPage.tsx","./src/pages/EditDocumentPage.tsx","./src/pages/FillTemplatePage.tsx","./src/pages/ForgotPasswordPage.tsx","./src/pages/LoginPage.tsx","./src/pages/LogsPage.tsx","./src/pages/RegisterPage.tsx","./src/pages/ResetPasswordPage.tsx","./src/pages/TemplateDetailPage.tsx","./src/pages/TemplatesPage.tsx","./src/pages/UsersPage.tsx"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user