mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Add audit system, invite links, and usage analytics (#4749)
# Description of Changes New Features Audit System: Complete audit logging with dashboard, event tracking, and export capabilities Invite Links: Secure invite system with email notifications and expiration Usage Analytics: Endpoint usage statistics and visualization License Management: User counting with grandfathering and license enforcement ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
f5c67a3239
commit
ac3e10eb99
@@ -71,15 +71,15 @@ export function useAdminSettings<T = any>(
|
||||
// Store raw settings (includes _pending if present)
|
||||
setRawSettings(rawData);
|
||||
|
||||
// Extract active settings (without _pending) for delta comparison
|
||||
const { _pending, ...activeOnly } = rawData;
|
||||
setOriginalSettings(activeOnly as T);
|
||||
console.log(`[useAdminSettings:${sectionName}] Original active settings:`, JSON.stringify(activeOnly, null, 2));
|
||||
|
||||
// Merge pending changes into settings for display
|
||||
const mergedSettings = mergePendingSettings(rawData);
|
||||
console.log(`[useAdminSettings:${sectionName}] Merged settings:`, JSON.stringify(mergedSettings, null, 2));
|
||||
|
||||
// Store merged settings as original for delta comparison
|
||||
// This ensures we compare against what the user SAW (with pending), not raw active values
|
||||
setOriginalSettings(mergedSettings as T);
|
||||
console.log(`[useAdminSettings:${sectionName}] Original settings (for comparison):`, JSON.stringify(mergedSettings, null, 2));
|
||||
|
||||
setSettings(mergedSettings as T);
|
||||
} catch (error) {
|
||||
console.error(`[useAdminSettings:${sectionName}] Failed to fetch:`, error);
|
||||
@@ -106,15 +106,46 @@ export function useAdminSettings<T = any>(
|
||||
// Use custom save logic for complex sections
|
||||
const { sectionData, deltaSettings } = saveTransformer(settings);
|
||||
|
||||
// Save section data (with delta applied)
|
||||
const sectionDelta = computeDelta(originalSettings, sectionData);
|
||||
// Get original sectionData using same transformer for fair comparison
|
||||
const { sectionData: originalSectionData } = saveTransformer(originalSettings);
|
||||
|
||||
// Save section data (with delta applied) - compare transformed vs transformed
|
||||
const sectionDelta = computeDelta(originalSectionData, sectionData);
|
||||
if (Object.keys(sectionDelta).length > 0) {
|
||||
await apiClient.put(`/api/v1/admin/settings/section/${sectionName}`, sectionDelta);
|
||||
}
|
||||
|
||||
// Save delta settings if provided
|
||||
// Save delta settings if provided (filter to only changed values)
|
||||
if (deltaSettings && Object.keys(deltaSettings).length > 0) {
|
||||
await apiClient.put('/api/v1/admin/settings', { settings: deltaSettings });
|
||||
// Build deltaSettings from original using same transformer to get correct structure
|
||||
const { deltaSettings: originalDeltaSettings } = saveTransformer(originalSettings);
|
||||
|
||||
console.log(`[useAdminSettings:${sectionName}] Comparing deltaSettings:`, {
|
||||
original: originalDeltaSettings,
|
||||
current: deltaSettings
|
||||
});
|
||||
|
||||
// Compare current vs original deltaSettings (both have same backend paths)
|
||||
const changedDeltaSettings: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(deltaSettings)) {
|
||||
const originalValue = originalDeltaSettings?.[key];
|
||||
|
||||
// Only include if value actually changed
|
||||
if (JSON.stringify(value) !== JSON.stringify(originalValue)) {
|
||||
changedDeltaSettings[key] = value;
|
||||
console.log(`[useAdminSettings:${sectionName}] Delta field changed: ${key}`, {
|
||||
original: originalValue,
|
||||
new: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(changedDeltaSettings).length > 0) {
|
||||
console.log(`[useAdminSettings:${sectionName}] Sending delta settings:`, changedDeltaSettings);
|
||||
await apiClient.put('/api/v1/admin/settings', { settings: changedDeltaSettings });
|
||||
} else {
|
||||
console.log(`[useAdminSettings:${sectionName}] No delta settings changed, skipping`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Simple single-endpoint save with delta
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import auditService, { AuditFilters } from '@app/services/auditService';
|
||||
|
||||
/**
|
||||
* Shared hook for managing audit filters across components
|
||||
*/
|
||||
export function useAuditFilters(initialFilters: Partial<AuditFilters> = {}) {
|
||||
const [eventTypes, setEventTypes] = useState<string[]>([]);
|
||||
const [users, setUsers] = useState<string[]>([]);
|
||||
const [filters, setFilters] = useState<AuditFilters>({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
...initialFilters,
|
||||
});
|
||||
|
||||
// Fetch metadata on mount
|
||||
useEffect(() => {
|
||||
const fetchMetadata = async () => {
|
||||
try {
|
||||
const [types, usersList] = await Promise.all([
|
||||
auditService.getEventTypes(),
|
||||
auditService.getUsers(),
|
||||
]);
|
||||
setEventTypes(types);
|
||||
setUsers(usersList);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch audit metadata:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMetadata();
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = (key: keyof AuditFilters, value: any) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleClearFilters = () => {
|
||||
setFilters({
|
||||
eventType: undefined,
|
||||
username: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
page: initialFilters.page,
|
||||
pageSize: initialFilters.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
filters,
|
||||
setFilters,
|
||||
eventTypes,
|
||||
users,
|
||||
handleFilterChange,
|
||||
handleClearFilters,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user