✨ Action logs
This commit is contained in:
		| @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using NodaTime.Extensions; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| @@ -390,7 +391,30 @@ public class AccountController( | ||||
|         var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true); | ||||
|         return Ok(calendar); | ||||
|     } | ||||
|  | ||||
|      | ||||
|     [Authorize] | ||||
|     [HttpGet("me/actions")] | ||||
|     [ProducesResponseType<List<ActionLog>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||||
|     public async Task<ActionResult<List<ActionLog>>> GetActionLogs([FromQuery] int take = 20, [FromQuery] int offset = 0) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|      | ||||
|         var query = db.ActionLogs | ||||
|             .Where(log => log.AccountId == currentUser.Id) | ||||
|             .OrderByDescending(log => log.CreatedAt); | ||||
|      | ||||
|         var total = await query.CountAsync(); | ||||
|         Response.Headers.Append("X-Total", total.ToString()); | ||||
|      | ||||
|         var logs = await query | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|      | ||||
|         return Ok(logs); | ||||
|     } | ||||
|      | ||||
|     [HttpGet("search")] | ||||
|     public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20) | ||||
|     { | ||||
|   | ||||
							
								
								
									
										57
									
								
								DysonNetwork.Sphere/Account/ActionLog.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								DysonNetwork.Sphere/Account/ActionLog.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| public class ActionLogType | ||||
| { | ||||
|     public const string NewLogin = "login"; | ||||
|     public const string ChallengeAttempt = "challenges.attempt"; | ||||
|     public const string ChallengeSuccess = "challenges.success"; | ||||
|     public const string ChallengeFailure = "challenges.failure"; | ||||
|     public const string PostCreate = "posts.create"; | ||||
|     public const string PostUpdate = "posts.update"; | ||||
|     public const string PostDelete = "posts.delete"; | ||||
|     public const string PostReact = "posts.react"; | ||||
|     public const string MessageCreate = "messages.create"; | ||||
|     public const string MessageUpdate = "messages.update"; | ||||
|     public const string MessageDelete = "messages.delete"; | ||||
|     public const string MessageReact = "messages.react"; | ||||
|     public const string PublisherCreate = "publishers.create"; | ||||
|     public const string PublisherUpdate = "publishers.update"; | ||||
|     public const string PublisherDelete = "publishers.delete"; | ||||
|     public const string PublisherMemberInvite = "publishers.members.invite"; | ||||
|     public const string PublisherMemberJoin = "publishers.members.join"; | ||||
|     public const string PublisherMemberLeave = "publishers.members.leave"; | ||||
|     public const string PublisherMemberKick = "publishers.members.kick"; | ||||
|     public const string RealmCreate = "realms.create"; | ||||
|     public const string RealmUpdate = "realms.update"; | ||||
|     public const string RealmDelete = "realms.delete"; | ||||
|     public const string RealmInvite = "realms.invite"; | ||||
|     public const string RealmJoin = "realms.join"; | ||||
|     public const string RealmLeave = "realms.leave"; | ||||
|     public const string RealmKick = "realms.kick"; | ||||
|     public const string ChatroomCreate = "chatrooms.create"; | ||||
|     public const string ChatroomUpdate = "chatrooms.update"; | ||||
|     public const string ChatroomDelete = "chatrooms.delete"; | ||||
|     public const string ChatroomInvite = "chatrooms.invite"; | ||||
|     public const string ChatroomJoin = "chatrooms.join"; | ||||
|     public const string ChatroomLeave = "chatrooms.leave"; | ||||
|     public const string ChatroomKick = "chatrooms.kick"; | ||||
| } | ||||
|  | ||||
| public class ActionLog : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(4096)] public string Action { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|     [MaxLength(512)] public string? UserAgent { get; set; } | ||||
|     [MaxLength(128)] public string? IpAddress { get; set; } | ||||
|     public Point? Location { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|     public Guid? SessionId { get; set; } | ||||
|     public Auth.Session? Session { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										80
									
								
								DysonNetwork.Sphere/Account/ActionLogService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								DysonNetwork.Sphere/Account/ActionLogService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| using Quartz; | ||||
| using System.Collections.Concurrent; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using Microsoft.AspNetCore.Http; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| public class ActionLogService(AppDatabase db, GeoIpService geo) : IDisposable | ||||
| { | ||||
|     private readonly ConcurrentQueue<ActionLog> _creationQueue = new(); | ||||
|  | ||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|         { | ||||
|             Action = action, | ||||
|             AccountId = accountId, | ||||
|             Meta = meta, | ||||
|         }; | ||||
|  | ||||
|         _creationQueue.Enqueue(log); | ||||
|     } | ||||
|      | ||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request) | ||||
|     { | ||||
|         if (request.HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|         if (request.HttpContext.Items["CurrentSession"] is not Auth.Session currentSession) | ||||
|             throw new ArgumentException("No session context was found"); | ||||
|          | ||||
|         var log = new ActionLog | ||||
|         { | ||||
|             Action = action, | ||||
|             AccountId = currentUser.Id, | ||||
|             SessionId = currentSession.Id, | ||||
|             Meta = meta, | ||||
|             UserAgent = request.Headers.UserAgent, | ||||
|             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
|             Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||
|         }; | ||||
|      | ||||
|         _creationQueue.Enqueue(log); | ||||
|     } | ||||
|      | ||||
|     public async Task FlushQueue() | ||||
|     { | ||||
|         var workingQueue = new List<ActionLog>(); | ||||
|         while (_creationQueue.TryDequeue(out var log)) | ||||
|             workingQueue.Add(log); | ||||
|  | ||||
|         if (workingQueue.Count != 0) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 await db.ActionLogs.AddRangeAsync(workingQueue); | ||||
|                 await db.SaveChangesAsync(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 foreach (var log in workingQueue) | ||||
|                     _creationQueue.Enqueue(log); | ||||
|                 throw; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         FlushQueue().Wait(); | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class ActionLogFlushJob(ActionLogService als) : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         await als.FlushQueue(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user