Compare commits

..

2 Commits

Author SHA1 Message Date
cde55eb237 ♻️ Still don't know what I am doing 2025-07-13 23:38:57 +08:00
03e26ef93c ♻️ I have no idea what I have done 2025-07-13 21:51:16 +08:00
47 changed files with 6224 additions and 806 deletions

View File

@@ -11,7 +11,11 @@
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MimeTypes" Version="2.5.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -20,30 +24,30 @@
<PackageReference Include="NetVips" Version="3.1.0" />
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
<PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/>
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1"/>
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1"/>
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5"/>
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0"/>
<PackageReference Include="Quartz" Version="3.14.0"/>
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0"/>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1"/>
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1"/>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
<PackageReference Include="Quartz" Version="3.14.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="SkiaSharp" Version="3.119.0" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.0" />

View File

@@ -0,0 +1,189 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Drive;
using DysonNetwork.Drive.Storage;
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("20250713121317_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
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<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Dictionary<string, object>>("FileMeta")
.HasColumnType("jsonb")
.HasColumnName("file_meta");
b.Property<bool>("HasCompression")
.HasColumnType("boolean")
.HasColumnName("has_compression");
b.Property<string>("Hash")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("hash");
b.Property<bool>("IsMarkedRecycle")
.HasColumnType("boolean")
.HasColumnName("is_marked_recycle");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
.HasColumnType("jsonb")
.HasColumnName("sensitive_marks");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<string>("StorageId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("storage_id");
b.Property<string>("StorageUrl")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("storage_url");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("UploadedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("uploaded_at");
b.Property<string>("UploadedTo")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("uploaded_to");
b.Property<Dictionary<string, object>>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<string>("FileId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("file_id");
b.Property<string>("ResourceId")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("resource_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("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
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using DysonNetwork.Drive.Storage;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Drive.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
migrationBuilder.CreateTable(
name: "files",
columns: table => new
{
id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
file_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
user_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
sensitive_marks = table.Column<List<ContentSensitiveMark>>(type: "jsonb", nullable: true),
mime_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
hash = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
size = table.Column<long>(type: "bigint", nullable: false),
uploaded_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
uploaded_to = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
has_compression = table.Column<bool>(type: "boolean", nullable: false),
is_marked_recycle = table.Column<bool>(type: "boolean", nullable: false),
storage_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
storage_url = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_files", x => x.id);
});
migrationBuilder.CreateTable(
name: "file_references",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
usage = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
resource_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_file_references", x => x.id);
table.ForeignKey(
name: "fk_file_references_files_file_id",
column: x => x.file_id,
principalTable: "files",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_file_references_file_id",
table: "file_references",
column: "file_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "file_references");
migrationBuilder.DropTable(
name: "files");
}
}
}

View File

@@ -0,0 +1,186 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Drive;
using DysonNetwork.Drive.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Drive.Migrations
{
[DbContext(typeof(AppDatabase))]
partial class AppDatabaseModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<string>("Id")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("Description")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("description");
b.Property<Dictionary<string, object>>("FileMeta")
.HasColumnType("jsonb")
.HasColumnName("file_meta");
b.Property<bool>("HasCompression")
.HasColumnType("boolean")
.HasColumnName("has_compression");
b.Property<string>("Hash")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("hash");
b.Property<bool>("IsMarkedRecycle")
.HasColumnType("boolean")
.HasColumnName("is_marked_recycle");
b.Property<string>("MimeType")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("mime_type");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("name");
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
.HasColumnType("jsonb")
.HasColumnName("sensitive_marks");
b.Property<long>("Size")
.HasColumnType("bigint")
.HasColumnName("size");
b.Property<string>("StorageId")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("storage_id");
b.Property<string>("StorageUrl")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("storage_url");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("UploadedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("uploaded_at");
b.Property<string>("UploadedTo")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("uploaded_to");
b.Property<Dictionary<string, object>>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expired_at");
b.Property<string>("FileId")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("file_id");
b.Property<string>("ResourceId")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("resource_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<string>("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
}
}
}

View File

@@ -10,7 +10,8 @@
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379"
"FastRetrieve": "localhost:6379",
"Etcd": "etcd.orb.local:2379"
},
"Authentication": {
"Schemes": {
@@ -125,5 +126,11 @@
"KnownProxies": [
"127.0.0.1",
"::1"
]
],
"Service": {
"Name": "DysonNetwork.Drive",
"Url": "http://localhost:5216",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
}
}

View File

@@ -5,11 +5,13 @@ using DysonNetwork.Pass.Email;
using DysonNetwork.Pass.Localization;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Proto;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;
using OtpNet;
using AuthSession = DysonNetwork.Pass.Auth.AuthSession;
namespace DysonNetwork.Pass.Account;
@@ -17,8 +19,8 @@ public class AccountService(
AppDatabase db,
MagicSpellService spells,
AccountUsernameService uname,
NotificationService nty,
EmailService mailer,
PusherService.PusherServiceClient pusher,
IStringLocalizer<NotificationResource> localizer,
ICacheService cache,
ILogger<AccountService> logger
@@ -353,13 +355,18 @@ public class AccountService(
if (await _GetFactorCode(factor) is not null)
throw new InvalidOperationException("A factor code has been sent and in active duration.");
await nty.SendNotification(
account,
"auth.verification",
localizer["AuthCodeTitle"],
null,
localizer["AuthCodeBody", code],
save: true
await pusher.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest
{
UserId = account.Id.ToString(),
Notification = new PushNotification
{
Topic = "auth.verification",
Title = localizer["AuthCodeTitle"],
Body = localizer["AuthCodeBody", code],
IsSavable = false
}
}
);
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
break;
@@ -395,16 +402,17 @@ public class AccountService(
return;
}
await mailer.SendTemplatedEmailAsync<DysonNetwork.Pass.Pages.Emails.VerificationEmail, VerificationEmailModel>(
account.Nick,
contact.Content,
localizer["VerificationEmail"],
new VerificationEmailModel
{
Name = account.Name,
Code = code
}
);
await mailer
.SendTemplatedEmailAsync<Pages.Emails.VerificationEmail, VerificationEmailModel>(
account.Nick,
contact.Content,
localizer["VerificationEmail"],
new VerificationEmailModel
{
Name = account.Name,
Code = code
}
);
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
break;
@@ -489,7 +497,10 @@ public class AccountService(
.ToListAsync();
if (session.Challenge.DeviceId is not null)
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
{
DeviceId = session.Challenge.DeviceId
});
// The current session should be included in the sessions' list
await db.AuthSessions
@@ -648,7 +659,8 @@ public class AccountService(
if (missingId.Count != 0)
{
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id }).ToList();
var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id })
.ToList();
await db.BulkInsertAsync(newProfiles);
}
}

View File

@@ -36,6 +36,24 @@ public class AccountServiceGrpc(
return account.ToProtoValue();
}
public override async Task<GetAccountBatchResponse> GetAccountBatch(GetAccountBatchRequest request, ServerCallContext context)
{
var accountIds = request.Id
.Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null)
.Where(id => id.HasValue)
.Select(id => id!.Value)
.ToList();
var accounts = await _db.Accounts
.AsNoTracking()
.Where(a => accountIds.Contains(a.Id))
.ToListAsync();
var response = new GetAccountBatchResponse();
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
return response;
}
public override async Task<Shared.Proto.Account> CreateAccount(CreateAccountRequest request,
ServerCallContext context)
{

View File

@@ -1,292 +0,0 @@
using System.Text;
using System.Text.Json;
using DysonNetwork.Pass;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Pass.Account;
public class NotificationService(
AppDatabase db,
IHttpClientFactory httpFactory,
IConfiguration config
)
{
private readonly string _notifyTopic = config["Notifications:Topic"]!;
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
public async Task UnsubscribePushNotifications(string deviceId)
{
await db.NotificationPushSubscriptions
.Where(s => s.DeviceId == deviceId)
.ExecuteDeleteAsync();
}
public async Task<NotificationPushSubscription> SubscribePushNotification(
Account account,
NotificationPushProvider provider,
string deviceId,
string deviceToken
)
{
var now = SystemClock.Instance.GetCurrentInstant();
// First check if a matching subscription exists
var existingSubscription = await db.NotificationPushSubscriptions
.Where(s => s.AccountId == account.Id)
.Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
.FirstOrDefaultAsync();
if (existingSubscription is not null)
{
// Update the existing subscription directly in the database
await db.NotificationPushSubscriptions
.Where(s => s.Id == existingSubscription.Id)
.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.DeviceId, deviceId)
.SetProperty(s => s.DeviceToken, deviceToken)
.SetProperty(s => s.UpdatedAt, now));
// Return the updated subscription
existingSubscription.DeviceId = deviceId;
existingSubscription.DeviceToken = deviceToken;
existingSubscription.UpdatedAt = now;
return existingSubscription;
}
var subscription = new NotificationPushSubscription
{
DeviceId = deviceId,
DeviceToken = deviceToken,
Provider = provider,
AccountId = account.Id,
};
db.NotificationPushSubscriptions.Add(subscription);
await db.SaveChangesAsync();
return subscription;
}
public async Task<Notification> SendNotification(
Account account,
string topic,
string? title = null,
string? subtitle = null,
string? content = null,
Dictionary<string, object>? meta = null,
string? actionUri = null,
bool isSilent = false,
bool save = true
)
{
if (title is null && subtitle is null && content is null)
throw new ArgumentException("Unable to send notification that completely empty.");
meta ??= new Dictionary<string, object>();
if (actionUri is not null) meta["action_uri"] = actionUri;
var notification = new Notification
{
Topic = topic,
Title = title,
Subtitle = subtitle,
Content = content,
Meta = meta,
AccountId = account.Id,
};
if (save)
{
db.Add(notification);
await db.SaveChangesAsync();
}
if (!isSilent) _ = DeliveryNotification(notification);
return notification;
}
public async Task DeliveryNotification(Notification notification)
{
// Pushing the notification
var subscribers = await db.NotificationPushSubscriptions
.Where(s => s.AccountId == notification.AccountId)
.ToListAsync();
await _PushNotification(notification, subscribers);
}
public async Task MarkNotificationsViewed(ICollection<Notification> notifications)
{
var now = SystemClock.Instance.GetCurrentInstant();
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
if (id.Count == 0) return;
await db.Notifications
.Where(n => id.Contains(n.Id))
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now)
);
}
public async Task BroadcastNotification(Notification notification, bool save = false)
{
var accounts = await db.Accounts.ToListAsync();
if (save)
{
var notifications = accounts.Select(x =>
{
var newNotification = new Notification
{
Topic = notification.Topic,
Title = notification.Title,
Subtitle = notification.Subtitle,
Content = notification.Content,
Meta = notification.Meta,
Priority = notification.Priority,
Account = x,
AccountId = x.Id
};
return newNotification;
}).ToList();
await db.BulkInsertAsync(notifications);
}
foreach (var account in accounts)
{
notification.Account = account;
notification.AccountId = account.Id;
}
var subscribers = await db.NotificationPushSubscriptions
.ToListAsync();
await _PushNotification(notification, subscribers);
}
public async Task SendNotificationBatch(Notification notification, List<Account> accounts, bool save = false)
{
if (save)
{
var notifications = accounts.Select(x =>
{
var newNotification = new Notification
{
Topic = notification.Topic,
Title = notification.Title,
Subtitle = notification.Subtitle,
Content = notification.Content,
Meta = notification.Meta,
Priority = notification.Priority,
Account = x,
AccountId = x.Id
};
return newNotification;
}).ToList();
await db.BulkInsertAsync(notifications);
}
foreach (var account in accounts)
{
notification.Account = account;
notification.AccountId = account.Id;
}
var accountsId = accounts.Select(x => x.Id).ToList();
var subscribers = await db.NotificationPushSubscriptions
.Where(s => accountsId.Contains(s.AccountId))
.ToListAsync();
await _PushNotification(notification, subscribers);
}
private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
IEnumerable<NotificationPushSubscription> subscriptions)
{
var subDict = subscriptions
.GroupBy(x => x.Provider)
.ToDictionary(x => x.Key, x => x.ToList());
var notifications = subDict.Select(value =>
{
var platformCode = value.Key switch
{
NotificationPushProvider.Apple => 1,
NotificationPushProvider.Google => 2,
_ => throw new InvalidOperationException($"Unknown push provider: {value.Key}")
};
var tokens = value.Value.Select(x => x.DeviceToken).ToList();
return _BuildNotificationPayload(notification, platformCode, tokens);
}).ToList();
return notifications.ToList();
}
private Dictionary<string, object> _BuildNotificationPayload(Notification notification, int platformCode,
IEnumerable<string> deviceTokens)
{
var alertDict = new Dictionary<string, object>();
var dict = new Dictionary<string, object>
{
["notif_id"] = notification.Id.ToString(),
["apns_id"] = notification.Id.ToString(),
["topic"] = _notifyTopic,
["tokens"] = deviceTokens,
["data"] = new Dictionary<string, object>
{
["type"] = notification.Topic,
["meta"] = notification.Meta ?? new Dictionary<string, object>(),
},
["mutable_content"] = true,
["priority"] = notification.Priority >= 5 ? "high" : "normal",
};
if (!string.IsNullOrWhiteSpace(notification.Title))
{
dict["title"] = notification.Title;
alertDict["title"] = notification.Title;
}
if (!string.IsNullOrWhiteSpace(notification.Content))
{
dict["message"] = notification.Content;
alertDict["body"] = notification.Content;
}
if (!string.IsNullOrWhiteSpace(notification.Subtitle))
{
dict["message"] = $"{notification.Subtitle}\n{dict["message"]}";
alertDict["subtitle"] = notification.Subtitle;
}
if (notification.Priority >= 5)
dict["name"] = "default";
dict["platform"] = platformCode;
dict["alert"] = alertDict;
return dict;
}
private async Task _PushNotification(Notification notification,
IEnumerable<NotificationPushSubscription> subscriptions)
{
var subList = subscriptions.ToList();
if (subList.Count == 0) return;
var requestDict = new Dictionary<string, object>
{
["notifications"] = _BuildNotificationPayload(notification, subList)
};
var client = httpFactory.CreateClient();
client.BaseAddress = _notifyEndpoint;
var request = await client.PostAsync("/push", new StringContent(
JsonSerializer.Serialize(requestDict),
Encoding.UTF8,
"application/json"
));
request.EnsureSuccessStatusCode();
}
}

View File

@@ -9,6 +9,10 @@
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />

View File

@@ -1,33 +1,16 @@
using dotnet_etcd;
using dotnet_etcd.interfaces;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Components;
namespace DysonNetwork.Pass.Email;
public class EmailService
public class EmailService(
PusherService.PusherServiceClient pusher,
RazorViewRenderer viewRenderer,
ILogger<EmailService> logger
)
{
private readonly PusherService.PusherServiceClient _client;
private readonly RazorViewRenderer _viewRenderer;
private readonly ILogger<EmailService> _logger;
public EmailService(
EtcdClient etcd,
RazorViewRenderer viewRenderer,
IConfiguration configuration,
ILogger<EmailService> logger,
PusherService.PusherServiceClient client
)
{
_client = GrpcClientHelper.CreatePusherServiceClient(
etcd,
configuration["Service:CertPath"]!,
configuration["Service:KeyPath"]!
).GetAwaiter().GetResult();
_viewRenderer = viewRenderer;
_logger = logger;
_client = client;
}
public async Task SendEmailAsync(
string? recipientName,
string recipientEmail,
@@ -37,7 +20,7 @@ public class EmailService
{
subject = $"[Solarpass] {subject}";
await _client.SendEmailAsync(
await pusher.SendEmailAsync(
new SendEmailRequest()
{
Email = new EmailMessage()
@@ -57,12 +40,12 @@ public class EmailService
{
try
{
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
var htmlBody = await viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
await SendEmailAsync(recipientName, recipientEmail, subject, htmlBody);
}
catch (Exception err)
{
_logger.LogError(err, "Failed to render email template...");
logger.LogError(err, "Failed to render email template...");
throw;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,998 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Developer;
using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore.Migrations;
using NetTopologySuite.Geometries;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
migrationBuilder.CreateTable(
name: "accounts",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
nick = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
language = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
activated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_superuser = table.Column<bool>(type: "boolean", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_accounts", x => x.id);
});
migrationBuilder.CreateTable(
name: "custom_apps",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
status = table.Column<int>(type: "integer", nullable: false),
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_custom_apps", x => x.id);
});
migrationBuilder.CreateTable(
name: "permission_groups",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
key = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_permission_groups", x => x.id);
});
migrationBuilder.CreateTable(
name: "wallet_coupons",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
code = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
affected_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
discount_amount = table.Column<decimal>(type: "numeric", nullable: true),
discount_rate = table.Column<double>(type: "double precision", nullable: true),
max_usage = table.Column<int>(type: "integer", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_coupons", x => x.id);
});
migrationBuilder.CreateTable(
name: "abuse_reports",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
resource_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
reason = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
resolved_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
resolution = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_abuse_reports", x => x.id);
table.ForeignKey(
name: "fk_abuse_reports_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_auth_factors",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
secret = table.Column<string>(type: "character varying(8196)", maxLength: 8196, nullable: true),
config = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
trustworthy = table.Column<int>(type: "integer", nullable: false),
enabled_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_auth_factors", x => x.id);
table.ForeignKey(
name: "fk_account_auth_factors_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_check_in_results",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
level = table.Column<int>(type: "integer", nullable: false),
reward_points = table.Column<decimal>(type: "numeric", nullable: true),
reward_experience = table.Column<int>(type: "integer", nullable: true),
tips = table.Column<ICollection<FortuneTip>>(type: "jsonb", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_check_in_results", x => x.id);
table.ForeignKey(
name: "fk_account_check_in_results_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_connections",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
provider = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
provided_identifier = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
access_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
refresh_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_connections", x => x.id);
table.ForeignKey(
name: "fk_account_connections_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_contacts",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
verified_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_primary = table.Column<bool>(type: "boolean", nullable: false),
content = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_contacts", x => x.id);
table.ForeignKey(
name: "fk_account_contacts_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_profiles",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
first_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
middle_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
last_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
bio = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
gender = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
pronouns = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
time_zone = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
location = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
birthday = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
last_seen_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
active_badge = table.Column<BadgeReferenceObject>(type: "jsonb", nullable: true),
experience = table.Column<int>(type: "integer", nullable: false),
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_profiles", x => x.id);
table.ForeignKey(
name: "fk_account_profiles_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_relationships",
columns: table => new
{
account_id = table.Column<Guid>(type: "uuid", nullable: false),
related_id = table.Column<Guid>(type: "uuid", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
status = table.Column<short>(type: "smallint", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_relationships", x => new { x.account_id, x.related_id });
table.ForeignKey(
name: "fk_account_relationships_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_account_relationships_accounts_related_id",
column: x => x.related_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "account_statuses",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
attitude = table.Column<int>(type: "integer", nullable: false),
is_invisible = table.Column<bool>(type: "boolean", nullable: false),
is_not_disturb = table.Column<bool>(type: "boolean", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
cleared_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_account_statuses", x => x.id);
table.ForeignKey(
name: "fk_account_statuses_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "action_logs",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
action = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
location = table.Column<Point>(type: "geometry", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
session_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_action_logs", x => x.id);
table.ForeignKey(
name: "fk_action_logs_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "auth_challenges",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
step_remain = table.Column<int>(type: "integer", nullable: false),
step_total = table.Column<int>(type: "integer", nullable: false),
failed_attempts = table.Column<int>(type: "integer", nullable: false),
platform = table.Column<int>(type: "integer", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
blacklist_factors = table.Column<List<Guid>>(type: "jsonb", nullable: false),
audiences = table.Column<List<string>>(type: "jsonb", nullable: false),
scopes = table.Column<List<string>>(type: "jsonb", nullable: false),
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
device_id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
nonce = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
location = table.Column<Point>(type: "geometry", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_auth_challenges", x => x.id);
table.ForeignKey(
name: "fk_auth_challenges_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "badges",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
caption = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
activated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_badges", x => x.id);
table.ForeignKey(
name: "fk_badges_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "magic_spells",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
spell = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
affected_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_magic_spells", x => x.id);
table.ForeignKey(
name: "fk_magic_spells_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "notification_push_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
device_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
device_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
provider = table.Column<int>(type: "integer", nullable: false),
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_notification_push_subscriptions", x => x.id);
table.ForeignKey(
name: "fk_notification_push_subscriptions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "notifications",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
topic = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
subtitle = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
priority = table.Column<int>(type: "integer", nullable: false),
viewed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_notifications", x => x.id);
table.ForeignKey(
name: "fk_notifications_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "wallets",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallets", x => x.id);
table.ForeignKey(
name: "fk_wallets_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "custom_app_secrets",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_oidc = table.Column<bool>(type: "boolean", nullable: false),
app_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_custom_app_secrets", x => x.id);
table.ForeignKey(
name: "fk_custom_app_secrets_custom_apps_app_id",
column: x => x.app_id,
principalTable: "custom_apps",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "permission_group_members",
columns: table => new
{
group_id = table.Column<Guid>(type: "uuid", nullable: false),
actor = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
affected_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_permission_group_members", x => new { x.group_id, x.actor });
table.ForeignKey(
name: "fk_permission_group_members_permission_groups_group_id",
column: x => x.group_id,
principalTable: "permission_groups",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "permission_nodes",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
actor = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
area = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
key = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
value = table.Column<JsonDocument>(type: "jsonb", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
affected_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
group_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_permission_nodes", x => x.id);
table.ForeignKey(
name: "fk_permission_nodes_permission_groups_group_id",
column: x => x.group_id,
principalTable: "permission_groups",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "wallet_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
begun_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
ended_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
is_active = table.Column<bool>(type: "boolean", nullable: false),
is_free_trial = table.Column<bool>(type: "boolean", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
payment_method = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
payment_details = table.Column<PaymentDetails>(type: "jsonb", nullable: false),
base_price = table.Column<decimal>(type: "numeric", nullable: false),
coupon_id = table.Column<Guid>(type: "uuid", nullable: true),
renewal_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_subscriptions", x => x.id);
table.ForeignKey(
name: "fk_wallet_subscriptions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_wallet_subscriptions_wallet_coupons_coupon_id",
column: x => x.coupon_id,
principalTable: "wallet_coupons",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "auth_sessions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
last_granted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
challenge_id = table.Column<Guid>(type: "uuid", nullable: false),
app_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_auth_sessions", x => x.id);
table.ForeignKey(
name: "fk_auth_sessions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id",
column: x => x.challenge_id,
principalTable: "auth_challenges",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_auth_sessions_custom_apps_app_id",
column: x => x.app_id,
principalTable: "custom_apps",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "payment_transactions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false),
remarks = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
type = table.Column<int>(type: "integer", nullable: false),
payer_wallet_id = table.Column<Guid>(type: "uuid", nullable: true),
payee_wallet_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_payment_transactions", x => x.id);
table.ForeignKey(
name: "fk_payment_transactions_wallets_payee_wallet_id",
column: x => x.payee_wallet_id,
principalTable: "wallets",
principalColumn: "id");
table.ForeignKey(
name: "fk_payment_transactions_wallets_payer_wallet_id",
column: x => x.payer_wallet_id,
principalTable: "wallets",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "wallet_pockets",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false),
wallet_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_pockets", x => x.id);
table.ForeignKey(
name: "fk_wallet_pockets_wallets_wallet_id",
column: x => x.wallet_id,
principalTable: "wallets",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "payment_orders",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
remarks = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
app_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
amount = table.Column<decimal>(type: "numeric", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
payee_wallet_id = table.Column<Guid>(type: "uuid", nullable: true),
transaction_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_payment_orders", x => x.id);
table.ForeignKey(
name: "fk_payment_orders_payment_transactions_transaction_id",
column: x => x.transaction_id,
principalTable: "payment_transactions",
principalColumn: "id");
table.ForeignKey(
name: "fk_payment_orders_wallets_payee_wallet_id",
column: x => x.payee_wallet_id,
principalTable: "wallets",
principalColumn: "id");
});
migrationBuilder.CreateIndex(
name: "ix_abuse_reports_account_id",
table: "abuse_reports",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_account_auth_factors_account_id",
table: "account_auth_factors",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_account_check_in_results_account_id",
table: "account_check_in_results",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_account_connections_account_id",
table: "account_connections",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_account_contacts_account_id",
table: "account_contacts",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_account_profiles_account_id",
table: "account_profiles",
column: "account_id",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_account_relationships_related_id",
table: "account_relationships",
column: "related_id");
migrationBuilder.CreateIndex(
name: "ix_account_statuses_account_id",
table: "account_statuses",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_accounts_name",
table: "accounts",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_action_logs_account_id",
table: "action_logs",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_challenges_account_id",
table: "auth_challenges",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_account_id",
table: "auth_sessions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_app_id",
table: "auth_sessions",
column: "app_id");
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_challenge_id",
table: "auth_sessions",
column: "challenge_id");
migrationBuilder.CreateIndex(
name: "ix_badges_account_id",
table: "badges",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_custom_app_secrets_app_id",
table: "custom_app_secrets",
column: "app_id");
migrationBuilder.CreateIndex(
name: "ix_magic_spells_account_id",
table: "magic_spells",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_magic_spells_spell",
table: "magic_spells",
column: "spell",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_account_id",
table: "notification_push_subscriptions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_device_token_device_id_acco",
table: "notification_push_subscriptions",
columns: new[] { "device_token", "device_id", "account_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_notifications_account_id",
table: "notifications",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_payment_orders_payee_wallet_id",
table: "payment_orders",
column: "payee_wallet_id");
migrationBuilder.CreateIndex(
name: "ix_payment_orders_transaction_id",
table: "payment_orders",
column: "transaction_id");
migrationBuilder.CreateIndex(
name: "ix_payment_transactions_payee_wallet_id",
table: "payment_transactions",
column: "payee_wallet_id");
migrationBuilder.CreateIndex(
name: "ix_payment_transactions_payer_wallet_id",
table: "payment_transactions",
column: "payer_wallet_id");
migrationBuilder.CreateIndex(
name: "ix_permission_nodes_group_id",
table: "permission_nodes",
column: "group_id");
migrationBuilder.CreateIndex(
name: "ix_permission_nodes_key_area_actor",
table: "permission_nodes",
columns: new[] { "key", "area", "actor" });
migrationBuilder.CreateIndex(
name: "ix_wallet_pockets_wallet_id",
table: "wallet_pockets",
column: "wallet_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_account_id",
table: "wallet_subscriptions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_coupon_id",
table: "wallet_subscriptions",
column: "coupon_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_identifier",
table: "wallet_subscriptions",
column: "identifier");
migrationBuilder.CreateIndex(
name: "ix_wallets_account_id",
table: "wallets",
column: "account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "abuse_reports");
migrationBuilder.DropTable(
name: "account_auth_factors");
migrationBuilder.DropTable(
name: "account_check_in_results");
migrationBuilder.DropTable(
name: "account_connections");
migrationBuilder.DropTable(
name: "account_contacts");
migrationBuilder.DropTable(
name: "account_profiles");
migrationBuilder.DropTable(
name: "account_relationships");
migrationBuilder.DropTable(
name: "account_statuses");
migrationBuilder.DropTable(
name: "action_logs");
migrationBuilder.DropTable(
name: "auth_sessions");
migrationBuilder.DropTable(
name: "badges");
migrationBuilder.DropTable(
name: "custom_app_secrets");
migrationBuilder.DropTable(
name: "magic_spells");
migrationBuilder.DropTable(
name: "notification_push_subscriptions");
migrationBuilder.DropTable(
name: "notifications");
migrationBuilder.DropTable(
name: "payment_orders");
migrationBuilder.DropTable(
name: "permission_group_members");
migrationBuilder.DropTable(
name: "permission_nodes");
migrationBuilder.DropTable(
name: "wallet_pockets");
migrationBuilder.DropTable(
name: "wallet_subscriptions");
migrationBuilder.DropTable(
name: "auth_challenges");
migrationBuilder.DropTable(
name: "custom_apps");
migrationBuilder.DropTable(
name: "payment_transactions");
migrationBuilder.DropTable(
name: "permission_groups");
migrationBuilder.DropTable(
name: "wallet_coupons");
migrationBuilder.DropTable(
name: "wallets");
migrationBuilder.DropTable(
name: "accounts");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ namespace DysonNetwork.Pass.Permission;
using System;
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
[AttributeUsage(AttributeTargets.Method)]
public class RequiredPermissionAttribute(string area, string key) : Attribute
{
public string Area { get; set; } = area;

View File

@@ -28,8 +28,6 @@ builder.Services.AddAppBusinessServices(builder.Configuration);
// Add scheduled jobs
builder.Services.AddAppScheduledJobs();
builder.Services.AddHostedService<ServiceRegistrationHostedService>();
var app = builder.Build();
// Run database migrations

View File

@@ -19,6 +19,7 @@ using DysonNetwork.Pass.Handlers;
using DysonNetwork.Pass.Wallet.PaymentHandlers;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Registry;
namespace DysonNetwork.Pass.Startup;
@@ -48,9 +49,8 @@ public static class ServiceCollectionExtensions
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
});
// Register gRPC reflection for service discovery
services.AddGrpc();
services.AddPusherService();
// Register gRPC services
services.AddScoped<AccountServiceGrpc>();
services.AddScoped<AuthServiceGrpc>();
@@ -194,7 +194,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<ActionLogService>();
services.AddScoped<RelationshipService>();
services.AddScoped<MagicSpellService>();
services.AddScoped<NotificationService>();
services.AddScoped<AuthService>();
services.AddScoped<AccountUsernameService>();
services.AddScoped<WalletService>();

View File

@@ -1,56 +0,0 @@
using DysonNetwork.Shared.Registry;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace DysonNetwork.Pass.Startup;
public class ServiceRegistrationHostedService : IHostedService
{
private readonly ServiceRegistry _serviceRegistry;
private readonly IConfiguration _configuration;
private readonly ILogger<ServiceRegistrationHostedService> _logger;
public ServiceRegistrationHostedService(
ServiceRegistry serviceRegistry,
IConfiguration configuration,
ILogger<ServiceRegistrationHostedService> logger)
{
_serviceRegistry = serviceRegistry;
_configuration = configuration;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var serviceName = "DysonNetwork.Pass"; // Preset service name
var serviceUrl = _configuration["Service:Url"];
if (string.IsNullOrEmpty(serviceName) || string.IsNullOrEmpty(serviceUrl))
{
_logger.LogWarning("Service name or URL not configured. Skipping Etcd registration.");
return;
}
_logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl);
try
{
await _serviceRegistry.RegisterService(serviceName, serviceUrl);
_logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
// The lease will expire automatically if the service stops.
// For explicit unregistration, you would implement it here.
_logger.LogInformation("Service registration hosted service is stopping.");
return Task.CompletedTask;
}
}

View File

@@ -1,17 +1,18 @@
using System.Globalization;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Localization;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Localization;
using NodaTime;
using AccountService = DysonNetwork.Pass.Account.AccountService;
namespace DysonNetwork.Pass.Wallet;
public class PaymentService(
AppDatabase db,
WalletService wat,
NotificationService nty,
PusherService.PusherServiceClient pusher,
IStringLocalizer<NotificationResource> localizer
)
{
@@ -205,16 +206,19 @@ public class PaymentService(
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
await nty.SendNotification(
account,
"wallets.orders.paid",
localizer["OrderPaidTitle", $"#{readableOrderId}"],
null,
localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
readableOrderRemark],
new Dictionary<string, object>()
await pusher.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest
{
["order_id"] = order.Id.ToString()
UserId = account.Id.ToString(),
Notification = new PushNotification
{
Topic = "wallets.orders.paid",
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
readableOrderRemark],
IsSavable = false
}
}
);
}

View File

@@ -1,11 +1,14 @@
using System.Text.Json;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Localization;
using DysonNetwork.Pass.Wallet.PaymentHandlers;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Proto;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;
using AccountService = DysonNetwork.Pass.Account.AccountService;
using Duration = NodaTime.Duration;
namespace DysonNetwork.Pass.Wallet;
@@ -13,7 +16,7 @@ public class SubscriptionService(
AppDatabase db,
PaymentService payment,
AccountService accounts,
NotificationService nty,
PusherService.PusherServiceClient pusher,
IStringLocalizer<NotificationResource> localizer,
IConfiguration configuration,
ICacheService cache,
@@ -352,15 +355,19 @@ public class SubscriptionService(
? subscription.EndedAt.Value.Minus(subscription.BegunAt).Days.ToString()
: "infinite";
await nty.SendNotification(
account,
"subscriptions.begun",
localizer["SubscriptionAppliedTitle", humanReadableName],
null,
localizer["SubscriptionAppliedBody", duration, humanReadableName],
new Dictionary<string, object>()
var notification = new PushNotification
{
Topic = "subscriptions.begun",
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
IsSavable = false,
};
notification.Meta.Add("subscription_id", Value.ForString(subscription.Id.ToString()));
await pusher.SendPushNotificationToUserAsync(
new SendPushNotificationToUserRequest
{
["subscription_id"] = subscription.Id.ToString(),
UserId = account.Id.ToString(),
Notification = notification
}
);
}

View File

@@ -9,8 +9,9 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379"
"App": "Host=localhost;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379",
"Etcd": "etcd.orb.local:2379"
},
"Authentication": {
"Schemes": {
@@ -36,60 +37,11 @@
"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"
},
@@ -125,5 +77,14 @@
"KnownProxies": [
"127.0.0.1",
"::1"
]
],
"Service": {
"Name": "DysonNetwork.Pass",
"Url": "http://localhost:5216",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
},
"Etcd": {
"Insecure": true
}
}

View File

@@ -20,31 +20,6 @@ public class WebSocketService
private static readonly ConcurrentDictionary<string, string> ActiveSubscriptions = new(); // deviceId -> chatRoomId
public void SubscribeToChatRoom(string chatRoomId, string deviceId)
{
ActiveSubscriptions[deviceId] = chatRoomId;
}
public void UnsubscribeFromChatRoom(string deviceId)
{
ActiveSubscriptions.TryRemove(deviceId, out _);
}
public bool IsUserSubscribedToChatRoom(string accountId, string chatRoomId)
{
var userDeviceIds = ActiveConnections.Keys.Where(k => k.AccountId == accountId).Select(k => k.DeviceId);
foreach (var deviceId in userDeviceIds)
{
if (ActiveSubscriptions.TryGetValue(deviceId, out var subscribedChatRoomId) &&
subscribedChatRoomId == chatRoomId)
{
return true;
}
}
return false;
}
public bool TryAdd(
(string AccountId, string DeviceId) key,
WebSocket socket,
@@ -67,7 +42,11 @@ public class WebSocketService
);
data.Cts.Cancel();
ActiveConnections.TryRemove(key, out _);
UnsubscribeFromChatRoom(key.DeviceId);
}
public bool GetDeviceIsConnected(string deviceId)
{
return ActiveConnections.Any(c => c.Key.DeviceId == deviceId);
}
public bool GetAccountIsConnected(string accountId)

View File

@@ -13,7 +13,11 @@
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
<PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />

View File

@@ -0,0 +1,151 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Pusher;
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.Pusher.Migrations
{
[DbContext(typeof(AppDatabase))]
[Migration("20250713122638_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<string>("Content")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("content");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Dictionary<string, object>>("Meta")
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<string>("Subtitle")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("subtitle");
b.Property<string>("Title")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("title");
b.Property<string>("Topic")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("topic");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("ViewedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("viewed_at");
b.HasKey("Id")
.HasName("pk_notifications");
b.ToTable("notifications", (string)null);
});
modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<int>("CountDelivered")
.HasColumnType("integer")
.HasColumnName("count_delivered");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("device_id");
b.Property<string>("DeviceToken")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("device_token");
b.Property<Instant?>("LastUsedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_used_at");
b.Property<int>("Provider")
.HasColumnType("integer")
.HasColumnName("provider");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_push_subscriptions");
b.HasIndex("AccountId", "DeviceId", "DeletedAt")
.IsUnique()
.HasDatabaseName("ix_push_subscriptions_account_id_device_id_deleted_at");
b.ToTable("push_subscriptions", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pusher.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "notifications",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
topic = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
subtitle = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
priority = table.Column<int>(type: "integer", nullable: false),
viewed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_notifications", x => x.id);
});
migrationBuilder.CreateTable(
name: "push_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
device_id = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
device_token = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
provider = table.Column<int>(type: "integer", nullable: false),
count_delivered = table.Column<int>(type: "integer", nullable: false),
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_push_subscriptions", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_push_subscriptions_account_id_device_id_deleted_at",
table: "push_subscriptions",
columns: new[] { "account_id", "device_id", "deleted_at" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "notifications");
migrationBuilder.DropTable(
name: "push_subscriptions");
}
}
}

View File

@@ -0,0 +1,148 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using DysonNetwork.Pusher;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DysonNetwork.Pusher.Migrations
{
[DbContext(typeof(AppDatabase))]
partial class AppDatabaseModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<string>("Content")
.HasMaxLength(4096)
.HasColumnType("character varying(4096)")
.HasColumnName("content");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Dictionary<string, object>>("Meta")
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<string>("Subtitle")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("subtitle");
b.Property<string>("Title")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("title");
b.Property<string>("Topic")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("topic");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<Instant?>("ViewedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("viewed_at");
b.HasKey("Id")
.HasName("pk_notifications");
b.ToTable("notifications", (string)null);
});
modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<int>("CountDelivered")
.HasColumnType("integer")
.HasColumnName("count_delivered");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("device_id");
b.Property<string>("DeviceToken")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("device_token");
b.Property<Instant?>("LastUsedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_used_at");
b.Property<int>("Provider")
.HasColumnType("integer")
.HasColumnName("provider");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_push_subscriptions");
b.HasIndex("AccountId", "DeviceId", "DeletedAt")
.IsUnique()
.HasDatabaseName("ix_push_subscriptions_account_id_device_id_deleted_at");
b.ToTable("push_subscriptions", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -19,6 +19,5 @@ public class Notification : ModelBase
public Instant? ViewedAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
}

View File

@@ -1,17 +1,20 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using AccountService = DysonNetwork.Shared.Proto.AccountService;
namespace DysonNetwork.Pass.Account;
namespace DysonNetwork.Pusher.Notification;
[ApiController]
[Route("/api/notifications")]
public class NotificationController(AppDatabase db, NotificationService nty) : ControllerBase
public class NotificationController(
AppDatabase db,
PushService nty,
AccountService.AccountServiceClient accounts) : ControllerBase
{
[HttpGet("count")]
[Authorize]
@@ -19,9 +22,10 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var count = await db.Notifications
.Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null)
.Where(s => s.AccountId == accountId && s.ViewedAt == null)
.CountAsync();
return Ok(count);
}
@@ -32,24 +36,25 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
[FromQuery] int offset = 0,
// The page size set to 5 is to avoid the client pulled the notification
// but didn't render it in the screen-viewable region.
[FromQuery] int take = 5
[FromQuery] int take = 8
)
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var totalCount = await db.Notifications
.Where(s => s.AccountId == currentUser.Id)
.Where(s => s.AccountId == accountId)
.CountAsync();
var notifications = await db.Notifications
.Where(s => s.AccountId == currentUser.Id)
.Where(s => s.AccountId == accountId)
.OrderByDescending(e => e.CreatedAt)
.Skip(offset)
.Take(take)
.ToListAsync();
Response.Headers["X-Total"] = totalCount.ToString();
await nty.MarkNotificationsViewed(notifications);
await nty.MarkNotificationsViewed(notifications.ToList());
return Ok(notifications);
}
@@ -57,14 +62,15 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
public class PushNotificationSubscribeRequest
{
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
public NotificationPushProvider Provider { get; set; }
public PushProvider Provider { get; set; }
}
[HttpPut("subscription")]
[Authorize]
public async Task<ActionResult<NotificationPushSubscription>> SubscribeToPushNotification(
[FromBody] PushNotificationSubscribeRequest request
)
public async Task<ActionResult<PushSubscription>>
SubscribeToPushNotification(
[FromBody] PushNotificationSubscribeRequest request
)
{
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
@@ -74,8 +80,12 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
if (currentSession == null) return Unauthorized();
var result =
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
request.DeviceToken);
await nty.SubscribeDevice(
currentSession.Challenge.DeviceId!,
request.DeviceToken,
request.Provider,
currentUser
);
return Ok(result);
}
@@ -90,10 +100,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
if (currentUser == null) return Unauthorized();
var currentSession = currentSessionValue as AuthSession;
if (currentSession == null) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var affectedRows = await db.NotificationPushSubscriptions
var affectedRows = await db.PushSubscriptions
.Where(s =>
s.AccountId == currentUser.Id &&
s.AccountId == accountId &&
s.DeviceId == currentSession.Challenge.DeviceId
).ExecuteDeleteAsync();
return Ok(affectedRows);
@@ -109,36 +120,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
public int Priority { get; set; } = 10;
}
[HttpPost("broadcast")]
[Authorize]
[RequiredPermission("global", "notifications.broadcast")]
public async Task<ActionResult> BroadcastNotification(
[FromBody] NotificationRequest request,
[FromQuery] bool save = false
)
{
await nty.BroadcastNotification(
new Notification
{
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
Topic = request.Topic,
Title = request.Title,
Subtitle = request.Subtitle,
Content = request.Content,
Meta = request.Meta,
Priority = request.Priority,
},
save
);
return Ok();
}
public class NotificationWithAimRequest : NotificationRequest
{
[Required] public List<Guid> AccountId { get; set; } = null!;
}
[HttpPost("send")]
[Authorize]
[RequiredPermission("global", "notifications.send")]
@@ -147,7 +133,6 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
[FromQuery] bool save = false
)
{
var accounts = await db.Accounts.Where(a => request.AccountId.Contains(a.Id)).ToListAsync();
await nty.SendNotificationBatch(
new Notification
{
@@ -159,7 +144,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
Content = request.Content,
Meta = request.Meta,
},
accounts,
request.AccountId,
save
);
return Ok();

View File

@@ -12,14 +12,14 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto
private readonly string _notifyTopic = config["Notifications:Topic"]!;
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
public async Task UnsubscribePushNotifications(string deviceId)
public async Task UnsubscribeDevice(string deviceId)
{
await db.PushSubscriptions
.Where(s => s.DeviceId == deviceId)
.ExecuteDeleteAsync();
}
public async Task<PushSubscription> SubscribePushNotification(
public async Task<PushSubscription> SubscribeDevice(
string deviceId,
string deviceToken,
PushProvider provider,

View File

@@ -9,7 +9,7 @@ namespace DysonNetwork.Pusher.Services;
public class PusherServiceGrpc(
EmailService emailService,
WebSocketService webSocketService,
WebSocketService websocket,
PushService pushService
) : PusherService.PusherServiceBase
{
@@ -32,7 +32,7 @@ public class PusherServiceGrpc(
Data = request.Packet.Data,
ErrorMessage = request.Packet.ErrorMessage
};
webSocketService.SendPacketToAccount(request.UserId, packet);
websocket.SendPacketToAccount(request.UserId, packet);
return Task.FromResult(new Empty());
}
@@ -46,7 +46,7 @@ public class PusherServiceGrpc(
ErrorMessage = request.Packet.ErrorMessage
};
foreach (var userId in request.UserIds)
webSocketService.SendPacketToAccount(userId, packet);
websocket.SendPacketToAccount(userId, packet);
return Task.FromResult(new Empty());
}
@@ -60,7 +60,7 @@ public class PusherServiceGrpc(
Data = request.Packet.Data,
ErrorMessage = request.Packet.ErrorMessage
};
webSocketService.SendPacketToDevice(request.DeviceId, packet);
websocket.SendPacketToDevice(request.DeviceId, packet);
return Task.FromResult(new Empty());
}
@@ -74,7 +74,7 @@ public class PusherServiceGrpc(
ErrorMessage = request.Packet.ErrorMessage
};
foreach (var deviceId in request.DeviceIds)
webSocketService.SendPacketToDevice(deviceId, packet);
websocket.SendPacketToDevice(deviceId, packet);
return Task.FromResult(new Empty());
}
@@ -159,4 +159,22 @@ public class PusherServiceGrpc(
await pushService.SendNotificationBatch(notification, accounts, request.Notification.IsSavable);
return new Empty();
}
public override async Task<Empty> UnsubscribePushNotifications(UnsubscribePushNotificationsRequest request, ServerCallContext context)
{
await pushService.UnsubscribeDevice(request.DeviceId);
return new Empty();
}
public override Task<GetWebsocketConnectionStatusResponse> GetWebsocketConnectionStatus(GetWebsocketConnectionStatusRequest request, ServerCallContext context)
{
var isConnected = request.IdCase switch
{
GetWebsocketConnectionStatusRequest.IdOneofCase.DeviceId => websocket.GetDeviceIsConnected(request.DeviceId),
GetWebsocketConnectionStatusRequest.IdOneofCase.UserId => websocket.GetAccountIsConnected(request.UserId),
_ => false
};
return Task.FromResult(new GetWebsocketConnectionStatusResponse { IsConnected = isConnected });
}
}

View File

@@ -33,8 +33,6 @@ public static class ApplicationConfiguration
app.UseAuthorization();
app.MapControllers().RequireRateLimiting("fixed");
app.MapStaticAssets().RequireRateLimiting("fixed");
app.MapRazorPages().RequireRateLimiting("fixed");
return app;
}

View File

@@ -1,11 +1,10 @@
using System.Text.Json;
using System.Threading.RateLimiting;
using dotnet_etcd.interfaces;
using DysonNetwork.Pusher.Connection;
using DysonNetwork.Pusher.Email;
using DysonNetwork.Pusher.Notification;
using DysonNetwork.Pusher.Services;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Proto;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.OpenApi.Models;
using NodaTime;
@@ -44,18 +43,6 @@ public static class ServiceCollectionExtensions
// Register gRPC services
services.AddScoped<PusherServiceGrpc>();
// Register AuthService.AuthServiceClient for AuthMiddleware
services.AddSingleton(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var configuration = sp.GetRequiredService<IConfiguration>();
var clientCertPath = configuration["ClientCert:Path"];
var clientKeyPath = configuration["ClientKey:Path"];
var clientCertPassword = configuration["ClientCert:Password"];
return GrpcClientHelper.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword);
});
// Register OIDC services
services.AddControllers().AddJsonOptions(options =>
{
@@ -144,6 +131,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services)
{
services.AddScoped<WebSocketService>();
services.AddScoped<EmailService>();
services.AddScoped<PushService>();

View File

@@ -1,56 +0,0 @@
using DysonNetwork.Shared.Registry;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace DysonNetwork.Pusher.Startup;
public class ServiceRegistrationHostedService : IHostedService
{
private readonly ServiceRegistry _serviceRegistry;
private readonly IConfiguration _configuration;
private readonly ILogger<ServiceRegistrationHostedService> _logger;
public ServiceRegistrationHostedService(
ServiceRegistry serviceRegistry,
IConfiguration configuration,
ILogger<ServiceRegistrationHostedService> logger)
{
_serviceRegistry = serviceRegistry;
_configuration = configuration;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var serviceName = "DysonNetwork.Pusher"; // Preset service name
var serviceUrl = _configuration["Service:Url"];
if (string.IsNullOrEmpty(serviceUrl))
{
_logger.LogWarning("Service URL not configured. Skipping Etcd registration.");
return;
}
_logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl);
try
{
await _serviceRegistry.RegisterService(serviceName, serviceUrl);
_logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
// The lease will expire automatically if the service stops.
// For explicit unregistration, you would implement it here.
_logger.LogInformation("Service registration hosted service is stopping.");
return Task.CompletedTask;
}
}

View File

@@ -1,6 +1,6 @@
{
"Debug": true,
"BaseUrl": "http://localhost:5071",
"BaseUrl": "http://localhost:5212",
"Logging": {
"LogLevel": {
"Default": "Information",
@@ -9,67 +9,9 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379"
},
"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"
"App": "Host=localhost;Port=5432;Database=dyson_pusher;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379",
"Etcd": "etcd.orb.local:2379"
},
"Notifications": {
"Topic": "dev.solsynth.solian",
@@ -85,45 +27,20 @@
"FromName": "Alphabot",
"SubjectPrefix": "Solar Network"
},
"RealtimeChat": {
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
"ApiKey": "APIs6TiL8wj3A4j",
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
},
"GeoIp": {
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
},
"Oidc": {
"Google": {
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"ClientSecret": ""
},
"Apple": {
"ClientId": "dev.solsynth.solian",
"TeamId": "W7HPZ53V6B",
"KeyId": "B668YP4KBG",
"PrivateKeyPath": "./Keys/Solarpass.p8"
},
"Microsoft": {
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
}
},
"Payment": {
"Auth": {
"Afdian": "<token here>"
},
"Subscriptions": {
"Afdian": {
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
}
}
},
"KnownProxies": [
"127.0.0.1",
"::1"
]
],
"Service": {
"Name": "DysonNetwork.Pusher",
"Url": "http://localhost:5212",
"ClientCert": "../Certificates/client.crt",
"ClientKey": "../Certificates/client.key"
},
"Etcd": {
"Insecure": true
}
}

View File

@@ -16,10 +16,9 @@ public class DysonTokenAuthHandler(
IOptionsMonitor<DysonTokenAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
AuthService.AuthServiceClient auth
)
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder, clock)
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{

View File

@@ -0,0 +1,72 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Shared.Auth
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RequiredPermissionAttribute(string area, string key) : Attribute
{
public string Area { get; set; } = area;
public string Key { get; } = key;
}
public class PermissionMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext httpContext, PermissionService.PermissionServiceClient permissionService, ILogger<PermissionMiddleware> logger)
{
var endpoint = httpContext.GetEndpoint();
var attr = endpoint?.Metadata
.OfType<RequiredPermissionAttribute>()
.FirstOrDefault();
if (attr != null)
{
if (httpContext.Items["CurrentUser"] is not Account currentUser)
{
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
await httpContext.Response.WriteAsync("Unauthorized");
return;
}
// Assuming Account proto has a bool field 'is_superuser' which is generated as 'IsSuperuser'
if (currentUser.IsSuperuser)
{
// Bypass the permission check for performance
await next(httpContext);
return;
}
var actor = $"user:{currentUser.Id}";
try
{
var permResp = await permissionService.HasPermissionAsync(new HasPermissionRequest
{
Actor = actor,
Area = attr.Area,
Key = attr.Key
});
if (!permResp.HasPermission)
{
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} was required.");
return;
}
}
catch (RpcException ex)
{
logger.LogError(ex, "gRPC call to PermissionService failed while checking permission {Area}/{Key} for actor {Actor}", attr.Area, attr.Key, actor);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync("Error checking permissions.");
return;
}
}
await next(httpContext);
}
}
}

View File

@@ -12,17 +12,20 @@ public static class DysonAuthStartup
IConfiguration configuration
)
{
services.AddSingleton(sp =>
services.AddSingleton<AuthService.AuthServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["ClientCert:Path"];
var clientKeyPath = config["ClientKey:Path"];
var clientCertPassword = config["ClientCert:Password"];
var clientCertPath = config["Service:ClientCert"];
var clientKeyPath = config["Service:ClientKey"];
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword);
return GrpcClientHelper
.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = AuthConstants.SchemeName;

View File

@@ -19,8 +19,7 @@
</PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@@ -33,17 +33,6 @@ public static class GrpcClientHelper
return response.Kvs[0].Value.ToStringUtf8();
}
public static AccountService.AccountServiceClient CreateAccountServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
@@ -51,22 +40,11 @@ public static class GrpcClientHelper
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService");
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static AuthService.AuthServiceClient CreateAuthServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
@@ -74,22 +52,11 @@ public static class GrpcClientHelper
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService");
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static PusherService.PusherServiceClient CreatePusherServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
@@ -97,7 +64,7 @@ public static class GrpcClientHelper
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService");
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pusher");
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}

View File

@@ -195,6 +195,7 @@ message LevelingInfo {
service AccountService {
// Account Operations
rpc GetAccount(GetAccountRequest) returns (Account) {}
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc CreateAccount(CreateAccountRequest) returns (Account) {}
rpc UpdateAccount(UpdateAccountRequest) returns (Account) {}
rpc DeleteAccount(DeleteAccountRequest) returns (google.protobuf.Empty) {}
@@ -243,6 +244,14 @@ message GetAccountRequest {
string id = 1; // Account ID to retrieve
}
message GetAccountBatchRequest {
repeated string id = 1; // Account ID to retrieve
}
message GetAccountBatchResponse {
repeated Account accounts = 1; // List of accounts
}
message CreateAccountRequest {
string name = 1; // Required: Unique username
string nick = 2; // Optional: Display name

View File

@@ -36,6 +36,12 @@ service PusherService {
// Sends a push notification to a list of users.
rpc SendPushNotificationToUsers(SendPushNotificationToUsersRequest) returns (google.protobuf.Empty) {}
// Unsubscribes a device from push notifications.
rpc UnsubscribePushNotifications(UnsubscribePushNotificationsRequest) returns (google.protobuf.Empty) {}
// Gets the WebSocket connection status for a device or user.
rpc GetWebsocketConnectionStatus(GetWebsocketConnectionStatusRequest) returns (GetWebsocketConnectionStatusResponse) {}
}
// Represents an email message.
@@ -108,3 +114,18 @@ message SendPushNotificationToUsersRequest {
repeated string user_ids = 1;
PushNotification notification = 2;
}
message UnsubscribePushNotificationsRequest {
string device_id = 1;
}
message GetWebsocketConnectionStatusRequest {
oneof id {
string device_id = 1;
string user_id = 2;
}
}
message GetWebsocketConnectionStatusResponse {
bool is_connected = 1;
}

View File

@@ -11,10 +11,17 @@ public class RegistryHostedService(
)
: IHostedService
{
private CancellationTokenSource? _cts;
public async Task StartAsync(CancellationToken cancellationToken)
{
var serviceName = configuration["Service:Name"];
var serviceUrl = configuration["Service:Url"];
var insecure = configuration.GetValue<bool>("Etcd:Insecure");
var remote = configuration.GetConnectionString("Etcd");
if (insecure)
logger.LogWarning("Etcd is configured to use insecure channel.");
if (string.IsNullOrEmpty(serviceUrl) || string.IsNullOrEmpty(serviceName))
{
@@ -22,10 +29,16 @@ public class RegistryHostedService(
return;
}
logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl);
logger.LogInformation(
"Registering service {ServiceName} at {ServiceUrl} with Etcd ({Remote}).",
serviceName,
serviceUrl,
remote
);
try
{
await serviceRegistry.RegisterService(serviceName, serviceUrl);
_cts = new CancellationTokenSource();
await serviceRegistry.RegisterService(serviceName, serviceUrl, cancellationToken: _cts.Token);
logger.LogInformation("Service {ServiceName} registered successfully.", serviceName);
}
catch (Exception ex)
@@ -36,6 +49,8 @@ public class RegistryHostedService(
public async Task StopAsync(CancellationToken cancellationToken)
{
_cts?.Cancel();
// The lease will expire automatically if the service stops ungracefully.
var serviceName = configuration["Service:Name"];
if (serviceName is not null)

View File

@@ -0,0 +1,47 @@
using dotnet_etcd.interfaces;
using DysonNetwork.Shared.Proto;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DysonNetwork.Shared.Registry;
public static class ServiceHelper
{
public static IServiceCollection AddPusherService(this IServiceCollection services)
{
services.AddSingleton<PusherService.PusherServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"];
var clientKeyPath = config["Service:ClientKey"];
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreatePusherServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
return services;
}
public static IServiceCollection AddAccountService(this IServiceCollection services)
{
services.AddSingleton<AccountService.AccountServiceClient>(sp =>
{
var etcdClient = sp.GetRequiredService<IEtcdClient>();
var config = sp.GetRequiredService<IConfiguration>();
var clientCertPath = config["Service:ClientCert"];
var clientKeyPath = config["Service:ClientKey"];
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
.CreateAccountServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
.GetAwaiter()
.GetResult();
});
return services;
}
}

View File

@@ -2,22 +2,38 @@ using System.Text;
using dotnet_etcd.interfaces;
using Etcdserverpb;
using Google.Protobuf;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Shared.Registry;
public class ServiceRegistry(IEtcdClient etcd)
public class ServiceRegistry(IEtcdClient etcd, ILogger<ServiceRegistry> logger)
{
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60)
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60,
CancellationToken cancellationToken = default)
{
var key = $"/services/{serviceName}";
var leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds });
var leaseResponse = await etcd.LeaseGrantAsync(
new LeaseGrantRequest { TTL = leaseTtlSeconds },
cancellationToken: cancellationToken
);
await etcd.PutAsync(new PutRequest
{
Key = ByteString.CopyFrom(key, Encoding.UTF8),
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
Lease = leaseResponse.ID
});
await etcd.LeaseKeepAlive(leaseResponse.ID, CancellationToken.None);
}, cancellationToken: cancellationToken);
_ = Task.Run(async () =>
{
try
{
await etcd.LeaseKeepAlive(leaseResponse.ID, cancellationToken);
}
catch (Exception ex)
{
logger.LogError($"Lease keep-alive failed: {ex.Message}");
}
}, cancellationToken);
}
public async Task UnregisterService(string serviceName)

View File

@@ -190,7 +190,6 @@ public class NotificationService(
Content = notification.Content,
Meta = notification.Meta,
Priority = notification.Priority,
Account = x,
AccountId = x.Id
};
return newNotification;

View File

@@ -30,7 +30,7 @@
<PackageReference Include="Markdig" Version="0.41.3" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -11,7 +11,7 @@
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
"FastRetrieve": "localhost:6379",
"Etcd": "localhost:2379"
"Etcd": "etcd.orb.local:2379"
},
"Authentication": {
"Schemes": {

View File

@@ -3,6 +3,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAny_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003F67_003F87f868e3_003FAny_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003Fc5_003F2a1973a9_003FApnSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApnSettings_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F0f_003F51443844_003FApnSettings_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentNullException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe6898c1ddf974e16b95b114722270029e55000_003Faf_003F30ff0e5c_003FArgumentNullException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F1f1354e4dbf943ecb04840af5ff9a527fa20_003F5d_003F1fb111f6_003FAuthenticationHandler_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationMiddleware_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe49de78932194d52a02b07486c6d023a24600_003F2f_003F7ab1cc57_003FAuthenticationMiddleware_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9b24a56e61ae4d86a9e8ba13482a2db924600_003F5b_003F9e854504_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -28,6 +29,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADiagnosticServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F47e01f36dea14a23aaea6e0391c1347ace00_003F3c_003F140e6d8b_003FDiagnosticServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointHttpContextExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc181aff8c6ec418494a7efcfec578fc154e00_003F81_003F048fd513_003FEndpointHttpContextExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -81,6 +83,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcbafb95b4df34952928f87356db00c8f2fe00_003F9b_003F8ba036bb_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARazorPage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F81d2924a2bbd4b0c864a1d23cbf5f0893d200_003F5f_003Fc110be1c_003FRazorPage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARepeatedField_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F331aca3f6f414013b09964063341351379060_003Fc1_003F67c16263_003FRepeatedField_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResizeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fef3339e864a448e2b1ec6fa7bbf4c6661fee00_003F48_003F0209e410_003FResizeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResourceManagerStringLocalizerFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb62f365d06c44ad695ff75960cdf97a2a800_003Fe4_003Ff6ba93b7_003FResourceManagerStringLocalizerFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARSA_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fee4f989f6b8042b59b2654fdc188e287243600_003F8b_003F44e5f855_003FRSA_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>