mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
1525 lines
41 KiB
Markdown
1525 lines
41 KiB
Markdown
# Database Direct Edit System
|
|
|
|
## Overview
|
|
A full-featured database management interface for administrators (admin/superuser roles only) that allows direct CRUD operations on all database tables with advanced spreadsheet-like functionality, comprehensive filtering, sorting, and inline editing capabilities.
|
|
|
|
**Status**: 📋 Planned (Not Yet Implemented)
|
|
|
|
**Target Users**: Administrators and Superusers only
|
|
|
|
**Security Level**: Requires AAL2 (MFA verification)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
1. [Architecture & Security](#architecture--security)
|
|
2. [Core Components](#core-components)
|
|
3. [Feature Specifications](#feature-specifications)
|
|
4. [Database Requirements](#database-requirements)
|
|
5. [Implementation Roadmap](#implementation-roadmap)
|
|
6. [Dependencies](#dependencies)
|
|
7. [Safety & UX Guidelines](#safety--ux-guidelines)
|
|
|
|
---
|
|
|
|
## Architecture & Security
|
|
|
|
### Access Control
|
|
- **Role Restriction**: Only `admin` and `superuser` roles can access
|
|
- **AAL2 Enforcement**: All database operations require MFA verification via `useSuperuserGuard()`
|
|
- **Audit Logging**: Every modification logged to `admin_audit_log`
|
|
- **Warning Banner**: Display risk disclaimer about direct database access
|
|
- **Read-Only Mode**: Toggle to prevent accidental edits
|
|
|
|
### Route Structure
|
|
```
|
|
/admin/database # Main database browser (table list)
|
|
/admin/database/:tableName # Spreadsheet editor for specific table
|
|
```
|
|
|
|
### Navigation
|
|
- Add "Database Editor" link to AdminSidebar
|
|
- Icon: `Database` from lucide-react
|
|
- Position: Below "User Management"
|
|
- Visibility: Superuser only (`isSuperuser()`)
|
|
|
|
---
|
|
|
|
## Core Components
|
|
|
|
### File Structure
|
|
```
|
|
src/
|
|
├── pages/admin/
|
|
│ └── AdminDatabase.tsx # Main page with routing
|
|
│
|
|
├── components/admin/database/
|
|
│ ├── index.ts # Barrel exports
|
|
│ ├── DatabaseTableBrowser.tsx # Table selector & overview
|
|
│ ├── DatabaseTableEditor.tsx # Main spreadsheet editor (TanStack Table)
|
|
│ ├── DatabaseTableFilters.tsx # Advanced filtering UI
|
|
│ ├── DatabaseColumnConfig.tsx # Column visibility/order management
|
|
│ ├── DatabaseRowEditor.tsx # Detailed row editor dialog
|
|
│ ├── DatabaseBulkActions.tsx # Bulk edit/delete operations
|
|
│ ├── DatabaseExportImport.tsx # CSV/JSON export/import
|
|
│ ├── DatabaseSchemaViewer.tsx # Table schema & ERD viewer
|
|
│ ├── DatabaseCellEditors.tsx # Type-specific cell editors
|
|
│ └── types.ts # TypeScript definitions
|
|
│
|
|
├── hooks/
|
|
│ ├── useTableSchema.ts # Fetch table schema from Supabase
|
|
│ ├── useTableData.ts # Fetch/edit table data with optimistic updates
|
|
│ ├── useDatabaseAudit.ts # Audit logging utilities
|
|
│ └── useDatabaseValidation.ts # Validation functions
|
|
│
|
|
└── lib/
|
|
├── database/
|
|
│ ├── cellEditors.tsx # Cell editor component factory
|
|
│ ├── filterFunctions.ts # Custom filter functions per data type
|
|
│ ├── validationRules.ts # Validation rules per column type
|
|
│ └── schemaParser.ts # Parse Supabase schema to table config
|
|
└── utils/
|
|
├── csvExport.ts # CSV export utilities
|
|
└── jsonImport.ts # JSON import/validation
|
|
```
|
|
|
|
---
|
|
|
|
## Feature Specifications
|
|
|
|
### Phase 1: Table Browser & Navigation
|
|
|
|
#### DatabaseTableBrowser Component
|
|
**Purpose**: Display all database tables with metadata and quick navigation
|
|
|
|
**Features**:
|
|
- **Table List Display**:
|
|
- Grid or list view toggle
|
|
- Show table name, row count, size, last modified
|
|
- Search/filter tables by name
|
|
- Sort by name, row count, or date
|
|
|
|
- **Table Categorization**:
|
|
```typescript
|
|
const tableCategories = {
|
|
auth: {
|
|
color: 'red',
|
|
tables: ['profiles', 'user_roles', 'user_preferences', 'user_sessions'],
|
|
icon: 'Shield'
|
|
},
|
|
content: {
|
|
color: 'yellow',
|
|
tables: ['parks', 'rides', 'companies', 'ride_models', 'locations'],
|
|
icon: 'MapPin'
|
|
},
|
|
submissions: {
|
|
color: 'green',
|
|
tables: ['content_submissions', 'submission_items', 'photo_submissions'],
|
|
icon: 'FileText'
|
|
},
|
|
moderation: {
|
|
color: 'blue',
|
|
tables: ['reports', 'admin_audit_log', 'review_reports'],
|
|
icon: 'Flag'
|
|
},
|
|
versioning: {
|
|
color: 'purple',
|
|
tables: ['park_versions', 'ride_versions', 'company_versions'],
|
|
icon: 'History'
|
|
},
|
|
system: {
|
|
color: 'gray',
|
|
tables: ['admin_settings', 'notification_logs', 'rate_limits'],
|
|
icon: 'Settings'
|
|
}
|
|
}
|
|
```
|
|
|
|
- **Quick Stats Cards**:
|
|
- Total tables count
|
|
- Total rows across all tables
|
|
- Database size
|
|
- Last modified timestamp
|
|
|
|
- **Table Actions**:
|
|
- Click table to open editor
|
|
- Quick view schema (hover tooltip)
|
|
- Export table data
|
|
- View recent changes (from versions tables)
|
|
|
|
**Data Fetching**:
|
|
```typescript
|
|
// Use Supabase RPC to get table metadata
|
|
const { data: tables } = await supabase.rpc('get_table_metadata')
|
|
|
|
interface TableMetadata {
|
|
table_name: string;
|
|
row_count: bigint;
|
|
total_size: string;
|
|
last_modified: string;
|
|
category?: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 2: Spreadsheet-Style Table Editor
|
|
|
|
#### DatabaseTableEditor Component
|
|
**Core Technology**: TanStack Table v8 with advanced features
|
|
|
|
#### 2.1 Data Grid Display
|
|
|
|
**Features**:
|
|
- **Virtual Scrolling**: Handle 10,000+ rows efficiently using `@tanstack/react-virtual`
|
|
- **Sticky Headers**: Column headers remain visible on scroll
|
|
- **Row Numbers**: Display row index in first column
|
|
- **Column Resizing**: Drag column borders to resize
|
|
- **Column Reordering**: Drag-drop column headers to reorder
|
|
- **Row Selection**:
|
|
- Single click to select row
|
|
- Shift+Click for range selection
|
|
- Ctrl+Click for multi-selection
|
|
- Checkbox column for bulk selection
|
|
- **Zebra Striping**: Alternate row colors for readability
|
|
- **Cell Highlighting**: Hover effect on cells
|
|
- **Responsive Design**: Horizontal scroll on smaller screens
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const table = useReactTable({
|
|
data: tableData,
|
|
columns: dynamicColumns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
enableRowSelection: true,
|
|
enableMultiSort: true,
|
|
enableColumnResizing: true,
|
|
columnResizeMode: 'onChange',
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
pagination
|
|
}
|
|
})
|
|
```
|
|
|
|
#### 2.2 Inline Editing
|
|
|
|
**Cell Editor Types** (auto-detected from column type):
|
|
|
|
| Data Type | Editor Component | Features |
|
|
|-----------|------------------|----------|
|
|
| `text`, `varchar` | `<Input>` | Text input with validation |
|
|
| `integer`, `bigint`, `numeric` | `<Input type="number">` | Number input with min/max |
|
|
| `boolean` | `<Switch>` | Toggle switch |
|
|
| `timestamp`, `date` | `<DatePicker>` | Calendar popup with time |
|
|
| `uuid` | `<Select>` or `<Input>` | FK lookup or manual entry |
|
|
| `jsonb`, `json` | `<Textarea>` + JSON validator | Syntax highlighting |
|
|
| `enum` | `<Select>` | Dropdown with allowed values |
|
|
| `text[]` | `<TagInput>` | Multi-value chips |
|
|
|
|
**Edit Flow**:
|
|
1. Click cell → Enter edit mode
|
|
2. Show appropriate editor component
|
|
3. Type/select new value
|
|
4. Press Enter to save OR Escape to cancel
|
|
5. Optimistic update → DB save → Rollback on error
|
|
6. Visual feedback: Yellow (editing) → Green (saved) → Red (error)
|
|
|
|
**Validation**:
|
|
- Check NOT NULL constraints
|
|
- Validate data type compatibility
|
|
- Verify foreign key references exist
|
|
- Check max length for strings
|
|
- Validate JSON syntax
|
|
- Custom validation rules per table
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const EditableCell = ({ getValue, row, column, table }) => {
|
|
const initialValue = getValue()
|
|
const [value, setValue] = useState(initialValue)
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
|
|
const handleSave = async () => {
|
|
const meta = table.options.meta
|
|
await meta?.updateData(row.index, column.id, value)
|
|
setIsEditing(false)
|
|
}
|
|
|
|
return (
|
|
<div onClick={() => setIsEditing(true)}>
|
|
{isEditing ? (
|
|
<Input
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
onBlur={handleSave}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleSave()
|
|
if (e.key === 'Escape') setIsEditing(false)
|
|
}}
|
|
/>
|
|
) : (
|
|
<span>{value}</span>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
#### 2.3 Advanced Filtering
|
|
|
|
**Filter Panel** (collapsible left sidebar):
|
|
|
|
**Filter Builder UI**:
|
|
- Add filter button
|
|
- Each filter row contains:
|
|
- Column selector dropdown
|
|
- Operator selector (based on column type)
|
|
- Value input (type-appropriate)
|
|
- Remove filter button
|
|
- AND/OR logic toggle between filters
|
|
- Clear all filters button
|
|
- Save filter preset (with name)
|
|
- Load saved preset
|
|
|
|
**Filter Types by Data Type**
|
|
|
|
**Text Columns**:
|
|
- Contains
|
|
- Equals
|
|
- Starts with
|
|
- Ends with
|
|
- Regex match
|
|
- Is empty / Is not empty
|
|
|
|
**Number Columns**:
|
|
- Equals
|
|
- Greater than (>)
|
|
- Less than (<)
|
|
- Greater than or equal (>=)
|
|
- Less than or equal (<=)
|
|
- Between (min, max)
|
|
- Is null / Is not null
|
|
|
|
**Date Columns**:
|
|
- Before
|
|
- After
|
|
- Between (start, end)
|
|
- Is null
|
|
- Last N days
|
|
- Next N days
|
|
|
|
**Boolean Columns**:
|
|
- Is true
|
|
- Is false
|
|
- Is null
|
|
|
|
**UUID Columns**:
|
|
- Equals
|
|
- In list (comma-separated)
|
|
- Is null
|
|
|
|
**JSON Columns**:
|
|
- Path exists
|
|
- Path equals value
|
|
- Contains key
|
|
|
|
**Quick Filters Bar** (above table):
|
|
- Chips showing active filters
|
|
- Click chip to edit filter
|
|
- X to remove filter
|
|
- "5 filters active" counter
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
interface ColumnFilter {
|
|
id: string; // column name
|
|
operator: FilterOperator;
|
|
value: any;
|
|
logicGate?: 'AND' | 'OR';
|
|
}
|
|
|
|
const filterFunctions = {
|
|
text: {
|
|
contains: (row, id, filterValue) =>
|
|
row.getValue(id)?.toLowerCase().includes(filterValue.toLowerCase()),
|
|
equals: (row, id, filterValue) =>
|
|
row.getValue(id) === filterValue,
|
|
startsWith: (row, id, filterValue) =>
|
|
row.getValue(id)?.startsWith(filterValue),
|
|
// ... more
|
|
},
|
|
number: {
|
|
equals: (row, id, filterValue) => row.getValue(id) === filterValue,
|
|
gt: (row, id, filterValue) => row.getValue(id) > filterValue,
|
|
// ... more
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2.4 Sorting & Pagination
|
|
|
|
**Sorting**:
|
|
- Click column header to sort ascending
|
|
- Click again to sort descending
|
|
- Click third time to remove sort
|
|
- Shift+Click to add multi-column sort
|
|
- Sort indicators: ↑ (asc), ↓ (desc)
|
|
- Multi-sort shows order numbers (1, 2, 3)
|
|
- Custom sort functions for complex types (JSON, arrays)
|
|
|
|
**Pagination**:
|
|
- Rows per page selector: 25, 50, 100, 500, 1000
|
|
- Page navigation: First, Prev, Next, Last
|
|
- Page input: "Jump to page X"
|
|
- Display: "Showing 1-25 of 1,234 rows"
|
|
- "Load all" button (with warning for tables > 1000 rows)
|
|
- Virtual scrolling for large datasets
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const [pagination, setPagination] = useState({
|
|
pageIndex: 0,
|
|
pageSize: 50
|
|
})
|
|
|
|
const table = useReactTable({
|
|
// ...
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
onPaginationChange: setPagination,
|
|
state: { pagination }
|
|
})
|
|
|
|
// Pagination controls
|
|
<div className="flex items-center gap-2">
|
|
<Button onClick={() => table.setPageIndex(0)}>First</Button>
|
|
<Button onClick={() => table.previousPage()}>Prev</Button>
|
|
<Input
|
|
type="number"
|
|
value={table.getState().pagination.pageIndex + 1}
|
|
onChange={(e) => table.setPageIndex(Number(e.target.value) - 1)}
|
|
/>
|
|
<span>of {table.getPageCount()}</span>
|
|
<Button onClick={() => table.nextPage()}>Next</Button>
|
|
<Button onClick={() => table.setPageIndex(table.getPageCount() - 1)}>Last</Button>
|
|
</div>
|
|
```
|
|
|
|
#### 2.5 Column Management
|
|
|
|
**Column Config Panel** (collapsible right sidebar):
|
|
|
|
**Features**:
|
|
- **Column Visibility**:
|
|
- Checkbox list of all columns
|
|
- Toggle individual columns on/off
|
|
- "Show all" / "Hide all" buttons
|
|
- Search column names
|
|
|
|
- **Column Reordering**:
|
|
- Drag-drop list to reorder columns
|
|
- Visual drag handle icon
|
|
- Smooth reorder animation
|
|
|
|
- **Column Pinning**:
|
|
- Pin columns to left (e.g., ID, name)
|
|
- Pin columns to right (e.g., actions)
|
|
- Unpin button
|
|
|
|
- **Column Presets**:
|
|
- Save current column layout with name
|
|
- Load saved layouts
|
|
- Default layouts per table:
|
|
- "Essential" (ID, name, status, dates)
|
|
- "All" (show everything)
|
|
- "Metadata" (created_at, updated_at, created_by)
|
|
|
|
- **Reset to Default**:
|
|
- Restore original column order/visibility
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const [columnVisibility, setColumnVisibility] = useState({})
|
|
const [columnOrder, setColumnOrder] = useState([])
|
|
const [columnPinning, setColumnPinning] = useState({ left: ['id'], right: [] })
|
|
|
|
const table = useReactTable({
|
|
// ...
|
|
state: {
|
|
columnVisibility,
|
|
columnOrder,
|
|
columnPinning
|
|
},
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onColumnOrderChange: setColumnOrder,
|
|
onColumnPinningChange: setColumnPinning
|
|
})
|
|
```
|
|
|
|
#### 2.6 Search & Navigation
|
|
|
|
**Global Search**:
|
|
- Search input above table
|
|
- Searches all visible columns
|
|
- Highlight matching text in cells
|
|
- "X matches found" counter
|
|
- Next/Prev navigation between matches
|
|
- Clear search button
|
|
|
|
**Quick Jump**:
|
|
- "Jump to row by ID" input
|
|
- Autofocus and scroll to row
|
|
- Highlight target row
|
|
|
|
**Keyboard Navigation**:
|
|
- Arrow keys: Navigate cells
|
|
- Tab: Move to next editable cell
|
|
- Enter: Edit current cell / Save edit
|
|
- Escape: Cancel edit
|
|
- Page Up/Down: Scroll page
|
|
- Home/End: Jump to first/last row
|
|
- Ctrl+F: Focus search
|
|
- Ctrl+C: Copy cell value
|
|
- Del: Delete selected rows (with confirmation)
|
|
|
|
---
|
|
|
|
### Phase 3: Advanced Features
|
|
|
|
#### 3.1 Bulk Operations (DatabaseBulkActions)
|
|
|
|
**Features**:
|
|
- **Row Selection**: Checkboxes in first column
|
|
- **Selection Actions**:
|
|
- Select all on page
|
|
- Select all matching filter
|
|
- Deselect all
|
|
- Invert selection
|
|
|
|
- **Bulk Edit**:
|
|
- Select column to edit
|
|
- Set value for all selected rows
|
|
- Preview changes before applying
|
|
- Apply button with confirmation
|
|
|
|
- **Bulk Delete**:
|
|
- Delete all selected rows
|
|
- Show count: "Delete 23 rows?"
|
|
- Confirmation dialog with row preview
|
|
- Cannot be undone warning
|
|
|
|
- **Batch Update**:
|
|
- Apply formula/function to column
|
|
- Examples:
|
|
- Set status to 'approved' for all pending
|
|
- Increment version number by 1
|
|
- Update timestamps to NOW()
|
|
|
|
- **Action History**:
|
|
- Show last 10 bulk operations
|
|
- Display: operation, row count, timestamp, user
|
|
- Undo last operation (if safe and within 5 minutes)
|
|
|
|
**UI**:
|
|
```tsx
|
|
<div className="bulk-actions-toolbar">
|
|
<span>{selectedRows.length} rows selected</span>
|
|
<Button onClick={handleBulkEdit}>Edit Selected</Button>
|
|
<Button onClick={handleBulkDelete} variant="destructive">Delete Selected</Button>
|
|
<Button onClick={handleDeselectAll} variant="ghost">Deselect All</Button>
|
|
</div>
|
|
```
|
|
|
|
#### 3.2 Export/Import (DatabaseExportImport)
|
|
|
|
**Export Features**:
|
|
- **Export Formats**:
|
|
- CSV (with headers)
|
|
- JSON (array of objects)
|
|
- SQL INSERT statements
|
|
|
|
- **Export Scope**:
|
|
- All rows
|
|
- Filtered rows only
|
|
- Selected rows only
|
|
- Visible columns only
|
|
|
|
- **Export Options**:
|
|
- Include/exclude NULL values
|
|
- Date format selection
|
|
- JSON pretty print
|
|
- SQL: Include CREATE TABLE statement
|
|
|
|
- **Batch Export**:
|
|
- Export all tables as ZIP file
|
|
- Progress indicator for large exports
|
|
|
|
**Import Features**:
|
|
- **Import Formats**:
|
|
- CSV (with column mapping UI)
|
|
- JSON (validate schema first)
|
|
|
|
- **Import Modes**:
|
|
- Insert (add new rows)
|
|
- Update (match by ID)
|
|
- Upsert (insert or update)
|
|
|
|
- **Import Validation**:
|
|
- Dry-run preview
|
|
- Show errors before import
|
|
- Skip invalid rows option
|
|
|
|
- **Import Progress**:
|
|
- Progress bar for large files
|
|
- Success/error counts
|
|
- Download error log
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const exportToCSV = (data: any[], columns: Column[]) => {
|
|
const headers = columns.map(col => col.header).join(',')
|
|
const rows = data.map(row =>
|
|
columns.map(col => JSON.stringify(row[col.id] ?? '')).join(',')
|
|
).join('\n')
|
|
|
|
const csv = `${headers}\n${rows}`
|
|
downloadFile(csv, 'table_export.csv', 'text/csv')
|
|
}
|
|
|
|
const importFromCSV = async (file: File) => {
|
|
const parsed = Papa.parse(file, { header: true })
|
|
|
|
// Validate schema
|
|
const validationErrors = validateImportData(parsed.data, tableSchema)
|
|
if (validationErrors.length > 0) {
|
|
showErrorDialog(validationErrors)
|
|
return
|
|
}
|
|
|
|
// Insert rows
|
|
for (const row of parsed.data) {
|
|
await supabase.from(tableName).insert(row)
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.3 Schema Viewer (DatabaseSchemaViewer)
|
|
|
|
**Features**:
|
|
- **Table Structure Display**:
|
|
- Column list with details:
|
|
- Name
|
|
- Data type
|
|
- Nullable (✓/✗)
|
|
- Default value
|
|
- Max length (for text)
|
|
- Primary key indicator (🔑)
|
|
- Foreign key indicators (→ target_table)
|
|
- Unique constraint indicators
|
|
- Index indicators (📊)
|
|
|
|
- **Relationships View**:
|
|
- Visual diagram (ERD style)
|
|
- Show foreign key relationships
|
|
- Click table to navigate
|
|
- Zoom/pan controls
|
|
|
|
- **Constraints**:
|
|
- Primary keys
|
|
- Foreign keys (with ON DELETE/UPDATE actions)
|
|
- Unique constraints
|
|
- Check constraints
|
|
- Indexes (btree, gin, gist)
|
|
|
|
- **RLS Policies** (read-only):
|
|
- List all policies for table
|
|
- Show policy name, operation, USING clause
|
|
- Syntax highlighting for SQL
|
|
|
|
- **SQL DDL View**:
|
|
- Show CREATE TABLE statement
|
|
- Show CREATE INDEX statements
|
|
- Show RLS policies
|
|
- Copy to clipboard button
|
|
- Syntax highlighting
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const SchemaViewer = ({ tableName }: { tableName: string }) => {
|
|
const { data: schema } = useTableSchema(tableName)
|
|
|
|
return (
|
|
<Tabs>
|
|
<TabsList>
|
|
<TabsTrigger value="columns">Columns</TabsTrigger>
|
|
<TabsTrigger value="relationships">Relationships</TabsTrigger>
|
|
<TabsTrigger value="constraints">Constraints</TabsTrigger>
|
|
<TabsTrigger value="rls">RLS Policies</TabsTrigger>
|
|
<TabsTrigger value="sql">SQL DDL</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="columns">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Nullable</TableHead>
|
|
<TableHead>Default</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{schema.columns.map(col => (
|
|
<TableRow key={col.name}>
|
|
<TableCell>
|
|
{col.isPrimaryKey && '🔑 '}
|
|
{col.name}
|
|
</TableCell>
|
|
<TableCell>{col.type}</TableCell>
|
|
<TableCell>{col.isNullable ? '✓' : '✗'}</TableCell>
|
|
<TableCell><code>{col.default}</code></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TabsContent>
|
|
|
|
{/* Other tabs */}
|
|
</Tabs>
|
|
)
|
|
}
|
|
```
|
|
|
|
#### 3.4 Row Editor Dialog (DatabaseRowEditor)
|
|
|
|
**Purpose**: Detailed form-based editor for complex edits
|
|
|
|
**Features**:
|
|
- **Trigger**: Double-click row to open
|
|
- **Layout**: Vertical form with all columns
|
|
- **Field Types**:
|
|
- Text: `<Textarea>` for long text
|
|
- JSON: Syntax-highlighted editor with validation
|
|
- Foreign Keys: Searchable `<Combobox>` with preview
|
|
- Images: Preview thumbnail + URL input
|
|
- Markdown: Split view (editor + preview)
|
|
- Arrays: Chip input for multi-value
|
|
- Timestamps: Calendar + time picker
|
|
|
|
- **Actions**:
|
|
- Save: Update row
|
|
- Save & Close: Update and return to table
|
|
- Clone Row: Duplicate with new ID
|
|
- Delete Row: With confirmation
|
|
- Cancel: Discard changes
|
|
|
|
- **Validation**:
|
|
- Real-time field validation
|
|
- Show error messages inline
|
|
- Disable save until valid
|
|
|
|
- **Metadata Section**:
|
|
- Show created_at, updated_at, created_by
|
|
- Show version history (if versioned table)
|
|
- Link to audit log entries
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Row: {rowId}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSave}>
|
|
{columns.map(col => (
|
|
<div key={col.name} className="space-y-2">
|
|
<Label>{col.name}</Label>
|
|
{renderFieldEditor(col, formData[col.name], handleChange)}
|
|
{errors[col.name] && (
|
|
<p className="text-sm text-destructive">{errors[col.name]}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<DialogFooter>
|
|
<Button variant="ghost" onClick={() => setIsOpen(false)}>Cancel</Button>
|
|
<Button variant="outline" onClick={handleClone}>Clone Row</Button>
|
|
<Button variant="destructive" onClick={handleDelete}>Delete</Button>
|
|
<Button type="submit">Save</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
```
|
|
|
|
#### 3.5 Real-time Updates
|
|
|
|
**Features**:
|
|
- **Realtime Subscriptions**:
|
|
- Subscribe to table changes via Supabase Realtime
|
|
- Listen for INSERT, UPDATE, DELETE events
|
|
- Show notification when another admin edits
|
|
|
|
- **Conflict Detection**:
|
|
- Detect concurrent edits on same row
|
|
- Show warning: "User X is editing this row"
|
|
- Lock row to prevent conflicts (optional)
|
|
|
|
- **Auto-refresh**:
|
|
- Toggle auto-refresh on/off
|
|
- Refresh interval: 10s, 30s, 60s
|
|
- Manual refresh button
|
|
|
|
- **Change Notifications**:
|
|
- Toast notification: "Table updated by Admin (3 rows changed)"
|
|
- Click to refresh table
|
|
- Dismiss notification
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
useEffect(() => {
|
|
const channel = supabase
|
|
.channel(`table:${tableName}`)
|
|
.on('postgres_changes',
|
|
{
|
|
event: '*',
|
|
schema: 'public',
|
|
table: tableName
|
|
},
|
|
(payload) => {
|
|
toast.info(`Table updated: ${payload.eventType}`)
|
|
refetch() // Refetch table data
|
|
}
|
|
)
|
|
.subscribe()
|
|
|
|
return () => {
|
|
supabase.removeChannel(channel)
|
|
}
|
|
}, [tableName])
|
|
```
|
|
|
|
#### 3.6 Audit Trail Integration
|
|
|
|
**Features**:
|
|
- **Automatic Logging**:
|
|
- Every edit logged to `admin_audit_log`
|
|
- Log: admin_user_id, table, row_id, action, old_values, new_values, timestamp
|
|
|
|
- **Recent Changes Panel** (sidebar):
|
|
- Show last 20 changes on current table
|
|
- Display: timestamp, user, action, affected columns
|
|
- Click to view full details
|
|
|
|
- **Change Details Dialog**:
|
|
- Show before/after diff
|
|
- Highlight changed fields
|
|
- Show user who made change
|
|
- Link to row in table
|
|
|
|
- **Revert Functionality**:
|
|
- Revert single change (if safe)
|
|
- Check for dependent changes
|
|
- Confirmation dialog
|
|
- Cannot revert if related data deleted
|
|
|
|
**Implementation**:
|
|
```tsx
|
|
const logDatabaseEdit = async (
|
|
tableName: string,
|
|
rowId: string,
|
|
operation: 'INSERT' | 'UPDATE' | 'DELETE',
|
|
oldValues: any,
|
|
newValues: any
|
|
) => {
|
|
await supabase.rpc('log_database_direct_edit', {
|
|
_table_name: tableName,
|
|
_row_id: rowId,
|
|
_operation: operation,
|
|
_old_values: oldValues,
|
|
_new_values: newValues
|
|
})
|
|
}
|
|
|
|
// In updateData function:
|
|
const handleUpdate = async (rowIndex, columnId, value) => {
|
|
const row = tableData[rowIndex]
|
|
const oldValue = row[columnId]
|
|
|
|
// Update database
|
|
await supabase.from(tableName).update({ [columnId]: value }).eq('id', row.id)
|
|
|
|
// Log audit trail
|
|
await logDatabaseEdit(
|
|
tableName,
|
|
row.id,
|
|
'UPDATE',
|
|
{ [columnId]: oldValue },
|
|
{ [columnId]: value }
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Database Requirements
|
|
|
|
### Supabase RPC Functions
|
|
|
|
#### 1. get_table_metadata()
|
|
Returns metadata for all tables in public schema.
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION get_table_metadata()
|
|
RETURNS TABLE (
|
|
table_name text,
|
|
row_count bigint,
|
|
total_size text,
|
|
last_modified timestamp with time zone
|
|
)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = 'public'
|
|
AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
t.tablename::text as table_name,
|
|
COALESCE(c.reltuples::bigint, 0) as row_count,
|
|
pg_size_pretty(pg_total_relation_size(t.schemaname||'.'||t.tablename)) as total_size,
|
|
COALESCE(
|
|
(SELECT MAX(created_at) FROM information_schema.tables WHERE table_name = t.tablename),
|
|
NOW()
|
|
) as last_modified
|
|
FROM pg_tables t
|
|
LEFT JOIN pg_class c ON c.relname = t.tablename
|
|
WHERE t.schemaname = 'public'
|
|
ORDER BY t.tablename;
|
|
END;
|
|
$$;
|
|
|
|
-- Grant execute to admins only
|
|
REVOKE EXECUTE ON FUNCTION get_table_metadata() FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION get_table_metadata() TO authenticated;
|
|
|
|
-- RLS policy: only admins/superusers can execute
|
|
CREATE POLICY "admin_only_table_metadata" ON ... -- (handled in calling code)
|
|
```
|
|
|
|
#### 2. get_table_schema(table_name text)
|
|
Returns detailed schema information for a specific table.
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION get_table_schema(_table_name text)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = 'public'
|
|
AS $$
|
|
DECLARE
|
|
schema_info jsonb;
|
|
BEGIN
|
|
-- Get column information
|
|
SELECT jsonb_agg(
|
|
jsonb_build_object(
|
|
'name', column_name,
|
|
'type', data_type,
|
|
'isNullable', is_nullable = 'YES',
|
|
'maxLength', character_maximum_length,
|
|
'numericPrecision', numeric_precision,
|
|
'defaultValue', column_default,
|
|
'isPrimaryKey', EXISTS (
|
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
JOIN information_schema.key_column_usage kcu
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
WHERE tc.table_name = _table_name
|
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
AND kcu.column_name = c.column_name
|
|
),
|
|
'foreignKey', (
|
|
SELECT jsonb_build_object(
|
|
'table', ccu.table_name,
|
|
'column', ccu.column_name
|
|
)
|
|
FROM information_schema.table_constraints tc
|
|
JOIN information_schema.key_column_usage kcu
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
JOIN information_schema.constraint_column_usage ccu
|
|
ON ccu.constraint_name = tc.constraint_name
|
|
WHERE tc.table_name = _table_name
|
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
AND kcu.column_name = c.column_name
|
|
LIMIT 1
|
|
),
|
|
'enumValues', (
|
|
SELECT array_agg(enumlabel)
|
|
FROM pg_enum e
|
|
JOIN pg_type t ON t.oid = e.enumtypid
|
|
WHERE t.typname = c.udt_name
|
|
)
|
|
)
|
|
)
|
|
INTO schema_info
|
|
FROM information_schema.columns c
|
|
WHERE table_name = _table_name
|
|
AND table_schema = 'public';
|
|
|
|
RETURN schema_info;
|
|
END;
|
|
$$;
|
|
```
|
|
|
|
#### 3. log_database_direct_edit(...)
|
|
Logs all direct database edits to audit trail.
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION log_database_direct_edit(
|
|
_table_name text,
|
|
_row_id uuid,
|
|
_operation text,
|
|
_old_values jsonb,
|
|
_new_values jsonb
|
|
)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = 'public'
|
|
AS $$
|
|
BEGIN
|
|
INSERT INTO admin_audit_log (
|
|
admin_user_id,
|
|
target_user_id,
|
|
action,
|
|
details
|
|
) VALUES (
|
|
auth.uid(),
|
|
NULL, -- No specific target user for direct DB edits
|
|
'direct_database_edit',
|
|
jsonb_build_object(
|
|
'table', _table_name,
|
|
'row_id', _row_id,
|
|
'operation', _operation,
|
|
'old_values', _old_values,
|
|
'new_values', _new_values,
|
|
'timestamp', NOW()
|
|
)
|
|
);
|
|
END;
|
|
$$;
|
|
```
|
|
|
|
### RLS Policies
|
|
|
|
All tables must enforce AAL2 for admin/superuser direct edits:
|
|
|
|
```sql
|
|
-- Example policy for parks table
|
|
CREATE POLICY "admin_direct_edit_requires_aal2"
|
|
ON parks
|
|
FOR ALL
|
|
USING (
|
|
(
|
|
EXISTS (
|
|
SELECT 1 FROM user_roles
|
|
WHERE user_id = auth.uid()
|
|
AND role IN ('admin', 'superuser')
|
|
)
|
|
)
|
|
AND (
|
|
(auth.jwt() ->> 'aal') = 'aal2'
|
|
OR NOT EXISTS (
|
|
SELECT 1 FROM auth.mfa_factors
|
|
WHERE user_id = auth.uid()
|
|
AND status = 'verified'
|
|
)
|
|
)
|
|
);
|
|
|
|
-- Apply to all tables:
|
|
-- parks, rides, companies, ride_models, locations, profiles, etc.
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Roadmap
|
|
|
|
### Sprint 1: Foundation (Days 1-3)
|
|
**Goal**: Set up routes, navigation, and basic table browser
|
|
|
|
**Tasks**:
|
|
1. ✅ Create `/admin/database` route in router
|
|
2. ✅ Create `AdminDatabase.tsx` page component
|
|
3. ✅ Add "Database Editor" link to AdminSidebar (superuser only)
|
|
4. ✅ Implement access control:
|
|
- Check `isAdmin()` or `isSuperuser()`
|
|
- Enforce AAL2 with `useSuperuserGuard()`
|
|
- Redirect if unauthorized
|
|
5. ✅ Create `DatabaseTableBrowser.tsx` component
|
|
6. ✅ Implement `get_table_metadata()` RPC function
|
|
7. ✅ Fetch and display table list with metadata
|
|
8. ✅ Add table categorization and color coding
|
|
9. ✅ Implement table search/filter
|
|
10. ✅ Add warning banner about direct DB access
|
|
|
|
**Deliverables**:
|
|
- Working `/admin/database` page
|
|
- Table list with categories
|
|
- Navigation from sidebar
|
|
- AAL2 enforcement
|
|
|
|
---
|
|
|
|
### Sprint 2: Core Editor (Days 4-7)
|
|
**Goal**: Build main spreadsheet editor with basic editing
|
|
|
|
**Tasks**:
|
|
1. ✅ Install dependencies: `@tanstack/react-virtual`
|
|
2. ✅ Create `/admin/database/:tableName` route
|
|
3. ✅ Create `DatabaseTableEditor.tsx` component
|
|
4. ✅ Implement `get_table_schema()` RPC function
|
|
5. ✅ Fetch table schema and generate dynamic columns
|
|
6. ✅ Set up TanStack Table with:
|
|
- Core row model
|
|
- Pagination
|
|
- Row selection
|
|
7. ✅ Implement basic inline editing:
|
|
- Text fields
|
|
- Number fields
|
|
- Boolean switches
|
|
8. ✅ Add optimistic updates with rollback
|
|
9. ✅ Implement save/error handling
|
|
10. ✅ Add visual feedback (yellow → green → red)
|
|
11. ✅ Create `log_database_direct_edit()` RPC function
|
|
12. ✅ Integrate audit logging on all edits
|
|
|
|
**Deliverables**:
|
|
- Working table editor page
|
|
- Inline editing for basic types
|
|
- Audit logging functional
|
|
|
|
---
|
|
|
|
### Sprint 3: Advanced Editing (Days 8-10)
|
|
**Goal**: Complete all cell editors, filtering, sorting, column config
|
|
|
|
**Tasks**:
|
|
1. ✅ Implement all cell editor types:
|
|
- Date/Timestamp picker
|
|
- UUID foreign key selector
|
|
- JSON editor with validation
|
|
- Enum dropdown
|
|
- Array/tag input
|
|
2. ✅ Create `DatabaseTableFilters.tsx` component
|
|
3. ✅ Implement filter builder UI
|
|
4. ✅ Add filter functions for all data types
|
|
5. ✅ Integrate TanStack Table filtering
|
|
6. ✅ Add quick filters bar
|
|
7. ✅ Implement saved filter presets
|
|
8. ✅ Add multi-column sorting (Shift+Click)
|
|
9. ✅ Create `DatabaseColumnConfig.tsx` component
|
|
10. ✅ Implement column visibility toggle
|
|
11. ✅ Add column reordering (drag-drop)
|
|
12. ✅ Implement column pinning
|
|
13. ✅ Add column layout presets
|
|
14. ✅ Implement keyboard navigation
|
|
15. ✅ Add global search
|
|
|
|
**Deliverables**:
|
|
- All cell editors working
|
|
- Advanced filtering UI
|
|
- Multi-column sorting
|
|
- Column management panel
|
|
- Keyboard shortcuts
|
|
|
|
---
|
|
|
|
### Sprint 4: Bulk & Export (Days 11-12)
|
|
**Goal**: Add bulk operations and import/export functionality
|
|
|
|
**Tasks**:
|
|
1. ✅ Install dependencies: `papaparse`
|
|
2. ✅ Create `DatabaseBulkActions.tsx` component
|
|
3. ✅ Implement bulk edit functionality
|
|
4. ✅ Implement bulk delete with confirmation
|
|
5. ✅ Add batch update feature
|
|
6. ✅ Create action history with undo
|
|
7. ✅ Create `DatabaseExportImport.tsx` component
|
|
8. ✅ Implement CSV export
|
|
9. ✅ Implement JSON export
|
|
10. ✅ Implement SQL export
|
|
11. ✅ Add export options (scope, format)
|
|
12. ✅ Implement CSV import with validation
|
|
13. ✅ Implement JSON import
|
|
14. ✅ Add import preview/dry-run
|
|
15. ✅ Create batch export (all tables as ZIP)
|
|
|
|
**Deliverables**:
|
|
- Bulk operations working
|
|
- Export in multiple formats
|
|
- Import with validation
|
|
- Action history with undo
|
|
|
|
---
|
|
|
|
### Sprint 5: Polish & Safety (Days 13-14)
|
|
**Goal**: Add schema viewer, safety features, and final polish
|
|
|
|
**Tasks**:
|
|
1. ✅ Create `DatabaseSchemaViewer.tsx` component
|
|
2. ✅ Implement columns view
|
|
3. ✅ Add relationships/ERD view
|
|
4. ✅ Display constraints and indexes
|
|
5. ✅ Show RLS policies (read-only)
|
|
6. ✅ Add SQL DDL view with syntax highlighting
|
|
7. ✅ Implement read-only mode toggle
|
|
8. ✅ Add confirmation dialogs for all destructive actions
|
|
9. ✅ Implement real-time updates (Supabase Realtime)
|
|
10. ✅ Create recent changes panel
|
|
11. ✅ Add change details dialog with diff view
|
|
12. ✅ Implement revert functionality (if safe)
|
|
13. ✅ Add all safety warnings and checks
|
|
14. ✅ Full testing (manual + automated if time)
|
|
15. ✅ Write documentation (this file)
|
|
|
|
**Deliverables**:
|
|
- Complete schema viewer
|
|
- All safety features
|
|
- Real-time updates
|
|
- Full testing
|
|
- Documentation
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
### Required npm Packages
|
|
|
|
```bash
|
|
# Virtual scrolling for large tables
|
|
npm install @tanstack/react-virtual
|
|
|
|
# JSON viewer/editor
|
|
npm install react-json-view-lite
|
|
|
|
# Syntax highlighting
|
|
npm install prismjs
|
|
npm install @types/prismjs --save-dev
|
|
|
|
# CSV parsing/generation
|
|
npm install papaparse
|
|
npm install @types/papaparse --save-dev
|
|
```
|
|
|
|
### Already Installed (✅)
|
|
- `@tanstack/react-table` - Table management
|
|
- `lucide-react` - Icons
|
|
- `react-hook-form` - Form handling
|
|
- `zod` - Validation
|
|
- `date-fns` - Date utilities
|
|
- `sonner` - Toast notifications
|
|
|
|
---
|
|
|
|
## Safety & UX Guidelines
|
|
|
|
### Safety Features Checklist
|
|
|
|
#### Confirmation Dialogs
|
|
- [ ] Delete single row → "Delete this row?"
|
|
- [ ] Delete multiple rows → "Delete X rows? This cannot be undone."
|
|
- [ ] Bulk edit → "Update X rows?"
|
|
- [ ] Drop table → "⚠️ DANGER: Delete entire table?"
|
|
- [ ] Import data → "Import X rows? Existing data may be overwritten."
|
|
|
|
#### Validation
|
|
- [ ] Enforce NOT NULL constraints
|
|
- [ ] Check data type compatibility
|
|
- [ ] Validate foreign key references exist
|
|
- [ ] Check max length for strings
|
|
- [ ] Validate JSON syntax
|
|
- [ ] Prevent orphan records (check dependencies)
|
|
|
|
#### Access Control
|
|
- [ ] AAL2 enforcement on page load
|
|
- [ ] Re-check AAL2 before each destructive operation
|
|
- [ ] Show MFA prompt if session drops to AAL1
|
|
- [ ] Log all operations to audit trail
|
|
|
|
#### Error Handling
|
|
- [ ] Rollback optimistic updates on error
|
|
- [ ] Show clear error messages
|
|
- [ ] Prevent partial updates (use transactions)
|
|
- [ ] Handle foreign key constraint violations gracefully
|
|
|
|
#### Data Integrity
|
|
- [ ] Warn before editing critical tables (auth, profiles)
|
|
- [ ] Prevent editing system columns (id, created_at, etc.)
|
|
- [ ] Check for circular foreign key dependencies
|
|
- [ ] Validate enum values against allowed list
|
|
|
|
---
|
|
|
|
### UX Best Practices
|
|
|
|
#### Loading States
|
|
```tsx
|
|
// Skeleton loader for table
|
|
{isLoading && <TableSkeleton rows={10} cols={8} />}
|
|
|
|
// Cell-level loading (during save)
|
|
{isSaving && <Spinner className="w-4 h-4" />}
|
|
|
|
// Progress bar for bulk operations
|
|
<Progress value={progress} max={total} />
|
|
```
|
|
|
|
#### Error Feedback
|
|
```tsx
|
|
// Toast notifications
|
|
toast.error('Failed to save: Foreign key violation')
|
|
toast.success('Successfully updated 5 rows')
|
|
toast.warning('Table has unsaved changes')
|
|
|
|
// Inline validation
|
|
<Input
|
|
error={errors.email?.message}
|
|
className={errors.email && 'border-destructive'}
|
|
/>
|
|
```
|
|
|
|
#### Keyboard Shortcuts
|
|
| Shortcut | Action |
|
|
|----------|--------|
|
|
| `Ctrl+F` | Focus search |
|
|
| `Ctrl+S` | Save current row |
|
|
| `Ctrl+Z` | Undo last change |
|
|
| `Del` | Delete selected rows |
|
|
| `Ctrl+E` | Toggle edit mode |
|
|
| `Escape` | Cancel edit / Close dialog |
|
|
| `Enter` | Save edit / Submit form |
|
|
| `Arrow keys` | Navigate cells |
|
|
| `Tab` | Move to next cell |
|
|
| `Shift+Tab` | Move to previous cell |
|
|
| `Page Up/Down` | Scroll page |
|
|
| `Home/End` | Jump to first/last row |
|
|
| `?` | Show keyboard shortcuts help |
|
|
|
|
#### Responsive Design
|
|
- Horizontal scroll for tables on mobile
|
|
- Collapsible filter/column panels
|
|
- Touch-friendly cell editors (larger tap targets)
|
|
- Stack filters vertically on small screens
|
|
- Hide less important columns by default on mobile
|
|
|
|
#### Dark Mode Support
|
|
```css
|
|
/* Table grid */
|
|
.table-cell {
|
|
@apply border-border/40;
|
|
@apply hover:bg-accent/50;
|
|
}
|
|
|
|
/* Cell states */
|
|
.cell-editing { @apply bg-yellow-500/20 dark:bg-yellow-500/10; }
|
|
.cell-saved { @apply bg-green-500/20 dark:bg-green-500/10; }
|
|
.cell-error { @apply bg-red-500/20 dark:bg-red-500/10; }
|
|
|
|
/* Syntax highlighting */
|
|
.json-editor {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Authentication & Authorization
|
|
1. **AAL2 Enforcement**: All database operations require MFA verification
|
|
2. **Role-Based Access**: Only admin/superuser roles can access
|
|
3. **Session Monitoring**: Detect if session drops to AAL1 during use
|
|
4. **Token Refresh**: Handle token expiration gracefully
|
|
|
|
### Data Protection
|
|
1. **Audit Logging**: Every edit logged with user, timestamp, before/after values
|
|
2. **RLS Policies**: Enforce row-level security even in admin UI
|
|
3. **Input Sanitization**: Validate all user input before DB writes
|
|
4. **SQL Injection Prevention**: Use parameterized queries (Supabase handles this)
|
|
|
|
### Rate Limiting
|
|
```sql
|
|
-- Prevent abuse: max 1000 edits per admin per hour
|
|
CREATE POLICY "rate_limit_admin_edits"
|
|
ON admin_audit_log
|
|
FOR INSERT
|
|
WITH CHECK (
|
|
(SELECT COUNT(*) FROM admin_audit_log
|
|
WHERE admin_user_id = auth.uid()
|
|
AND created_at > NOW() - INTERVAL '1 hour'
|
|
AND action = 'direct_database_edit') < 1000
|
|
);
|
|
```
|
|
|
|
### Dangerous Operations Warning
|
|
Display prominent warnings for:
|
|
- Editing auth-related tables (profiles, user_roles)
|
|
- Deleting > 100 rows at once
|
|
- Importing > 1000 rows
|
|
- Editing production environment (if detected)
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Manual Testing Checklist
|
|
- [ ] AAL2 enforcement blocks AAL1 users
|
|
- [ ] Non-admin users cannot access page
|
|
- [ ] All table categories display correctly
|
|
- [ ] Table search/filter works
|
|
- [ ] Table editor loads data correctly
|
|
- [ ] Inline editing saves successfully
|
|
- [ ] Optimistic updates rollback on error
|
|
- [ ] All cell editor types work
|
|
- [ ] Filtering works for all data types
|
|
- [ ] Multi-column sorting works
|
|
- [ ] Column visibility toggle works
|
|
- [ ] Column reordering works
|
|
- [ ] Pagination works correctly
|
|
- [ ] Global search highlights matches
|
|
- [ ] Keyboard navigation works
|
|
- [ ] Bulk edit updates multiple rows
|
|
- [ ] Bulk delete confirms and deletes
|
|
- [ ] Export CSV/JSON/SQL works
|
|
- [ ] Import validates and inserts data
|
|
- [ ] Schema viewer displays correctly
|
|
- [ ] Real-time updates show notifications
|
|
- [ ] Audit trail logs all edits
|
|
- [ ] Recent changes panel displays edits
|
|
- [ ] Revert functionality works
|
|
- [ ] Confirmation dialogs show for destructive actions
|
|
- [ ] Read-only mode prevents edits
|
|
- [ ] Dark mode styling looks good
|
|
|
|
### Automated Testing (Future)
|
|
```typescript
|
|
// Example Playwright test
|
|
test('admin can edit table cell', async ({ page }) => {
|
|
await loginAsAdmin(page)
|
|
await page.goto('/admin/database/parks')
|
|
|
|
// Click cell to edit
|
|
await page.click('[data-cell="name-0"]')
|
|
await page.fill('input', 'New Park Name')
|
|
await page.press('input', 'Enter')
|
|
|
|
// Wait for save
|
|
await page.waitForSelector('.cell-saved')
|
|
|
|
// Verify in database
|
|
const park = await getParkByName('New Park Name')
|
|
expect(park).toBeTruthy()
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
### Phase 2 Features (Post-MVP)
|
|
1. **SQL Query Console**:
|
|
- Raw SQL query interface
|
|
- Query history
|
|
- Result set export
|
|
- Query templates/snippets
|
|
|
|
2. **Visual Query Builder**:
|
|
- Drag-drop JOIN builder
|
|
- Visual WHERE clause builder
|
|
- Preview results
|
|
|
|
3. **Scheduled Jobs**:
|
|
- Schedule bulk updates
|
|
- Automated cleanup scripts
|
|
- Email reports
|
|
|
|
4. **Advanced Analytics**:
|
|
- Table usage statistics
|
|
- Most edited tables
|
|
- Edit frequency heatmap
|
|
- Admin activity dashboard
|
|
|
|
5. **Version Control Integration**:
|
|
- Git-like diff view
|
|
- Branch/merge for schema changes
|
|
- Rollback to previous states
|
|
|
|
6. **Collaboration Features**:
|
|
- Live presence indicators (who's viewing)
|
|
- Comments on rows
|
|
- Change proposals (before applying)
|
|
- Team chat in context
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
**Issue**: "AAL2 required" error blocks access
|
|
- **Solution**: Ensure user has MFA enrolled and verified. Prompt to verify MFA if session is at AAL1.
|
|
|
|
**Issue**: Table loads slowly (> 5s)
|
|
- **Solution**: Add pagination, reduce pageSize, add indexes to frequently filtered columns.
|
|
|
|
**Issue**: Foreign key dropdown shows too many options
|
|
- **Solution**: Add search/autocomplete to dropdown, paginate results, show only recent 100.
|
|
|
|
**Issue**: JSON editor loses formatting on save
|
|
- **Solution**: Ensure JSON is pretty-printed before display, preserve whitespace.
|
|
|
|
**Issue**: Real-time updates cause table to jump
|
|
- **Solution**: Use optimistic updates, throttle refresh notifications, show "refresh available" button instead of auto-refresh.
|
|
|
|
**Issue**: Export fails for large tables (> 10k rows)
|
|
- **Solution**: Stream export instead of loading all in memory, show progress bar, split into chunks.
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
- [DATABASE_ARCHITECTURE.md](./DATABASE_ARCHITECTURE.md) - Database schema and RLS policies
|
|
- [ARCHITECTURE_OVERVIEW.md](./ARCHITECTURE_OVERVIEW.md) - System architecture
|
|
- [ADMIN_FEATURES.md](./ADMIN_FEATURES.md) - Other admin features (if exists)
|
|
|
|
---
|
|
|
|
## Change Log
|
|
|
|
| Date | Version | Changes | Author |
|
|
|------|---------|---------|--------|
|
|
| 2025-01-XX | 1.0.0 | Initial documentation | System |
|
|
|
|
---
|
|
|
|
## Approval & Sign-off
|
|
|
|
**Status**: 📋 Planned (awaiting approval)
|
|
|
|
**Reviewed By**: _Pending_
|
|
|
|
**Approved By**: _Pending_
|
|
|
|
**Implementation Start Date**: _TBD_
|
|
|
|
**Expected Completion**: _TBD_ (14 days after start)
|
|
|
|
---
|
|
|
|
*End of Document*
|