diff --git a/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs b/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs new file mode 100644 index 0000000..a450ff7 --- /dev/null +++ b/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using System.Collections.Generic; +using DysonNetwork.Drive; +using DysonNetwork.Shared.Data; +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.Drive.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250715080004_ReinitalMigration")] + partial class ReinitalMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property>("FileMeta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("file_meta"); + + b.Property("HasCompression") + .HasColumnType("boolean") + .HasColumnName("has_compression"); + + b.Property("Hash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("hash"); + + b.Property("IsMarkedRecycle") + .HasColumnType("boolean") + .HasColumnName("is_marked_recycle"); + + b.Property("MimeType") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("mime_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property>("SensitiveMarks") + .HasColumnType("jsonb") + .HasColumnName("sensitive_marks"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("StorageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("storage_id"); + + b.Property("StorageUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("storage_url"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("uploaded_at"); + + b.Property("UploadedTo") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("uploaded_to"); + + b.Property>("UserMeta") + .HasColumnType("jsonb") + .HasColumnName("user_meta"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", 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("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FileId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("file_id"); + + b.Property("ResourceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("resource_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("usage"); + + b.HasKey("Id") + .HasName("pk_file_references"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_file_references_file_id"); + + b.ToTable("file_references", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => + { + b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_file_references_files_file_id"); + + b.Navigation("File"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.cs b/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.cs new file mode 100644 index 0000000..da08cf3 --- /dev/null +++ b/DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Drive.Migrations +{ + /// + public partial class ReinitalMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>( + name: "file_meta", + table: "files", + type: "jsonb", + nullable: false, + oldClrType: typeof(Dictionary), + oldType: "jsonb", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn>( + name: "file_meta", + table: "files", + type: "jsonb", + nullable: true, + oldClrType: typeof(Dictionary), + oldType: "jsonb"); + } + } +} diff --git a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs index 62b7a87..039bdfc 100644 --- a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using DysonNetwork.Drive; -using DysonNetwork.Drive.Storage; using DysonNetwork.Shared.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -52,6 +51,7 @@ namespace DysonNetwork.Drive.Migrations .HasColumnName("description"); b.Property>("FileMeta") + .IsRequired() .HasColumnType("jsonb") .HasColumnName("file_meta"); diff --git a/DysonNetwork.Drive/Program.cs b/DysonNetwork.Drive/Program.cs index d538e61..a759e6f 100644 --- a/DysonNetwork.Drive/Program.cs +++ b/DysonNetwork.Drive/Program.cs @@ -17,7 +17,7 @@ builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); -builder.Services.AddDysonAuth(builder.Configuration); +builder.Services.AddDysonAuth(); builder.Services.AddAppFileStorage(builder.Configuration); diff --git a/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs b/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs new file mode 100644 index 0000000..847168a --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.Designer.cs @@ -0,0 +1,1967 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Pass; +using DysonNetwork.Pass.Account; +using DysonNetwork.Pass.Developer; +using DysonNetwork.Pass.Wallet; +using DysonNetwork.Shared.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250715075623_ReinitalMigration")] + partial class ReinitalMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", 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.Pass.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_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("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("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.Pass.Account.AccountAuthFactor", 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.Pass.Account.AccountBadge", 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.Pass.Account.AccountConnection", 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.Pass.Account.AccountContact", 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("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.Pass.Account.AccountProfile", 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("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("TimeZone") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("time_zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + 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.Pass.Account.ActionLog", 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("geometry") + .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.Pass.Account.CheckInResult", 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("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.Pass.Account.MagicSpell", 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.Pass.Account.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .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>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Subtitle") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("subtitle"); + + b.Property("Title") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("title"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("topic"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("viewed_at"); + + b.HasKey("Id") + .HasName("pk_notifications"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notifications_account_id"); + + b.ToTable("notifications", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.NotificationPushSubscription", 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(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_id"); + + b.Property("DeviceToken") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_token"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property("Provider") + .HasColumnType("integer") + .HasColumnName("provider"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_push_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notification_push_subscriptions_account_id"); + + b.HasIndex("DeviceToken", "DeviceId", "AccountId") + .IsUnique() + .HasDatabaseName("ix_notification_push_subscriptions_device_token_device_id_acco"); + + b.ToTable("notification_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", 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.Pass.Account.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + 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("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("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.Pass.Auth.AuthChallenge", 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") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("device_id"); + + 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("geometry") + .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.Pass.Auth.AuthSession", 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("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("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + 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("AppId") + .HasDatabaseName("ix_auth_sessions_app_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Developer.CustomApp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("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") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Links") + .HasColumnType("jsonb") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("OauthConfig") + .HasColumnType("jsonb") + .HasColumnName("oauth_config"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_custom_apps"); + + b.ToTable("custom_apps", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Developer.CustomAppSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_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("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("IsOidc") + .HasColumnType("boolean") + .HasColumnName("is_oidc"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("secret"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_custom_app_secrets"); + + b.HasIndex("AppId") + .HasDatabaseName("ix_custom_app_secrets_app_id"); + + b.ToTable("custom_app_secrets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", 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.Pass.Permission.PermissionGroupMember", 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.Pass.Permission.PermissionNode", 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("Area") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("area"); + + 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("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", "Area", "Actor") + .HasDatabaseName("ix_permission_nodes_key_area_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Coupon", 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.Pass.Wallet.Order", 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("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.Pass.Wallet.Subscription", 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.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", 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.Pass.Wallet.Wallet", 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.Pass.Wallet.WalletPocket", 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.Pass.Account.AbuseReport", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountBadge", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountConnection", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountContact", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.AccountProfile", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Pass.Account.AccountProfile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.ActionLog", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.CheckInResult", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.MagicSpell", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Notification", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifications_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.NotificationPushSubscription", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notification_push_subscriptions_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Relationship", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Account.Account", "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.Pass.Account.Status", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthChallenge", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Developer.CustomApp", "App") + .WithMany() + .HasForeignKey("AppId") + .HasConstraintName("fk_auth_sessions_custom_apps_app_id"); + + b.HasOne("DysonNetwork.Pass.Auth.AuthChallenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("App"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Developer.CustomAppSecret", b => + { + b.HasOne("DysonNetwork.Pass.Developer.CustomApp", "App") + .WithMany("Secrets") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); + + b.Navigation("App"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionNode", b => + { + b.HasOne("DysonNetwork.Pass.Permission.PermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Order", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Subscription", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Transaction", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + { + b.HasOne("DysonNetwork.Pass.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.WalletPocket", b => + { + b.HasOne("DysonNetwork.Pass.Wallet.Wallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Account.Account", 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.Pass.Developer.CustomApp", b => + { + b.Navigation("Secrets"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Permission.PermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Pass.Wallet.Wallet", b => + { + b.Navigation("Pockets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.cs b/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.cs new file mode 100644 index 0000000..b649b94 --- /dev/null +++ b/DysonNetwork.Pass/Migrations/20250715075623_ReinitalMigration.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Pass.Migrations +{ + /// + public partial class ReinitalMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "background_id", + table: "account_profiles"); + + migrationBuilder.DropColumn( + name: "picture_id", + table: "account_profiles"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "background_id", + table: "account_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: true); + + migrationBuilder.AddColumn( + name: "picture_id", + table: "account_profiles", + type: "character varying(32)", + maxLength: 32, + nullable: true); + } + } +} diff --git a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs index cfd2d9d..1dae7ff 100644 --- a/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Pass/Migrations/AppDatabaseModelSnapshot.cs @@ -392,11 +392,6 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("jsonb") .HasColumnName("background"); - b.Property("BackgroundId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("background_id"); - b.Property("Bio") .HasMaxLength(4096) .HasColumnType("character varying(4096)") @@ -451,11 +446,6 @@ namespace DysonNetwork.Pass.Migrations .HasColumnType("jsonb") .HasColumnName("picture"); - b.Property("PictureId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("picture_id"); - b.Property("Pronouns") .HasMaxLength(1024) .HasColumnType("character varying(1024)") diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs index 56c449e..e1cec59 100644 --- a/DysonNetwork.Pass/Program.cs +++ b/DysonNetwork.Pass/Program.cs @@ -1,5 +1,4 @@ using DysonNetwork.Pass; -using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Startup; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Registry; diff --git a/DysonNetwork.Sphere/Safety/AbuseReportController.cs b/DysonNetwork.Pass/Safety/AbuseReportController.cs similarity index 91% rename from DysonNetwork.Sphere/Safety/AbuseReportController.cs rename to DysonNetwork.Pass/Safety/AbuseReportController.cs index fdaba61..d26675a 100644 --- a/DysonNetwork.Sphere/Safety/AbuseReportController.cs +++ b/DysonNetwork.Pass/Safety/AbuseReportController.cs @@ -1,10 +1,10 @@ using System.ComponentModel.DataAnnotations; -using DysonNetwork.Sphere.Account; -using DysonNetwork.Sphere.Permission; +using DysonNetwork.Pass.Account; +using DysonNetwork.Pass.Permission; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace DysonNetwork.Sphere.Safety; +namespace DysonNetwork.Pass.Safety; [ApiController] [Route("/api/safety/reports")] @@ -30,7 +30,7 @@ public class AbuseReportController( [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateReport([FromBody] CreateReportRequest request) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); try { @@ -75,7 +75,7 @@ public class AbuseReportController( [FromQuery] bool includeResolved = false ) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved); var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved); @@ -101,7 +101,7 @@ public class AbuseReportController( [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> GetMyReportById(Guid id) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); var report = await safety.GetReportById(id); if (report == null) return NotFound(); diff --git a/DysonNetwork.Sphere/Safety/SafetyService.cs b/DysonNetwork.Pass/Safety/SafetyService.cs similarity index 97% rename from DysonNetwork.Sphere/Safety/SafetyService.cs rename to DysonNetwork.Pass/Safety/SafetyService.cs index f74a501..c6ee055 100644 --- a/DysonNetwork.Sphere/Safety/SafetyService.cs +++ b/DysonNetwork.Pass/Safety/SafetyService.cs @@ -1,8 +1,8 @@ -using DysonNetwork.Sphere.Account; +using DysonNetwork.Pass.Account; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace DysonNetwork.Sphere.Safety; +namespace DysonNetwork.Pass.Safety; public class SafetyService(AppDatabase db, ILogger logger) { diff --git a/DysonNetwork.Pass/Wallet/PaymentService.cs b/DysonNetwork.Pass/Wallet/PaymentService.cs index abaf064..98d6e50 100644 --- a/DysonNetwork.Pass/Wallet/PaymentService.cs +++ b/DysonNetwork.Pass/Wallet/PaymentService.cs @@ -217,7 +217,7 @@ public class PaymentService( Title = localizer["OrderPaidTitle", $"#{readableOrderId}"], Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency, readableOrderRemark], - IsSavable = false + IsSavable = true } } ); diff --git a/DysonNetwork.Pass/Wallet/SubscriptionService.cs b/DysonNetwork.Pass/Wallet/SubscriptionService.cs index f6e625e..bd71d7b 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionService.cs +++ b/DysonNetwork.Pass/Wallet/SubscriptionService.cs @@ -360,7 +360,7 @@ public class SubscriptionService( Topic = "subscriptions.begun", Title = localizer["SubscriptionAppliedTitle", humanReadableName], Body = localizer["SubscriptionAppliedBody", duration, humanReadableName], - IsSavable = false, + IsSavable = true }; notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString())); await pusher.SendPushNotificationToUserAsync( diff --git a/DysonNetwork.Pusher/Program.cs b/DysonNetwork.Pusher/Program.cs index c3e1599..150912c 100644 --- a/DysonNetwork.Pusher/Program.cs +++ b/DysonNetwork.Pusher/Program.cs @@ -15,7 +15,7 @@ builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); -builder.Services.AddDysonAuth(builder.Configuration); +builder.Services.AddDysonAuth(); // Add flush handlers and websocket handlers builder.Services.AddAppFlushHandlers(); diff --git a/DysonNetwork.Shared/Auth/Startup.cs b/DysonNetwork.Shared/Auth/Startup.cs index 7da4185..103aac2 100644 --- a/DysonNetwork.Shared/Auth/Startup.cs +++ b/DysonNetwork.Shared/Auth/Startup.cs @@ -8,8 +8,7 @@ namespace DysonNetwork.Shared.Auth; public static class DysonAuthStartup { public static IServiceCollection AddDysonAuth( - this IServiceCollection services, - IConfiguration configuration + this IServiceCollection services ) { services.AddSingleton(sp => diff --git a/DysonNetwork.Sphere/Chat/ChatRoom.cs b/DysonNetwork.Sphere/Chat/ChatRoom.cs index c37684c..6322029 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoom.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoom.cs @@ -74,7 +74,7 @@ public class ChatMember : ModelBase public Guid ChatRoomId { get; set; } public ChatRoom ChatRoom { get; set; } = null!; public Guid AccountId { get; set; } - public Account Account { get; set; } = null!; + [NotMapped] public Account Account { get; set; } = null!; [MaxLength(1024)] public string? Nick { get; set; } @@ -106,7 +106,7 @@ public class ChatMemberTransmissionObject : ModelBase public Guid Id { get; set; } public Guid ChatRoomId { get; set; } public Guid AccountId { get; set; } - public Account Account { get; set; } = null!; + [NotMapped] public Account Account { get; set; } = null!; [MaxLength(1024)] public string? Nick { get; set; } diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index c5304d9..23535cc 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -2,10 +2,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using DysonNetwork.Shared; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; using DysonNetwork.Sphere.Localization; -using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Realm; using Grpc.Core; using Microsoft.AspNetCore.Authorization; @@ -962,7 +962,7 @@ public class ChatRoomController( Topic = "invites.chats", Title = title, Body = body, - IsSavable = false + IsSavable = true } } ); diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs.bak b/DysonNetwork.Sphere/Chat/ChatRoomController.cs.bak deleted file mode 100644 index f5d702c..0000000 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs.bak +++ /dev/null @@ -1,947 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using DysonNetwork.Shared.Data; -using DysonNetwork.Shared.Proto; -using DysonNetwork.Sphere.Localization; -using DysonNetwork.Sphere.Permission; -using DysonNetwork.Sphere.Realm; -using Grpc.Core; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Localization; -using NodaTime; - -namespace DysonNetwork.Sphere.Chat; - -[ApiController] -[Route("/api/chat")] -public class ChatRoomController( - AppDatabase db, - ChatRoomService crs, - RealmService rs, - IStringLocalizer localizer, - AccountService.AccountServiceClient accounts, - FileService.FileServiceClient files, - FileReferenceService.FileReferenceServiceClient fileRefs, - ActionLogService.ActionLogServiceClient als -) : ControllerBase -{ - [HttpGet("{id:guid}")] - public async Task> GetChatRoom(Guid id) - { - var chatRoom = await db.ChatRooms - .Where(c => c.Id == id) - .Include(e => e.Realm) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom); - - if (HttpContext.Items["CurrentUser"] is Account currentUser) - chatRoom = await crs.LoadDirectMessageMembers(chatRoom, Guid.Parse(currentUser.Id)); - - return Ok(chatRoom); - } - - [HttpGet] - [Authorize] - public async Task>> ListJoinedChatRooms() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return Unauthorized(); - var accountId = Guid.Parse(currentUser.Id); - - var chatRooms = await db.ChatMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.JoinedAt != null) - .Where(m => m.LeaveAt == null) - .Include(m => m.ChatRoom) - .Select(m => m.ChatRoom) - .ToListAsync(); - chatRooms = await crs.LoadDirectMessageMembers(chatRooms, accountId); - chatRooms = await crs.SortChatRoomByLastMessage(chatRooms); - - return Ok(chatRooms); - } - - public class DirectMessageRequest - { - [Required] public Guid RelatedUserId { get; set; } - } - - [HttpPost("direct")] - [Authorize] - public async Task> CreateDirectMessage([FromBody] DirectMessageRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) - return Unauthorized(); - - var relatedUser = await accounts.GetAccountAsync( - new GetAccountRequest { Id = request.RelatedUserId.ToString() } - ); - if (relatedUser is null) - return BadRequest("Related user was not found"); - - var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest() - { - AccountId = currentUser.Id, - RelatedId = request.RelatedUserId.ToString(), - Status = -100 - }); - if (hasBlocked?.Value ?? false) - return StatusCode(403, "You cannot create direct message with a user that blocked you."); - - // Check if DM already exists between these users - var existingDm = await db.ChatRooms - .Include(c => c.Members) - .Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2) - .Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id))) - .Where(c => c.Members.Any(m => m.AccountId == request.RelatedUserId)) - .FirstOrDefaultAsync(); - - if (existingDm != null) - return BadRequest("You already have a DM with this user."); - - // Create new DM chat room - var dmRoom = new ChatRoom - { - Type = ChatRoomType.DirectMessage, - IsPublic = false, - Members = new List - { - new() - { - AccountId = Guid.Parse(currentUser.Id), - Role = ChatMemberRole.Owner, - JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow) - }, - new() - { - AccountId = request.RelatedUserId, - Role = ChatMemberRole.Member, - JoinedAt = null, // Pending status - } - } - }; - - db.ChatRooms.Add(dmRoom); - await db.SaveChangesAsync(); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.create", - Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(dmRoom.Id.ToString()) } }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId); - invitedMember.ChatRoom = dmRoom; - await _SendInviteNotify(invitedMember, currentUser); - - return Ok(dmRoom); - } - - [HttpGet("direct/{accountId:guid}")] - [Authorize] - public async Task> GetDirectChatRoom(Guid accountId) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) - return Unauthorized(); - - var room = await db.ChatRooms - .Include(c => c.Members) - .Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2) - .Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id))) - .Where(c => c.Members.Any(m => m.AccountId == accountId)) - .FirstOrDefaultAsync(); - if (room is null) return NotFound(); - - return Ok(room); - } - - public class ChatRoomRequest - { - [Required] [MaxLength(1024)] public string? Name { get; set; } - [MaxLength(4096)] public string? Description { get; set; } - [MaxLength(32)] public string? PictureId { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - public Guid? RealmId { get; set; } - public bool? IsCommunity { get; set; } - public bool? IsPublic { get; set; } - } - - [HttpPost] - [Authorize] - [RequiredPermission("global", "chat.create")] - public async Task> CreateChatRoom(ChatRoomRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); - if (request.Name is null) return BadRequest("You cannot create a chat room without a name."); - - var chatRoom = new ChatRoom - { - Name = request.Name, - Description = request.Description ?? string.Empty, - IsCommunity = request.IsCommunity ?? false, - IsPublic = request.IsPublic ?? false, - Type = ChatRoomType.Group, - Members = new List - { - new() - { - Role = ChatMemberRole.Owner, - AccountId = Guid.Parse(currentUser.Id), - JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) - } - } - }; - - if (request.RealmId is not null) - { - if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id), - RealmMemberRole.Moderator)) - return StatusCode(403, "You need at least be a moderator to create chat linked to the realm."); - chatRoom.RealmId = request.RealmId; - } - - if (request.PictureId is not null) - { - try - { - var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); - if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse); - - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = fileResponse.Id, - Usage = "chatroom.picture", - ResourceId = chatRoom.ResourceIdentifier, - }); - } - catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) - { - return BadRequest("Invalid picture id, unable to find the file on cloud."); - } - } - - if (request.BackgroundId is not null) - { - try - { - var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); - if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud."); - chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse); - - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = fileResponse.Id, - Usage = "chatroom.background", - ResourceId = chatRoom.ResourceIdentifier, - }); - } - catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) - { - return BadRequest("Invalid background id, unable to find the file on cloud."); - } - } - - db.ChatRooms.Add(chatRoom); - await db.SaveChangesAsync(); - - var chatRoomResourceId = $"chatroom:{chatRoom.Id}"; - - if (chatRoom.Picture is not null) - { - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = chatRoom.Picture.Id, - Usage = "chat.room.picture", - ResourceId = chatRoomResourceId - }); - } - - if (chatRoom.Background is not null) - { - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = chatRoom.Background.Id, - Usage = "chat.room.background", - ResourceId = chatRoomResourceId - }); - } - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.create", - Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return Ok(chatRoom); - } - - - [HttpPatch("{id:guid}")] - public async Task> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(e => e.Id == id) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - if (chatRoom.RealmId is not null) - { - if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id), - RealmMemberRole.Moderator)) - return StatusCode(403, "You need at least be a realm moderator to update the chat."); - } - else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator)) - return StatusCode(403, "You need at least be a moderator to update the chat."); - - if (request.RealmId is not null) - { - var member = await db.RealmMembers - .Where(m => m.AccountId == Guid.Parse(currentUser.Id)) - .Where(m => m.RealmId == request.RealmId) - .FirstOrDefaultAsync(); - if (member is null || member.Role < RealmMemberRole.Moderator) - return StatusCode(403, "You need at least be a moderator to transfer the chat linked to the realm."); - chatRoom.RealmId = member.RealmId; - } - - if (request.PictureId is not null) - { - try - { - var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); - if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - - // Remove old references for pictures - await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest - { - ResourceId = chatRoom.ResourceIdentifier, - Usage = "chat.room.picture" - }); - - // Add a new reference - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = fileResponse.Id, - Usage = "chat.room.picture", - ResourceId = chatRoom.ResourceIdentifier - }); - - chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse); - } - catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) - { - return BadRequest("Invalid picture id, unable to find the file on cloud."); - } - } - - if (request.BackgroundId is not null) - { - try - { - var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); - if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud."); - - // Remove old references for backgrounds - await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest - { - ResourceId = chatRoom.ResourceIdentifier, - Usage = "chat.room.background" - }); - - // Add a new reference - await fileRefs.CreateReferenceAsync(new CreateReferenceRequest - { - FileId = fileResponse.Id, - Usage = "chat.room.background", - ResourceId = chatRoom.ResourceIdentifier - }); - - chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse); - } - catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) - { - return BadRequest("Invalid background id, unable to find the file on cloud."); - } - } - - if (request.Name is not null) - chatRoom.Name = request.Name; - if (request.Description is not null) - chatRoom.Description = request.Description; - if (request.IsCommunity is not null) - chatRoom.IsCommunity = request.IsCommunity.Value; - if (request.IsPublic is not null) - chatRoom.IsPublic = request.IsPublic.Value; - - db.ChatRooms.Update(chatRoom); - await db.SaveChangesAsync(); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.update", - Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return Ok(chatRoom); - } - - [HttpDelete("{id:guid}")] - public async Task DeleteChatRoom(Guid id) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(e => e.Id == id) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - if (chatRoom.RealmId is not null) - { - if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id), - RealmMemberRole.Moderator)) - return StatusCode(403, "You need at least be a realm moderator to delete the chat."); - } - else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner)) - return StatusCode(403, "You need at least be the owner to delete the chat."); - - var chatRoomResourceId = $"chatroom:{chatRoom.Id}"; - - // Delete all file references for this chat room - await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest - { - ResourceId = chatRoomResourceId - }); - - db.ChatRooms.Remove(chatRoom); - await db.SaveChangesAsync(); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.delete", - Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return NoContent(); - } - - [HttpGet("{roomId:guid}/members/me")] - [Authorize] - public async Task> GetRoomIdentity(Guid roomId) - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) - return Unauthorized(); - - var member = await db.ChatMembers - .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId) - .Include(m => m.Account) - .Include(m => m.Account.Profile) - .FirstOrDefaultAsync(); - - if (member == null) - return NotFound(); - - return Ok(member); - } - - [HttpGet("{roomId:guid}/members")] - public async Task>> ListMembers(Guid roomId, [FromQuery] int take = 20, - [FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null) - { - var currentUser = HttpContext.Items["CurrentUser"] as Shared.Proto.Account; - - var room = await db.ChatRooms - .FirstOrDefaultAsync(r => r.Id == roomId); - if (room is null) return NotFound(); - - if (!room.IsPublic) - { - if (currentUser is null) return Unauthorized(); - var member = await db.ChatMembers - .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id)); - if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room."); - } - - IQueryable query = db.ChatMembers - .Where(m => m.ChatRoomId == roomId) - .Where(m => m.LeaveAt == null) // Add this condition to exclude left members - .Include(m => m.Account) - .Include(m => m.Account.Profile); - - // if (withStatus) - // { - // var members = await query - // .OrderBy(m => m.JoinedAt) - // .ToListAsync(); - // - // var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); - // - // if (!string.IsNullOrEmpty(status)) - // { - // members = members.Where(m => - // memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && - // s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); - // } - // - // members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) - // .ToList(); - // - // var total = members.Count; - // Response.Headers.Append("X-Total", total.ToString()); - // - // var result = members.Skip(skip).Take(take).ToList(); - // - // return Ok(result); - // } - // else - // { - var total = await query.CountAsync(); - Response.Headers.Append("X-Total", total.ToString()); - - var members = await query - .OrderBy(m => m.JoinedAt) - .Skip(skip) - .Take(take) - .ToListAsync(); - - return Ok(members); - // } - } - - - public class ChatMemberRequest - { - [Required] public Guid RelatedUserId { get; set; } - [Required] public int Role { get; set; } - } - - [HttpPost("invites/{roomId:guid}")] - [Authorize] - public async Task> InviteMember(Guid roomId, - [FromBody] ChatMemberRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var accountId = Guid.Parse(currentUser.Id); - - // Get related user account - var relatedUser = await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() }); - if (relatedUser == null) return BadRequest("Related user was not found"); - - // Check if the user has blocked the current user - var relationship = await accounts.GetRelationshipAsync(new GetRelationshipRequest - { - AccountId = currentUser.Id, - RelatedId = relatedUser.Id, - Status = -100 - }); - - if (relationship != null && relationship.Relationship.Status == -100) - return StatusCode(403, "You cannot invite a user that blocked you."); - - var chatRoom = await db.ChatRooms - .Where(p => p.Id == roomId) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - // Handle realm-owned chat rooms - if (chatRoom.RealmId is not null) - { - var realmMember = await db.RealmMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.RealmId == chatRoom.RealmId) - .FirstOrDefaultAsync(); - if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) - return StatusCode(403, "You need at least be a realm moderator to invite members to this chat."); - } - else - { - var chatMember = await db.ChatMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.ChatRoomId == roomId) - .FirstOrDefaultAsync(); - if (chatMember is null) return StatusCode(403, "You are not even a member of the targeted chat room."); - if (chatMember.Role < ChatMemberRole.Moderator) - return StatusCode(403, - "You need at least be a moderator to invite other members to this chat room."); - if (chatMember.Role < request.Role) - return StatusCode(403, "You cannot invite member with higher permission than yours."); - } - - var hasExistingMember = await db.ChatMembers - .Where(m => m.AccountId == request.RelatedUserId) - .Where(m => m.ChatRoomId == roomId) - .Where(m => m.LeaveAt == null) - .AnyAsync(); - if (hasExistingMember) - return BadRequest("This user has been joined the chat cannot be invited again."); - - var newMember = new ChatMember - { - AccountId = Guid.Parse(relatedUser.Id), - ChatRoomId = roomId, - Role = request.Role, - }; - - db.ChatMembers.Add(newMember); - await db.SaveChangesAsync(); - - newMember.ChatRoom = chatRoom; - await _SendInviteNotify(newMember, currentUser); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.invite", - Meta = - { - { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) }, - { "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(relatedUser.Id.ToString()) } - }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return Ok(newMember); - } - - [HttpGet("invites")] - [Authorize] - public async Task>> ListChatInvites() - { - if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized(); - var accountId = Guid.Parse(currentUser.Id); - - var members = await db.ChatMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.JoinedAt == null) - .Include(e => e.ChatRoom) - .Include(e => e.Account) - .Include(e => e.Account.Profile) - .ToListAsync(); - - var chatRooms = members.Select(m => m.ChatRoom).ToList(); - var directMembers = - (await crs.LoadDirectMessageMembers(chatRooms, accountId)).ToDictionary(c => c.Id, c => c.Members); - - foreach (var member in members.Where(member => member.ChatRoom.Type == ChatRoomType.DirectMessage)) - member.ChatRoom.Members = directMembers[member.ChatRoom.Id]; - - return members.ToList(); - } - - [HttpPost("invites/{roomId:guid}/accept")] - [Authorize] - public async Task> AcceptChatInvite(Guid roomId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var accountId = Guid.Parse(currentUser.Id); - - var member = await db.ChatMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.ChatRoomId == roomId) - .Where(m => m.JoinedAt == null) - .FirstOrDefaultAsync(); - if (member is null) return NotFound(); - - member.JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); - db.Update(member); - await db.SaveChangesAsync(); - _ = crs.PurgeRoomMembersCache(roomId); - - als.CreateActionLogFromRequest( - ActionLogType.ChatroomJoin, - new Dictionary { { "chatroom_id", roomId } }, Request - ); - - return Ok(member); - } - - [HttpPost("invites/{roomId:guid}/decline")] - [Authorize] - public async Task DeclineChatInvite(Guid roomId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var accountId = Guid.Parse(currentUser.Id); - - var member = await db.ChatMembers - .Where(m => m.AccountId == accountId) - .Where(m => m.ChatRoomId == roomId) - .Where(m => m.JoinedAt == null) - .FirstOrDefaultAsync(); - if (member is null) return NotFound(); - - member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); - await db.SaveChangesAsync(); - - return NoContent(); - } - - public class ChatMemberNotifyRequest - { - public ChatMemberNotify? NotifyLevel { get; set; } - public Instant? BreakUntil { get; set; } - } - - [HttpPatch("{roomId:guid}/members/me/notify")] - [Authorize] - public async Task> UpdateChatMemberNotify( - Guid roomId, - [FromBody] ChatMemberNotifyRequest request - ) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(r => r.Id == roomId) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - var accountId = Guid.Parse(currentUser.Id); - var targetMember = await db.ChatMembers - .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId) - .FirstOrDefaultAsync(); - if (targetMember is null) return BadRequest("You have not joined this chat room."); - if (request.NotifyLevel is not null) - targetMember.Notify = request.NotifyLevel.Value; - if (request.BreakUntil is not null) - targetMember.BreakUntil = request.BreakUntil.Value; - - db.ChatMembers.Update(targetMember); - await db.SaveChangesAsync(); - - await crs.PurgeRoomMembersCache(roomId); - - return Ok(targetMember); - } - - [HttpPatch("{roomId:guid}/members/{memberId:guid}/role")] - [Authorize] - public async Task> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole) - { - if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role."); - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(r => r.Id == roomId) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - // Check if the chat room is owned by a realm - if (chatRoom.RealmId is not null) - { - var realmMember = await db.RealmMembers - .Where(m => m.AccountId == Guid.Parse(currentUser.Id)) - .Where(m => m.RealmId == chatRoom.RealmId) - .FirstOrDefaultAsync(); - if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) - return StatusCode(403, "You need at least be a realm moderator to change member roles."); - } - else - { - var targetMember = await db.ChatMembers - .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId) - .FirstOrDefaultAsync(); - if (targetMember is null) return NotFound(); - - // Check if the current user has permission to change roles - if ( - !await crs.IsMemberWithRole( - chatRoom.Id, - Guid.Parse(currentUser.Id), - ChatMemberRole.Moderator, - targetMember.Role, - newRole - ) - ) - return StatusCode(403, "You don't have enough permission to edit the roles of members."); - - targetMember.Role = newRole; - db.ChatMembers.Update(targetMember); - await db.SaveChangesAsync(); - - await crs.PurgeRoomMembersCache(roomId); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.role.edit", - Meta = - { - { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) }, - { "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) }, - { "new_role", Google.Protobuf.WellKnownTypes.Value.ForNumber(newRole) } - }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return Ok(targetMember); - } - - return BadRequest(); - } - - [HttpDelete("{roomId:guid}/members/{memberId:guid}")] - [Authorize] - public async Task RemoveChatMember(Guid roomId, Guid memberId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(r => r.Id == roomId) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - - // Check if the chat room is owned by a realm - if (chatRoom.RealmId is not null) - { - if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id), - RealmMemberRole.Moderator)) - return StatusCode(403, "You need at least be a realm moderator to remove members."); - } - else - { - if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator)) - return StatusCode(403, "You need at least be a moderator to remove members."); - - // Find the target member - var member = await db.ChatMembers - .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId) - .FirstOrDefaultAsync(); - if (member is null) return NotFound(); - - // Check if the current user has sufficient permissions - if (!await crs.IsMemberWithRole(chatRoom.Id, memberId, member.Role)) - return StatusCode(403, "You cannot remove members with equal or higher roles."); - - member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); - await db.SaveChangesAsync(); - _ = crs.PurgeRoomMembersCache(roomId); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.kick", - Meta = - { - { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) }, - { "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) } - }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return NoContent(); - } - - return BadRequest(); - } - - - [HttpPost("{roomId:guid}/members/me")] - [Authorize] - public async Task> JoinChatRoom(Guid roomId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var chatRoom = await db.ChatRooms - .Where(r => r.Id == roomId) - .FirstOrDefaultAsync(); - if (chatRoom is null) return NotFound(); - if (!chatRoom.IsCommunity) - return StatusCode(403, "This chat room isn't a community. You need an invitation to join."); - - var existingMember = await db.ChatMembers - .FirstOrDefaultAsync(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId); - if (existingMember != null) - return BadRequest("You are already a member of this chat room."); - - var newMember = new ChatMember - { - AccountId = Guid.Parse(currentUser.Id), - ChatRoomId = roomId, - Role = ChatMemberRole.Member, - JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) - }; - - db.ChatMembers.Add(newMember); - await db.SaveChangesAsync(); - _ = crs.PurgeRoomMembersCache(roomId); - - als.CreateActionLogFromRequest( - ActionLogType.ChatroomJoin, - new Dictionary { { "chatroom_id", roomId } }, Request - ); - - return Ok(chatRoom); - } - - [HttpDelete("{roomId:guid}/members/me")] - [Authorize] - public async Task LeaveChat(Guid roomId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var member = await db.ChatMembers - .Where(m => m.AccountId == Guid.Parse(currentUser.Id)) - .Where(m => m.ChatRoomId == roomId) - .FirstOrDefaultAsync(); - if (member is null) return NotFound(); - - if (member.Role == ChatMemberRole.Owner) - { - // Check if this is the only owner - var otherOwners = await db.ChatMembers - .Where(m => m.ChatRoomId == roomId) - .Where(m => m.Role == ChatMemberRole.Owner) - .Where(m => m.AccountId != Guid.Parse(currentUser.Id)) - .AnyAsync(); - - if (!otherOwners) - return BadRequest("The last owner cannot leave the chat. Transfer ownership first or delete the chat."); - } - - member.LeaveAt = Instant.FromDateTimeUtc(DateTime.UtcNow); - await db.SaveChangesAsync(); - await crs.PurgeRoomMembersCache(roomId); - - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = "chatrooms.leave", - Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) } }, - AccountId = currentUser.Id, - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); - - return NoContent(); - } - - private async Task _SendInviteNotify(ChatMember member, Account sender) - { - string title = localizer["ChatInviteTitle"]; - - string body = member.ChatRoom.Type == ChatRoomType.DirectMessage - ? localizer["ChatInviteDirectBody", sender.Nick] - : localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"]; - - AccountService.SetCultureInfo(member.Account); - await nty.SendNotification(member.Account, "invites.chats", title, null, body, actionUri: "/chat"); - } -} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs index af40154..465297e 100644 --- a/DysonNetwork.Sphere/Chat/ChatService.cs +++ b/DysonNetwork.Sphere/Chat/ChatService.cs @@ -233,6 +233,7 @@ public partial class ChatService( Body = !string.IsNullOrEmpty(message.Content) ? message.Content[..Math.Min(message.Content.Length, 100)] : "", + IsSavable = false }; notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(metaDict)); diff --git a/DysonNetwork.Sphere/Developer/DeveloperController.cs b/DysonNetwork.Sphere/Developer/DeveloperController.cs index 2f251ea..79e43c5 100644 --- a/DysonNetwork.Sphere/Developer/DeveloperController.cs +++ b/DysonNetwork.Sphere/Developer/DeveloperController.cs @@ -1,5 +1,5 @@ +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Proto; -using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Publisher; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 3af0f0c..00b5407 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Content; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; -using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Publisher; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 452e629..8471b02 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -1,3 +1,5 @@ +using DysonNetwork.Shared.Auth; +using DysonNetwork.Shared.Registry; using DysonNetwork.Sphere; using DysonNetwork.Sphere.Startup; using Microsoft.EntityFrameworkCore; @@ -12,13 +14,15 @@ builder.ConfigureAppKestrel(); builder.Services.AddAppMetrics(); // Add application services +builder.Services.AddRegistryService(builder.Configuration); builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); - -// Add file storage -builder.Services.AddAppFileStorage(builder.Configuration); +builder.Services.AddDysonAuth(); +builder.Services.AddAccountService(); +builder.Services.AddPusherService(); +builder.Services.AddDriveService(); // Add flush handlers and websocket handlers builder.Services.AddAppFlushHandlers(); @@ -38,10 +42,7 @@ using (var scope = app.Services.CreateScope()) await db.Database.MigrateAsync(); } -// Get the TusDiskStore instance -var tusDiskStore = app.Services.GetRequiredService(); - // Configure application middleware pipeline -app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); +app.ConfigureAppMiddleware(builder.Configuration); app.Run(); \ No newline at end of file diff --git a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs index 36585ba..517b00a 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs @@ -1,7 +1,8 @@ -using DysonNetwork.Sphere.Account; +using DysonNetwork.Shared; +using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Proto; using DysonNetwork.Sphere.Localization; using DysonNetwork.Sphere.Post; -using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; @@ -9,10 +10,11 @@ namespace DysonNetwork.Sphere.Publisher; public class PublisherSubscriptionService( AppDatabase db, - NotificationService nty, PostService ps, IStringLocalizer localizer, - ICacheService cache + ICacheService cache, + PusherService.PusherServiceClient pusher, + AccountService.AccountServiceClient accounts ) { /// @@ -50,7 +52,6 @@ public class PublisherSubscriptionService( public async Task NotifySubscriberPost(Post.Post post) { var subscribers = await db.PublisherSubscriptions - .Include(p => p.Account) .Where(p => p.PublisherId == post.PublisherId && p.Status == PublisherSubscriptionStatus.Active) .ToListAsync(); @@ -67,23 +68,35 @@ public class PublisherSubscriptionService( { "publisher_id", post.Publisher.Id.ToString() } }; + + var queryRequest = new GetAccountBatchRequest(); + queryRequest.Id.AddRange(subscribers.DistinctBy(s => s.AccountId).Select(m => m.AccountId.ToString())); + var queryResponse = await accounts.GetAccountBatchAsync(queryRequest); + + var notification = new PushNotification + { + Topic = "posts.new", + Title = localizer["PostSubscriptionTitle", post.Publisher.Name, title], + Body = message, + IsSavable = true, + ActionUri = $"/posts/{post.Id}" + }; + notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(data)); + // Notify each subscriber var notifiedCount = 0; - foreach (var subscription in subscribers.DistinctBy(s => s.AccountId)) + foreach (var target in queryResponse.Accounts) { try { - AccountService.SetCultureInfo(subscription.Account); - await nty.SendNotification( - subscription.Account, - "posts.new", - localizer["PostSubscriptionTitle", post.Publisher.Name, title], - null, - message, - data, - actionUri: $"/posts/{post.Id}" + CultureService.SetCultureInfo(target); + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = target.Id, + Notification = notification + } ); - notifiedCount++; } catch (Exception) @@ -117,7 +130,6 @@ public class PublisherSubscriptionService( public async Task> GetPublisherSubscribersAsync(Guid publisherId) { return await db.PublisherSubscriptions - .Include(ps => ps.Account) .Where(ps => ps.PublisherId == publisherId && ps.Status == PublisherSubscriptionStatus.Active) .ToListAsync(); } diff --git a/DysonNetwork.Sphere/Realm/Realm.cs b/DysonNetwork.Sphere/Realm/Realm.cs index fdd5d8c..6b41e8f 100644 --- a/DysonNetwork.Sphere/Realm/Realm.cs +++ b/DysonNetwork.Sphere/Realm/Realm.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; +using DysonNetwork.Shared.Data; using DysonNetwork.Sphere.Chat; -using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -25,7 +25,7 @@ public class Realm : ModelBase, IIdentifiedResource [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } - [Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; } + [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; } [JsonIgnore] public ICollection Members { get; set; } = new List(); [JsonIgnore] public ICollection ChatRooms { get; set; } = new List(); @@ -48,7 +48,6 @@ public class RealmMember : ModelBase public Guid RealmId { get; set; } public Realm Realm { get; set; } = null!; public Guid AccountId { get; set; } - public Account Account { get; set; } = null!; public int Role { get; set; } = RealmMemberRole.Normal; public Instant? JoinedAt { get; set; } diff --git a/DysonNetwork.Sphere/Realm/RealmChatController.cs b/DysonNetwork.Sphere/Realm/RealmChatController.cs index 61567e6..a820972 100644 --- a/DysonNetwork.Sphere/Realm/RealmChatController.cs +++ b/DysonNetwork.Sphere/Realm/RealmChatController.cs @@ -1,3 +1,4 @@ +using DysonNetwork.Shared.Proto; using DysonNetwork.Sphere.Chat; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,6 +15,7 @@ public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBa public async Task>> ListRealmChat(string slug) { var currentUser = HttpContext.Items["CurrentUser"] as Account; + var accountId = currentUser is null ? Guid.Empty : Guid.Parse(currentUser.Id); var realm = await db.Realms .Where(r => r.Slug == slug) @@ -22,7 +24,7 @@ public class RealmChatController(AppDatabase db, RealmService rs) : ControllerBa if (!realm.IsPublic) { if (currentUser is null) return Unauthorized(); - if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal)) + if (!await rs.IsMemberWithRole(realm.Id, accountId, RealmMemberRole.Normal)) return StatusCode(403, "You need at least one member to view the realm's chat."); } diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs b/DysonNetwork.Sphere/Realm/RealmController.cs index ca73f2b..66debd6 100644 --- a/DysonNetwork.Sphere/Realm/RealmController.cs +++ b/DysonNetwork.Sphere/Realm/RealmController.cs @@ -1,8 +1,11 @@ using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Mvc; +using DysonNetwork.Shared.Data; +using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; +using Google.Protobuf.WellKnownTypes; namespace DysonNetwork.Sphere.Realm; @@ -11,10 +14,10 @@ namespace DysonNetwork.Sphere.Realm; public class RealmController( AppDatabase db, RealmService rs, - FileReferenceService fileRefService, - RelationshipService rels, - ActionLogService als, - AccountEventService aes + FileService.FileServiceClient files, + FileReferenceService.FileReferenceServiceClient fileRefs, + ActionLogService.ActionLogServiceClient als, + AccountService.AccountServiceClient accounts ) : Controller { [HttpGet("{slug}")] @@ -33,10 +36,10 @@ public class RealmController( public async Task>> ListJoinedRealms() { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var members = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.JoinedAt != null) .Where(m => m.LeaveAt == null) .Include(e => e.Realm) @@ -51,10 +54,10 @@ public class RealmController( public async Task>> ListInvites() { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var members = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.JoinedAt == null) .Include(e => e.Realm) .ToListAsync(); @@ -74,12 +77,19 @@ public class RealmController( [FromBody] RealmMemberRequest request) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); - var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); - if (relatedUser is null) return BadRequest("Related user was not found"); + var relatedUser = + await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() }); + if (relatedUser == null) return BadRequest("Related user was not found"); - if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) + var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest() + { + AccountId = currentUser.Id, + RelatedId = request.RelatedUserId.ToString(), + Status = -100 + }); + if (hasBlocked?.Value ?? false) return StatusCode(403, "You cannot invite a user that blocked you."); var realm = await db.Realms @@ -87,11 +97,11 @@ public class RealmController( .FirstOrDefaultAsync(); if (realm is null) return NotFound(); - if (!await rs.IsMemberWithRole(realm.Id, userId, request.Role)) + if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role)) return StatusCode(403, "You cannot invite member has higher permission than yours."); var hasExistingMember = await db.RealmMembers - .Where(m => m.AccountId == request.RelatedUserId) + .Where(m => m.AccountId == Guid.Parse(relatedUser.Id)) .Where(m => m.RealmId == realm.Id) .Where(m => m.LeaveAt == null) .AnyAsync(); @@ -100,7 +110,7 @@ public class RealmController( var member = new RealmMember { - AccountId = relatedUser.Id, + AccountId = Guid.Parse(relatedUser.Id), RealmId = realm.Id, Role = request.Role, }; @@ -108,12 +118,21 @@ public class RealmController( db.RealmMembers.Add(member); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmInvite, - new Dictionary { { "realm_id", realm.Id }, { "account_id", member.AccountId } }, Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.invite", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) }, + { "role", Value.ForNumber(request.Role) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); - member.Account = relatedUser; + member.AccountId = Guid.Parse(relatedUser.Id); member.Realm = realm; await rs.SendInviteNotify(member); @@ -125,10 +144,10 @@ public class RealmController( public async Task> AcceptMemberInvite(string slug) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var member = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.Realm.Slug == slug) .Where(m => m.JoinedAt == null) .FirstOrDefaultAsync(); @@ -138,11 +157,18 @@ public class RealmController( db.Update(member); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmJoin, - new Dictionary { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.join", + Meta = + { + { "realm_id", Value.ForString(member.RealmId.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return Ok(member); } @@ -152,10 +178,10 @@ public class RealmController( public async Task DeclineMemberInvite(string slug) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var member = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.Realm.Slug == slug) .Where(m => m.JoinedAt == null) .FirstOrDefaultAsync(); @@ -164,11 +190,19 @@ public class RealmController( member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmLeave, - new Dictionary { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.decline_invite", + Meta = + { + { "realm_id", Value.ForString(member.RealmId.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) }, + { "decliner_id", Value.ForString(currentUser.Id) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return NoContent(); } @@ -191,43 +225,41 @@ public class RealmController( if (!realm.IsPublic) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Normal)) + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal)) return StatusCode(403, "You must be a member to view this realm's members."); } - IQueryable query = db.RealmMembers + var query = db.RealmMembers .Where(m => m.RealmId == realm.Id) - .Where(m => m.LeaveAt == null) - .Include(m => m.Account) - .Include(m => m.Account.Profile); + .Where(m => m.LeaveAt == null); - if (withStatus) - { - var members = await query - .OrderBy(m => m.CreatedAt) - .ToListAsync(); - - var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); - - if (!string.IsNullOrEmpty(status)) - { - members = members.Where(m => - memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && - s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); - } - - members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) - .ToList(); - - var total = members.Count; - Response.Headers["X-Total"] = total.ToString(); - - var result = members.Skip(offset).Take(take).ToList(); - - return Ok(result); - } - else - { + // if (withStatus) + // { + // var members = await query + // .OrderBy(m => m.CreatedAt) + // .ToListAsync(); + // + // var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); + // + // if (!string.IsNullOrEmpty(status)) + // { + // members = members.Where(m => + // memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && + // s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); + // } + // + // members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) + // .ToList(); + // + // var total = members.Count; + // Response.Headers["X-Total"] = total.ToString(); + // + // var result = members.Skip(offset).Take(take).ToList(); + // + // return Ok(result); + // } + // else + // { var total = await query.CountAsync(); Response.Headers["X-Total"] = total.ToString(); @@ -238,23 +270,20 @@ public class RealmController( .ToListAsync(); return Ok(members); - } + // } } - [HttpGet("{slug}/members/me")] [Authorize] public async Task> GetCurrentIdentity(string slug) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var member = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.Realm.Slug == slug) - .Include(m => m.Account) - .Include(m => m.Account.Profile) .FirstOrDefaultAsync(); if (member is null) return NotFound(); @@ -266,10 +295,10 @@ public class RealmController( public async Task LeaveRealm(string slug) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; + var accountId = Guid.Parse(currentUser.Id); var member = await db.RealmMembers - .Where(m => m.AccountId == userId) + .Where(m => m.AccountId == accountId) .Where(m => m.Realm.Slug == slug) .Where(m => m.JoinedAt != null) .FirstOrDefaultAsync(); @@ -281,11 +310,19 @@ public class RealmController( member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmLeave, - new Dictionary { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.leave", + Meta = + { + { "realm_id", Value.ForString(member.RealmId.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) }, + { "leaver_id", Value.ForString(currentUser.Id) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return NoContent(); } @@ -317,7 +354,7 @@ public class RealmController( Name = request.Name!, Slug = request.Slug!, Description = request.Description!, - AccountId = currentUser.Id, + AccountId = Guid.Parse(currentUser.Id), IsCommunity = request.IsCommunity ?? false, IsPublic = request.IsPublic ?? false, Members = new List @@ -325,7 +362,7 @@ public class RealmController( new() { Role = RealmMemberRole.Owner, - AccountId = currentUser.Id, + AccountId = Guid.Parse(currentUser.Id), JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) } } @@ -333,42 +370,56 @@ public class RealmController( if (request.PictureId is not null) { - realm.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject(); - if (realm.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); + if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult); } - if (request.BackgroundId is not null) { - realm.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject(); - if (realm.Background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); + if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult); } db.Realms.Add(realm); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmCreate, - new Dictionary { { "realm_id", realm.Id } }, Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.create", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "name", Value.ForString(realm.Name) }, + { "slug", Value.ForString(realm.Slug) }, + { "is_community", Value.ForBool(realm.IsCommunity) }, + { "is_public", Value.ForBool(realm.IsPublic) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); var realmResourceId = $"realm:{realm.Id}"; if (realm.Picture is not null) { - await fileRefService.CreateReferenceAsync( - realm.Picture.Id, - "realm.picture", - realmResourceId - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Picture.Id, + Usage = "realm.picture", + ResourceId = realmResourceId + }); } if (realm.Background is not null) { - await fileRefService.CreateReferenceAsync( - realm.Background.Id, - "realm.background", - realmResourceId - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Background.Id, + Usage = "realm.background", + ResourceId = realmResourceId + }); } return Ok(realm); @@ -385,8 +436,9 @@ public class RealmController( .FirstOrDefaultAsync(); if (realm is null) return NotFound(); + var accountId = Guid.Parse(currentUser.Id); var member = await db.RealmMembers - .Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id && m.JoinedAt != null) + .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null) .FirstOrDefaultAsync(); if (member is null || member.Role < RealmMemberRole.Moderator) return StatusCode(403, "You do not have permission to update this realm."); @@ -409,53 +461,75 @@ public class RealmController( if (request.PictureId is not null) { - var picture = await db.Files.FindAsync(request.PictureId); - if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); + if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); // Remove old references for the realm picture if (realm.Picture is not null) { - await fileRefService.DeleteResourceReferencesAsync(realm.ResourceIdentifier, "realm.picture"); + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realm.ResourceIdentifier + }); } - realm.Picture = picture.ToReferenceObject(); + realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult); // Create a new reference - await fileRefService.CreateReferenceAsync( - picture.Id, - "realm.picture", - realm.ResourceIdentifier - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Picture.Id, + Usage = "realm.picture", + ResourceId = realm.ResourceIdentifier + }); } if (request.BackgroundId is not null) { - var background = await db.Files.FindAsync(request.BackgroundId); - if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); + if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud."); // Remove old references for the realm background if (realm.Background is not null) { - await fileRefService.DeleteResourceReferencesAsync(realm.ResourceIdentifier, "realm.background"); + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realm.ResourceIdentifier + }); } - realm.Background = background.ToReferenceObject(); + realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult); // Create a new reference - await fileRefService.CreateReferenceAsync( - background.Id, - "realm.background", - realm.ResourceIdentifier - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Background.Id, + Usage = "realm.background", + ResourceId = realm.ResourceIdentifier + }); } db.Realms.Update(realm); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmUpdate, - new Dictionary { { "realm_id", realm.Id } }, Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.update", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "name_updated", Value.ForBool(request.Name != null) }, + { "slug_updated", Value.ForBool(request.Slug != null) }, + { "description_updated", Value.ForBool(request.Description != null) }, + { "picture_updated", Value.ForBool(request.PictureId != null) }, + { "background_updated", Value.ForBool(request.BackgroundId != null) }, + { "is_community_updated", Value.ForBool(request.IsCommunity != null) }, + { "is_public_updated", Value.ForBool(request.IsPublic != null) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return Ok(realm); } @@ -475,14 +549,14 @@ public class RealmController( return StatusCode(403, "Only community realms can be joined without invitation."); var existingMember = await db.RealmMembers - .Where(m => m.AccountId == currentUser.Id && m.RealmId == realm.Id) + .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id) .FirstOrDefaultAsync(); if (existingMember is not null) return BadRequest("You are already a member of this realm."); var member = new RealmMember { - AccountId = currentUser.Id, + AccountId = Guid.Parse(currentUser.Id), RealmId = realm.Id, Role = RealmMemberRole.Normal, JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) @@ -491,11 +565,19 @@ public class RealmController( db.RealmMembers.Add(member); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmJoin, - new Dictionary { { "realm_id", realm.Id }, { "account_id", currentUser.Id } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.join", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(currentUser.Id) }, + { "is_community", Value.ForBool(realm.IsCommunity) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return Ok(member); } @@ -516,17 +598,25 @@ public class RealmController( .FirstOrDefaultAsync(); if (member is null) return NotFound(); - if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role)) + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role)) return StatusCode(403, "You do not have permission to remove members from this realm."); member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.ChatroomKick, - new Dictionary { { "realm_id", realm.Id }, { "account_id", memberId } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.kick", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(memberId.ToString()) }, + { "kicker_id", Value.ForString(currentUser.Id) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return NoContent(); } @@ -545,23 +635,31 @@ public class RealmController( var member = await db.RealmMembers .Where(m => m.AccountId == memberId && m.RealmId == realm.Id) - .Include(m => m.Account) .FirstOrDefaultAsync(); if (member is null) return NotFound(); - if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Moderator, member.Role, newRole)) + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role, + newRole)) return StatusCode(403, "You do not have permission to update member roles in this realm."); member.Role = newRole; db.RealmMembers.Update(member); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmAdjustRole, - new Dictionary - { { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } }, - Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.role_update", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(memberId.ToString()) }, + { "new_role", Value.ForNumber(newRole) }, + { "updater_id", Value.ForString(currentUser.Id) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); return Ok(member); } @@ -574,26 +672,36 @@ public class RealmController( var realm = await db.Realms .Where(r => r.Slug == slug) - .Include(r => r.Picture) - .Include(r => r.Background) .FirstOrDefaultAsync(); if (realm is null) return NotFound(); - if (!await rs.IsMemberWithRole(realm.Id, currentUser.Id, RealmMemberRole.Owner)) + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner)) return StatusCode(403, "Only the owner can delete this realm."); db.Realms.Remove(realm); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest( - ActionLogType.RealmDelete, - new Dictionary { { "realm_id", realm.Id } }, Request - ); + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.delete", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "realm_name", Value.ForString(realm.Name) }, + { "realm_slug", Value.ForString(realm.Slug) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); // Delete all file references for this realm var realmResourceId = $"realm:{realm.Id}"; - await fileRefService.DeleteResourceReferencesAsync(realmResourceId); + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realmResourceId + }); return NoContent(); } -} +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs.bak b/DysonNetwork.Sphere/Realm/RealmController.cs.bak new file mode 100644 index 0000000..449df8e --- /dev/null +++ b/DysonNetwork.Sphere/Realm/RealmController.cs.bak @@ -0,0 +1,696 @@ +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Shared.Data; +using DysonNetwork.Shared.Proto; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Google.Protobuf.WellKnownTypes; + +namespace DysonNetwork.Sphere.Realm; + +[ApiController] +[Route("/api/realms")] +public class RealmController( + AppDatabase db, + RealmService rs, + FileReferenceService.FileReferenceServiceClient fileRefs, + ActionLogService.ActionLogServiceClient als, + AccountService.AccountServiceClient accounts +) : Controller +{ + [HttpGet("{slug}")] + public async Task> GetRealm(string slug) + { + var realm = await db.Realms + .Where(e => e.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + return Ok(realm); + } + + [HttpGet] + [Authorize] + public async Task>> ListJoinedRealms() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var members = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.JoinedAt != null) + .Where(m => m.LeaveAt == null) + .Include(e => e.Realm) + .Select(m => m.Realm) + .ToListAsync(); + + return members.ToList(); + } + + [HttpGet("invites")] + [Authorize] + public async Task>> ListInvites() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var members = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.JoinedAt == null) + .Include(e => e.Realm) + .ToListAsync(); + + return members.ToList(); + } + + public class RealmMemberRequest + { + [Required] public Guid RelatedUserId { get; set; } + [Required] public int Role { get; set; } + } + + [HttpPost("invites/{slug}")] + [Authorize] + public async Task> InviteMember(string slug, + [FromBody] RealmMemberRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var relatedUser = + await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() }); + if (relatedUser == null) return BadRequest("Related user was not found"); + + var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest() + { + AccountId = currentUser.Id, + RelatedId = request.RelatedUserId.ToString(), + Status = -100 + }); + if (hasBlocked?.Value ?? false) + return StatusCode(403, "You cannot invite a user that blocked you."); + + var realm = await db.Realms + .Where(p => p.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + if (!await rs.IsMemberWithRole(realm.Id, accountId, request.Role)) + return StatusCode(403, "You cannot invite member has higher permission than yours."); + + var hasExistingMember = await db.RealmMembers + .Where(m => m.AccountId == Guid.Parse(relatedUser.Id)) + .Where(m => m.RealmId == realm.Id) + .Where(m => m.LeaveAt == null) + .AnyAsync(); + if (hasExistingMember) + return BadRequest("This user has been joined the realm or leave cannot be invited again."); + + var member = new RealmMember + { + AccountId = Guid.Parse(relatedUser.Id), + RealmId = realm.Id, + Role = request.Role, + }; + + db.RealmMembers.Add(member); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.invite", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) }, + { "role", Value.ForNumber(request.Role) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + member.Account = relatedUser; + member.Realm = realm; + await rs.SendInviteNotify(member); + + return Ok(member); + } + + [HttpPost("invites/{slug}/accept")] + [Authorize] + public async Task> AcceptMemberInvite(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var member = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.Realm.Slug == slug) + .Where(m => m.JoinedAt == null) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow); + db.Update(member); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.join", + Meta = + { + { "realm_id", Value.ForString(member.RealmId.ToString()) }, + { "account_id", Value.ForString(member.AccountId.ToString()) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + return Ok(member); + } + + [HttpPost("invites/{slug}/decline")] + [Authorize] + public async Task DeclineMemberInvite(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var member = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.Realm.Slug == slug) + .Where(m => m.JoinedAt == null) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); + await db.SaveChangesAsync(); + + als.CreateActionLogFromRequest( + ActionLogType.RealmLeave, + new Dictionary { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, + Request + ); + + return NoContent(); + } + + + [HttpGet("{slug}/members")] + public async Task>> ListMembers( + string slug, + [FromQuery] int offset = 0, + [FromQuery] int take = 20, + [FromQuery] bool withStatus = false, + [FromQuery] string? status = null + ) + { + var realm = await db.Realms + .Where(r => r.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + if (!realm.IsPublic) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Normal)) + return StatusCode(403, "You must be a member to view this realm's members."); + } + + IQueryable query = db.RealmMembers + .Where(m => m.RealmId == realm.Id) + .Where(m => m.LeaveAt == null) + .Include(m => m.Account) + .Include(m => m.Account.Profile); + + if (withStatus) + { + var members = await query + .OrderBy(m => m.CreatedAt) + .ToListAsync(); + + var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); + + if (!string.IsNullOrEmpty(status)) + { + members = members.Where(m => + memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && + s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) + .ToList(); + + var total = members.Count; + Response.Headers["X-Total"] = total.ToString(); + + var result = members.Skip(offset).Take(take).ToList(); + + return Ok(result); + } + else + { + var total = await query.CountAsync(); + Response.Headers["X-Total"] = total.ToString(); + + var members = await query + .OrderBy(m => m.CreatedAt) + .Skip(offset) + .Take(take) + .ToListAsync(); + + return Ok(members); + } + } + + + [HttpGet("{slug}/members/me")] + [Authorize] + public async Task> GetCurrentIdentity(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var member = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.Realm.Slug == slug) + .FirstOrDefaultAsync(); + + if (member is null) return NotFound(); + return Ok(member); + } + + [HttpDelete("{slug}/members/me")] + [Authorize] + public async Task LeaveRealm(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var member = await db.RealmMembers + .Where(m => m.AccountId == accountId) + .Where(m => m.Realm.Slug == slug) + .Where(m => m.JoinedAt != null) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + if (member.Role == RealmMemberRole.Owner) + return StatusCode(403, "Owner cannot leave their own realm."); + + member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); + await db.SaveChangesAsync(); + + als.CreateActionLogFromRequest( + ActionLogType.RealmLeave, + new Dictionary { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, + Request + ); + + return NoContent(); + } + + public class RealmRequest + { + [MaxLength(1024)] public string? Slug { get; set; } + [MaxLength(1024)] public string? Name { get; set; } + [MaxLength(4096)] public string? Description { get; set; } + public string? PictureId { get; set; } + public string? BackgroundId { get; set; } + public bool? IsCommunity { get; set; } + public bool? IsPublic { get; set; } + } + + [HttpPost] + [Authorize] + public async Task> CreateRealm(RealmRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("You cannot create a realm without a name."); + if (string.IsNullOrWhiteSpace(request.Slug)) return BadRequest("You cannot create a realm without a slug."); + + var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug); + if (slugExists) return BadRequest("Realm with this slug already exists."); + + var realm = new Realm + { + Name = request.Name!, + Slug = request.Slug!, + Description = request.Description!, + AccountId = currentUser.Id, + IsCommunity = request.IsCommunity ?? false, + IsPublic = request.IsPublic ?? false, + Members = new List + { + new() + { + Role = RealmMemberRole.Owner, + AccountId = Guid.Parse(currentUser.Id), + JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) + } + } + }; + + if (request.PictureId is not null) + { + var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); + if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult); + } + + if (request.BackgroundId is not null) + { + var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); + if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult); + } + + db.Realms.Add(realm); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.create", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "name", Value.ForString(realm.Name) }, + { "slug", Value.ForString(realm.Slug) }, + { "is_community", Value.ForBool(realm.IsCommunity) }, + { "is_public", Value.ForBool(realm.IsPublic) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + var realmResourceId = $"realm:{realm.Id}"; + + if (realm.Picture is not null) + { + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Picture.Id, + Usage = "realm.picture", + ResourceId = realmResourceId + }); + } + + if (realm.Background is not null) + { + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Background.Id, + Usage = "realm.background", + ResourceId = realmResourceId + }); + } + + return Ok(realm); + } + + [HttpPatch("{slug}")] + [Authorize] + public async Task> Update(string slug, [FromBody] RealmRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var realm = await db.Realms + .Where(r => r.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + var accountId = Guid.Parse(currentUser.Id); + var member = await db.RealmMembers + .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null) + .FirstOrDefaultAsync(); + if (member is null || member.Role < RealmMemberRole.Moderator) + return StatusCode(403, "You do not have permission to update this realm."); + + if (request.Slug is not null && request.Slug != realm.Slug) + { + var slugExists = await db.Realms.AnyAsync(r => r.Slug == request.Slug); + if (slugExists) return BadRequest("Realm with this slug already exists."); + realm.Slug = request.Slug; + } + + if (request.Name is not null) + realm.Name = request.Name; + if (request.Description is not null) + realm.Description = request.Description; + if (request.IsCommunity is not null) + realm.IsCommunity = request.IsCommunity.Value; + if (request.IsPublic is not null) + realm.IsPublic = request.IsPublic.Value; + + if (request.PictureId is not null) + { + var pictureResult = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); + if (pictureResult is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + + // Remove old references for the realm picture + if (realm.Picture is not null) + { + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realm.ResourceIdentifier + }); + } + + realm.Picture = CloudFileReferenceObject.FromProtoValue(pictureResult); + + // Create a new reference + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Picture.Id, + Usage = "realm.picture", + ResourceId = realm.ResourceIdentifier + }); + } + + if (request.BackgroundId is not null) + { + var backgroundResult = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); + if (backgroundResult is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + + // Remove old references for the realm background + if (realm.Background is not null) + { + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realm.ResourceIdentifier + }); + } + + realm.Background = CloudFileReferenceObject.FromProtoValue(backgroundResult); + + // Create a new reference + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = realm.Background.Id, + Usage = "realm.background", + ResourceId = realm.ResourceIdentifier + }); + } + + db.Realms.Update(realm); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.update", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "name_updated", Value.ForBool(request.Name != null) }, + { "slug_updated", Value.ForBool(request.Slug != null) }, + { "description_updated", Value.ForBool(request.Description != null) }, + { "picture_updated", Value.ForBool(request.PictureId != null) }, + { "background_updated", Value.ForBool(request.BackgroundId != null) }, + { "is_community_updated", Value.ForBool(request.IsCommunity != null) }, + { "is_public_updated", Value.ForBool(request.IsPublic != null) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + return Ok(realm); + } + + [HttpPost("{slug}/members/me")] + [Authorize] + public async Task> JoinRealm(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var realm = await db.Realms + .Where(r => r.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + if (!realm.IsCommunity) + return StatusCode(403, "Only community realms can be joined without invitation."); + + var existingMember = await db.RealmMembers + .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id) + .FirstOrDefaultAsync(); + if (existingMember is not null) + return BadRequest("You are already a member of this realm."); + + var member = new RealmMember + { + AccountId = currentUser.Id, + RealmId = realm.Id, + Role = RealmMemberRole.Normal, + JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) + }; + + db.RealmMembers.Add(member); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.join", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(currentUser.Id) }, + { "is_community", Value.ForBool(realm.IsCommunity) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + return Ok(member); + } + + [HttpDelete("{slug}/members/{memberId:guid}")] + [Authorize] + public async Task RemoveMember(string slug, Guid memberId) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var realm = await db.Realms + .Where(r => r.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + var member = await db.RealmMembers + .Where(m => m.AccountId == memberId && m.RealmId == realm.Id) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role)) + return StatusCode(403, "You do not have permission to remove members from this realm."); + + member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.kick", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(memberId.ToString()) }, + { "kicker_id", Value.ForString(currentUser.Id) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + return NoContent(); + } + + [HttpPatch("{slug}/members/{memberId:guid}/role")] + [Authorize] + public async Task> UpdateMemberRole(string slug, Guid memberId, [FromBody] int newRole) + { + if (newRole >= RealmMemberRole.Owner) return BadRequest("Unable to set realm member to owner or greater role."); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var realm = await db.Realms + .Where(r => r.Slug == slug) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + var member = await db.RealmMembers + .Where(m => m.AccountId == memberId && m.RealmId == realm.Id) + .Include(m => m.Account) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Moderator, member.Role, + newRole)) + return StatusCode(403, "You do not have permission to update member roles in this realm."); + + member.Role = newRole; + db.RealmMembers.Update(member); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.members.role_update", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "account_id", Value.ForString(memberId.ToString()) }, + { "new_role", Value.ForNumber(newRole) }, + { "updater_id", Value.ForString(currentUser.Id) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + return Ok(member); + } + + [HttpDelete("{slug}")] + [Authorize] + public async Task Delete(string slug) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var realm = await db.Realms + .Where(r => r.Slug == slug) + .Include(r => r.Picture) + .Include(r => r.Background) + .FirstOrDefaultAsync(); + if (realm is null) return NotFound(); + + if (!await rs.IsMemberWithRole(realm.Id, Guid.Parse(currentUser.Id), RealmMemberRole.Owner)) + return StatusCode(403, "Only the owner can delete this realm."); + + db.Realms.Remove(realm); + await db.SaveChangesAsync(); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "realms.delete", + Meta = + { + { "realm_id", Value.ForString(realm.Id.ToString()) }, + { "realm_name", Value.ForString(realm.Name) }, + { "realm_slug", Value.ForString(realm.Slug) } + }, + UserId = currentUser.Id, + UserAgent = Request.Headers.UserAgent.ToString(), + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "" + }); + + // Delete all file references for this realm + var realmResourceId = $"realm:{realm.Id}"; + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest + { + ResourceId = realmResourceId + }); + + return NoContent(); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Realm/RealmService.cs b/DysonNetwork.Sphere/Realm/RealmService.cs index 605d23d..b5b1a5e 100644 --- a/DysonNetwork.Sphere/Realm/RealmService.cs +++ b/DysonNetwork.Sphere/Realm/RealmService.cs @@ -1,22 +1,36 @@ -using DysonNetwork.Sphere.Account; +using DysonNetwork.Shared; +using DysonNetwork.Shared.Proto; using DysonNetwork.Sphere.Localization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; namespace DysonNetwork.Sphere.Realm; -public class RealmService(AppDatabase db, NotificationService nty, IStringLocalizer localizer) +public class RealmService( + AppDatabase db, + PusherService.PusherServiceClient pusher, + AccountService.AccountServiceClient accounts, + IStringLocalizer localizer +) { public async Task SendInviteNotify(RealmMember member) { - AccountService.SetCultureInfo(member.Account); - await nty.SendNotification( - member.Account, - "invites.realms", - localizer["RealmInviteTitle"], - null, - localizer["RealmInviteBody", member.Realm.Name], - actionUri: "/realms" + var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() }); + CultureService.SetCultureInfo(account); + + await pusher.SendPushNotificationToUserAsync( + new SendPushNotificationToUserRequest + { + UserId = account.Id, + Notification = new PushNotification + { + Topic = "invites.realms", + Title = localizer["RealmInviteTitle"], + Body = localizer["RealmInviteBody", member.Realm.Name], + ActionUri = "/realms", + IsSavable = true + } + } ); } diff --git a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs index d71d898..ee62b91 100644 --- a/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Sphere/Startup/ApplicationConfiguration.cs @@ -1,21 +1,17 @@ using System.Net; -using DysonNetwork.Sphere.Connection; -using DysonNetwork.Sphere.Permission; -using DysonNetwork.Sphere.Storage; +using DysonNetwork.Shared.Auth; using Microsoft.AspNetCore.HttpOverrides; using Prometheus; using tusdotnet; -using tusdotnet.Stores; namespace DysonNetwork.Sphere.Startup; public static class ApplicationConfiguration { - public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration, TusDiskStore tusDiskStore) + public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) { app.MapMetrics(); app.MapOpenApi(); - app.UseMiddleware(); app.UseSwagger(); app.UseSwaggerUI(); @@ -44,8 +40,6 @@ public static class ApplicationConfiguration app.MapStaticAssets().RequireRateLimiting("fixed"); app.MapRazorPages().RequireRateLimiting("fixed"); - app.MapTus("/files/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusDiskStore))); - return app; } diff --git a/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs index 43cd0b1..521a9c6 100644 --- a/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs +++ b/DysonNetwork.Sphere/Startup/ScheduledJobsConfiguration.cs @@ -1,7 +1,4 @@ -using DysonNetwork.Sphere.Storage; using DysonNetwork.Sphere.WebReader; -using DysonNetwork.Sphere.Storage.Handlers; -using DysonNetwork.Sphere.Wallet; using Quartz; namespace DysonNetwork.Sphere.Startup; @@ -19,63 +16,15 @@ public static class ScheduledJobsConfiguration .WithIdentity("AppDatabaseRecyclingTrigger") .WithCronSchedule("0 0 0 * * ?")); - var cloudFilesRecyclingJob = new JobKey("CloudFilesUnusedRecycling"); - q.AddJob(opts => opts.WithIdentity(cloudFilesRecyclingJob)); - q.AddTrigger(opts => opts - .ForJob(cloudFilesRecyclingJob) - .WithIdentity("CloudFilesUnusedRecyclingTrigger") - .WithSimpleSchedule(o => o.WithIntervalInHours(1).RepeatForever()) - ); - - var actionLogFlushJob = new JobKey("ActionLogFlush"); - q.AddJob(opts => opts.WithIdentity(actionLogFlushJob)); - q.AddTrigger(opts => opts - .ForJob(actionLogFlushJob) - .WithIdentity("ActionLogFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(5) - .RepeatForever()) - ); - - var readReceiptFlushJob = new JobKey("ReadReceiptFlush"); - q.AddJob(opts => opts.WithIdentity(readReceiptFlushJob)); - q.AddTrigger(opts => opts - .ForJob(readReceiptFlushJob) - .WithIdentity("ReadReceiptFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInSeconds(60) - .RepeatForever()) - ); - - var lastActiveFlushJob = new JobKey("LastActiveFlush"); - q.AddJob(opts => opts.WithIdentity(lastActiveFlushJob)); - q.AddTrigger(opts => opts - .ForJob(lastActiveFlushJob) - .WithIdentity("LastActiveFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(5) - .RepeatForever()) - ); - - var postViewFlushJob = new JobKey("PostViewFlush"); - q.AddJob(opts => opts.WithIdentity(postViewFlushJob)); - q.AddTrigger(opts => opts - .ForJob(postViewFlushJob) - .WithIdentity("PostViewFlushTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(1) - .RepeatForever()) - ); - - var subscriptionRenewalJob = new JobKey("SubscriptionRenewal"); - q.AddJob(opts => opts.WithIdentity(subscriptionRenewalJob)); - q.AddTrigger(opts => opts - .ForJob(subscriptionRenewalJob) - .WithIdentity("SubscriptionRenewalTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(30) - .RepeatForever()) - ); + // var postViewFlushJob = new JobKey("PostViewFlush"); + // q.AddJob(opts => opts.WithIdentity(postViewFlushJob)); + // q.AddTrigger(opts => opts + // .ForJob(postViewFlushJob) + // .WithIdentity("PostViewFlushTrigger") + // .WithSimpleSchedule(o => o + // .WithIntervalInMinutes(1) + // .RepeatForever()) + // ); var webFeedScraperJob = new JobKey("WebFeedScraper"); q.AddJob(opts => opts.WithIdentity(webFeedScraperJob)); diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 502039f..d4dfa39 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -21,9 +21,7 @@ using DysonNetwork.Shared.Proto; using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.Developer; using DysonNetwork.Sphere.Discovery; -using DysonNetwork.Sphere.Safety; using tusdotnet.Stores; -using PermissionService = DysonNetwork.Sphere.Permission.PermissionService; namespace DysonNetwork.Sphere.Startup; @@ -90,12 +88,6 @@ public static class ServiceCollectionExtensions { services.AddCors(); services.AddAuthorization(); - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = AuthConstants.SchemeName; - options.DefaultChallengeScheme = AuthConstants.SchemeName; - }) - .AddScheme(AuthConstants.SchemeName, _ => { }); return services; } @@ -146,17 +138,6 @@ public static class ServiceCollectionExtensions return services; } - public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration) - { - var tusStorePath = configuration.GetSection("Tus").GetValue("StorePath")!; - Directory.CreateDirectory(tusStorePath); - var tusDiskStore = new TusDiskStore(tusStorePath); - - services.AddSingleton(tusDiskStore); - - return services; - } - public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services) { services.AddSingleton(); @@ -169,7 +150,6 @@ public static class ServiceCollectionExtensions { services.Configure(configuration.GetSection("GeoIP")); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -181,7 +161,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Sphere/Sticker/StickerController.cs b/DysonNetwork.Sphere/Sticker/StickerController.cs index 7bbb840..e82288e 100644 --- a/DysonNetwork.Sphere/Sticker/StickerController.cs +++ b/DysonNetwork.Sphere/Sticker/StickerController.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; -using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Publisher; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/DysonNetwork.Sphere/Sticker/StickerService.cs b/DysonNetwork.Sphere/Sticker/StickerService.cs index cd4d01e..d912a10 100644 --- a/DysonNetwork.Sphere/Sticker/StickerService.cs +++ b/DysonNetwork.Sphere/Sticker/StickerService.cs @@ -23,12 +23,12 @@ public class StickerService( db.Stickers.Add(sticker); await db.SaveChangesAsync(); - var stickerResourceId = $"sticker:{sticker.Id}"; - await fileRefService.CreateReferenceAsync( - sticker.Image.Id, - StickerFileUsageIdentifier, - stickerResourceId - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = sticker.Image.Id, + Usage = StickerFileUsageIdentifier, + ResourceId = sticker.ResourceIdentifier + }); return sticker; } @@ -37,24 +37,17 @@ public class StickerService( { if (newImage is not null) { - var stickerResourceId = $"sticker:{sticker.Id}"; + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = sticker.ResourceIdentifier }); - // Delete old references - var oldRefs = - await fileRefService.GetResourceReferencesAsync(stickerResourceId, StickerFileUsageIdentifier); - foreach (var oldRef in oldRefs) - { - await fileRefService.DeleteReferenceAsync(oldRef.Id); - } - - sticker.Image = newImage.ToReferenceObject(); + sticker.Image = newImage; // Create new reference - await fileRefService.CreateReferenceAsync( - newImage.Id, - StickerFileUsageIdentifier, - stickerResourceId - ); + await fileRefs.CreateReferenceAsync(new CreateReferenceRequest + { + FileId = newImage.Id, + Usage = StickerFileUsageIdentifier, + ResourceId = sticker.ResourceIdentifier + }); } db.Stickers.Update(sticker); @@ -71,7 +64,7 @@ public class StickerService( var stickerResourceId = $"sticker:{sticker.Id}"; // Delete all file references for this sticker - await fileRefService.DeleteResourceReferencesAsync(stickerResourceId); + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = stickerResourceId }); db.Stickers.Remove(sticker); await db.SaveChangesAsync(); @@ -90,13 +83,11 @@ public class StickerService( // Delete all file references for each sticker in the pack foreach (var stickerResourceId in stickers.Select(sticker => $"sticker:{sticker.Id}")) - { - await fileRefService.DeleteResourceReferencesAsync(stickerResourceId); - } + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = stickerResourceId }); // Delete any references for the pack itself var packResourceId = $"stickerpack:{pack.Id}"; - await fileRefService.DeleteResourceReferencesAsync(packResourceId); + await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = packResourceId }); db.Stickers.RemoveRange(stickers); db.StickerPacks.Remove(pack); diff --git a/DysonNetwork.Sphere/WebReader/WebReaderController.cs b/DysonNetwork.Sphere/WebReader/WebReaderController.cs index c400d0e..0f6f266 100644 --- a/DysonNetwork.Sphere/WebReader/WebReaderController.cs +++ b/DysonNetwork.Sphere/WebReader/WebReaderController.cs @@ -1,4 +1,4 @@ -using DysonNetwork.Sphere.Permission; +using DysonNetwork.Shared.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; diff --git a/DysonNetwork.Sphere/appsettings.json b/DysonNetwork.Sphere/appsettings.json index 2437c68..f7023f6 100644 --- a/DysonNetwork.Sphere/appsettings.json +++ b/DysonNetwork.Sphere/appsettings.json @@ -13,84 +13,6 @@ "FastRetrieve": "localhost:6379", "Etcd": "etcd.orb.local:2379" }, - "Authentication": { - "Schemes": { - "Bearer": { - "ValidAudiences": [ - "http://localhost:5071", - "https://localhost:7099" - ], - "ValidIssuer": "solar-network" - } - } - }, - "AuthToken": { - "PublicKeyPath": "Keys/PublicKey.pem", - "PrivateKeyPath": "Keys/PrivateKey.pem" - }, - "OidcProvider": { - "IssuerUri": "https://nt.solian.app", - "PublicKeyPath": "Keys/PublicKey.pem", - "PrivateKeyPath": "Keys/PrivateKey.pem", - "AccessTokenLifetime": "01:00:00", - "RefreshTokenLifetime": "30.00:00:00", - "AuthorizationCodeLifetime": "00:30:00", - "RequireHttpsMetadata": true - }, - "Tus": { - "StorePath": "Uploads" - }, - "Storage": { - "PreferredRemote": "minio", - "Remote": [ - { - "Id": "minio", - "Label": "Minio", - "Region": "auto", - "Bucket": "solar-network-development", - "Endpoint": "localhost:9000", - "SecretId": "littlesheep", - "SecretKey": "password", - "EnabledSigned": true, - "EnableSsl": false - }, - { - "Id": "cloudflare", - "Label": "Cloudflare R2", - "Region": "auto", - "Bucket": "solar-network", - "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", - "SecretId": "8ff5d06c7b1639829d60bc6838a542e6", - "SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67", - "EnableSigned": true, - "EnableSsl": true - } - ] - }, - "Captcha": { - "Provider": "cloudflare", - "ApiKey": "0x4AAAAAABCDUdOujj4feOb_", - "ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U" - }, - "Notifications": { - "Topic": "dev.solsynth.solian", - "Endpoint": "http://localhost:8088" - }, - "Email": { - "Server": "smtp4dev.orb.local", - "Port": 25, - "UseSsl": false, - "Username": "no-reply@mail.solsynth.dev", - "Password": "password", - "FromAddress": "no-reply@mail.solsynth.dev", - "FromName": "Alphabot", - "SubjectPrefix": "Solar Network" - }, - "RealtimeChat": { - "Endpoint": "https://solar-network-im44o8gq.livekit.cloud", - "ApiKey": "APIs6TiL8wj3A4j", - "ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE" - }, "GeoIp": { "DatabasePath": "./Keys/GeoLite2-City.mmdb" }, @@ -111,23 +33,17 @@ "DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT" } }, - "Payment": { - "Auth": { - "Afdian": "" - }, - "Subscriptions": { - "Afdian": { - "7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary", - "7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova", - "141713ee3d6211f085b352540025c377": "solian.stellar.supernova" - } - } - }, "KnownProxies": [ "127.0.0.1", "::1" ], "Etcd": { "Insecure": true + }, + "Service": { + "Name": "DysonNetwork.Sphere", + "Url": "https://localhost:7099", + "ClientCert": "../Certificates/client.crt", + "ClientKey": "../Certificates/client.key" } }