✨ Action logs
This commit is contained in:
parent
6358c49090
commit
aabe8269f5
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using NodaTime.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
@ -391,6 +392,29 @@ public class AccountController(
|
||||
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();
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ public class AppDatabase(
|
||||
public DbSet<Account.Notification> Notifications { get; set; }
|
||||
public DbSet<Account.NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
||||
public DbSet<Account.Badge> Badges { get; set; }
|
||||
public DbSet<Account.ActionLog> ActionLogs { get; set; }
|
||||
|
||||
public DbSet<Auth.Session> AuthSessions { get; set; }
|
||||
public DbSet<Auth.Challenge> AuthChallenges { get; set; }
|
||||
@ -85,7 +86,10 @@ public class AppDatabase(
|
||||
|
||||
optionsBuilder.UseNpgsql(
|
||||
dataSource,
|
||||
opt => opt.UseNodaTime().UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
opt => opt
|
||||
.UseNodaTime()
|
||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
.UseNetTopologySuite()
|
||||
).UseSnakeCaseNamingConvention();
|
||||
|
||||
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||
|
@ -6,6 +6,7 @@ using NodaTime;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
@ -15,14 +16,15 @@ public class AuthController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
IConfiguration configuration
|
||||
GeoIpService geo,
|
||||
ActionLogService als
|
||||
) : ControllerBase
|
||||
{
|
||||
public class ChallengeRequest
|
||||
{
|
||||
[Required] public ChallengePlatform Platform { get; set; }
|
||||
[Required] [MaxLength(256)] public string Account { get; set; } = string.Empty;
|
||||
[Required] [MaxLength(512)] public string DeviceId { get; set; }
|
||||
[Required] [MaxLength(256)] public string Account { get; set; } = null!;
|
||||
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
|
||||
public List<string> Audiences { get; set; } = new();
|
||||
public List<string> Scopes { get; set; } = new();
|
||||
}
|
||||
@ -57,12 +59,18 @@ public class AuthController(
|
||||
Scopes = request.Scopes,
|
||||
IpAddress = ipAddress,
|
||||
UserAgent = userAgent,
|
||||
Location = geo.GetPointFromIp(ipAddress),
|
||||
DeviceId = request.DeviceId,
|
||||
AccountId = account.Id
|
||||
}.Normalize();
|
||||
|
||||
await db.AuthChallenges.AddAsync(challenge);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
||||
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request
|
||||
);
|
||||
|
||||
return challenge;
|
||||
}
|
||||
|
||||
@ -124,6 +132,12 @@ public class AuthController(
|
||||
challenge.StepRemain--;
|
||||
challenge.BlacklistFactors.Add(factor.Id);
|
||||
db.Update(challenge);
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
new Dictionary<string, object> {
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "factor_id", factor.Id }
|
||||
}, Request
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -134,10 +148,26 @@ public class AuthController(
|
||||
{
|
||||
challenge.FailedAttempts++;
|
||||
db.Update(challenge);
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
new Dictionary<string, object> {
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "factor_id", factor.Id }
|
||||
}, Request
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
return BadRequest("Invalid password.");
|
||||
}
|
||||
|
||||
if (challenge.StepRemain == 0)
|
||||
{
|
||||
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
new Dictionary<string, object> {
|
||||
{ "challenge_id", challenge.Id },
|
||||
{ "account_id", challenge.AccountId }
|
||||
}, Request
|
||||
);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return challenge;
|
||||
}
|
||||
@ -210,20 +240,6 @@ public class AuthController(
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("test")]
|
||||
public async Task<ActionResult> Test()
|
||||
{
|
||||
var sessionIdClaim = HttpContext.User.FindFirst("session_id")?.Value;
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return Unauthorized();
|
||||
|
||||
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null) return NotFound();
|
||||
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
[HttpPost("captcha")]
|
||||
public async Task<ActionResult> ValidateCaptcha([FromBody] string token)
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
|
||||
using NodaTime;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
@ -50,6 +51,7 @@ public class Challenge : ModelBase
|
||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||
[MaxLength(256)] public string? DeviceId { get; set; }
|
||||
[MaxLength(1024)] public string? Nonce { get; set; }
|
||||
public Point? Location { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
@ -10,7 +11,12 @@ namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/chat")]
|
||||
public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService crs, RealmService rs) : ControllerBase
|
||||
public class ChatRoomController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
ChatRoomService crs,
|
||||
RealmService rs,
|
||||
ActionLogService als) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<ChatRoom>> GetChatRoom(Guid id)
|
||||
@ -126,6 +132,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
db.ChatRooms.Add(dmRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } }, Request
|
||||
);
|
||||
|
||||
var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId);
|
||||
await crs.SendInviteNotify(invitedMember);
|
||||
|
||||
@ -194,6 +205,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
if (chatRoom.Background is not null)
|
||||
await fs.MarkUsageAsync(chatRoom.Background, 1);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
|
||||
@ -255,6 +271,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
db.ChatRooms.Update(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomUpdate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
|
||||
@ -286,6 +307,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
if (chatRoom.Background is not null)
|
||||
await fs.MarkUsageAsync(chatRoom.Background, -1);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomDelete,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -401,6 +427,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
|
||||
await crs.SendInviteNotify(newMember);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomInvite,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(newMember);
|
||||
}
|
||||
|
||||
@ -440,6 +471,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomJoin,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
@ -559,6 +595,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
db.ChatMembers.Remove(targetMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomKick,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -593,6 +634,11 @@ public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService
|
||||
db.ChatMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomLeave,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
56
DysonNetwork.Sphere/Connection/GeoIpService.cs
Normal file
56
DysonNetwork.Sphere/Connection/GeoIpService.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using MaxMind.GeoIP2;
|
||||
using NetTopologySuite.Geometries;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection;
|
||||
|
||||
public class GeoIpOptions
|
||||
{
|
||||
public string DatabasePath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class GeoIpService(IOptions<GeoIpOptions> options)
|
||||
{
|
||||
private readonly string _databasePath = options.Value.DatabasePath;
|
||||
private readonly GeometryFactory _geometryFactory = new(new PrecisionModel(), 4326); // 4326 is the SRID for WGS84
|
||||
|
||||
public Point? GetPointFromIp(string? ipAddress)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipAddress))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new DatabaseReader(_databasePath);
|
||||
var city = reader.City(ipAddress);
|
||||
|
||||
if (city?.Location == null || !city.Location.HasCoordinates)
|
||||
return null;
|
||||
|
||||
return _geometryFactory.CreatePoint(new Coordinate(
|
||||
city.Location.Longitude ?? 0,
|
||||
city.Location.Latitude ?? 0));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public MaxMind.GeoIP2.Responses.CityResponse? GetFromIp(string? ipAddress)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipAddress))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new DatabaseReader(_databasePath);
|
||||
return reader.City(ipAddress);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
@ -35,6 +36,7 @@
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
|
||||
|
3379
DysonNetwork.Sphere/Migrations/20250515165017_AddActionLogs.Designer.cs
generated
Normal file
3379
DysonNetwork.Sphere/Migrations/20250515165017_AddActionLogs.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NetTopologySuite.Geometries;
|
||||
using NodaTime;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Sphere.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddActionLogs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
|
||||
migrationBuilder.AddColumn<Point>(
|
||||
name: "location",
|
||||
table: "auth_challenges",
|
||||
type: "geometry",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "action_logs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
action = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
|
||||
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
location = table.Column<Point>(type: "geometry", nullable: true),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
session_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_action_logs", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_action_logs_accounts_account_id",
|
||||
column: x => x.account_id,
|
||||
principalTable: "accounts",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_action_logs_auth_sessions_session_id",
|
||||
column: x => x.session_id,
|
||||
principalTable: "auth_sessions",
|
||||
principalColumn: "id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_action_logs_account_id",
|
||||
table: "action_logs",
|
||||
column: "account_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_action_logs_session_id",
|
||||
table: "action_logs",
|
||||
column: "session_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "action_logs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "location",
|
||||
table: "auth_challenges");
|
||||
|
||||
migrationBuilder.AlterDatabase()
|
||||
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
}
|
||||
}
|
||||
}
|
@ -7,9 +7,11 @@ using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NetTopologySuite.Geometries;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using NpgsqlTypes;
|
||||
using Point = NetTopologySuite.Geometries.Point;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@ -25,6 +27,7 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b =>
|
||||
@ -169,6 +172,70 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.ToTable("account_contacts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("action");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("ip_address");
|
||||
|
||||
b.Property<Point>("Location")
|
||||
.HasColumnType("geometry")
|
||||
.HasColumnName("location");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Meta")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("meta");
|
||||
|
||||
b.Property<Guid?>("SessionId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("session_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasColumnName("user_agent");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_action_logs");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.HasDatabaseName("ix_action_logs_account_id");
|
||||
|
||||
b.HasIndex("SessionId")
|
||||
.HasDatabaseName("ix_action_logs_session_id");
|
||||
|
||||
b.ToTable("action_logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -714,6 +781,10 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("ip_address");
|
||||
|
||||
b.Property<Point>("Location")
|
||||
.HasColumnType("geometry")
|
||||
.HasColumnName("location");
|
||||
|
||||
b.Property<string>("Nonce")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
@ -2485,6 +2556,25 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
b.Navigation("Account");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
.WithMany()
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_action_logs_accounts_account_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Sphere.Auth.Session", "Session")
|
||||
.WithMany()
|
||||
.HasForeignKey("SessionId")
|
||||
.HasConstraintName("fk_action_logs_auth_sessions_session_id");
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
|
@ -17,7 +17,8 @@ public class PostController(
|
||||
PostService ps,
|
||||
PublisherService pub,
|
||||
RelationshipService rels,
|
||||
IServiceScopeFactory factory
|
||||
IServiceScopeFactory factory,
|
||||
ActionLogService als
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
@ -227,6 +228,11 @@ public class PostController(
|
||||
await subs.NotifySubscribersPostAsync(post);
|
||||
});
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostCreate,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
);
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
@ -268,6 +274,12 @@ public class PostController(
|
||||
var isRemoving = await ps.ModifyPostVotes(post, reaction, isExistingReaction, isSelfReact);
|
||||
|
||||
if (isRemoving) return NoContent();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostReact,
|
||||
new Dictionary<string, object> { { "post_id", post.Id }, { "reaction", request.Symbol } }, Request
|
||||
);
|
||||
|
||||
return Ok(reaction);
|
||||
}
|
||||
|
||||
@ -312,6 +324,11 @@ public class PostController(
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostUpdate,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
@ -331,6 +348,12 @@ public class PostController(
|
||||
return StatusCode(403, "You need at least be an editor to delete the publisher's post.");
|
||||
|
||||
await ps.DeletePostAsync(post);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostDelete,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@ -148,11 +148,14 @@ builder.Services.AddSingleton(tusDiskStore);
|
||||
builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>();
|
||||
|
||||
// Services
|
||||
builder.Services.Configure<GeoIpOptions>(builder.Configuration.GetSection("GeoIP"));
|
||||
builder.Services.AddScoped<GeoIpService>();
|
||||
builder.Services.AddScoped<WebSocketService>();
|
||||
builder.Services.AddScoped<EmailService>();
|
||||
builder.Services.AddScoped<PermissionService>();
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<AccountEventService>();
|
||||
builder.Services.AddSingleton<ActionLogService>();
|
||||
builder.Services.AddScoped<RelationshipService>();
|
||||
builder.Services.AddScoped<MagicSpellService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
@ -188,6 +191,16 @@ builder.Services.AddQuartz(q =>
|
||||
.WithIdentity("CloudFilesUnusedRecyclingTrigger")
|
||||
.WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever())
|
||||
);
|
||||
|
||||
var actionLogFlushJob = new JobKey("ActionLogFlush");
|
||||
q.AddJob<ActionLogFlushJob>(opts => opts.WithIdentity(actionLogFlushJob));
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(actionLogFlushJob)
|
||||
.WithIdentity("ActionLogFlushTrigger")
|
||||
.WithSimpleSchedule(o => o
|
||||
.WithIntervalInMinutes(5)
|
||||
.RepeatForever())
|
||||
);
|
||||
});
|
||||
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
@ -12,11 +13,11 @@ namespace DysonNetwork.Sphere.Publisher;
|
||||
|
||||
[ApiController]
|
||||
[Route("/publishers")]
|
||||
public class PublisherController(AppDatabase db, PublisherService ps, FileService fs)
|
||||
public class PublisherController(AppDatabase db, PublisherService ps, FileService fs, ActionLogService als)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> GetPublisher(string name)
|
||||
public async Task<ActionResult<Publisher>> GetPublisher(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@ -38,7 +39,7 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Sphere.Publisher.Publisher>>> ListManagedPublishers()
|
||||
public async Task<ActionResult<List<Publisher>>> ListManagedPublishers()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
@ -96,15 +97,7 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var member = await db.PublisherMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.PublisherId == publisher.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return StatusCode(403, "You are not even a member of the targeted publisher.");
|
||||
if (member.Role < PublisherMemberRole.Manager)
|
||||
return StatusCode(403,
|
||||
"You need at least be a manager to invite other members to collaborate this publisher.");
|
||||
if (member.Role < request.Role)
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, request.Role))
|
||||
return StatusCode(403, "You cannot invite member has higher permission than yours.");
|
||||
|
||||
var newMember = new PublisherMember
|
||||
@ -117,12 +110,21 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
db.PublisherMembers.Add(newMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberInvite,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "publisher_id", publisher.Id },
|
||||
{ "account_id", relatedUser.Id }
|
||||
}, Request
|
||||
);
|
||||
|
||||
return Ok(newMember);
|
||||
}
|
||||
|
||||
[HttpPost("invites/{name}/accept")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> AcceptMemberInvite(string name)
|
||||
public async Task<ActionResult<Publisher>> AcceptMemberInvite(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
@ -138,6 +140,11 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberJoin,
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } }, Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
@ -158,6 +165,45 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
db.PublisherMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberLeave,
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{name}/members/{memberId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> RemoveMember(string name, Guid memberId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
var publisher = await db.Publishers
|
||||
.Where(p => p.Name == name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (publisher is null) return NotFound();
|
||||
|
||||
var member = await db.PublisherMembers
|
||||
.Where(m => m.AccountId == memberId)
|
||||
.Where(m => m.PublisherId == publisher.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound("Member was not found");
|
||||
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Manager))
|
||||
return StatusCode(403, "You need at least be a manager to remove members from this publisher.");
|
||||
|
||||
db.PublisherMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberKick,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "publisher_id", publisher.Id },
|
||||
{ "account_id", memberId }
|
||||
}, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -174,7 +220,7 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
[HttpPost("individual")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "publishers.create")]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> CreatePublisherIndividual([FromBody] PublisherRequest request)
|
||||
public async Task<ActionResult<Publisher>> CreatePublisherIndividual([FromBody] PublisherRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@ -212,13 +258,19 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
background
|
||||
);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherCreate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
[HttpPost("organization/{realmSlug}")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "publishers.create")]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> CreatePublisherOrganization(string realmSlug, [FromBody] PublisherRequest request)
|
||||
public async Task<ActionResult<Publisher>> CreatePublisherOrganization(string realmSlug,
|
||||
[FromBody] PublisherRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
|
||||
@ -226,8 +278,10 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
if (realm == null) return NotFound("Realm not found");
|
||||
|
||||
var isAdmin = await db.RealmMembers
|
||||
.AnyAsync(m => m.RealmId == realm.Id && m.AccountId == currentUser.Id && m.Role >= RealmMemberRole.Moderator);
|
||||
if (!isAdmin) return StatusCode(403, "You need to be a moderator of the realm to create an organization publisher");
|
||||
.AnyAsync(m =>
|
||||
m.RealmId == realm.Id && m.AccountId == currentUser.Id && m.Role >= RealmMemberRole.Moderator);
|
||||
if (!isAdmin)
|
||||
return StatusCode(403, "You need to be a moderator of the realm to create an organization publisher");
|
||||
|
||||
var takenName = request.Name ?? realm.Slug;
|
||||
var duplicateNameCount = await db.Publishers
|
||||
@ -259,14 +313,18 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
background
|
||||
);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherCreate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPatch("{name}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> UpdatePublisher(string name, PublisherRequest request)
|
||||
public async Task<ActionResult<Publisher>> UpdatePublisher(string name, PublisherRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
@ -312,12 +370,17 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
db.Update(publisher);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherUpdate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Sphere.Publisher.Publisher>> DeletePublisher(string name)
|
||||
public async Task<ActionResult<Publisher>> DeletePublisher(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
@ -345,6 +408,11 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic
|
||||
db.Publishers.Remove(publisher);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherDelete,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -8,7 +9,7 @@ namespace DysonNetwork.Sphere.Realm;
|
||||
|
||||
[ApiController]
|
||||
[Route("/realms")]
|
||||
public class RealmController(AppDatabase db, RealmService rs, FileService fs) : Controller
|
||||
public class RealmController(AppDatabase db, RealmService rs, FileService fs, ActionLogService als) : Controller
|
||||
{
|
||||
[HttpGet("{slug}")]
|
||||
public async Task<ActionResult<Realm>> GetRealm(string slug)
|
||||
@ -91,6 +92,11 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.RealmMembers.Add(newMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmInvite,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", newMember.AccountId } }, Request
|
||||
);
|
||||
|
||||
newMember.Account = relatedUser;
|
||||
await rs.SendInviteNotify(newMember);
|
||||
|
||||
@ -115,6 +121,12 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmJoin,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
@ -135,6 +147,12 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.RealmMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmLeave,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -214,6 +232,12 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.RealmMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmLeave,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -273,6 +297,11 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.Realms.Add(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmCreate,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
);
|
||||
|
||||
if (realm.Picture is not null) await fs.MarkUsageAsync(realm.Picture, 1);
|
||||
if (realm.Background is not null) await fs.MarkUsageAsync(realm.Background, 1);
|
||||
|
||||
@ -334,6 +363,12 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
|
||||
db.Realms.Update(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmUpdate,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
);
|
||||
|
||||
return Ok(realm);
|
||||
}
|
||||
|
||||
@ -359,6 +394,11 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) :
|
||||
db.Realms.Remove(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmDelete,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
);
|
||||
|
||||
if (realm.Picture is not null)
|
||||
await fs.MarkUsageAsync(realm.Picture, -1);
|
||||
if (realm.Background is not null)
|
||||
|
Loading…
x
Reference in New Issue
Block a user