Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped
227 lines
7.2 KiB
Markdown
227 lines
7.2 KiB
Markdown
# Milestone 10: Admin UI
|
|
|
|
**Goal:** MadBase Studio is a functional admin dashboard for core operations.
|
|
|
|
**Depends on:** M0 (Security), M1 (Foundation), M3 (Auth), M9 (Control Plane)
|
|
|
|
---
|
|
|
|
## 10.1 — Authentication
|
|
|
|
### 10.1.1 Real login form
|
|
|
|
**File:** `web/js/admin.js`
|
|
|
|
Replace the current auth check (hitting `/platform/v1/projects` and checking for 401) with a proper login flow:
|
|
|
|
```javascript
|
|
async login() {
|
|
const resp = await fetch('/platform/v1/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password: this.loginPassword }),
|
|
});
|
|
if (resp.ok) {
|
|
this.isAuthenticated = true;
|
|
this.loginError = '';
|
|
await this.loadDashboard();
|
|
} else {
|
|
this.loginError = 'Invalid password';
|
|
}
|
|
}
|
|
```
|
|
|
|
The server sets an `HttpOnly` session cookie on success (implemented in M0).
|
|
|
|
### 10.1.2 Add logout
|
|
|
|
```javascript
|
|
async logout() {
|
|
await fetch('/platform/v1/logout', { method: 'POST' });
|
|
this.isAuthenticated = false;
|
|
// Clear all reactive state
|
|
}
|
|
```
|
|
|
|
### 10.1.3 CSRF protection
|
|
|
|
Generate a CSRF token on page load, include in all mutation requests:
|
|
|
|
```javascript
|
|
// On page load
|
|
const csrfResp = await fetch('/platform/v1/csrf-token');
|
|
this.csrfToken = (await csrfResp.json()).token;
|
|
|
|
// On mutations
|
|
headers: { 'X-CSRF-Token': this.csrfToken }
|
|
```
|
|
|
|
---
|
|
|
|
## 10.2 — Security Fixes
|
|
|
|
### 10.2.1 Stop sending service role key to browser
|
|
|
|
**File:** `web/js/admin.js` line ~121
|
|
|
|
Remove `fetchAdminConfig()` and the `serviceRoleKey` reactive variable. All admin API calls should use session auth (the `HttpOnly` cookie), not the service role key.
|
|
|
|
Replace storage/data API calls that use the service role key with admin-proxied endpoints:
|
|
```javascript
|
|
// BEFORE
|
|
headers: { 'Authorization': `Bearer ${this.serviceRoleKey}` }
|
|
|
|
// AFTER — use session cookie (automatic with same-origin requests)
|
|
// No Authorization header needed for /platform/v1/* routes
|
|
```
|
|
|
|
### 10.2.2 Bundle CDN dependencies
|
|
|
|
Replace CDN script tags with locally bundled files. Options:
|
|
1. **Simple:** Download Vue, Chart.js, Tailwind to `web/vendor/` and serve statically
|
|
2. **Better:** Add a minimal build step with Vite that bundles everything into `web/dist/`
|
|
|
|
For air-gapped deployments, option 1 is essential.
|
|
|
|
### 10.2.3 Fix Tailwind @apply
|
|
|
|
**File:** `web/css/admin.css`
|
|
|
|
`@apply` directives don't work with CDN Tailwind JIT. Either:
|
|
1. Remove `@apply` and use inline Tailwind classes in the HTML
|
|
2. Or add a build step that processes the CSS with Tailwind CLI
|
|
|
|
---
|
|
|
|
## 10.3 — Missing Views
|
|
|
|
### 10.3.1 Auth management tab
|
|
|
|
Add a view showing:
|
|
- User list with search/filter
|
|
- User detail: email, created_at, confirmed_at, last_sign_in_at, providers
|
|
- Actions: ban/unban, confirm email, delete user, reset password
|
|
|
|
### 10.3.2 Realtime console
|
|
|
|
Add a view showing:
|
|
- Active WebSocket connections count
|
|
- Active channel subscriptions
|
|
- Live event stream (filterable by table/event type)
|
|
- Presence information per channel
|
|
|
|
### 10.3.3 Object deletion in Storage view
|
|
|
|
Add a delete button next to each object in the storage file browser:
|
|
```javascript
|
|
async deleteObject(bucketId, objectName) {
|
|
if (!confirm(`Delete ${objectName}?`)) return;
|
|
await fetch(`/platform/v1/storage/${bucketId}/${objectName}`, { method: 'DELETE' });
|
|
await this.fetchObjects(bucketId);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 10.4 — Usability
|
|
|
|
### 10.4.1 Configurable Grafana URL
|
|
|
|
**File:** `web/admin.html` — Grafana iframe (line ~414)
|
|
|
|
```html
|
|
<!-- BEFORE -->
|
|
<iframe src="http://localhost:3000" ...></iframe>
|
|
|
|
<!-- AFTER -->
|
|
<iframe :src="grafanaUrl" ...></iframe>
|
|
```
|
|
|
|
```javascript
|
|
// In admin.js data
|
|
grafanaUrl: window.MADBASE_GRAFANA_URL || '/grafana',
|
|
```
|
|
|
|
Set via env var or server-rendered config.
|
|
|
|
### 10.4.2 Confirmation dialogs
|
|
|
|
Add `confirm()` before all destructive operations:
|
|
- Delete project
|
|
- Delete user
|
|
- Delete storage object
|
|
- Remove server
|
|
|
|
### 10.4.3 Error handling
|
|
|
|
Add global error display:
|
|
```javascript
|
|
methods: {
|
|
async apiCall(url, options) {
|
|
try {
|
|
const resp = await fetch(url, options);
|
|
if (!resp.ok) {
|
|
const err = await resp.json();
|
|
this.showError(err.error || 'Request failed');
|
|
return null;
|
|
}
|
|
return resp;
|
|
} catch (e) {
|
|
this.showError(e.message);
|
|
return null;
|
|
}
|
|
},
|
|
showError(msg) {
|
|
this.errorMessage = msg;
|
|
setTimeout(() => this.errorMessage = '', 5000);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Completion Requirements
|
|
|
|
This milestone is **not complete** until every item below is satisfied.
|
|
|
|
### 1. Full Test Suite — All Green
|
|
|
|
- [ ] `cargo test --workspace` passes with **zero failures** (backend unchanged, but verify no regressions)
|
|
- [ ] All **pre-existing tests** still pass
|
|
- [ ] **New end-to-end / browser tests** cover the admin UI:
|
|
|
|
| Test | Location | What it validates |
|
|
|------|----------|-------------------|
|
|
| `test_login_success` | `tests/e2e/admin_ui.rs` or Playwright | Correct password → dashboard loads, session cookie set |
|
|
| `test_login_failure` | `tests/e2e/admin_ui.rs` or Playwright | Wrong password → error message shown, no cookie |
|
|
| `test_logout` | `tests/e2e/admin_ui.rs` or Playwright | Logout → redirected to login, session cookie cleared |
|
|
| `test_no_service_key_in_client` | `tests/e2e/admin_ui.rs` or Playwright | Service role key absent from page source and network requests |
|
|
| `test_auth_user_list` | `tests/e2e/admin_ui.rs` or Playwright | Auth tab renders user list from API |
|
|
| `test_auth_user_search` | `tests/e2e/admin_ui.rs` or Playwright | Typing in search filters the user list |
|
|
| `test_storage_delete_object` | `tests/e2e/admin_ui.rs` or Playwright | Delete button removes object; confirm dialog appears first |
|
|
| `test_grafana_iframe_configurable` | `tests/e2e/admin_ui.rs` or Playwright | Iframe `src` matches configured `MADBASE_GRAFANA_URL` |
|
|
| `test_delete_project_confirmation` | `tests/e2e/admin_ui.rs` or Playwright | Delete project requires confirmation dialog |
|
|
| `test_no_cdn_dependencies` | `web/admin.html` (static analysis) | No `<script>` or `<link>` tags referencing external CDN URLs |
|
|
| `test_csrf_token_present` | `tests/e2e/admin_ui.rs` or Playwright | Mutating requests include a CSRF token |
|
|
|
|
### 2. Manual / Visual Verification
|
|
|
|
- [ ] Login with correct password → dashboard loads
|
|
- [ ] Login with wrong password → error message shown
|
|
- [ ] Logout → redirected to login, session cookie cleared
|
|
- [ ] Service role key never appears in browser DevTools (Network, Application tabs)
|
|
- [ ] Auth tab shows user list with working search
|
|
- [ ] Storage tab allows deleting objects
|
|
- [ ] Grafana iframe loads from configured URL
|
|
- [ ] Delete project shows confirmation dialog
|
|
- [ ] Works in air-gapped environment (no CDN dependencies)
|
|
- [ ] Responsive layout works on 1024px and 1440px viewports
|
|
- [ ] Error toast appears on API failures (e.g., network down)
|
|
|
|
### 3. CI Gate
|
|
|
|
- [ ] `cargo test --workspace` green (backend)
|
|
- [ ] E2E tests (Playwright or equivalent) run in CI against a `docker compose up` stack
|
|
- [ ] Static analysis confirms no external CDN references in `web/` HTML/JS files
|
|
- [ ] All destructive API calls in the UI are confirmed via a dialog (code review checklist)
|