feat: add multi-administrator support with RBAC and fix backup/restore for S3
## Multi-Administrator System
- Add role-based access control (RBAC) with predefined roles (Super Admin, Admin, Editor, Viewer)
- Add granular permissions system for all admin operations
- Add admin user management page with invite functionality
- Add email invitation system for new administrators
- Add permission middleware protecting all admin routes
- Add PermissionGate component for frontend permission checks
- Track event creator (created_by) for audit purposes
## Backup & Restore Fixes
- Fix S3 backup: endpoint URL handling, manifest loading, field name compatibility
- Fix S3 restore: add list-backups endpoint, transform S3 config from frontend format
- Fix PostgreSQL compatibility: add .returning('id') for insert operations
- Fix disk space check: use df command, handle unknown space gracefully
- Fix dry-run validation to not block on warnings
- Fix req.user → req.admin in restore routes
## Database Migrations
- 054: Add roles table with predefined roles
- 055: Add permissions table
- 056: Add role_permissions junction table
- 057: Add role_id to admin_users
- 058: Add admin_invitations table
- 059: Add admin email templates
- 060: Add created_by to events table
## Other Improvements
- Update .gitignore to exclude planning docs and local backup directory
- Remove SQLite database file from tracking
- Add i18n translations for user management (EN/DE)
This commit is contained in:
@@ -8,4 +8,5 @@ export { emailService } from './email.service';
|
||||
export { settingsService } from './settings.service';
|
||||
export { cmsService } from './cms.service';
|
||||
export { notificationsService } from './notifications.service';
|
||||
export { feedbackService } from './feedback.service';
|
||||
export { feedbackService } from './feedback.service';
|
||||
export { userManagementService } from './userManagement.service';
|
||||
@@ -0,0 +1,187 @@
|
||||
import { api } from '../config/api';
|
||||
import type { AdminUser, AdminRole, AdminInvitation } from '../types';
|
||||
|
||||
// Transform snake_case API response to camelCase for frontend
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function transformUser(user: any): AdminUser {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
isActive: user.isActive ?? user.is_active,
|
||||
lastLogin: user.lastLogin ?? user.last_login,
|
||||
lastLoginIp: user.lastLoginIp ?? user.last_login_ip,
|
||||
createdAt: user.createdAt ?? user.created_at,
|
||||
updatedAt: user.updatedAt ?? user.updated_at,
|
||||
roleId: user.roleId ?? user.role_id,
|
||||
roleName: user.roleName ?? user.role_name,
|
||||
roleDisplayName: user.roleDisplayName ?? user.role_display_name,
|
||||
createdByUsername: user.createdByUsername ?? user.created_by_username,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function transformRole(role: any): AdminRole {
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
displayName: role.displayName ?? role.display_name,
|
||||
description: role.description,
|
||||
isSystem: role.isSystem ?? role.is_system,
|
||||
priority: role.priority,
|
||||
};
|
||||
}
|
||||
|
||||
interface GetUsersResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
users: any[];
|
||||
}
|
||||
|
||||
interface GetUserResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
user: any;
|
||||
}
|
||||
|
||||
interface GetRolesResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
roles: any[];
|
||||
}
|
||||
|
||||
interface GetInvitationsResponse {
|
||||
invitations: AdminInvitation[];
|
||||
}
|
||||
|
||||
interface CreateInvitationData {
|
||||
email: string;
|
||||
role_id: number;
|
||||
}
|
||||
|
||||
interface CreateInvitationResponse {
|
||||
invitation: AdminInvitation;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface UpdateUserData {
|
||||
roleId?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
interface UpdateUserResponse {
|
||||
user: AdminUser;
|
||||
}
|
||||
|
||||
interface DeactivateUserResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ResetPasswordResponse {
|
||||
temporaryPassword: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ValidateInvitationResponse {
|
||||
valid: boolean;
|
||||
email: string;
|
||||
roleName: string;
|
||||
invitedBy: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface AcceptInvitationData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface AcceptInvitationResponse {
|
||||
message: string;
|
||||
user: AdminUser;
|
||||
}
|
||||
|
||||
export const userManagementService = {
|
||||
/**
|
||||
* Get all admin users
|
||||
*/
|
||||
async getUsers(): Promise<AdminUser[]> {
|
||||
const response = await api.get<GetUsersResponse>('/admin/users');
|
||||
return response.data.users.map(transformUser);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a single admin user by ID
|
||||
*/
|
||||
async getUser(id: number): Promise<AdminUser> {
|
||||
const response = await api.get<GetUserResponse>(`/admin/users/${id}`);
|
||||
return transformUser(response.data.user);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all available roles
|
||||
*/
|
||||
async getRoles(): Promise<AdminRole[]> {
|
||||
const response = await api.get<GetRolesResponse>('/admin/users/roles');
|
||||
return response.data.roles.map(transformRole);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all pending invitations
|
||||
*/
|
||||
async getInvitations(): Promise<AdminInvitation[]> {
|
||||
const response = await api.get<GetInvitationsResponse>('/admin/users/invitations');
|
||||
return response.data.invitations;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new invitation
|
||||
*/
|
||||
async createInvitation(data: CreateInvitationData): Promise<AdminInvitation> {
|
||||
const response = await api.post<CreateInvitationResponse>('/admin/users/invite', data);
|
||||
return response.data.invitation;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel a pending invitation
|
||||
*/
|
||||
async cancelInvitation(id: number): Promise<void> {
|
||||
await api.delete(`/admin/users/invitations/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an admin user
|
||||
*/
|
||||
async updateUser(id: number, data: UpdateUserData): Promise<AdminUser> {
|
||||
const response = await api.put<UpdateUserResponse>(`/admin/users/${id}`, data);
|
||||
return transformUser(response.data.user);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deactivate an admin user
|
||||
*/
|
||||
async deactivateUser(id: number): Promise<string> {
|
||||
const response = await api.post<DeactivateUserResponse>(`/admin/users/${id}/deactivate`);
|
||||
return response.data.message;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset an admin user's password
|
||||
*/
|
||||
async resetPassword(id: number): Promise<ResetPasswordResponse> {
|
||||
const response = await api.post<ResetPasswordResponse>(`/admin/users/${id}/reset-password`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate an invitation token (public endpoint)
|
||||
*/
|
||||
async validateInvitation(token: string): Promise<ValidateInvitationResponse> {
|
||||
const response = await api.get<ValidateInvitationResponse>(`/invite/${token}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Accept an invitation and create account (public endpoint)
|
||||
*/
|
||||
async acceptInvitation(token: string, data: AcceptInvitationData): Promise<AcceptInvitationResponse> {
|
||||
const response = await api.post<AcceptInvitationResponse>(`/invite/${token}`, data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user