AuditLogs sayfasında güncellemeler

This commit is contained in:
Sedat ÖZTÜRK 2025-06-16 15:16:27 +03:00
parent 6723ce4e66
commit d9712403f8
15 changed files with 463 additions and 228 deletions

View file

@ -0,0 +1,15 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.AuditLogs;
public class AuditLogActionDto : EntityDto<Guid>
{
public virtual Guid AuditLogId { get; protected set; }
public virtual string ServiceName { get; protected set; }
public virtual string MethodName { get; protected set; }
public virtual string Parameters { get; protected set; }
public virtual DateTime ExecutionTime { get; protected set; }
public virtual int ExecutionDuration { get; protected set; }
}

View file

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.AuditLogging;
using static Kurs.Platform.Data.Seeds.SeedConsts;
namespace Kurs.Platform.AuditLogs;
public interface IAuditLogAppService
: ICrudAppService<AuditLogDto, Guid>
{
}
[Authorize(AppCodes.AuditLogs)]
public class AuditLogAppService
: CrudAppService<AuditLog, AuditLogDto, Guid>
, IAuditLogAppService
{
public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository)
{
}
public override async Task<AuditLogDto> GetAsync(Guid id)
{
var entity = await Repository.GetAsync(id, includeDetails: true);
return await MapToGetOutputDtoAsync(entity);
}
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var auditLogList = await AsyncExecuter.ToListAsync(query);
var entityDtos = new List<AuditLogDto>();
foreach (var item in auditLogList)
{
var dto = await MapToGetListOutputDtoAsync(item);
dto.EntityChangeCount = item.EntityChanges?.Count ?? 0; // null kontrolü artık burada güvenli
entityDtos.Add(dto);
}
return new PagedResultDto<AuditLogDto>(
totalCount,
entityDtos
);
}
// Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
[RemoteService(IsEnabled = false)]
public override Task<AuditLogDto> CreateAsync(AuditLogDto input)
{
return null;
}
[RemoteService(IsEnabled = false)]
public override Task<AuditLogDto> UpdateAsync(Guid id, AuditLogDto input)
{
return null;
}
[RemoteService(IsEnabled = false)]
public override Task DeleteAsync(Guid id)
{
return null;
}
}

View file

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.AuditLogs;
public class AuditLogDto : EntityDto<Guid>
{
public AuditLogDto()
{
Actions = new List<AuditLogActionDto>();
EntityChanges = new List<EntityChangeDto>();
}
public string ApplicationName { get; set; }
public Guid? UserId { get; protected set; }
public string UserName { get; protected set; }
public DateTime ExecutionTime { get; protected set; }
public int ExecutionDuration { get; protected set; }
public string ClientIpAddress { get; protected set; }
public string ClientName { get; protected set; }
public string ClientId { get; set; }
public string CorrelationId { get; set; }
public string BrowserInfo { get; protected set; }
public string HttpMethod { get; protected set; }
public string Url { get; protected set; }
public string Exceptions { get; protected set; }
public string Comments { get; protected set; }
public int? HttpStatusCode { get; set; }
public ICollection<EntityChangeDto> EntityChanges { get; protected set; }
public ICollection<AuditLogActionDto> Actions { get; protected set; }
public int? EntityChangeCount { get; set; }
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
namespace Kurs.Platform.AuditLogs;
public class EntityChangeDto : EntityDto<Guid>
{
public EntityChangeDto()
{
PropertyChanges = new List<EntityPropertyChangeDto>();
}
public virtual Guid AuditLogId { get; protected set; }
public virtual DateTime ChangeTime { get; protected set; }
public virtual EntityChangeType ChangeType { get; protected set; }
public virtual Guid? EntityTenantId { get; protected set; }
public virtual string EntityId { get; protected set; }
public virtual string EntityTypeFullName { get; protected set; }
public virtual ICollection<EntityPropertyChangeDto> PropertyChanges { get; protected set; }
}

View file

@ -0,0 +1,13 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.AuditLogs;
public class EntityPropertyChangeDto : EntityDto<Guid>
{
public virtual Guid EntityChangeId { get; protected set; }
public virtual string NewValue { get; protected set; }
public virtual string OriginalValue { get; protected set; }
public virtual string PropertyName { get; protected set; }
public virtual string PropertyTypeFullName { get; protected set; }
}

View file

@ -0,0 +1,15 @@
using AutoMapper;
using Volo.Abp.AuditLogging;
namespace Kurs.Platform.AuditLogs;
public class LogsAutoMapperProfile : Profile
{
public LogsAutoMapperProfile()
{
CreateMap<AuditLog, AuditLogDto>();
CreateMap<AuditLogAction, AuditLogActionDto>();
CreateMap<EntityChange, EntityChangeDto>();
CreateMap<EntityPropertyChange, EntityPropertyChangeDto>();
}
}

View file

@ -7341,6 +7341,17 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
LoadPanelEnabled = "auto",
LoadPanelText = "Loading..."
}),
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
new CommandColumnDto() {
Hint = "Details",
Text = "Details",
AuthName = AppCodes.AuditLogs,
DialogName = "AuditLogDetail",
DialogParameters = JsonSerializer.Serialize(new {
id = "@Id",
})
},
}),
}
);
@ -7584,183 +7595,6 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
#endregion
#endregion
#region Entity Changes
var listFormEntityChange = await _listFormRepository.InsertAsync(
new ListForm()
{
CultureName = LanguageCodes.En,
ListFormCode = ListFormCodes.EntityChange,
Name = AppCodes.EntityChanges,
Title = AppCodes.EntityChanges,
DataSourceCode = SeedConsts.DataSources.DefaultCode,
IsTenant = true,
IsOrganizationUnit = false,
Description = AppCodes.EntityChanges,
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = "AbpEntityChanges",
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = JsonSerializer.Serialize(new GridFilterRowDto
{
Visible = true
}),
HeaderFilterJson = JsonSerializer.Serialize(new
{
Visible = true
}),
SearchPanelJson = JsonSerializer.Serialize(new
{
Visible = true
}),
GroupPanelJson = JsonSerializer.Serialize(new
{
Visible = true
}),
SelectionJson = JsonSerializer.Serialize(new SelectionDto
{
Mode = GridOptions.SelectionModeSingle,
AllowSelectAll = false
}),
ColumnOptionJson = JsonSerializer.Serialize(new
{
ColumnFixingEnabled = true,
ColumnChooserEnabled = true
}),
PermissionJson = JsonSerializer.Serialize(new PermissionCrudDto
{
C = AppCodes.EntityChanges + ".Create",
R = AppCodes.EntityChanges,
U = AppCodes.EntityChanges + ".Update",
D = AppCodes.EntityChanges + ".Delete",
E = AppCodes.EntityChanges + ".Export",
}),
PagerOptionJson = JsonSerializer.Serialize(new GridPagerOptionDto
{
Visible = true,
AllowedPageSizes = "10,20,50,100",
ShowPageSizeSelector = true,
ShowNavigationButtons = true,
ShowInfo = false,
InfoText = "Page {0} of {1} ({2} items)",
DisplayMode = GridColumnOptions.PagerDisplayModeAdaptive,
ScrollingMode = GridColumnOptions.ScrollingModeStandard,
LoadPanelEnabled = "auto",
LoadPanelText = "Loading..."
}),
}
);
#region Entity Changes Fields
await _listFormFieldRepository.InsertManyAsync(
[
new() {
ListFormCode = listFormEntityChange.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
Width = 100,
ListOrderNo = 1,
Visible = false,
IsActive = true,
IsDeleted = false,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.AuditLogs + ".Create",
R = AppCodes.AuditLogs,
U = AppCodes.AuditLogs + ".Update",
E = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormEntityChange.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "ChangeTime",
Width = 150,
ListOrderNo = 2,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.AuditLogs + ".Create",
R = AppCodes.AuditLogs,
U = AppCodes.AuditLogs + ".Update",
E = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormEntityChange.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ChangeType",
Width = 125,
ListOrderNo = 3,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.AuditLogs + ".Create",
R = AppCodes.AuditLogs,
U = AppCodes.AuditLogs + ".Update",
E = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
new() {
ListFormCode = listFormEntityChange.ListFormCode,
RoleId = null,
UserId = null,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "EntityTypeFullName",
Width = 300,
ListOrderNo = 4,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.AuditLogs + ".Create",
R = AppCodes.AuditLogs,
U = AppCodes.AuditLogs + ".Update",
E = true,
Deny = false
}),
PivotSettingsJson = JsonSerializer.Serialize(new ListFormFieldPivotSettingsDto
{
IsPivot = true
})
},
]);
#endregion
#endregion
#region Branches
var listFormBranches = await _listFormRepository.InsertAsync(
new ListForm()

View file

@ -501,22 +501,6 @@ public class MenuSeeder : IDataSeedContributor, ITransientDependency
RequiredPermissionName = AppCodes.AuditLogs
});
//Entity Changes
await _repository.InsertAsync(new Menu
{
Code = AppCodes.EntityChanges,
DisplayName = AppCodes.EntityChanges,
Order = 9,
IsDisabled = false,
ParentCode = menuAdministration.Code,
Icon = "FcSurvey",
Target = null,
ElementId = null,
CssClass = null,
Url = $"/list/{PlatformConsts.ListFormCodes.EntityChange}",
RequiredPermissionName = AppCodes.EntityChanges
});
#endregion
}
}

View file

@ -837,39 +837,6 @@ public class PermissionSeeder : IDataSeedContributor, ITransientDependency
}
#endregion
#region EntityChanges
await repositoryGroup.InsertAsync(new PermissionGroupDefinitionRecord
{
Name = AppCodes.EntityChanges,
DisplayName = AppCodes.EntityChanges
});
var permEntityChanges = await repository.InsertAsync(
GetNewPermission(
AppCodes.EntityChanges,
AppCodes.EntityChanges,
AppCodes.EntityChanges,
null,
true,
MultiTenancySides.Both
));
foreach (var item in CUDE)
{
permissionName = AppCodes.EntityChanges + "." + item;
await repository.InsertAsync(
GetNewPermission(
AppCodes.EntityChanges,
permissionName,
item,
permEntityChanges.Name,
true,
MultiTenancySides.Both
));
}
#endregion
#region Branches
await repositoryGroup.InsertAsync(new PermissionGroupDefinitionRecord
{

View file

@ -327,7 +327,6 @@ public static class PlatformConsts
public const string GlobalSearch = "List-0018";
public const string SecurityLog = "List-0019";
public const string AuditLog = "List-0020";
public const string EntityChange = "List-0021";
public const string Branch = "List-0022";
public const string ListformField = "List-1000";
public const string Order = "List-Order";

View file

@ -362,7 +362,6 @@ public static class SeedConsts
public const string IpRestrictions = Prefix.App + ".IpRestrictions";
public const string PublicApis = Prefix.App + ".PublicApis";
public const string AuditLogs = Prefix.App + ".AuditLogs";
public const string EntityChanges = Prefix.App + ".EntityChanges";
public const string Branches = Prefix.App + ".Branches";
}

View file

@ -8,6 +8,7 @@ import {
UserInfoViewModel,
} from '@/proxy/admin'
import apiService from '@/services/api.service'
import { AuditLogDto } from '../auditLog/audit-log'
export const getRoles = (skipCount = 0, maxResultCount = 10) =>
apiService.fetchData<ListResultDto<IdentityRoleDto>>({
@ -65,3 +66,9 @@ export const updatePermissions = (
url: `/api/permission-management/permissions?providerName=${providerName}&providerKey=${providerKey}`,
data: input,
})
export const getAuditLogs = (id: string) =>
apiService.fetchData<AuditLogDto>({
method: 'GET',
url: `/api/app/audit-log/${id}`,
})

View file

@ -0,0 +1,44 @@
export interface AuditLogActionDto {
serviceName: string
methodName: string
executionTime: string // Date string
executionDuration: number
parameters: string
}
export interface EntityPropertyChangeDto {
propertyName: string
propertyTypeFullName: string
originalValue: string | null
newValue: string | null
}
export interface EntityChangeDto {
changeTime: string // Date string
changeType: number
entityId: string
entityTypeFullName: string
propertyChanges: EntityPropertyChangeDto[]
}
export interface AuditLogDto {
id: string
applicationName: string
userId?: string
userName?: string
executionTime: string
executionDuration: number
clientIpAddress?: string
clientName?: string
clientId?: string
correlationId?: string
browserInfo?: string
httpMethod?: string
url?: string
exceptions?: string
comments?: string
httpStatusCode?: number
entityChangeCount?: number
entityChanges: EntityChangeDto[]
actions: AuditLogActionDto[]
}

View file

@ -0,0 +1,214 @@
import { useEffect, useState } from 'react'
import Dialog from '@/components/ui/Dialog'
import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav'
import TabContent from '@/components/ui/Tabs/TabContent'
import { Tabs } from '@/components/ui'
import { AdaptableCard } from '@/components/shared'
import { getAuditLogs } from '@/proxy/admin/identity.service'
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
function AuditLogs({
open,
onDialogClose,
id,
}: {
open: boolean
onDialogClose: () => void
id: string
}) {
const [selectedLog, setSelectedLog] = useState<AuditLogDto>()
useEffect(() => {
if (open && id) {
fetchAuditLog(id)
}
}, [open, id])
const fetchAuditLog = async (logId: string) => {
const response = await getAuditLogs(logId)
setSelectedLog(response.data)
}
return (
<Dialog width="80%" isOpen={open} onClose={onDialogClose} onRequestClose={onDialogClose}>
<h5 className="text-lg font-semibold mb-4">
Audit Log Detail
{selectedLog?.id && (
<span className="text-xs font-normal text-slate-500 ml-2">({selectedLog.id})</span>
)}
</h5>
{!selectedLog ? (
<div className="text-center py-6 text-gray-500">Loading...</div>
) : (
<Tabs defaultValue="log" variant="pill">
<TabList className="flex-wrap border-b mb-4 bg-slate-50 rounded-t">
<TabNav value="log">Log</TabNav>
<TabNav value="actions">
Actions {selectedLog.actions?.length > 0 && `(${selectedLog.actions.length})`}
</TabNav>
<TabNav value="changes">
Entity Changes{' '}
{selectedLog.entityChanges?.length > 0 && `(${selectedLog.entityChanges.length})`}
</TabNav>
</TabList>
{/* LOG DETAILS */}
<TabContent value="log">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
<AdaptableCard>
<h6 className="font-medium mb-3 text-slate-600 text-sm">Request Info</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>User</td>
<td>{selectedLog.userName}</td>
</tr>
<tr>
<td>Time</td>
<td>{new Date(selectedLog.executionTime).toLocaleString()}</td>
</tr>
<tr>
<td>Duration</td>
<td>{selectedLog.executionDuration} ms</td>
</tr>
<tr>
<td>IP</td>
<td>{selectedLog.clientIpAddress}</td>
</tr>
<tr>
<td>Method</td>
<td>{selectedLog.httpMethod}</td>
</tr>
<tr>
<td>Status Code</td>
<td>{selectedLog.httpStatusCode}</td>
</tr>
<tr>
<td>URL</td>
<td>{selectedLog.url}</td>
</tr>
</tbody>
</table>
</AdaptableCard>
<AdaptableCard>
<h6 className="font-medium mb-3 text-slate-600 text-sm">Client Info</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Browser</td>
<td>{selectedLog.browserInfo}</td>
</tr>
<tr>
<td>Exceptions</td>
<td>
<pre className="bg-slate-100 text-red-500 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug">
{selectedLog.exceptions || 'None'}
</pre>
</td>
</tr>
</tbody>
</table>
</AdaptableCard>
</div>
</TabContent>
{/* ACTIONS */}
<TabContent value="actions">
{selectedLog.actions?.map((action, index) => (
<AdaptableCard key={index} className="mb-4">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Action #{index + 1}</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Service</td>
<td>{action.serviceName}</td>
</tr>
<tr>
<td>Method</td>
<td>{action.methodName}</td>
</tr>
<tr>
<td>Time</td>
<td>{action.executionTime}</td>
</tr>
<tr>
<td>Duration</td>
<td>{action.executionDuration} ms</td>
</tr>
<tr>
<td>Parameters</td>
<td>
<pre className="bg-slate-100 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug">
{action.parameters}
</pre>
</td>
</tr>
</tbody>
</table>
</AdaptableCard>
))}
</TabContent>
{/* ENTITY CHANGES */}
<TabContent value="changes">
{selectedLog.entityChanges?.map((change, index) => (
<AdaptableCard key={index} className="mb-4">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Change #{index + 1}</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Time</td>
<td>{new Date(change.changeTime).toLocaleString()}</td>
</tr>
<tr>
<td>Type</td>
<td>{change.changeType}</td>
</tr>
<tr>
<td>Entity ID</td>
<td>{change.entityId}</td>
</tr>
<tr>
<td>Entity Type</td>
<td>{change.entityTypeFullName}</td>
</tr>
<tr>
<td>Property Changes</td>
<td>
<ul className="list-disc pl-4 space-y-1">
{change.propertyChanges.map((p, i) => (
<li key={i}>
<code>{p.propertyName}</code> ({p.propertyTypeFullName}):{' '}
{!p.originalValue || p.originalValue === 'null' ? (
<span className="text-green-600 font-semibold ml-1">
{p.newValue}
</span>
) : (
<>
<span className="text-gray-500">{p.originalValue}</span>{' '}
<span className="mx-1"></span>
<span className="text-yellow-600 font-semibold">
{p.newValue}
</span>
</>
)}
</li>
))}
</ul>
</td>
</tr>
</tbody>
</table>
</AdaptableCard>
))}
</TabContent>
</Tabs>
)}
</Dialog>
)
}
export default AuditLogs

View file

@ -3,6 +3,7 @@ import UsersPermission from '@/views/admin/identity/Users/UsersPermission'
import TenantsConnectionString from '@/views/admin/tenant-management/TenantsConnectionString'
import { useDialogContext } from './DialogProvider'
import CreateNotification from '@/views/admin/notification/CreateNotification'
import AuditLogDetail from '@/views/admin/auditLog/AuditLogDetail'
const DialogShowComponent = (): JSX.Element => {
const dialogContext: any = useDialogContext()
@ -41,6 +42,14 @@ const DialogShowComponent = (): JSX.Element => {
{...dialogContext.config?.props}
></CreateNotification>
)
case 'AuditLogDetail':
return (
<AuditLogDetail
open={true}
onDialogClose={() => dialogContext.setConfig({})}
{...dialogContext.config?.props}
></AuditLogDetail>
)
default:
return <></>
}