feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
This commit is contained in:
+71
-18
@@ -810,35 +810,57 @@
|
||||
<!-- Admin: Audit Log -->
|
||||
<div x-show="activeSection==='admin-audit'">
|
||||
<h2>Audit Log</h2>
|
||||
<p class="section-desc">Login history and security events.</p>
|
||||
<p class="section-desc">Actions API, changements de permissions et connexions SSO (unifiés).</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center;">
|
||||
<template x-for="s in ['all','api','permissions','sso']" :key="s">
|
||||
<button class="btn" :class="{ 'btn-primary': auditSource===s }" style="font-size:12px;" @click="auditSource=s; loadAuditLogs()" x-text="s"></button>
|
||||
</template>
|
||||
<input type="text" placeholder="actor (user id)" x-model="auditActor" style="font-size:12px;padding:6px 8px;" @keyup.enter="loadAuditLogs()">
|
||||
<input type="text" placeholder="action (LIKE)" x-model="auditAction" style="font-size:12px;padding:6px 8px;" @keyup.enter="loadAuditLogs()">
|
||||
<button class="btn" style="font-size:12px;" @click="loadAuditLogs()">Apply</button>
|
||||
<span style="flex:1;"></span>
|
||||
<a class="btn" style="font-size:12px;" :href="auditCsvUrl()" download>Export CSV</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>User</th>
|
||||
<th>IP Address</th>
|
||||
<th>User Agent</th>
|
||||
<th>Source</th>
|
||||
<th>Actor</th>
|
||||
<th>Action</th>
|
||||
<th>Resource</th>
|
||||
<th>IP</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="e in auditEntries" :key="e.id">
|
||||
<template x-for="e in auditLogs" :key="e.at + '-' + e.source + '-' + e.actor">
|
||||
<tr>
|
||||
<td><span style="font-size:12px;" x-text="new Date(e.logged_at+'Z').toLocaleString()"></span></td>
|
||||
<td>
|
||||
<span style="font-weight:500;" x-text="e.full_name || e.login"></span>
|
||||
<span style="font-size:11px;color:var(--text-dim);display:block;" x-text="e.login"></span>
|
||||
</td>
|
||||
<td><code style="font-size:11px;" x-text="e.ip_address || '—'"></code></td>
|
||||
<td><span style="font-size:11px;color:var(--text-dim);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;" x-text="e.user_agent || '—'"></span></td>
|
||||
<td><span style="font-size:12px;" x-text="new Date(e.at).toLocaleString()"></span></td>
|
||||
<td><span style="font-size:11px;" :class="'audit-src audit-src-'+e.source" x-text="e.source"></span></td>
|
||||
<td><code style="font-size:11px;" x-text="e.actor"></code></td>
|
||||
<td><code style="font-size:11px;" x-text="e.action"></code></td>
|
||||
<td><span style="font-size:11px;" x-text="e.resource"></span></td>
|
||||
<td><code style="font-size:11px;" x-text="e.ip || '—'"></code></td>
|
||||
<td style="font-size:11px;color:var(--text-dim);max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" :title="e.detail" x-text="e.detail || '—'"></td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr x-show="auditEntries.length === 0">
|
||||
<td colspan="4" style="text-align:center;color:var(--text-dim);padding:20px;">No login events yet</td>
|
||||
<tr x-show="!auditLogs.length && !auditLoading">
|
||||
<td colspan="7" style="text-align:center;color:var(--text-dim);padding:20px;">No audit events yet</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:center;margin-top:12px;">
|
||||
<button class="btn" style="font-size:12px;" x-show="auditHasMore" @click="auditOffset+=auditPageSize; loadAuditLogs(false)">Load more</button>
|
||||
</div>
|
||||
<style>
|
||||
.audit-src{display:inline-block;padding:1px 7px;border-radius:8px;font-weight:600;}
|
||||
.audit-src-api{background:rgba(76,154,255,.15);color:#4c9aff;}
|
||||
.audit-src-permissions{background:rgba(255,176,0,.15);color:#ffb000;}
|
||||
.audit-src-sso{background:rgba(0,200,100,.15);color:#00cc66;}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- Admin: Backups -->
|
||||
@@ -1203,6 +1225,7 @@ function settingsInit() {
|
||||
adminUsers: [],
|
||||
adminStats: {},
|
||||
auditEntries: [],
|
||||
auditLogs: [],
|
||||
showCreateUser: false,
|
||||
newUser: {login:'',name:'',email:'',password:'',is_admin:false},
|
||||
editingUser: null,
|
||||
@@ -1394,11 +1417,41 @@ function settingsInit() {
|
||||
},
|
||||
|
||||
async loadAdminAudit() {
|
||||
await this.loadAuditLogs();
|
||||
},
|
||||
|
||||
// ── Unified Audit API (v7.3.0) ──
|
||||
auditSource: 'all',
|
||||
auditActor: '',
|
||||
auditAction: '',
|
||||
auditOffset: 0,
|
||||
auditPageSize: 50,
|
||||
auditLoading: false,
|
||||
auditHasMore: false,
|
||||
async loadAuditLogs(reset) {
|
||||
if (reset !== false) { this.auditOffset = 0; }
|
||||
this.auditLoading = true;
|
||||
try {
|
||||
var r = await this.adminFetch('/api/admin/audit?limit=200');
|
||||
var d = await r.json();
|
||||
this.auditEntries = d.entries || [];
|
||||
} catch(e) { this.auditEntries = []; }
|
||||
const params = new URLSearchParams({ source: this.auditSource, limit: this.auditPageSize, offset: this.auditOffset });
|
||||
if (this.auditActor) params.set('actor', this.auditActor);
|
||||
if (this.auditAction) params.set('action', this.auditAction);
|
||||
const r = await fetch('/api/v2/audit/logs?' + params.toString());
|
||||
if (!r.ok) { throw new Error('HTTP ' + r.status); }
|
||||
const d = await r.json();
|
||||
const logs = d.logs || [];
|
||||
this.auditLogs = (reset !== false) ? logs : this.auditLogs.concat(logs);
|
||||
this.auditHasMore = logs.length >= this.auditPageSize;
|
||||
} catch(e) {
|
||||
if (reset !== false) this.auditLogs = [];
|
||||
this.auditHasMore = false;
|
||||
}
|
||||
this.auditLoading = false;
|
||||
},
|
||||
auditCsvUrl() {
|
||||
const params = new URLSearchParams({ source: this.auditSource, limit: 500, format: 'csv' });
|
||||
if (this.auditActor) params.set('actor', this.auditActor);
|
||||
if (this.auditAction) params.set('action', this.auditAction);
|
||||
return '/api/v2/audit/logs?' + params.toString();
|
||||
},
|
||||
|
||||
async adminCreateUser() {
|
||||
|
||||
Reference in New Issue
Block a user