refactor: load RBAC users from API for audit log user filter

Instead of extracting users from log entries, fetch all users
with RBAC roles via dedicated endpoint for complete user list.
This commit is contained in:
Fringg
2026-03-07 06:23:39 +03:00
parent 40722747e3
commit c3cf805827
2 changed files with 32 additions and 32 deletions

View File

@@ -49,6 +49,16 @@ export interface UserRoleAssignment {
user_telegram_id: number | null; user_telegram_id: number | null;
} }
export interface AdminWithRoles {
user_id: number;
telegram_id: number | null;
username: string | null;
first_name: string | null;
last_name: string | null;
email: string | null;
role_names: string[];
}
export interface AssignRolePayload { export interface AssignRolePayload {
user_id: number; user_id: number;
role_id: number; role_id: number;
@@ -167,6 +177,13 @@ export const rbacApi = {
return response.data; return response.data;
}, },
// --- RBAC Users ---
getRbacUsers: async (): Promise<AdminWithRoles[]> => {
const response = await apiClient.get<AdminWithRoles[]>(`${BASE}/users`);
return response.data;
},
// --- Role Users --- // --- Role Users ---
getRoleUsers: async (roleId: number): Promise<UserRoleAssignment[]> => { getRoleUsers: async (roleId: number): Promise<UserRoleAssignment[]> => {

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useMemo, useEffect } from 'react'; import { useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -415,11 +415,6 @@ function LogEntryCard({ entry, isExpanded, onToggle }: LogEntryCardProps) {
// === Main Page === // === Main Page ===
interface KnownUser {
name: string;
email: string | null;
}
export default function AdminAuditLog() { export default function AdminAuditLog() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -440,9 +435,6 @@ export default function AdminAuditLog() {
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
const [exportError, setExportError] = useState<string | null>(null); const [exportError, setExportError] = useState<string | null>(null);
// Known users accumulated from audit log entries
const [knownUsers, setKnownUsers] = useState<Map<number, KnownUser>>(new Map());
// Build query params // Build query params
const queryParams = useMemo((): AuditLogFilters => { const queryParams = useMemo((): AuditLogFilters => {
const params: AuditLogFilters = { const params: AuditLogFilters = {
@@ -480,23 +472,12 @@ export default function AdminAuditLog() {
refetchInterval: autoRefresh ? AUTO_REFRESH_INTERVAL : false, refetchInterval: autoRefresh ? AUTO_REFRESH_INTERVAL : false,
}); });
// Accumulate unique users from audit log entries // RBAC users for filter chips
useEffect(() => { const { data: rbacUsers } = useQuery({
if (!data?.items?.length) return; queryKey: ['rbac-users'],
setKnownUsers((prev) => { queryFn: () => rbacApi.getRbacUsers(),
const next = new Map(prev); staleTime: 5 * 60 * 1000,
let changed = false; });
for (const entry of data.items) {
const name = entry.user_first_name || entry.user_email || `#${entry.user_id}`;
const existing = next.get(entry.user_id);
if (!existing || existing.name !== name || existing.email !== entry.user_email) {
next.set(entry.user_id, { name, email: entry.user_email });
changed = true;
}
}
return changed ? next : prev;
});
}, [data?.items]);
const entries = data?.items ?? []; const entries = data?.items ?? [];
const total = data?.total ?? 0; const total = data?.total ?? 0;
@@ -669,23 +650,25 @@ export default function AdminAuditLog() {
<div className="border-t border-dark-700 p-4"> <div className="border-t border-dark-700 p-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* User filter */} {/* User filter */}
{knownUsers.size > 0 && ( {rbacUsers && rbacUsers.length > 0 && (
<div className="sm:col-span-2 lg:col-span-3"> <div className="sm:col-span-2 lg:col-span-3">
<label className="mb-1 block text-sm font-medium text-dark-300"> <label className="mb-1 block text-sm font-medium text-dark-300">
{t('admin.auditLog.filters.user')} {t('admin.auditLog.filters.user')}
</label> </label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{Array.from(knownUsers.entries()).map(([uid, user]) => { {rbacUsers.map((ru) => {
const isSelected = filters.userId === String(uid); const isSelected = filters.userId === String(ru.user_id);
const displayName =
ru.first_name || ru.email || ru.username || `#${ru.user_id}`;
return ( return (
<button <button
key={uid} key={ru.user_id}
type="button" type="button"
aria-pressed={isSelected} aria-pressed={isSelected}
onClick={() => onClick={() =>
setFilters((prev) => ({ setFilters((prev) => ({
...prev, ...prev,
userId: isSelected ? '' : String(uid), userId: isSelected ? '' : String(ru.user_id),
})) }))
} }
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1 focus-visible:ring-offset-dark-900 ${ className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1 focus-visible:ring-offset-dark-900 ${
@@ -703,7 +686,7 @@ export default function AdminAuditLog() {
> >
{isSelected && '✓'} {isSelected && '✓'}
</span> </span>
<span>{user.name}</span> <span>{displayName}</span>
</button> </button>
); );
})} })}