✨ 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