mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +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
@@ -0,0 +1,115 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface AuditSystemStatus {
|
||||
enabled: boolean;
|
||||
level: string;
|
||||
retentionDays: number;
|
||||
totalEvents: number;
|
||||
}
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
eventType: string;
|
||||
username: string;
|
||||
ipAddress: string;
|
||||
details: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AuditEventsResponse {
|
||||
events: AuditEvent[];
|
||||
totalEvents: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
labels: string[];
|
||||
values: number[];
|
||||
}
|
||||
|
||||
export interface AuditChartsData {
|
||||
eventsByType: ChartData;
|
||||
eventsByUser: ChartData;
|
||||
eventsOverTime: ChartData;
|
||||
}
|
||||
|
||||
export interface AuditFilters {
|
||||
eventType?: string;
|
||||
username?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
const auditService = {
|
||||
/**
|
||||
* Get audit system status
|
||||
*/
|
||||
async getSystemStatus(): Promise<AuditSystemStatus> {
|
||||
const response = await apiClient.get<any>('/api/v1/proprietary/ui-data/audit-dashboard');
|
||||
const data = response.data;
|
||||
|
||||
// Map V1 response to expected format
|
||||
return {
|
||||
enabled: data.auditEnabled,
|
||||
level: data.auditLevel,
|
||||
retentionDays: data.retentionDays,
|
||||
totalEvents: 0, // Will be fetched separately
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get audit events with pagination and filters
|
||||
*/
|
||||
async getEvents(filters: AuditFilters = {}): Promise<AuditEventsResponse> {
|
||||
const response = await apiClient.get<AuditEventsResponse>('/api/v1/proprietary/ui-data/audit-events', {
|
||||
params: filters,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get chart data for dashboard
|
||||
*/
|
||||
async getChartsData(timePeriod: 'day' | 'week' | 'month' = 'week'): Promise<AuditChartsData> {
|
||||
const response = await apiClient.get<AuditChartsData>('/api/v1/proprietary/ui-data/audit-charts', {
|
||||
params: { period: timePeriod },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Export audit data
|
||||
*/
|
||||
async exportData(
|
||||
format: 'csv' | 'json',
|
||||
filters: AuditFilters = {}
|
||||
): Promise<Blob> {
|
||||
const response = await apiClient.get('/api/v1/proprietary/ui-data/audit-export', {
|
||||
params: { format, ...filters },
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get available event types for filtering
|
||||
*/
|
||||
async getEventTypes(): Promise<string[]> {
|
||||
const response = await apiClient.get<string[]>('/api/v1/proprietary/ui-data/audit-event-types');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of users for filtering
|
||||
*/
|
||||
async getUsers(): Promise<string[]> {
|
||||
const response = await apiClient.get<string[]>('/api/v1/proprietary/ui-data/audit-users');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default auditService;
|
||||
@@ -1,107 +0,0 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
name: string;
|
||||
userCount?: number;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
id: number;
|
||||
username: string;
|
||||
email?: string;
|
||||
roleName: string;
|
||||
enabled: boolean;
|
||||
team?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
lastRequest?: Date | null;
|
||||
}
|
||||
|
||||
export interface TeamDetailsResponse {
|
||||
team: Team;
|
||||
members: TeamMember[];
|
||||
availableUsers: TeamMember[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Team Management Service
|
||||
* Provides functions to interact with team-related backend APIs
|
||||
*/
|
||||
export const teamService = {
|
||||
/**
|
||||
* Get all teams with user counts
|
||||
*/
|
||||
async getTeams(): Promise<Team[]> {
|
||||
const response = await apiClient.get<{ teamsWithCounts: Team[] }>('/api/v1/proprietary/ui-data/teams');
|
||||
return response.data.teamsWithCounts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get team details including members
|
||||
*/
|
||||
async getTeamDetails(teamId: number): Promise<any> {
|
||||
const response = await apiClient.get(`/api/v1/proprietary/ui-data/teams/${teamId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new team
|
||||
*/
|
||||
async createTeam(name: string): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
await apiClient.post('/api/v1/team/create', formData, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename an existing team
|
||||
*/
|
||||
async renameTeam(teamId: number, newName: string): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('teamId', teamId.toString());
|
||||
formData.append('newName', newName);
|
||||
await apiClient.post('/api/v1/team/rename', formData, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a team (only if it has no members)
|
||||
*/
|
||||
async deleteTeam(teamId: number): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('teamId', teamId.toString());
|
||||
await apiClient.post('/api/v1/team/delete', formData, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a user to a team
|
||||
*/
|
||||
async addUserToTeam(teamId: number, userId: number): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('teamId', teamId.toString());
|
||||
formData.append('userId', userId.toString());
|
||||
await apiClient.post('/api/v1/team/addUser', formData, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
|
||||
/**
|
||||
* Move a user to a specific team (used when "removing" from a team - moves to Default)
|
||||
*/
|
||||
async moveUserToTeam(username: string, currentRole: string, teamId: number): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('role', currentRole);
|
||||
formData.append('teamId', teamId.toString());
|
||||
await apiClient.post('/api/v1/user/admin/changeRole', formData, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface EndpointStatistic {
|
||||
endpoint: string;
|
||||
visits: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface EndpointStatisticsResponse {
|
||||
endpoints: EndpointStatistic[];
|
||||
totalEndpoints: number;
|
||||
totalVisits: number;
|
||||
}
|
||||
|
||||
export interface UsageChartData {
|
||||
labels: string[];
|
||||
values: number[];
|
||||
}
|
||||
|
||||
const usageAnalyticsService = {
|
||||
/**
|
||||
* Get endpoint statistics
|
||||
*/
|
||||
async getEndpointStatistics(
|
||||
limit?: number,
|
||||
dataType: 'all' | 'api' | 'ui' = 'all'
|
||||
): Promise<EndpointStatisticsResponse> {
|
||||
const params: Record<string, any> = {};
|
||||
|
||||
if (limit !== undefined) {
|
||||
params.limit = limit;
|
||||
}
|
||||
|
||||
if (dataType !== 'all') {
|
||||
params.dataType = dataType;
|
||||
}
|
||||
|
||||
const response = await apiClient.get<EndpointStatisticsResponse>(
|
||||
'/api/v1/proprietary/ui-data/usage-endpoint-statistics',
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get chart data for endpoint usage
|
||||
*/
|
||||
async getChartData(
|
||||
limit?: number,
|
||||
dataType: 'all' | 'api' | 'ui' = 'all'
|
||||
): Promise<UsageChartData> {
|
||||
const stats = await this.getEndpointStatistics(limit, dataType);
|
||||
|
||||
return {
|
||||
labels: stats.endpoints.map((e) => e.endpoint),
|
||||
values: stats.endpoints.map((e) => e.visits),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default usageAnalyticsService;
|
||||
@@ -31,6 +31,12 @@ export interface AdminSettingsData {
|
||||
roleDetails?: Record<string, string>;
|
||||
teams?: any[];
|
||||
maxPaidUsers?: number;
|
||||
// License information
|
||||
maxAllowedUsers: number;
|
||||
availableSlots: number;
|
||||
grandfatheredUserCount: number;
|
||||
licenseMaxUsers: number;
|
||||
premiumEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
@@ -62,6 +68,35 @@ export interface InviteUsersResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface InviteLinkRequest {
|
||||
email: string;
|
||||
role: string;
|
||||
teamId?: number;
|
||||
expiryHours?: number;
|
||||
sendEmail?: boolean;
|
||||
}
|
||||
|
||||
export interface InviteLinkResponse {
|
||||
token: string;
|
||||
inviteUrl: string;
|
||||
email: string;
|
||||
expiresAt: string;
|
||||
expiryHours: number;
|
||||
emailSent?: boolean;
|
||||
emailError?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface InviteToken {
|
||||
id: number;
|
||||
email: string;
|
||||
role: string;
|
||||
teamId?: number;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User Management Service
|
||||
* Provides functions to interact with user management backend APIs
|
||||
@@ -163,4 +198,60 @@ export const userManagementService = {
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an invite link (admin only)
|
||||
*/
|
||||
async generateInviteLink(data: InviteLinkRequest): Promise<InviteLinkResponse> {
|
||||
const formData = new FormData();
|
||||
// Only append email if it's provided and not empty
|
||||
if (data.email && data.email.trim()) {
|
||||
formData.append('email', data.email);
|
||||
}
|
||||
formData.append('role', data.role);
|
||||
if (data.teamId) {
|
||||
formData.append('teamId', data.teamId.toString());
|
||||
}
|
||||
if (data.expiryHours) {
|
||||
formData.append('expiryHours', data.expiryHours.toString());
|
||||
}
|
||||
if (data.sendEmail !== undefined) {
|
||||
formData.append('sendEmail', data.sendEmail.toString());
|
||||
}
|
||||
|
||||
const response = await apiClient.post<InviteLinkResponse>(
|
||||
'/api/v1/invite/generate',
|
||||
formData,
|
||||
{
|
||||
suppressErrorToast: true,
|
||||
} as any
|
||||
);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of active invite links (admin only)
|
||||
*/
|
||||
async getInviteLinks(): Promise<InviteToken[]> {
|
||||
const response = await apiClient.get<{ invites: InviteToken[] }>('/api/v1/invite/list');
|
||||
return response.data.invites;
|
||||
},
|
||||
|
||||
/**
|
||||
* Revoke an invite link (admin only)
|
||||
*/
|
||||
async revokeInviteLink(inviteId: number): Promise<void> {
|
||||
await apiClient.delete(`/api/v1/invite/revoke/${inviteId}`, {
|
||||
suppressErrorToast: true,
|
||||
} as any);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up expired invite links (admin only)
|
||||
*/
|
||||
async cleanupExpiredInvites(): Promise<{ deletedCount: number }> {
|
||||
const response = await apiClient.post<{ deletedCount: number }>('/api/v1/invite/cleanup');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user