From 158cc75c5bd0c5ac017457f1e17f894cfbf0597c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 2 Dec 2025 21:42:26 +0800 Subject: [PATCH] :boom: Simplified permission node system and data structure --- .../Identity/DeveloperController.cs | 2 +- .../Startup/ApplicationConfiguration.cs | 2 +- DysonNetwork.Drive/Storage/FileController.cs | 2 +- .../Storage/FileUploadController.cs | 2 +- .../Account/AccountController.cs | 5 +- .../Account/AccountCurrentController.cs | 5 +- DysonNetwork.Pass/Account/AccountService.cs | 2 +- .../Account/MagicSpellService.cs | 2 +- DysonNetwork.Pass/AppDatabase.cs | 2 +- .../Lotteries/LotteryController.cs | 3 +- ...34035_SimplifiedPermissionNode.Designer.cs | 2872 +++++++++++++++++ ...20251202134035_SimplifiedPermissionNode.cs | 59 + ...leware.cs => LocalPermissionMiddleware.cs} | 37 +- .../Permission/PermissionService.cs | 146 +- .../Permission/PermissionServiceGrpc.cs | 69 +- DysonNetwork.Pass/PermissionController.cs | 65 +- .../Safety/AbuseReportController.cs | 9 +- .../Startup/ApplicationConfiguration.cs | 2 +- DysonNetwork.Pass/Wallet/WalletController.cs | 3 +- .../Notification/NotificationController.cs | 2 +- .../Auth/PermissionMiddleware.cs | 72 - .../Auth/RemotePermissionMiddleware.cs | 72 + DysonNetwork.Shared/Models/Permission.cs | 27 +- DysonNetwork.Shared/Proto/auth.proto | 220 +- DysonNetwork.Sphere/Chat/ChatController.cs | 2 +- .../Chat/ChatRoomController.cs | 2 +- DysonNetwork.Sphere/Post/PostController.cs | 4 +- .../Publisher/PublisherController.cs | 10 +- .../Startup/ApplicationConfiguration.cs | 2 +- .../Sticker/StickerController.cs | 4 +- .../WebReader/WebReaderController.cs | 4 +- .../Startup/ApplicationConfiguration.cs | 2 +- 32 files changed, 3333 insertions(+), 379 deletions(-) create mode 100644 DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs create mode 100644 DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.cs rename DysonNetwork.Pass/Permission/{PermissionMiddleware.cs => LocalPermissionMiddleware.cs} (69%) delete mode 100644 DysonNetwork.Shared/Auth/PermissionMiddleware.cs create mode 100644 DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs diff --git a/DysonNetwork.Develop/Identity/DeveloperController.cs b/DysonNetwork.Develop/Identity/DeveloperController.cs index 1f5e6b0..ec822c3 100644 --- a/DysonNetwork.Develop/Identity/DeveloperController.cs +++ b/DysonNetwork.Develop/Identity/DeveloperController.cs @@ -69,7 +69,7 @@ public class DeveloperController( [HttpPost("{name}/enroll")] [Authorize] - [RequiredPermission("global", "developers.create")] + [AskPermission("developers.create")] public async Task> EnrollDeveloperProgram(string name) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); diff --git a/DysonNetwork.Develop/Startup/ApplicationConfiguration.cs b/DysonNetwork.Develop/Startup/ApplicationConfiguration.cs index 90c1d0d..098557e 100644 --- a/DysonNetwork.Develop/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Develop/Startup/ApplicationConfiguration.cs @@ -16,7 +16,7 @@ public static class ApplicationConfiguration app.UseAuthentication(); app.UseAuthorization(); - app.UseMiddleware(); + app.UseMiddleware(); app.MapControllers(); diff --git a/DysonNetwork.Drive/Storage/FileController.cs b/DysonNetwork.Drive/Storage/FileController.cs index 086f51c..006b6bd 100644 --- a/DysonNetwork.Drive/Storage/FileController.cs +++ b/DysonNetwork.Drive/Storage/FileController.cs @@ -381,7 +381,7 @@ public class FileController( [Authorize] [HttpDelete("recycle")] - [RequiredPermission("maintenance", "files.delete.recycle")] + [AskPermission("files.delete.recycle")] public async Task DeleteAllRecycledFiles() { var count = await fs.DeleteAllRecycledFilesAsync(); diff --git a/DysonNetwork.Drive/Storage/FileUploadController.cs b/DysonNetwork.Drive/Storage/FileUploadController.cs index 6d8c79d..76509c9 100644 --- a/DysonNetwork.Drive/Storage/FileUploadController.cs +++ b/DysonNetwork.Drive/Storage/FileUploadController.cs @@ -113,7 +113,7 @@ public class FileUploadController( if (currentUser.IsSuperuser) return null; var allowed = await permission.HasPermissionAsync(new HasPermissionRequest - { Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" }); + { Actor = currentUser.Id, Key = "files.create" }); return allowed.HasPermission ? null diff --git a/DysonNetwork.Pass/Account/AccountController.cs b/DysonNetwork.Pass/Account/AccountController.cs index 543ff40..def2d91 100644 --- a/DysonNetwork.Pass/Account/AccountController.cs +++ b/DysonNetwork.Pass/Account/AccountController.cs @@ -4,6 +4,7 @@ using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Wallet; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Models; @@ -319,7 +320,7 @@ public class AccountController( [HttpPost("credits/validate")] [Authorize] - [RequiredPermission("maintenance", "credits.validate.perform")] + [AskPermission("credits.validate.perform")] public async Task PerformSocialCreditValidation() { await socialCreditService.ValidateSocialCredits(); @@ -328,7 +329,7 @@ public class AccountController( [HttpDelete("{name}")] [Authorize] - [RequiredPermission("maintenance", "accounts.deletion")] + [AskPermission("accounts.deletion")] public async Task AdminDeleteAccount(string name) { var account = await accounts.LookupAccount(name); diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index 745c38d..a7aa84a 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Wallet; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; @@ -194,7 +195,7 @@ public class AccountCurrentController( } [HttpPatch("statuses")] - [RequiredPermission("global", "accounts.statuses.update")] + [AskPermission("accounts.statuses.update")] public async Task> UpdateStatus([FromBody] AccountController.StatusRequest request) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); @@ -228,7 +229,7 @@ public class AccountCurrentController( } [HttpPost("statuses")] - [RequiredPermission("global", "accounts.statuses.create")] + [AskPermission("accounts.statuses.create")] public async Task> CreateStatus([FromBody] AccountController.StatusRequest request) { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index e716f84..061f725 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -158,7 +158,7 @@ public class AccountService( { db.PermissionGroupMembers.Add(new SnPermissionGroupMember { - Actor = $"user:{account.Id}", + Actor = account.Id.ToString(), Group = defaultGroup }); } diff --git a/DysonNetwork.Pass/Account/MagicSpellService.cs b/DysonNetwork.Pass/Account/MagicSpellService.cs index 2ba1e07..ea812ba 100644 --- a/DysonNetwork.Pass/Account/MagicSpellService.cs +++ b/DysonNetwork.Pass/Account/MagicSpellService.cs @@ -194,7 +194,7 @@ public class MagicSpellService( { db.PermissionGroupMembers.Add(new SnPermissionGroupMember { - Actor = $"user:{account.Id}", + Actor = account.Id.ToString(), Group = defaultGroup }); } diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index 9465dac..a749516 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -103,7 +103,7 @@ public class AppDatabase( "stickers.packs.create", "stickers.create" }.Select(permission => - PermissionService.NewPermissionNode("group:default", "global", permission, true)) + PermissionService.NewPermissionNode("group:default", permission, true)) .ToList() }); await context.SaveChangesAsync(cancellationToken); diff --git a/DysonNetwork.Pass/Lotteries/LotteryController.cs b/DysonNetwork.Pass/Lotteries/LotteryController.cs index fea18b5..2138c66 100644 --- a/DysonNetwork.Pass/Lotteries/LotteryController.cs +++ b/DysonNetwork.Pass/Lotteries/LotteryController.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Shared.Models; using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Permission; +using DysonNetwork.Shared.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -81,7 +82,7 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) : [HttpPost("draw")] [Authorize] - [RequiredPermission("maintenance", "lotteries.draw.perform")] + [AskPermission("lotteries.draw.perform")] public async Task PerformLotteryDraw() { await lotteryService.DrawLotteries(); diff --git a/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs b/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs new file mode 100644 index 0000000..52598be --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.Designer.cs @@ -0,0 +1,2872 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Pass; +using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20251202134035_SimplifiedPermissionNode")] + partial class SimplifiedPermissionNode + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Resolution") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("resolution"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("resource_identifier"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_abuse_reports"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_abuse_reports_account_id"); + + b.ToTable("abuse_reports", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("AutomatedId") + .HasColumnType("uuid") + .HasColumnName("automated_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsSuperuser") + .HasColumnType("boolean") + .HasColumnName("is_superuser"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("language"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("region"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_accounts_name"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Config") + .HasColumnType("jsonb") + .HasColumnName("config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EnabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("enabled_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Secret") + .HasMaxLength(8196) + .HasColumnType("character varying(8196)") + .HasColumnName("secret"); + + b.Property("Trustworthy") + .HasColumnType("integer") + .HasColumnName("trustworthy"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_auth_factors"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_auth_factors_account_id"); + + b.ToTable("account_auth_factors", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_badges_account_id"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("access_token"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("ProvidedIdentifier") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("provided_identifier"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("provider"); + + b.Property("RefreshToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("refresh_token"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_connections"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_connections_account_id"); + + b.ToTable("account_connections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_account_contacts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_contacts_account_id"); + + b.ToTable("account_contacts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActiveBadge") + .HasColumnType("jsonb") + .HasColumnName("active_badge"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone") + .HasColumnName("birthday"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("FirstName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("first_name"); + + b.Property("Gender") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("gender"); + + b.Property("LastName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("last_name"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property>("Links") + .HasColumnType("jsonb") + .HasColumnName("links"); + + b.Property("Location") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("location"); + + b.Property("MiddleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("middle_name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Pronouns") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("pronouns"); + + b.Property("SocialCredits") + .HasColumnType("double precision") + .HasColumnName("social_credits"); + + b.Property("TimeZone") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("time_zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UsernameColor") + .HasColumnType("jsonb") + .HasColumnName("username_color"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_account_profiles"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_account_profiles_account_id"); + + b.ToTable("account_profiles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("BlockedPermissions") + .HasColumnType("jsonb") + .HasColumnName("blocked_permissions"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_punishments"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_punishments_account_id"); + + b.ToTable("punishments", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => + { + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("RelatedId") + .HasColumnType("uuid") + .HasColumnName("related_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Status") + .HasColumnType("smallint") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("AccountId", "RelatedId") + .HasName("pk_account_relationships"); + + b.HasIndex("RelatedId") + .HasDatabaseName("ix_account_relationships_related_id"); + + b.ToTable("account_relationships", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cleared_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsAutomated") + .HasColumnType("boolean") + .HasColumnName("is_automated"); + + b.Property("IsInvisible") + .HasColumnType("boolean") + .HasColumnName("is_invisible"); + + b.Property("IsNotDisturb") + .HasColumnType("boolean") + .HasColumnName("is_not_disturb"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_statuses"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_statuses_account_id"); + + b.ToTable("account_statuses", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("action"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("jsonb") + .HasColumnName("location"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("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.ToTable("action_logs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("resource_identifier"); + + b.Property("SpellId") + .HasColumnType("uuid") + .HasColumnName("spell_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_affiliation_results"); + + b.HasIndex("SpellId") + .HasDatabaseName("ix_affiliation_results_spell_id"); + + b.ToTable("affiliation_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_affiliation_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_affiliation_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_affiliation_spells_spell"); + + b.ToTable("affiliation_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_api_keys"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_api_keys_account_id"); + + b.HasIndex("SessionId") + .HasDatabaseName("ix_api_keys_session_id"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Audiences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("audiences"); + + b.Property>("BlacklistFactors") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("blacklist_factors"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("device_id"); + + b.Property("DeviceName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_name"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FailedAttempts") + .HasColumnType("integer") + .HasColumnName("failed_attempts"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("jsonb") + .HasColumnName("location"); + + b.Property("Nonce") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nonce"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("StepRemain") + .HasColumnType("integer") + .HasColumnName("step_remain"); + + b.Property("StepTotal") + .HasColumnType("integer") + .HasColumnName("step_total"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_challenges_account_id"); + + b.ToTable("auth_challenges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_id"); + + b.Property("DeviceLabel") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_label"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("device_name"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_clients"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_clients_account_id"); + + b.ToTable("auth_clients", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("ChallengeId") + .HasColumnType("uuid") + .HasColumnName("challenge_id"); + + b.Property("ClientId") + .HasColumnType("uuid") + .HasColumnName("client_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + b.Property("ParentSessionId") + .HasColumnType("uuid") + .HasColumnName("parent_session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_sessions_account_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.HasIndex("ClientId") + .HasDatabaseName("ix_auth_sessions_client_id"); + + b.HasIndex("ParentSessionId") + .HasDatabaseName("ix_auth_sessions_parent_session_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BackdatedFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("backdated_from"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("tips"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_check_in_results"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_check_in_results_account_id"); + + b.ToTable("account_check_in_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BonusMultiplier") + .HasColumnType("double precision") + .HasColumnName("bonus_multiplier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("bigint") + .HasColumnName("delta"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_experience_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_experience_records_account_id"); + + b.ToTable("experience_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DrawDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("draw_date"); + + b.Property("DrawStatus") + .HasColumnType("integer") + .HasColumnName("draw_status"); + + b.Property>("MatchedRegionOneNumbers") + .HasColumnType("jsonb") + .HasColumnName("matched_region_one_numbers"); + + b.Property("MatchedRegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("matched_region_two_number"); + + b.Property("Multiplier") + .HasColumnType("integer") + .HasColumnName("multiplier"); + + b.Property>("RegionOneNumbers") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("region_one_numbers"); + + b.Property("RegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("region_two_number"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_lotteries"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_lotteries_account_id"); + + b.ToTable("lotteries", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLotteryRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DrawDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("draw_date"); + + b.Property("TotalPrizeAmount") + .HasColumnType("bigint") + .HasColumnName("total_prize_amount"); + + b.Property("TotalPrizesAwarded") + .HasColumnType("integer") + .HasColumnName("total_prizes_awarded"); + + b.Property("TotalTickets") + .HasColumnType("integer") + .HasColumnName("total_tickets"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property>("WinningRegionOneNumbers") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("winning_region_one_numbers"); + + b.Property("WinningRegionTwoNumber") + .HasColumnType("integer") + .HasColumnName("winning_region_two_number"); + + b.HasKey("Id") + .HasName("pk_lottery_records"); + + b.ToTable("lottery_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_magic_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_magic_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_magic_spells_spell"); + + b.ToTable("magic_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Actor") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("GroupId", "Actor") + .HasName("pk_permission_group_members"); + + b.ToTable("permission_group_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_permission_nodes"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permission_nodes_group_id"); + + b.HasIndex("Key", "Actor") + .HasDatabaseName("ix_permission_nodes_key_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LargeImage") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("large_image"); + + b.Property("LeaseExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("lease_expires_at"); + + b.Property("LeaseMinutes") + .HasColumnType("integer") + .HasColumnName("lease_minutes"); + + b.Property("ManualId") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("manual_id"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SmallImage") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("small_image"); + + b.Property("Subtitle") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subtitle"); + + b.Property("SubtitleUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subtitle_url"); + + b.Property("Title") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title"); + + b.Property("TitleUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title_url"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_presence_activities"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_presence_activities_account_id"); + + b.ToTable("presence_activities", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_realms"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_realms_slug"); + + b.ToTable("realms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => + { + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("RealmId", "AccountId") + .HasName("pk_realm_members"); + + b.ToTable("realm_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Delta") + .HasColumnType("double precision") + .HasColumnName("delta"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason"); + + b.Property("ReasonType") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("reason_type"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_social_credit_records"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_social_credit_records_account_id"); + + b.ToTable("social_credit_records", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallets"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallets_account_id"); + + b.ToTable("wallets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletCoupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Code") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DiscountAmount") + .HasColumnType("numeric") + .HasColumnName("discount_amount"); + + b.Property("DiscountRate") + .HasColumnType("double precision") + .HasColumnName("discount_rate"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Identifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("MaxUsage") + .HasColumnType("integer") + .HasColumnName("max_usage"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_coupons"); + + b.ToTable("wallet_coupons", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AmountOfSplits") + .HasColumnType("integer") + .HasColumnName("amount_of_splits"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatorAccountId") + .HasColumnType("uuid") + .HasColumnName("creator_account_id"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("IsOpen") + .HasColumnType("boolean") + .HasColumnName("is_open"); + + b.Property("Message") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("RemainingAmount") + .HasColumnType("numeric") + .HasColumnName("remaining_amount"); + + b.Property("SplitType") + .HasColumnType("integer") + .HasColumnName("split_type"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TotalAmount") + .HasColumnType("numeric") + .HasColumnName("total_amount"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_funds"); + + b.HasIndex("CreatorAccountId") + .HasDatabaseName("ix_wallet_funds_creator_account_id"); + + b.ToTable("wallet_funds", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFundRecipient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("FundId") + .HasColumnType("uuid") + .HasColumnName("fund_id"); + + b.Property("IsReceived") + .HasColumnType("boolean") + .HasColumnName("is_received"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("received_at"); + + b.Property("RecipientAccountId") + .HasColumnType("uuid") + .HasColumnName("recipient_account_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_fund_recipients"); + + b.HasIndex("FundId") + .HasDatabaseName("ix_wallet_fund_recipients_fund_id"); + + b.HasIndex("RecipientAccountId") + .HasDatabaseName("ix_wallet_fund_recipients_recipient_account_id"); + + b.ToTable("wallet_fund_recipients", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("FinalPrice") + .HasColumnType("numeric") + .HasColumnName("final_price"); + + b.Property("GiftCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("gift_code"); + + b.Property("GifterId") + .HasColumnType("uuid") + .HasColumnName("gifter_id"); + + b.Property("IsOpenGift") + .HasColumnType("boolean") + .HasColumnName("is_open_gift"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("message"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RecipientId") + .HasColumnType("uuid") + .HasColumnName("recipient_id"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("redeemed_at"); + + b.Property("RedeemerId") + .HasColumnType("uuid") + .HasColumnName("redeemer_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("SubscriptionId") + .HasColumnType("uuid") + .HasColumnName("subscription_id"); + + b.Property("SubscriptionIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("subscription_identifier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_gifts"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_gifts_coupon_id"); + + b.HasIndex("GiftCode") + .HasDatabaseName("ix_wallet_gifts_gift_code"); + + b.HasIndex("GifterId") + .HasDatabaseName("ix_wallet_gifts_gifter_id"); + + b.HasIndex("RecipientId") + .HasDatabaseName("ix_wallet_gifts_recipient_id"); + + b.HasIndex("RedeemerId") + .HasDatabaseName("ix_wallet_gifts_redeemer_id"); + + b.HasIndex("SubscriptionId") + .IsUnique() + .HasDatabaseName("ix_wallet_gifts_subscription_id"); + + b.ToTable("wallet_gifts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("ProductIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("product_identifier"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasColumnType("uuid") + .HasColumnName("transaction_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_orders"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_orders_payee_wallet_id"); + + b.HasIndex("TransactionId") + .HasDatabaseName("ix_payment_orders_transaction_id"); + + b.ToTable("payment_orders", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WalletId") + .HasColumnType("uuid") + .HasColumnName("wallet_id"); + + b.HasKey("Id") + .HasName("pk_wallet_pockets"); + + b.HasIndex("WalletId") + .HasDatabaseName("ix_wallet_pockets_wallet_id"); + + b.ToTable("wallet_pockets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("BegunAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("begun_at"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsFreeTrial") + .HasColumnType("boolean") + .HasColumnName("is_free_trial"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RenewalAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("renewal_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallet_subscriptions_account_id"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_subscriptions_coupon_id"); + + b.HasIndex("Identifier") + .HasDatabaseName("ix_wallet_subscriptions_identifier"); + + b.HasIndex("Status") + .HasDatabaseName("ix_wallet_subscriptions_status"); + + b.HasIndex("AccountId", "Identifier") + .HasDatabaseName("ix_wallet_subscriptions_account_id_identifier"); + + b.HasIndex("AccountId", "IsActive") + .HasDatabaseName("ix_wallet_subscriptions_account_id_is_active"); + + b.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("PayerWalletId") + .HasColumnType("uuid") + .HasColumnName("payer_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_transactions"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_transactions_payee_wallet_id"); + + b.HasIndex("PayerWalletId") + .HasDatabaseName("ix_payment_transactions_payer_wallet_id"); + + b.ToTable("payment_transactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAbuseReport", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountBadge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountConnection", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountContact", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountProfile", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Shared.Models.SnAccountProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountPunishment", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_punishments_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountRelationship", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Related") + .WithMany("IncomingRelationships") + .HasForeignKey("RelatedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_related_id"); + + b.Navigation("Account"); + + b.Navigation("Related"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccountStatus", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnActionLog", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAffiliationSpell", "Spell") + .WithMany() + .HasForeignKey("SpellId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_affiliation_results_affiliation_spells_spell_id"); + + b.Navigation("Spell"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_affiliation_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_keys_auth_sessions_session_id"); + + b.Navigation("Account"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthChallenge", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_clients_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthSession", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .HasConstraintName("fk_auth_sessions_auth_clients_client_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "ParentSession") + .WithMany() + .HasForeignKey("ParentSessionId") + .HasConstraintName("fk_auth_sessions_auth_sessions_parent_session_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + + b.Navigation("Client"); + + b.Navigation("ParentSession"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnExperienceRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_experience_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnLottery", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lotteries_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnMagicSpell", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionNode", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnPermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPresenceActivity", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_presence_activities_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealmMember", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnRealm", "Realm") + .WithMany("Members") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_realms_realm_id"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnSocialCreditRecord", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_social_credit_records_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "CreatorAccount") + .WithMany() + .HasForeignKey("CreatorAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_funds_accounts_creator_account_id"); + + b.Navigation("CreatorAccount"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFundRecipient", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWalletFund", "Fund") + .WithMany("Recipients") + .HasForeignKey("FundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_fund_recipients_wallet_funds_fund_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "RecipientAccount") + .WithMany() + .HasForeignKey("RecipientAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_fund_recipients_accounts_recipient_account_id"); + + b.Navigation("Fund"); + + b.Navigation("RecipientAccount"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletGift", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_gifts_wallet_coupons_coupon_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Gifter") + .WithMany() + .HasForeignKey("GifterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_gifts_accounts_gifter_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .HasConstraintName("fk_wallet_gifts_accounts_recipient_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Redeemer") + .WithMany() + .HasForeignKey("RedeemerId") + .HasConstraintName("fk_wallet_gifts_accounts_redeemer_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletSubscription", "Subscription") + .WithOne("Gift") + .HasForeignKey("DysonNetwork.Shared.Models.SnWalletGift", "SubscriptionId") + .HasConstraintName("fk_wallet_gifts_wallet_subscriptions_subscription_id"); + + b.Navigation("Coupon"); + + b.Navigation("Gifter"); + + b.Navigation("Recipient"); + + b.Navigation("Redeemer"); + + b.Navigation("Subscription"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletOrder", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletTransaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletPocket", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWalletCoupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletTransaction", b => + { + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Shared.Models.SnWallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnAccount", b => + { + b.Navigation("AuthFactors"); + + b.Navigation("Badges"); + + b.Navigation("Challenges"); + + b.Navigation("Connections"); + + b.Navigation("Contacts"); + + b.Navigation("IncomingRelationships"); + + b.Navigation("OutgoingRelationships"); + + b.Navigation("Profile") + .IsRequired(); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnPermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnRealm", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWallet", b => + { + b.Navigation("Pockets"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletFund", b => + { + b.Navigation("Recipients"); + }); + + modelBuilder.Entity("DysonNetwork.Shared.Models.SnWalletSubscription", b => + { + b.Navigation("Gift"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.cs b/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.cs new file mode 100644 index 0000000..0f2ce4d --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20251202134035_SimplifiedPermissionNode.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + /// + public partial class SimplifiedPermissionNode : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_permission_nodes_key_area_actor", + table: "permission_nodes"); + + migrationBuilder.DropColumn( + name: "area", + table: "permission_nodes"); + + migrationBuilder.AddColumn( + name: "type", + table: "permission_nodes", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "ix_permission_nodes_key_actor", + table: "permission_nodes", + columns: new[] { "key", "actor" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_permission_nodes_key_actor", + table: "permission_nodes"); + + migrationBuilder.DropColumn( + name: "type", + table: "permission_nodes"); + + migrationBuilder.AddColumn( + name: "area", + table: "permission_nodes", + type: "character varying(1024)", + maxLength: 1024, + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "ix_permission_nodes_key_area_actor", + table: "permission_nodes", + columns: new[] { "key", "area", "actor" }); + } + } +} diff --git a/DysonNetwork.Pass/Permission/PermissionMiddleware.cs b/DysonNetwork.Pass/Permission/LocalPermissionMiddleware.cs similarity index 69% rename from DysonNetwork.Pass/Permission/PermissionMiddleware.cs rename to DysonNetwork.Pass/Permission/LocalPermissionMiddleware.cs index 223b8a3..1198c97 100644 --- a/DysonNetwork.Pass/Permission/PermissionMiddleware.cs +++ b/DysonNetwork.Pass/Permission/LocalPermissionMiddleware.cs @@ -1,17 +1,12 @@ +using DysonNetwork.Shared.Auth; + namespace DysonNetwork.Pass.Permission; using System; using Microsoft.Extensions.Logging; -using DysonNetwork.Shared.Models; +using Shared.Models; -[AttributeUsage(AttributeTargets.Method)] -public class RequiredPermissionAttribute(string area, string key) : Attribute -{ - public string Area { get; set; } = area; - public string Key { get; } = key; -} - -public class PermissionMiddleware(RequestDelegate next, ILogger logger) +public class LocalPermissionMiddleware(RequestDelegate next, ILogger logger) { private const string ForbiddenMessage = "Insufficient permissions"; private const string UnauthorizedMessage = "Authentication required"; @@ -21,15 +16,15 @@ public class PermissionMiddleware(RequestDelegate next, ILogger() + .OfType() .FirstOrDefault(); if (attr != null) { // Validate permission attributes - if (string.IsNullOrWhiteSpace(attr.Area) || string.IsNullOrWhiteSpace(attr.Key)) + if (string.IsNullOrWhiteSpace(attr.Key)) { - logger.LogWarning("Invalid permission attribute: Area='{Area}', Key='{Key}'", attr.Area, attr.Key); + logger.LogWarning("Invalid permission attribute: Key='{Key}'", attr.Key); httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; await httpContext.Response.WriteAsync("Server configuration error"); return; @@ -37,7 +32,7 @@ public class PermissionMiddleware(RequestDelegate next, ILogger(actor, attr.Area, attr.Key); + var permNode = await pm.GetPermissionAsync(actor, attr.Key); if (!permNode) { - logger.LogWarning("Permission denied for user {UserId}: {Area}/{Key}", - currentUser.Id, attr.Area, attr.Key); + logger.LogWarning("Permission denied for user {UserId}: {Key}", currentUser.Id, attr.Key); httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; await httpContext.Response.WriteAsync(ForbiddenMessage); return; } - logger.LogDebug("Permission granted for user {UserId}: {Area}/{Key}", - currentUser.Id, attr.Area, attr.Key); + logger.LogDebug("Permission granted for user {UserId}: {Key}", currentUser.Id, attr.Key); } catch (Exception ex) { - logger.LogError(ex, "Error checking permission for user {UserId}: {Area}/{Key}", - currentUser.Id, attr.Area, attr.Key); + logger.LogError(ex, "Error checking permission for user {UserId}: {Key}", currentUser.Id, attr.Key); httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; await httpContext.Response.WriteAsync("Permission check failed"); return; diff --git a/DysonNetwork.Pass/Permission/PermissionService.cs b/DysonNetwork.Pass/Permission/PermissionService.cs index 368dea8..057cd60 100644 --- a/DysonNetwork.Pass/Permission/PermissionService.cs +++ b/DysonNetwork.Pass/Permission/PermissionService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using NodaTime; using System.Text.Json; using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; namespace DysonNetwork.Pass.Permission; @@ -28,8 +29,8 @@ public class PermissionService( private const string PermissionGroupCacheKeyPrefix = "perm-cg:"; private const string PermissionGroupPrefix = "perm-g:"; - private static string GetPermissionCacheKey(string actor, string area, string key) => - PermissionCacheKeyPrefix + actor + ":" + area + ":" + key; + private static string GetPermissionCacheKey(string actor, string key) => + PermissionCacheKeyPrefix + actor + ":" + key; private static string GetGroupsCacheKey(string actor) => PermissionGroupCacheKeyPrefix + actor; @@ -37,50 +38,56 @@ public class PermissionService( private static string GetPermissionGroupKey(string actor) => PermissionGroupPrefix + actor; - public async Task HasPermissionAsync(string actor, string area, string key) + public async Task HasPermissionAsync( + string actor, + string key, + PermissionNodeActorType type = PermissionNodeActorType.Account + ) { - var value = await GetPermissionAsync(actor, area, key); + var value = await GetPermissionAsync(actor, key, type); return value; } - public async Task GetPermissionAsync(string actor, string area, string key) + public async Task GetPermissionAsync( + string actor, + string key, + PermissionNodeActorType type = PermissionNodeActorType.Account + ) { // Input validation if (string.IsNullOrWhiteSpace(actor)) throw new ArgumentException("Actor cannot be null or empty", nameof(actor)); - if (string.IsNullOrWhiteSpace(area)) - throw new ArgumentException("Area cannot be null or empty", nameof(area)); if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("Key cannot be null or empty", nameof(key)); - var cacheKey = GetPermissionCacheKey(actor, area, key); + var cacheKey = GetPermissionCacheKey(actor, key); try { var (hit, cachedValue) = await cache.GetAsyncWithStatus(cacheKey); if (hit) { - logger.LogDebug("Permission cache hit for {Actor}:{Area}:{Key}", actor, area, key); + logger.LogDebug("Permission cache hit for {Type}:{Actor}:{Key}", type, actor, key); return cachedValue; } var now = SystemClock.Instance.GetCurrentInstant(); var groupsId = await GetOrCacheUserGroupsAsync(actor, now); - var permission = await FindPermissionNodeAsync(actor, area, key, groupsId, now); + var permission = await FindPermissionNodeAsync(type, actor, key, groupsId); var result = permission != null ? DeserializePermissionValue(permission.Value) : default; await cache.SetWithGroupsAsync(cacheKey, result, [GetPermissionGroupKey(actor)], _options.CacheExpiration); - logger.LogDebug("Permission resolved for {Actor}:{Area}:{Key} = {Result}", - actor, area, key, result != null); + logger.LogDebug("Permission resolved for {Type}:{Actor}:{Key} = {Result}", type, actor, key, + result != null); return result; } catch (Exception ex) { - logger.LogError(ex, "Error retrieving permission for {Actor}:{Area}:{Key}", actor, area, key); + logger.LogError(ex, "Error retrieving permission for {Type}:{Actor}:{Key}", type, actor, key); throw; } } @@ -109,33 +116,34 @@ public class PermissionService( return groupsId; } - private async Task FindPermissionNodeAsync(string actor, string area, string key, - List groupsId, Instant now) + private async Task FindPermissionNodeAsync( + PermissionNodeActorType type, + string actor, + string key, + List groupsId + ) { + var now = SystemClock.Instance.GetCurrentInstant(); // First try exact match (highest priority) var exactMatch = await db.PermissionNodes - .Where(n => (n.GroupId == null && n.Actor == actor) || + .Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) || (n.GroupId != null && groupsId.Contains(n.GroupId.Value))) - .Where(n => n.Key == key && n.Area == area) + .Where(n => n.Key == key) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now) .FirstOrDefaultAsync(); if (exactMatch != null) - { return exactMatch; - } // If no exact match and wildcards are enabled, try wildcard matches if (!_options.EnableWildcardMatching) - { return null; - } var wildcardMatches = await db.PermissionNodes - .Where(n => (n.GroupId == null && n.Actor == actor) || + .Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) || (n.GroupId != null && groupsId.Contains(n.GroupId.Value))) - .Where(n => (n.Key.Contains("*") || n.Area.Contains("*"))) + .Where(n => EF.Functions.Like(n.Key, "%*%")) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now) .Take(_options.MaxWildcardMatches) @@ -147,36 +155,21 @@ public class PermissionService( foreach (var node in wildcardMatches) { - var score = CalculateWildcardMatchScore(node.Area, node.Key, area, key); - if (score > bestMatchScore) - { - bestMatch = node; - bestMatchScore = score; - } + var score = CalculateWildcardMatchScore(node.Key, key); + if (score <= bestMatchScore) continue; + bestMatch = node; + bestMatchScore = score; } if (bestMatch != null) - { - logger.LogDebug("Found wildcard permission match: {NodeArea}:{NodeKey} for {Area}:{Key}", - bestMatch.Area, bestMatch.Key, area, key); - } + logger.LogDebug("Found wildcard permission match: {NodeKey} for {Key}", bestMatch.Key, key); return bestMatch; } - private static int CalculateWildcardMatchScore(string nodeArea, string nodeKey, string targetArea, string targetKey) + private static int CalculateWildcardMatchScore(string nodeKey, string targetKey) { - // Calculate how well the wildcard pattern matches - // Higher score = better match - var areaScore = CalculatePatternMatchScore(nodeArea, targetArea); - var keyScore = CalculatePatternMatchScore(nodeKey, targetKey); - - // Perfect match gets highest score - if (areaScore == int.MaxValue && keyScore == int.MaxValue) - return int.MaxValue; - - // Prefer area matches over key matches, more specific patterns over general ones - return (areaScore * 1000) + keyScore; + return CalculatePatternMatchScore(nodeKey, targetKey); } private static int CalculatePatternMatchScore(string pattern, string target) @@ -184,31 +177,30 @@ public class PermissionService( if (pattern == target) return int.MaxValue; // Exact match - if (!pattern.Contains("*")) + if (!pattern.Contains('*')) return -1; // No wildcard, not a match // Simple wildcard matching: * matches any sequence of characters var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*") + "$"; - var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); + var regex = new System.Text.RegularExpressions.Regex(regexPattern, + System.Text.RegularExpressions.RegexOptions.IgnoreCase); - if (regex.IsMatch(target)) - { - // Score based on specificity (shorter patterns are less specific) - var wildcardCount = pattern.Count(c => c == '*'); - var length = pattern.Length; - return Math.Max(1, 1000 - (wildcardCount * 100) - length); - } + if (!regex.IsMatch(target)) return -1; // No match - return -1; // No match + // Score based on specificity (shorter patterns are less specific) + var wildcardCount = pattern.Count(c => c == '*'); + var length = pattern.Length; + + return Math.Max(1, 1000 - wildcardCount * 100 - length); } public async Task AddPermissionNode( string actor, - string area, string key, T value, Instant? expiredAt = null, - Instant? affectedAt = null + Instant? affectedAt = null, + PermissionNodeActorType type = PermissionNodeActorType.Account ) { if (value is null) throw new ArgumentNullException(nameof(value)); @@ -216,8 +208,8 @@ public class PermissionService( var node = new SnPermissionNode { Actor = actor, + Type = type, Key = key, - Area = area, Value = SerializePermissionValue(value), ExpiredAt = expiredAt, AffectedAt = affectedAt @@ -227,7 +219,7 @@ public class PermissionService( await db.SaveChangesAsync(); // Invalidate related caches - await InvalidatePermissionCacheAsync(actor, area, key); + await InvalidatePermissionCacheAsync(actor, key); return node; } @@ -235,11 +227,11 @@ public class PermissionService( public async Task AddPermissionNodeToGroup( SnPermissionGroup group, string actor, - string area, string key, T value, Instant? expiredAt = null, - Instant? affectedAt = null + Instant? affectedAt = null, + PermissionNodeActorType type = PermissionNodeActorType.Account ) { if (value is null) throw new ArgumentNullException(nameof(value)); @@ -247,8 +239,8 @@ public class PermissionService( var node = new SnPermissionNode { Actor = actor, + Type = type, Key = key, - Area = area, Value = SerializePermissionValue(value), ExpiredAt = expiredAt, AffectedAt = affectedAt, @@ -260,44 +252,45 @@ public class PermissionService( await db.SaveChangesAsync(); // Invalidate related caches - await InvalidatePermissionCacheAsync(actor, area, key); + await InvalidatePermissionCacheAsync(actor, key); await cache.RemoveAsync(GetGroupsCacheKey(actor)); await cache.RemoveGroupAsync(GetPermissionGroupKey(actor)); return node; } - public async Task RemovePermissionNode(string actor, string area, string key) + public async Task RemovePermissionNode(string actor, string key, PermissionNodeActorType? type) { var node = await db.PermissionNodes - .Where(n => n.Actor == actor && n.Area == area && n.Key == key) + .Where(n => n.Actor == actor && n.Key == key) + .If(type is not null, q => q.Where(n => n.Type == type)) .FirstOrDefaultAsync(); if (node is not null) db.PermissionNodes.Remove(node); await db.SaveChangesAsync(); // Invalidate cache - await InvalidatePermissionCacheAsync(actor, area, key); + await InvalidatePermissionCacheAsync(actor, key); } - public async Task RemovePermissionNodeFromGroup(SnPermissionGroup group, string actor, string area, string key) + public async Task RemovePermissionNodeFromGroup(SnPermissionGroup group, string actor, string key) { var node = await db.PermissionNodes .Where(n => n.GroupId == group.Id) - .Where(n => n.Actor == actor && n.Area == area && n.Key == key) + .Where(n => n.Actor == actor && n.Key == key && n.Type == PermissionNodeActorType.Group) .FirstOrDefaultAsync(); if (node is null) return; db.PermissionNodes.Remove(node); await db.SaveChangesAsync(); // Invalidate caches - await InvalidatePermissionCacheAsync(actor, area, key); + await InvalidatePermissionCacheAsync(actor, key); await cache.RemoveAsync(GetGroupsCacheKey(actor)); await cache.RemoveGroupAsync(GetPermissionGroupKey(actor)); } - private async Task InvalidatePermissionCacheAsync(string actor, string area, string key) + private async Task InvalidatePermissionCacheAsync(string actor, string key) { - var cacheKey = GetPermissionCacheKey(actor, area, key); + var cacheKey = GetPermissionCacheKey(actor, key); await cache.RemoveAsync(cacheKey); } @@ -312,12 +305,11 @@ public class PermissionService( return JsonDocument.Parse(str); } - public static SnPermissionNode NewPermissionNode(string actor, string area, string key, T value) + public static SnPermissionNode NewPermissionNode(string actor, string key, T value) { return new SnPermissionNode { Actor = actor, - Area = area, Key = key, Value = SerializePermissionValue(value), }; @@ -341,8 +333,7 @@ public class PermissionService( (n.GroupId != null && groupsId.Contains(n.GroupId.Value))) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now) - .OrderBy(n => n.Area) - .ThenBy(n => n.Key) + .OrderBy(n => n.Key) .ToListAsync(); logger.LogDebug("Listed {Count} effective permissions for actor {Actor}", permissions.Count, actor); @@ -370,8 +361,7 @@ public class PermissionService( .Where(n => n.GroupId == null && n.Actor == actor) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now) - .OrderBy(n => n.Area) - .ThenBy(n => n.Key) + .OrderBy(n => n.Key) .ToListAsync(); logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor); @@ -424,4 +414,4 @@ public class PermissionService( throw; } } -} +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Permission/PermissionServiceGrpc.cs b/DysonNetwork.Pass/Permission/PermissionServiceGrpc.cs index 27580ab..4607143 100644 --- a/DysonNetwork.Pass/Permission/PermissionServiceGrpc.cs +++ b/DysonNetwork.Pass/Permission/PermissionServiceGrpc.cs @@ -9,31 +9,33 @@ using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Pass.Permission; public class PermissionServiceGrpc( - PermissionService permissionService, + PermissionService psv, AppDatabase db, ILogger logger ) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase { public override async Task HasPermission(HasPermissionRequest request, ServerCallContext context) { + var type = SnPermissionNode.ConvertProtoActorType(request.Type); try { - var hasPermission = await permissionService.HasPermissionAsync(request.Actor, request.Area, request.Key); + var hasPermission = await psv.HasPermissionAsync(request.Actor, request.Key, type); return new HasPermissionResponse { HasPermission = hasPermission }; } catch (Exception ex) { - logger.LogError(ex, "Error checking permission for actor {Actor}, area {Area}, key {Key}", - request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error checking permission for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Permission check failed")); } } public override async Task GetPermission(GetPermissionRequest request, ServerCallContext context) { + var type = SnPermissionNode.ConvertProtoActorType(request.Type); try { - var permissionValue = await permissionService.GetPermissionAsync(request.Actor, request.Area, request.Key); + var permissionValue = await psv.GetPermissionAsync(request.Actor, request.Key, type); return new GetPermissionResponse { Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null @@ -41,14 +43,15 @@ public class PermissionServiceGrpc( } catch (Exception ex) { - logger.LogError(ex, "Error getting permission for actor {Actor}, area {Area}, key {Key}", - request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error getting permission for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission")); } } public override async Task AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context) { + var type = SnPermissionNode.ConvertProtoActorType(request.Type); try { JsonDocument jsonValue; @@ -58,18 +61,18 @@ public class PermissionServiceGrpc( } catch (JsonException ex) { - logger.LogWarning(ex, "Invalid JSON in permission value for actor {Actor}, area {Area}, key {Key}", - request.Actor, request.Area, request.Key); + logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format")); } - var node = await permissionService.AddPermissionNode( + var node = await psv.AddPermissionNode( request.Actor, - request.Area, request.Key, jsonValue, request.ExpiredAt?.ToInstant(), - request.AffectedAt?.ToInstant() + request.AffectedAt?.ToInstant(), + type ); return new AddPermissionNodeResponse { Node = node.ToProtoValue() }; } @@ -79,14 +82,15 @@ public class PermissionServiceGrpc( } catch (Exception ex) { - logger.LogError(ex, "Error adding permission node for actor {Actor}, area {Area}, key {Key}", - request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node")); } } public override async Task AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context) { + var type = SnPermissionNode.ConvertProtoActorType(request.Type); try { var group = await FindPermissionGroupAsync(request.Group.Id); @@ -102,19 +106,19 @@ public class PermissionServiceGrpc( } catch (JsonException ex) { - logger.LogWarning(ex, "Invalid JSON in permission value for group {GroupId}, actor {Actor}, area {Area}, key {Key}", - request.Group.Id, request.Actor, request.Area, request.Key); + logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format")); } - var node = await permissionService.AddPermissionNodeToGroup( + var node = await psv.AddPermissionNodeToGroup( group, request.Actor, - request.Area, request.Key, jsonValue, request.ExpiredAt?.ToInstant(), - request.AffectedAt?.ToInstant() + request.AffectedAt?.ToInstant(), + type ); return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() }; } @@ -124,23 +128,24 @@ public class PermissionServiceGrpc( } catch (Exception ex) { - logger.LogError(ex, "Error adding permission node to group {GroupId} for actor {Actor}, area {Area}, key {Key}", - request.Group.Id, request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group")); } } public override async Task RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context) { + var type = SnPermissionNode.ConvertProtoActorType(request.Type); try { - await permissionService.RemovePermissionNode(request.Actor, request.Area, request.Key); + await psv.RemovePermissionNode(request.Actor, request.Key, type); return new RemovePermissionNodeResponse { Success = true }; } catch (Exception ex) { - logger.LogError(ex, "Error removing permission node for actor {Actor}, area {Area}, key {Key}", - request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error removing permission for {Type}:{Area}:{Key}", + type, request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node")); } } @@ -155,7 +160,7 @@ public class PermissionServiceGrpc( throw new RpcException(new Status(StatusCode.NotFound, "Permission group not found")); } - await permissionService.RemovePermissionNodeFromGroup(group, request.Actor, request.Area, request.Key); + await psv.RemovePermissionNodeFromGroup(group, request.Actor, request.Key); return new RemovePermissionNodeFromGroupResponse { Success = true }; } catch (RpcException) @@ -164,20 +169,18 @@ public class PermissionServiceGrpc( } catch (Exception ex) { - logger.LogError(ex, "Error removing permission node from group {GroupId} for actor {Actor}, area {Area}, key {Key}", - request.Group.Id, request.Actor, request.Area, request.Key); + logger.LogError(ex, "Error removing permission from group for {Area}:{Key}", + request.Actor, request.Key); throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group")); } } private async Task FindPermissionGroupAsync(string groupId) { - if (!Guid.TryParse(groupId, out var guid)) - { - logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId); - return null; - } + if (Guid.TryParse(groupId, out var guid)) + return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid); + logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId); + return null; - return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid); } } diff --git a/DysonNetwork.Pass/PermissionController.cs b/DysonNetwork.Pass/PermissionController.cs index c10d9b4..54fc72d 100644 --- a/DysonNetwork.Pass/PermissionController.cs +++ b/DysonNetwork.Pass/PermissionController.cs @@ -5,6 +5,7 @@ using DysonNetwork.Pass.Permission; using DysonNetwork.Shared.Models; using NodaTime; using System.Text.Json; +using DysonNetwork.Shared.Auth; namespace DysonNetwork.Pass; @@ -19,16 +20,20 @@ public class PermissionController( /// /// Check if an actor has a specific permission /// - [HttpGet("check/{actor}/{area}/{key}")] - [RequiredPermission("maintenance", "permissions.check")] + [HttpGet("check/{actor}/{key}")] + [AskPermission("permissions.check")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CheckPermission(string actor, string area, string key) + public async Task CheckPermission( + [FromRoute] string actor, + [FromRoute] string key, + [FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account + ) { try { - var hasPermission = await permissionService.HasPermissionAsync(actor, area, key); + var hasPermission = await permissionService.HasPermissionAsync(actor, key, type); return Ok(hasPermission); } catch (ArgumentException ex) @@ -45,7 +50,7 @@ public class PermissionController( /// Get all effective permissions for an actor (including group permissions) /// [HttpGet("actors/{actor}/permissions/effective")] - [RequiredPermission("maintenance", "permissions.check")] + [AskPermission("permissions.check")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -70,7 +75,7 @@ public class PermissionController( /// Get all direct permissions for an actor (excluding group permissions) /// [HttpGet("actors/{actor}/permissions/direct")] - [RequiredPermission("maintenance", "permissions.check")] + [AskPermission("permissions.check")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -94,28 +99,27 @@ public class PermissionController( /// /// Give a permission to an actor /// - [HttpPost("actors/{actor}/permissions/{area}/{key}")] - [RequiredPermission("maintenance", "permissions.manage")] + [HttpPost("actors/{actor}/permissions/{key}")] + [AskPermission("permissions.manage")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task GivePermission( string actor, - string area, string key, - [FromBody] PermissionRequest request) + [FromBody] PermissionRequest request + ) { try { var permission = await permissionService.AddPermissionNode( actor, - area, key, JsonDocument.Parse(JsonSerializer.Serialize(request.Value)), request.ExpiredAt, request.AffectedAt ); - return Created($"/api/permissions/actors/{actor}/permissions/{area}/{key}", permission); + return Created($"/api/permissions/actors/{actor}/permissions/{key}", permission); } catch (ArgumentException ex) { @@ -130,16 +134,20 @@ public class PermissionController( /// /// Remove a permission from an actor /// - [HttpDelete("actors/{actor}/permissions/{area}/{key}")] - [RequiredPermission("maintenance", "permissions.manage")] + [HttpDelete("actors/{actor}/permissions/{key}")] + [AskPermission("permissions.manage")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task RemovePermission(string actor, string area, string key) + public async Task RemovePermission( + string actor, + string key, + [FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account + ) { try { - await permissionService.RemovePermissionNode(actor, area, key); + await permissionService.RemovePermissionNode(actor, key, type); return NoContent(); } catch (ArgumentException ex) @@ -156,7 +164,7 @@ public class PermissionController( /// Get all groups for an actor /// [HttpGet("actors/{actor}/groups")] - [RequiredPermission("maintenance", "permissions.groups.check")] + [AskPermission("permissions.groups.check")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -183,8 +191,8 @@ public class PermissionController( /// /// Add an actor to a permission group /// - [HttpPost("actors/{actor}/groups/{groupId}")] - [RequiredPermission("maintenance", "permissions.groups.manage")] + [HttpPost("actors/{actor}/groups/{groupId:guid}")] + [AskPermission("permissions.groups.manage")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -192,7 +200,8 @@ public class PermissionController( public async Task AddActorToGroup( string actor, Guid groupId, - [FromBody] GroupMembershipRequest? request = null) + [FromBody] GroupMembershipRequest? request = null + ) { try { @@ -238,7 +247,7 @@ public class PermissionController( /// Remove an actor from a permission group /// [HttpDelete("actors/{actor}/groups/{groupId}")] - [RequiredPermission("maintenance", "permissions.groups.manage")] + [AskPermission("permissions.groups.manage")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -272,7 +281,7 @@ public class PermissionController( /// Clear permission cache for an actor /// [HttpPost("actors/{actor}/cache/clear")] - [RequiredPermission("maintenance", "permissions.cache.manage")] + [AskPermission("permissions.cache.manage")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] @@ -297,7 +306,7 @@ public class PermissionController( /// Validate a permission pattern /// [HttpPost("validate-pattern")] - [RequiredPermission("maintenance", "permissions.check")] + [AskPermission("permissions.check")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public IActionResult ValidatePattern([FromBody] PatternValidationRequest request) @@ -322,14 +331,14 @@ public class PermissionController( public class PermissionRequest { public object? Value { get; set; } - public NodaTime.Instant? ExpiredAt { get; set; } - public NodaTime.Instant? AffectedAt { get; set; } + public Instant? ExpiredAt { get; set; } + public Instant? AffectedAt { get; set; } } public class GroupMembershipRequest { - public NodaTime.Instant? ExpiredAt { get; set; } - public NodaTime.Instant? AffectedAt { get; set; } + public Instant? ExpiredAt { get; set; } + public Instant? AffectedAt { get; set; } } public class PatternValidationRequest @@ -342,4 +351,4 @@ public class PatternValidationResponse public string Pattern { get; set; } = string.Empty; public bool IsValid { get; set; } public string Message { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Safety/AbuseReportController.cs b/DysonNetwork.Pass/Safety/AbuseReportController.cs index 9d0b16b..55eccab 100644 --- a/DysonNetwork.Pass/Safety/AbuseReportController.cs +++ b/DysonNetwork.Pass/Safety/AbuseReportController.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Pass.Permission; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -51,7 +52,7 @@ public class SnAbuseReportController( [HttpGet("")] [Authorize] - [RequiredPermission("safety", "reports.view")] + [AskPermission("reports.view")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetReports( [FromQuery] int offset = 0, @@ -85,7 +86,7 @@ public class SnAbuseReportController( [HttpGet("{id}")] [Authorize] - [RequiredPermission("safety", "reports.view")] + [AskPermission("reports.view")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetReportById(Guid id) @@ -122,7 +123,7 @@ public class SnAbuseReportController( [HttpPost("{id}/resolve")] [Authorize] - [RequiredPermission("safety", "reports.resolve")] + [AskPermission("reports.resolve")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> ResolveReport(Guid id, [FromBody] ResolveReportRequest request) @@ -144,7 +145,7 @@ public class SnAbuseReportController( [HttpGet("count")] [Authorize] - [RequiredPermission("safety", "reports.view")] + [AskPermission("reports.view")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetReportsCount() { diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs index 2df40b0..105559f 100644 --- a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs @@ -22,7 +22,7 @@ public static class ApplicationConfiguration app.UseWebSockets(); app.UseAuthentication(); app.UseAuthorization(); - app.UseMiddleware(); + app.UseMiddleware(); app.MapControllers().RequireRateLimiting("fixed"); diff --git a/DysonNetwork.Pass/Wallet/WalletController.cs b/DysonNetwork.Pass/Wallet/WalletController.cs index e1163dd..76282fd 100644 --- a/DysonNetwork.Pass/Wallet/WalletController.cs +++ b/DysonNetwork.Pass/Wallet/WalletController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Permission; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; using Microsoft.AspNetCore.Authorization; @@ -196,7 +197,7 @@ public class WalletController( [HttpPost("balance")] [Authorize] - [RequiredPermission("maintenance", "wallets.balance.modify")] + [AskPermission("wallets.balance.modify")] public async Task> ModifyWalletBalance([FromBody] WalletBalanceRequest request) { var wallet = await ws.GetWalletAsync(request.AccountId); diff --git a/DysonNetwork.Ring/Notification/NotificationController.cs b/DysonNetwork.Ring/Notification/NotificationController.cs index fb1a97d..1624539 100644 --- a/DysonNetwork.Ring/Notification/NotificationController.cs +++ b/DysonNetwork.Ring/Notification/NotificationController.cs @@ -139,7 +139,7 @@ public class NotificationController( [HttpPost("send")] [Authorize] - [RequiredPermission("global", "notifications.send")] + [AskPermission("notifications.send")] public async Task SendNotification( [FromBody] NotificationWithAimRequest request, [FromQuery] bool save = false diff --git a/DysonNetwork.Shared/Auth/PermissionMiddleware.cs b/DysonNetwork.Shared/Auth/PermissionMiddleware.cs deleted file mode 100644 index 30711be..0000000 --- a/DysonNetwork.Shared/Auth/PermissionMiddleware.cs +++ /dev/null @@ -1,72 +0,0 @@ -using DysonNetwork.Shared.Proto; -using Grpc.Core; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace DysonNetwork.Shared.Auth -{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class RequiredPermissionAttribute(string area, string key) : Attribute - { - public string Area { get; set; } = area; - public string Key { get; } = key; - } - - public class PermissionMiddleware(RequestDelegate next) - { - public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService, ILogger logger) - { - var endpoint = httpContext.GetEndpoint(); - - var attr = endpoint?.Metadata - .OfType() - .FirstOrDefault(); - - if (attr != null) - { - if (httpContext.Items["CurrentUser"] is not Account currentUser) - { - httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; - await httpContext.Response.WriteAsync("Unauthorized"); - return; - } - - // Assuming Account proto has a bool field 'is_superuser' which is generated as 'IsSuperuser' - if (currentUser.IsSuperuser) - { - // Bypass the permission check for performance - await next(httpContext); - return; - } - - var actor = $"user:{currentUser.Id}"; - - try - { - var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest - { - Actor = actor, - Area = attr.Area, - Key = attr.Key - }); - - if (!permResp.HasPermission) - { - httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; - await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} was required."); - return; - } - } - catch (RpcException ex) - { - logger.LogError(ex, "gRPC call to PermissionService failed while checking permission {Area}/{Key} for actor {Actor}", attr.Area, attr.Key, actor); - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - await httpContext.Response.WriteAsync("Error checking permissions."); - return; - } - } - - await next(httpContext); - } - } -} diff --git a/DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs b/DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs new file mode 100644 index 0000000..be759fb --- /dev/null +++ b/DysonNetwork.Shared/Auth/RemotePermissionMiddleware.cs @@ -0,0 +1,72 @@ +using DysonNetwork.Shared.Proto; +using Grpc.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace DysonNetwork.Shared.Auth; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class AskPermissionAttribute(string key, PermissionNodeActorType type = PermissionNodeActorType.Account) + : Attribute +{ + public string Key { get; } = key; + public PermissionNodeActorType Type { get; } = type; +} + +public class RemotePermissionMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService, + ILogger logger) + { + var endpoint = httpContext.GetEndpoint(); + + var attr = endpoint?.Metadata + .OfType() + .FirstOrDefault(); + + if (attr != null) + { + if (httpContext.Items["CurrentUser"] is not Account currentUser) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + await httpContext.Response.WriteAsync("Unauthorized"); + return; + } + + // Superuser will bypass all the permission check + if (currentUser.IsSuperuser) + { + await next(httpContext); + return; + } + + try + { + var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest + { + Actor = currentUser.Id, + Key = attr.Key + }); + + if (!permResp.HasPermission) + { + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + await httpContext.Response.WriteAsync($"Permission {attr.Key} was required."); + return; + } + } + catch (RpcException ex) + { + logger.LogError(ex, + "gRPC call to PermissionService failed while checking permission {Key} for actor {Actor}", attr.Key, + currentUser.Id + ); + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + await httpContext.Response.WriteAsync("Error checking permissions."); + return; + } + } + + await next(httpContext); + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Models/Permission.cs b/DysonNetwork.Shared/Models/Permission.cs index b24d2b9..535ecb2 100644 --- a/DysonNetwork.Shared/Models/Permission.cs +++ b/DysonNetwork.Shared/Models/Permission.cs @@ -8,6 +8,12 @@ using NodaTime.Serialization.Protobuf; namespace DysonNetwork.Shared.Models; +public enum PermissionNodeActorType +{ + Account, + Group +} + /// The permission node model provides the infrastructure of permission control in Dyson Network. /// It based on the ABAC permission model. /// @@ -19,12 +25,12 @@ namespace DysonNetwork.Shared.Models; /// And the actor shows who owns the permission, in most cases, the user:<userId> /// and when the permission node has a GroupId, the actor will be set to the group, but it won't work on checking /// expect the member of that permission group inherent the permission from the group. -[Index(nameof(Key), nameof(Area), nameof(Actor))] +[Index(nameof(Key), nameof(Actor))] public class SnPermissionNode : ModelBase, IDisposable { public Guid Id { get; set; } = Guid.NewGuid(); + public PermissionNodeActorType Type { get; set; } = PermissionNodeActorType.Account; [MaxLength(1024)] public string Actor { get; set; } = null!; - [MaxLength(1024)] public string Area { get; set; } = null!; [MaxLength(1024)] public string Key { get; set; } = null!; [Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!; public Instant? ExpiredAt { get; set; } = null; @@ -39,7 +45,12 @@ public class SnPermissionNode : ModelBase, IDisposable { Id = Id.ToString(), Actor = Actor, - Area = Area, + Type = Type switch + { + PermissionNodeActorType.Account => Proto.PermissionNodeActorType.Account, + PermissionNodeActorType.Group => Proto.PermissionNodeActorType.Group, + _ => throw new ArgumentOutOfRangeException() + }, Key = Key, Value = Google.Protobuf.WellKnownTypes.Value.Parser.ParseJson(Value.RootElement.GetRawText()), ExpiredAt = ExpiredAt?.ToTimestamp(), @@ -48,6 +59,16 @@ public class SnPermissionNode : ModelBase, IDisposable }; } + public static PermissionNodeActorType ConvertProtoActorType(Proto.PermissionNodeActorType? val) + { + return val switch + { + Proto.PermissionNodeActorType.Account => PermissionNodeActorType.Account, + Proto.PermissionNodeActorType.Group => PermissionNodeActorType.Group, + _ => PermissionNodeActorType.Account + }; + } + public void Dispose() { Value.Dispose(); diff --git a/DysonNetwork.Shared/Proto/auth.proto b/DysonNetwork.Shared/Proto/auth.proto index 9e96313..7e4d39e 100644 --- a/DysonNetwork.Shared/Proto/auth.proto +++ b/DysonNetwork.Shared/Proto/auth.proto @@ -12,193 +12,197 @@ import "account.proto"; // Represents a user session message AuthSession { - string id = 1; - optional google.protobuf.Timestamp last_granted_at = 3; - optional google.protobuf.Timestamp expired_at = 4; - string account_id = 5; - Account account = 6; - string challenge_id = 7; - AuthChallenge challenge = 8; - google.protobuf.StringValue app_id = 9; - optional string client_id = 10; - optional string parent_session_id = 11; - AuthClient client = 12; + string id = 1; + optional google.protobuf.Timestamp last_granted_at = 3; + optional google.protobuf.Timestamp expired_at = 4; + string account_id = 5; + Account account = 6; + string challenge_id = 7; + AuthChallenge challenge = 8; + google.protobuf.StringValue app_id = 9; + optional string client_id = 10; + optional string parent_session_id = 11; + AuthClient client = 12; } // Represents an authentication challenge message AuthChallenge { - string id = 1; - google.protobuf.Timestamp expired_at = 2; - int32 step_remain = 3; - int32 step_total = 4; - int32 failed_attempts = 5; - ChallengeType type = 7; - repeated string blacklist_factors = 8; - repeated string audiences = 9; - repeated string scopes = 10; - google.protobuf.StringValue ip_address = 11; - google.protobuf.StringValue user_agent = 12; - google.protobuf.StringValue device_id = 13; - google.protobuf.StringValue nonce = 14; - // Point location is omitted as there is no direct proto equivalent. - string account_id = 15; - google.protobuf.StringValue device_name = 16; - ClientPlatform platform = 17; + string id = 1; + google.protobuf.Timestamp expired_at = 2; + int32 step_remain = 3; + int32 step_total = 4; + int32 failed_attempts = 5; + ChallengeType type = 7; + repeated string blacklist_factors = 8; + repeated string audiences = 9; + repeated string scopes = 10; + google.protobuf.StringValue ip_address = 11; + google.protobuf.StringValue user_agent = 12; + google.protobuf.StringValue device_id = 13; + google.protobuf.StringValue nonce = 14; + // Point location is omitted as there is no direct proto equivalent. + string account_id = 15; + google.protobuf.StringValue device_name = 16; + ClientPlatform platform = 17; } message AuthClient { - string id = 1; - ClientPlatform platform = 2; - google.protobuf.StringValue device_name = 3; - google.protobuf.StringValue device_label = 4; - string device_id = 5; - string account_id = 6; + string id = 1; + ClientPlatform platform = 2; + google.protobuf.StringValue device_name = 3; + google.protobuf.StringValue device_label = 4; + string device_id = 5; + string account_id = 6; } // Enum for challenge types enum ChallengeType { - CHALLENGE_TYPE_UNSPECIFIED = 0; - LOGIN = 1; - OAUTH = 2; - OIDC = 3; + CHALLENGE_TYPE_UNSPECIFIED = 0; + LOGIN = 1; + OAUTH = 2; + OIDC = 3; } // Enum for client platforms enum ClientPlatform { - CLIENT_PLATFORM_UNSPECIFIED = 0; - UNIDENTIFIED = 1; - WEB = 2; - IOS = 3; - ANDROID = 4; - MACOS = 5; - WINDOWS = 6; - LINUX = 7; + CLIENT_PLATFORM_UNSPECIFIED = 0; + UNIDENTIFIED = 1; + WEB = 2; + IOS = 3; + ANDROID = 4; + MACOS = 5; + WINDOWS = 6; + LINUX = 7; } service AuthService { - rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {} + rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {} - rpc ValidatePin(ValidatePinRequest) returns (ValidateResponse) {} - rpc ValidateCaptcha(ValidateCaptchaRequest) returns (ValidateResponse) {} + rpc ValidatePin(ValidatePinRequest) returns (ValidateResponse) {} + rpc ValidateCaptcha(ValidateCaptchaRequest) returns (ValidateResponse) {} } message AuthenticateRequest { - string token = 1; - optional google.protobuf.StringValue ip_address = 2; + string token = 1; + optional google.protobuf.StringValue ip_address = 2; } message AuthenticateResponse { - bool valid = 1; - optional string message = 2; - optional AuthSession session = 3; + bool valid = 1; + optional string message = 2; + optional AuthSession session = 3; } message ValidatePinRequest { - string account_id = 1; - string pin = 2; + string account_id = 1; + string pin = 2; } message ValidateCaptchaRequest { - string token = 1; + string token = 1; } message ValidateResponse { - bool valid = 1; + bool valid = 1; +} + +enum PermissionNodeActorType { + ACCOUNT = 0; + GROUP = 1; } // Permission related messages and services message PermissionNode { - string id = 1; - string actor = 2; - string area = 3; - string key = 4; - google.protobuf.Value value = 5; // Using Value to represent generic type - google.protobuf.Timestamp expired_at = 6; - google.protobuf.Timestamp affected_at = 7; - string group_id = 8; // Optional group ID + string id = 1; + string actor = 2; + PermissionNodeActorType type = 3; + string key = 4; + google.protobuf.Value value = 5; // Using Value to represent generic type + google.protobuf.Timestamp expired_at = 6; + google.protobuf.Timestamp affected_at = 7; + string group_id = 8; // Optional group ID } message PermissionGroup { - string id = 1; - string name = 2; - google.protobuf.Timestamp created_at = 3; + string id = 1; + string name = 2; + google.protobuf.Timestamp created_at = 3; } message HasPermissionRequest { - string actor = 1; - string area = 2; - string key = 3; + string actor = 1; + string key = 2; + optional PermissionNodeActorType type = 3; } message HasPermissionResponse { - bool has_permission = 1; + bool has_permission = 1; } message GetPermissionRequest { - string actor = 1; - string area = 2; - string key = 3; + string actor = 1; + optional PermissionNodeActorType type = 2; + string key = 3; } message GetPermissionResponse { - google.protobuf.Value value = 1; // Using Value to represent generic type + google.protobuf.Value value = 1; // Using Value to represent generic type } message AddPermissionNodeRequest { - string actor = 1; - string area = 2; - string key = 3; - google.protobuf.Value value = 4; - google.protobuf.Timestamp expired_at = 5; - google.protobuf.Timestamp affected_at = 6; + string actor = 1; + optional PermissionNodeActorType type = 2; + string key = 3; + google.protobuf.Value value = 4; + google.protobuf.Timestamp expired_at = 5; + google.protobuf.Timestamp affected_at = 6; } message AddPermissionNodeResponse { - PermissionNode node = 1; + PermissionNode node = 1; } message AddPermissionNodeToGroupRequest { - PermissionGroup group = 1; - string actor = 2; - string area = 3; - string key = 4; - google.protobuf.Value value = 5; - google.protobuf.Timestamp expired_at = 6; - google.protobuf.Timestamp affected_at = 7; + PermissionGroup group = 1; + string actor = 2; + optional PermissionNodeActorType type = 3; + string key = 4; + google.protobuf.Value value = 5; + google.protobuf.Timestamp expired_at = 6; + google.protobuf.Timestamp affected_at = 7; } message AddPermissionNodeToGroupResponse { - PermissionNode node = 1; + PermissionNode node = 1; } message RemovePermissionNodeRequest { - string actor = 1; - string area = 2; - string key = 3; + string actor = 1; + optional PermissionNodeActorType type = 2; + string key = 3; } message RemovePermissionNodeResponse { - bool success = 1; + bool success = 1; } message RemovePermissionNodeFromGroupRequest { - PermissionGroup group = 1; - string actor = 2; - string area = 3; - string key = 4; + PermissionGroup group = 1; + string actor = 2; + string key = 4; } message RemovePermissionNodeFromGroupResponse { - bool success = 1; + bool success = 1; } service PermissionService { - rpc HasPermission(HasPermissionRequest) returns (HasPermissionResponse) {} - rpc GetPermission(GetPermissionRequest) returns (GetPermissionResponse) {} - rpc AddPermissionNode(AddPermissionNodeRequest) returns (AddPermissionNodeResponse) {} - rpc AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest) returns (AddPermissionNodeToGroupResponse) {} - rpc RemovePermissionNode(RemovePermissionNodeRequest) returns (RemovePermissionNodeResponse) {} - rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest) - returns (RemovePermissionNodeFromGroupResponse) {} + rpc HasPermission(HasPermissionRequest) returns (HasPermissionResponse) {} + rpc GetPermission(GetPermissionRequest) returns (GetPermissionResponse) {} + rpc AddPermissionNode(AddPermissionNodeRequest) returns (AddPermissionNodeResponse) {} + rpc AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest) returns (AddPermissionNodeToGroupResponse) {} + rpc RemovePermissionNode(RemovePermissionNodeRequest) returns (RemovePermissionNodeResponse) {} + rpc RemovePermissionNodeFromGroup(RemovePermissionNodeFromGroupRequest) + returns (RemovePermissionNodeFromGroupResponse) {} } diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index 2a16591..e38dd00 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -243,7 +243,7 @@ public partial class ChatController( [HttpPost("{roomId:guid}/messages")] [Authorize] - [RequiredPermission("global", "chat.messages.create")] + [AskPermission("chat.messages.create")] public async Task SendMessage([FromBody] SendMessageRequest request, Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index dc40018..7d52059 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -179,7 +179,7 @@ public class ChatRoomController( [HttpPost] [Authorize] - [RequiredPermission("global", "chat.create")] + [AskPermission("chat.create")] public async Task> CreateChatRoom(ChatRoomRequest request) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 37983bc..04f09ba 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -525,7 +525,7 @@ public class PostController( } [HttpPost] - [RequiredPermission("global", "posts.create")] + [AskPermission("posts.create")] public async Task> CreatePost( [FromBody] PostRequest request, [FromQuery(Name = "pub")] string? pubName @@ -725,7 +725,7 @@ public class PostController( [HttpPost("{id:guid}/reactions")] [Authorize] - [RequiredPermission("global", "posts.react")] + [AskPermission("posts.react")] public async Task> ReactPost( Guid id, [FromBody] PostReactionRequest request diff --git a/DysonNetwork.Sphere/Publisher/PublisherController.cs b/DysonNetwork.Sphere/Publisher/PublisherController.cs index 32e730e..d303896 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherController.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherController.cs @@ -341,7 +341,7 @@ public class PublisherController( [HttpPost("individual")] [Authorize] - [RequiredPermission("global", "publishers.create")] + [AskPermission("publishers.create")] public async Task> CreatePublisherIndividual( [FromBody] PublisherRequest request ) @@ -426,7 +426,7 @@ public class PublisherController( [HttpPost("organization/{realmSlug}")] [Authorize] - [RequiredPermission("global", "publishers.create")] + [AskPermission("publishers.create")] public async Task> CreatePublisherOrganization( string realmSlug, [FromBody] PublisherRequest request @@ -833,7 +833,7 @@ public class PublisherController( [HttpPost("{name}/features")] [Authorize] - [RequiredPermission("maintenance", "publishers.features")] + [AskPermission("publishers.features")] public async Task> AddPublisherFeature( string name, [FromBody] PublisherFeatureRequest request @@ -858,7 +858,7 @@ public class PublisherController( [HttpDelete("{name}/features/{flag}")] [Authorize] - [RequiredPermission("maintenance", "publishers.features")] + [AskPermission("publishers.features")] public async Task RemovePublisherFeature(string name, string flag) { var publisher = await db.Publishers.Where(p => p.Name == name).FirstOrDefaultAsync(); @@ -880,7 +880,7 @@ public class PublisherController( [HttpPost("rewards/settle")] [Authorize] - [RequiredPermission("maintenance", "publishers.reward.settle")] + [AskPermission("publishers.reward.settle")] public async Task PerformLotteryDraw() { await ps.SettlePublisherRewards(); diff --git a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs index f7af964..a619859 100644 --- a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs @@ -16,7 +16,7 @@ public static class ApplicationConfiguration app.UseWebSockets(); app.UseAuthentication(); app.UseAuthorization(); - app.UseMiddleware(); + app.UseMiddleware(); app.MapControllers(); diff --git a/DysonNetwork.Sphere/Sticker/StickerController.cs b/DysonNetwork.Sphere/Sticker/StickerController.cs index c4ac8c3..808ef83 100644 --- a/DysonNetwork.Sphere/Sticker/StickerController.cs +++ b/DysonNetwork.Sphere/Sticker/StickerController.cs @@ -120,7 +120,7 @@ public class StickerController( } [HttpPost] - [RequiredPermission("global", "stickers.packs.create")] + [AskPermission("stickers.packs.create")] public async Task> CreateStickerPack( [FromBody] StickerPackRequest request, [FromQuery(Name = "pub")] string publisherName @@ -334,7 +334,7 @@ public class StickerController( public const int MaxStickersPerPack = 24; [HttpPost("{packId:guid}/content")] - [RequiredPermission("global", "stickers.create")] + [AskPermission("stickers.create")] public async Task CreateSticker(Guid packId, [FromBody] StickerRequest request) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) diff --git a/DysonNetwork.Sphere/WebReader/WebReaderController.cs b/DysonNetwork.Sphere/WebReader/WebReaderController.cs index 0f6f266..c9843a5 100644 --- a/DysonNetwork.Sphere/WebReader/WebReaderController.cs +++ b/DysonNetwork.Sphere/WebReader/WebReaderController.cs @@ -59,7 +59,7 @@ public class WebReaderController(WebReaderService reader, ILogger [HttpDelete("link/cache")] [Authorize] - [RequiredPermission("maintenance", "cache.scrap")] + [AskPermission("cache.scrap")] public async Task InvalidateCache([FromQuery] string url) { if (string.IsNullOrEmpty(url)) @@ -76,7 +76,7 @@ public class WebReaderController(WebReaderService reader, ILogger [HttpDelete("cache/all")] [Authorize] - [RequiredPermission("maintenance", "cache.scrap")] + [AskPermission("cache.scrap")] public async Task InvalidateAllCache() { await reader.InvalidateAllCachedPreviewsAsync(); diff --git a/DysonNetwork.Zone/Startup/ApplicationConfiguration.cs b/DysonNetwork.Zone/Startup/ApplicationConfiguration.cs index 653e30a..b88c2cf 100644 --- a/DysonNetwork.Zone/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Zone/Startup/ApplicationConfiguration.cs @@ -14,7 +14,7 @@ public static class ApplicationConfiguration app.UseWebSockets(); app.UseAuthentication(); app.UseAuthorization(); - app.UseMiddleware(); + app.UseMiddleware(); app.MapControllers();