♻️ Moving to MagicOnion
This commit is contained in:
		| @@ -1,7 +1,9 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Caching.Distributed; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
|  | ||||
| @@ -9,11 +11,9 @@ namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class AccountEventService( | ||||
|     AppDatabase db, | ||||
|     // WebSocketService ws, | ||||
|     // ICacheService cache, | ||||
|     // PaymentService payment, | ||||
|     ICacheService cache, | ||||
|     IStringLocalizer<Localization.AccountEventResource> localizer | ||||
| ) | ||||
| ) : ServiceBase<IAccountEventService>, IAccountEventService | ||||
| { | ||||
|     private static readonly Random Random = new(); | ||||
|     private const string StatusCacheKey = "AccountStatus_"; | ||||
| @@ -21,18 +21,18 @@ public class AccountEventService( | ||||
|     public void PurgeStatusCache(Guid userId) | ||||
|     { | ||||
|         var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|         // cache.RemoveAsync(cacheKey); | ||||
|         cache.RemoveAsync(cacheKey); | ||||
|     } | ||||
|  | ||||
|     public async Task<Status> GetStatus(Guid userId) | ||||
|     { | ||||
|         var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|         // var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|         // if (cachedStatus is not null) | ||||
|         // { | ||||
|         //     cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||
|         //     return cachedStatus; | ||||
|         // } | ||||
|         var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|         if (cachedStatus is not null) | ||||
|         { | ||||
|             cachedStatus!.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/; | ||||
|             return cachedStatus; | ||||
|         } | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
| @@ -45,8 +45,12 @@ public class AccountEventService( | ||||
|         if (status is not null) | ||||
|         { | ||||
|             status.IsOnline = !status.IsInvisible && isOnline; | ||||
|             // await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"], | ||||
|             //     TimeSpan.FromMinutes(5)); | ||||
|             await cache.SetWithGroupsAsync( | ||||
|                 cacheKey, | ||||
|                 status, | ||||
|                 [$"{AccountService.AccountCachePrefix}{status.AccountId}"], | ||||
|                 TimeSpan.FromMinutes(5) | ||||
|             ); | ||||
|             return status; | ||||
|         } | ||||
|  | ||||
| @@ -62,7 +66,7 @@ public class AccountEventService( | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|                 return new Status | ||||
|         return new Status | ||||
|         { | ||||
|             Attitude = StatusAttitude.Neutral, | ||||
|             IsOnline = false, | ||||
| @@ -88,7 +92,7 @@ public class AccountEventService( | ||||
|             // } | ||||
|             // else | ||||
|             // { | ||||
|                 cacheMissUserIds.Add(userId); | ||||
|             cacheMissUserIds.Add(userId); | ||||
|             // } | ||||
|         } | ||||
|  | ||||
| @@ -192,27 +196,28 @@ public class AccountEventService( | ||||
|         return lastDate < currentDate; | ||||
|     } | ||||
|  | ||||
|     public const string CheckInLockKey = "CheckInLock_"; | ||||
|     private const string CheckInLockKey = "checkin-lock:"; | ||||
|  | ||||
|     public async Task<CheckInResult> CheckInDaily(Shared.Models.Account user) | ||||
|     { | ||||
|         var lockKey = $"{CheckInLockKey}{user.Id}"; | ||||
|          | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             // var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(100)); | ||||
|              | ||||
|             // if (lk != null) | ||||
|             //     await lk.ReleaseAsync(); | ||||
|             var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(100)); | ||||
|  | ||||
|             if (lk != null) | ||||
|                 await lk.ReleaseAsync(); | ||||
|         } | ||||
|         catch | ||||
|         { | ||||
|             // Ignore errors from this pre-check | ||||
|         } | ||||
|          | ||||
|  | ||||
|         // Now try to acquire the lock properly | ||||
|         // await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         // if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); | ||||
|         await using var lockObj = | ||||
|             await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); | ||||
|  | ||||
|         var cultureInfo = new CultureInfo(user.Language, false); | ||||
|         CultureInfo.CurrentCulture = cultureInfo; | ||||
| @@ -274,12 +279,53 @@ public class AccountEventService( | ||||
|                 s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience) | ||||
|             ); | ||||
|         db.AccountCheckInResults.Add(result); | ||||
|         await db.SaveChangesAsync();  // Don't forget to save changes to the database | ||||
|         await db.SaveChangesAsync(); // Remember to save changes to the database | ||||
|  | ||||
|         // The lock will be automatically released by the await using statement | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public async Task<int> GetCheckInStreak(Shared.Models.Account user) | ||||
|     { | ||||
|         var today = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||
|         var yesterdayEnd = today.PlusDays(-1).AtMidnight().InUtc().ToInstant(); | ||||
|         var yesterdayStart = today.PlusDays(-1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); | ||||
|         var tomorrowEnd = today.PlusDays(1).AtMidnight().InUtc().ToInstant(); | ||||
|         var tomorrowStart = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); | ||||
|  | ||||
|         var yesterdayResult = await db.AccountCheckInResults | ||||
|             .Where(x => x.AccountId == user.Id) | ||||
|             .Where(x => x.CreatedAt >= yesterdayStart) | ||||
|             .Where(x => x.CreatedAt < yesterdayEnd) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         var tomorrowResult = await db.AccountCheckInResults | ||||
|             .Where(x => x.AccountId == user.Id) | ||||
|             .Where(x => x.CreatedAt >= tomorrowStart) | ||||
|             .Where(x => x.CreatedAt < tomorrowEnd) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         if (yesterdayResult is null && tomorrowResult is null) | ||||
|             return 1; | ||||
|  | ||||
|         var results = await db.AccountCheckInResults | ||||
|             .Where(x => x.AccountId == user.Id) | ||||
|             .OrderByDescending(x => x.CreatedAt) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var streak = 0; | ||||
|         var day = today; | ||||
|         while (results.Any(x => | ||||
|                    x.CreatedAt >= day.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant() && | ||||
|                    x.CreatedAt < day.AtMidnight().InUtc().ToInstant())) | ||||
|         { | ||||
|             streak++; | ||||
|             day = day.PlusDays(-1); | ||||
|         } | ||||
|  | ||||
|         return streak; | ||||
|     } | ||||
|  | ||||
|     public async Task<List<DailyEventResponse>> GetEventCalendar(Shared.Models.Account user, int month, int year = 0, | ||||
|         bool replaceInvisible = false) | ||||
|     { | ||||
|   | ||||
| @@ -1,61 +0,0 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Protos.Account; | ||||
| using Grpc.Core; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Pass.Auth; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class AccountGrpcService(AppDatabase db, AuthService auth) | ||||
|     : DysonNetwork.Shared.Protos.Account.AccountService.AccountServiceBase | ||||
| { | ||||
|     public override async Task<AccountResponse> GetAccount(Empty request, ServerCallContext context) | ||||
|     { | ||||
|         var account = await GetAccountFromContext(context); | ||||
|         return ToAccountResponse(account); | ||||
|     } | ||||
|  | ||||
|     public override async Task<AccountResponse> UpdateAccount(UpdateAccountRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var account = await GetAccountFromContext(context); | ||||
|  | ||||
|         // TODO: implement | ||||
|  | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return ToAccountResponse(account); | ||||
|     } | ||||
|  | ||||
|     private async Task<Shared.Models.Account> GetAccountFromContext(ServerCallContext context) | ||||
|     { | ||||
|         var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization"); | ||||
|         if (authorizationHeader == null) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Missing authorization header.")); | ||||
|         } | ||||
|  | ||||
|         var token = authorizationHeader.Value.Replace("Bearer ", ""); | ||||
|         if (!auth.ValidateToken(token, out var sessionId)) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid token.")); | ||||
|         } | ||||
|  | ||||
|         var session = await db.AuthSessions.Include(s => s.Account).ThenInclude(a => a.Contacts) | ||||
|             .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|         if (session == null) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Session not found.")); | ||||
|         } | ||||
|  | ||||
|         return session.Account; | ||||
|     } | ||||
|  | ||||
|     private AccountResponse ToAccountResponse(Shared.Models.Account account) | ||||
|     { | ||||
|         // TODO: implement | ||||
|         return new AccountResponse | ||||
|         { | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -1,14 +1,16 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| using OtpNet; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using EFCore.BulkExtensions; | ||||
| using MagicOnion.Server; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| @@ -21,7 +23,7 @@ public class AccountService( | ||||
|     // IStringLocalizer<NotificationResource> localizer, | ||||
|     ICacheService cache, | ||||
|     ILogger<AccountService> logger | ||||
| ) | ||||
| ) : ServiceBase<IAccountService>, IAccountService | ||||
| { | ||||
|     public static void SetCultureInfo(Shared.Models.Account account) | ||||
|     { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| @@ -6,7 +8,7 @@ namespace DysonNetwork.Pass.Account; | ||||
| /// <summary> | ||||
| /// Service for handling username generation and validation | ||||
| /// </summary> | ||||
| public class AccountUsernameService(AppDatabase db) | ||||
| public class AccountUsernameService(AppDatabase db) : ServiceBase<IAccountUsernameService>, IAccountUsernameService | ||||
| { | ||||
|     private readonly Random _random = new(); | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,24 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.AspNetCore.Http; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class ActionLogService( | ||||
|     // GeoIpService geo, | ||||
|     // FlushBufferService fbs | ||||
| ) | ||||
| public class ActionLogService : ServiceBase<IActionLogService>, IActionLogService | ||||
| { | ||||
|     // private readonly GeoIpService _geo; | ||||
|     // private readonly FlushBufferService _fbs; | ||||
|  | ||||
|     public ActionLogService( | ||||
|         // GeoIpService geo, | ||||
|         // FlushBufferService fbs | ||||
|     ) | ||||
|     { | ||||
|         // _geo = geo; | ||||
|         // _fbs = fbs; | ||||
|     } | ||||
|      | ||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public enum MagicSpellType | ||||
| { | ||||
|     AccountActivation, | ||||
|     AccountDeactivation, | ||||
|     AccountRemoval, | ||||
|     AuthPasswordReset, | ||||
|     ContactVerification, | ||||
| } | ||||
|  | ||||
| [Index(nameof(Spell), IsUnique = true)] | ||||
| public class MagicSpell : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [JsonIgnore] [MaxLength(1024)] public string Spell { get; set; } = null!; | ||||
|     public MagicSpellType Type { get; set; } | ||||
|     public Instant? ExpiresAt { get; set; } | ||||
|     public Instant? AffectedAt { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|  | ||||
|     public Guid? AccountId { get; set; } | ||||
|     public Shared.Models.Account? Account { get; set; } | ||||
| } | ||||
| @@ -2,6 +2,8 @@ using System.Globalization; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| @@ -12,11 +14,9 @@ namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class MagicSpellService( | ||||
|     AppDatabase db, | ||||
|     // EmailService email, | ||||
|     IConfiguration configuration, | ||||
|     ILogger<MagicSpellService> logger | ||||
|     // IStringLocalizer<Localization.EmailResource> localizer | ||||
| ) | ||||
| ) : ServiceBase<IMagicSpellService>, IMagicSpellService | ||||
| { | ||||
|     public async Task<MagicSpell> CreateMagicSpell( | ||||
|         Shared.Models.Account account, | ||||
| @@ -59,6 +59,17 @@ public class MagicSpellService( | ||||
|         return spell; | ||||
|     } | ||||
|  | ||||
|     public async Task<MagicSpell?> GetMagicSpellAsync(string token) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var spell = await db.MagicSpells | ||||
|             .Where(s => s.Spell == token) | ||||
|             .Where(s => s.ExpiresAt == null || s.ExpiresAt > now) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         return spell; | ||||
|     } | ||||
|  | ||||
|     public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) | ||||
|     { | ||||
|         var contact = await db.AccountContacts | ||||
| @@ -144,8 +155,15 @@ public class MagicSpellService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task ApplyMagicSpell(MagicSpell spell) | ||||
|     public async Task ApplyMagicSpell(string token) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var spell = await db.MagicSpells | ||||
|             .Where(s => s.Spell == token) | ||||
|             .Where(s => s.ExpiresAt == null || s.ExpiresAt > now) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (spell is null) throw new ArgumentException("Magic spell not found."); | ||||
|          | ||||
|         switch (spell.Type) | ||||
|         { | ||||
|             case MagicSpellType.AuthPasswordReset: | ||||
|   | ||||
| @@ -1,29 +1,29 @@ | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using EFCore.BulkExtensions; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using System.Net.Http; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class NotificationService( | ||||
|     AppDatabase db | ||||
|     // WebSocketService ws, | ||||
|     // IHttpClientFactory httpFactory, | ||||
|     // IConfiguration config | ||||
| ) | ||||
|     AppDatabase db, | ||||
|     IConfiguration config, | ||||
|     IHttpClientFactory httpFactory | ||||
| ) : ServiceBase<INotificationService>, INotificationService | ||||
| { | ||||
|     // private readonly string _notifyTopic = config["Notifications:Topic"]!; | ||||
|     // private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); | ||||
|     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(); | ||||
|         await db.NotificationPushSubscriptions | ||||
|             .Where(s => s.DeviceId == deviceId) | ||||
|             .ExecuteDeleteAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<NotificationPushSubscription> SubscribePushNotification( | ||||
| @@ -34,29 +34,29 @@ public class NotificationService( | ||||
|     ) | ||||
|     { | ||||
|         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(); | ||||
|         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)); | ||||
|         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; | ||||
|         // } | ||||
|             // Return the updated subscription | ||||
|             existingSubscription.DeviceId = deviceId; | ||||
|             existingSubscription.DeviceToken = deviceToken; | ||||
|             existingSubscription.UpdatedAt = now; | ||||
|             return existingSubscription; | ||||
|         } | ||||
|  | ||||
|         var subscription = new NotificationPushSubscription | ||||
|         { | ||||
| @@ -102,11 +102,12 @@ public class NotificationService( | ||||
|  | ||||
|         if (save) | ||||
|         { | ||||
|             // db.Add(notification); | ||||
|             // await db.SaveChangesAsync(); | ||||
|             db.Add(notification); | ||||
|             await db.SaveChangesAsync(); | ||||
|         } | ||||
|  | ||||
|         if (!isSilent) Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification); | ||||
|         if (!isSilent) | ||||
|             Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification); | ||||
|  | ||||
|         return notification; | ||||
|     } | ||||
| @@ -120,11 +121,11 @@ public class NotificationService( | ||||
|         // }); | ||||
|  | ||||
|         // Pushing the notification | ||||
|         // var subscribers = await db.NotificationPushSubscriptions | ||||
|         //     .Where(s => s.AccountId == notification.AccountId) | ||||
|         //     .ToListAsync(); | ||||
|         var subscribers = await db.NotificationPushSubscriptions | ||||
|             .Where(s => s.AccountId == notification.AccountId) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         // await _PushNotification(notification, subscribers); | ||||
|         await _PushNotification(notification, subscribers); | ||||
|     } | ||||
|  | ||||
|     public async Task MarkNotificationsViewed(ICollection<Notification> notifications) | ||||
| @@ -174,12 +175,13 @@ public class NotificationService( | ||||
|             // }); | ||||
|         } | ||||
|  | ||||
|         // var subscribers = await db.NotificationPushSubscriptions | ||||
|         //     .ToListAsync(); | ||||
|         // await _PushNotification(notification, subscribers); | ||||
|         var subscribers = await db.NotificationPushSubscriptions | ||||
|             .ToListAsync(); | ||||
|         await _PushNotification(notification, subscribers); | ||||
|     } | ||||
|  | ||||
|     public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts, bool save = false) | ||||
|     public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts, | ||||
|         bool save = false) | ||||
|     { | ||||
|         if (save) | ||||
|         { | ||||
| @@ -198,7 +200,7 @@ public class NotificationService( | ||||
|                 }; | ||||
|                 return newNotification; | ||||
|             }).ToList(); | ||||
|             // await db.BulkInsertAsync(notifications); | ||||
|             await db.BulkInsertAsync(notifications); | ||||
|         } | ||||
|  | ||||
|         foreach (var account in accounts) | ||||
| @@ -219,93 +221,93 @@ public class NotificationService( | ||||
|         // 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()); | ||||
|     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 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(); | ||||
|             var tokens = value.Value.Select(x => x.DeviceToken).ToList(); | ||||
|             return _BuildNotificationPayload(notification, platformCode, tokens); | ||||
|         }).ToList(); | ||||
|  | ||||
|     //     return notifications.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", | ||||
|     //     }; | ||||
|     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.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.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 (!string.IsNullOrWhiteSpace(notification.Subtitle)) | ||||
|         { | ||||
|             dict["message"] = $"{notification.Subtitle}\n{dict["message"]}"; | ||||
|             alertDict["subtitle"] = notification.Subtitle; | ||||
|         } | ||||
|  | ||||
|     //     if (notification.Priority >= 5) | ||||
|     //         dict["name"] = "default"; | ||||
|         if (notification.Priority >= 5) | ||||
|             dict["name"] = "default"; | ||||
|  | ||||
|     //     dict["platform"] = platformCode; | ||||
|     //     dict["alert"] = alertDict; | ||||
|         dict["platform"] = platformCode; | ||||
|         dict["alert"] = alertDict; | ||||
|  | ||||
|     //     return dict; | ||||
|     // } | ||||
|         return dict; | ||||
|     } | ||||
|  | ||||
|     // private async Task _PushNotification(Notification notification, | ||||
|     //     IEnumerable<NotificationPushSubscription> subscriptions) | ||||
|     // { | ||||
|     //     var subList = subscriptions.ToList(); | ||||
|     //     if (subList.Count == 0) return; | ||||
|     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 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(); | ||||
|     // } | ||||
|         var client = httpFactory.CreateClient(); | ||||
|         client.BaseAddress = _notifyEndpoint; | ||||
|         var request = await client.PostAsync("/push", new StringContent( | ||||
|             JsonSerializer.Serialize(requestDict), | ||||
|             Encoding.UTF8, | ||||
|             "application/json" | ||||
|         )); | ||||
|         request.EnsureSuccessStatusCode(); | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,13 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class RelationshipService( | ||||
|     AppDatabase db | ||||
|     // ICacheService cache | ||||
| ) | ||||
| public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceBase<IRelationshipService>, IRelationshipService | ||||
| { | ||||
|     private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; | ||||
|     private const string UserBlockedCacheKeyPrefix = "accounts:blocked:"; | ||||
| @@ -150,7 +150,7 @@ public class RelationshipService( | ||||
|         db.Update(relationship); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|         // await PurgeRelationshipCache(accountId, relatedId); | ||||
|         await PurgeRelationshipCache(accountId, relatedId); | ||||
|          | ||||
|         return relationship; | ||||
|     } | ||||
| @@ -158,8 +158,7 @@ public class RelationshipService( | ||||
|     public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account) | ||||
|     { | ||||
|         var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; | ||||
|         // var friends = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|         var friends = new List<Guid>(); // Placeholder | ||||
|         var friends = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|          | ||||
|         if (friends == null) | ||||
|         { | ||||
| @@ -169,17 +168,16 @@ public class RelationshipService( | ||||
|                 .Select(r => r.AccountId) | ||||
|                 .ToListAsync(); | ||||
|                  | ||||
|             // await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1)); | ||||
|             await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1)); | ||||
|         } | ||||
|  | ||||
|         return friends ?? []; | ||||
|         return friends; | ||||
|     } | ||||
|      | ||||
|     public async Task<List<Guid>> ListAccountBlocked(Shared.Models.Account account) | ||||
|     { | ||||
|         var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}"; | ||||
|         // var blocked = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|         var blocked = new List<Guid>(); // Placeholder | ||||
|         var blocked = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|          | ||||
|         if (blocked == null) | ||||
|         { | ||||
| @@ -189,10 +187,10 @@ public class RelationshipService( | ||||
|                 .Select(r => r.AccountId) | ||||
|                 .ToListAsync(); | ||||
|                  | ||||
|             // await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1)); | ||||
|             await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1)); | ||||
|         } | ||||
|  | ||||
|         return blocked ?? []; | ||||
|         return blocked; | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, | ||||
| @@ -204,9 +202,9 @@ public class RelationshipService( | ||||
|      | ||||
|     private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId) | ||||
|     { | ||||
|         // await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}"); | ||||
|         // await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}"); | ||||
|         // await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}"); | ||||
|         // await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}"); | ||||
|         await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}"); | ||||
|         await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}"); | ||||
|         await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}"); | ||||
|         await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}"); | ||||
|     } | ||||
| } | ||||
| @@ -1,96 +0,0 @@ | ||||
| using DysonNetwork.Shared.Protos.Auth; | ||||
| using Grpc.Core; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using Challenge = DysonNetwork.Shared.Models.Challenge; | ||||
| using Session = DysonNetwork.Shared.Models.Session; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
|  | ||||
| public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth) | ||||
|     : DysonNetwork.Shared.Protos.Auth.AuthService.AuthServiceBase | ||||
| { | ||||
|     public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var account = await accounts.LookupAccount(request.Username); | ||||
|         if (account == null) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found.")); | ||||
|         } | ||||
|  | ||||
|         var factor = await db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password); | ||||
|         if (factor == null || !factor.VerifyPassword(request.Password)) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid credentials.")); | ||||
|         } | ||||
|  | ||||
|         var session = new Session | ||||
|         { | ||||
|             LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), | ||||
|             ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), | ||||
|             Account = account, | ||||
|             Challenge = new Challenge() | ||||
|         }; | ||||
|  | ||||
|         db.AuthSessions.Add(session); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         var token = auth.CreateToken(session); | ||||
|  | ||||
|         return new LoginResponse | ||||
|         { | ||||
|             AccessToken = token, | ||||
|             ExpiresIn = (long)(session.ExpiredAt.Value - session.LastGrantedAt.Value).TotalSeconds | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public override async Task<IntrospectionResponse> IntrospectToken(IntrospectTokenRequest request, ServerCallContext context) | ||||
|     { | ||||
|         if (auth.ValidateToken(request.Token, out var sessionId)) | ||||
|         { | ||||
|             var session = await db.AuthSessions | ||||
|                 .Include(s => s.Account) | ||||
|                 .Include(s => s.Challenge) | ||||
|                 .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|  | ||||
|             if (session != null) | ||||
|             { | ||||
|                 return new IntrospectionResponse | ||||
|                 { | ||||
|                     Active = true, | ||||
|                     Claims = JsonSerializer.Serialize(new { sub = session.AccountId }), | ||||
|                     ClientId = session.AppId?.ToString() ?? "", | ||||
|                     Username = session.Account.Name, | ||||
|                     Scope = string.Join(" ", session.Challenge.Scopes), | ||||
|                     Iat = Timestamp.FromDateTime(session.CreatedAt.ToDateTimeUtc()), | ||||
|                     Exp = Timestamp.FromDateTime(session.ExpiredAt?.ToDateTimeUtc() ?? DateTime.MaxValue) | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return new IntrospectionResponse { Active = false }; | ||||
|     } | ||||
|  | ||||
|     public override async Task<Empty> Logout(Empty request, ServerCallContext context) | ||||
|     { | ||||
|         var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization"); | ||||
|         if (authorizationHeader != null) | ||||
|         { | ||||
|             var token = authorizationHeader.Value.Replace("Bearer ", ""); | ||||
|             if (auth.ValidateToken(token, out var sessionId)) | ||||
|             { | ||||
|                 var session = await db.AuthSessions.FindAsync(sessionId); | ||||
|                 if (session != null) | ||||
|                 { | ||||
|                     db.AuthSessions.Remove(session); | ||||
|                     await db.SaveChangesAsync(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return new Empty(); | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.Logging; | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
|  | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|  | ||||
| public class AppleMobileConnectRequest | ||||
| { | ||||
|   | ||||
| @@ -4,7 +4,7 @@ using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.Extensions.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
|   | ||||
| @@ -3,7 +3,6 @@ using System.Net.Http.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Configuration; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
|  | ||||
| /// <summary> | ||||
| /// Represents the state parameter used in OpenID Connect flows. | ||||
|   | ||||
| @@ -1,49 +0,0 @@ | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
|  | ||||
| /// <summary> | ||||
| /// Represents the user information from an OIDC provider | ||||
| /// </summary> | ||||
| public class OidcUserInfo | ||||
| { | ||||
|     public string? UserId { get; set; } | ||||
|     public string? Email { get; set; } | ||||
|     public bool EmailVerified { get; set; } | ||||
|     public string FirstName { get; set; } = ""; | ||||
|     public string LastName { get; set; } = ""; | ||||
|     public string DisplayName { get; set; } = ""; | ||||
|     public string PreferredUsername { get; set; } = ""; | ||||
|     public string? ProfilePictureUrl { get; set; } | ||||
|     public string Provider { get; set; } = ""; | ||||
|     public string? RefreshToken { get; set; } | ||||
|     public string? AccessToken { get; set; } | ||||
|  | ||||
|     public Dictionary<string, object> ToMetadata() | ||||
|     { | ||||
|         var metadata = new Dictionary<string, object>(); | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(UserId)) | ||||
|             metadata["user_id"] = UserId; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(Email)) | ||||
|             metadata["email"] = Email; | ||||
|  | ||||
|         metadata["email_verified"] = EmailVerified; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(FirstName)) | ||||
|             metadata["first_name"] = FirstName; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(LastName)) | ||||
|             metadata["last_name"] = LastName; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(DisplayName)) | ||||
|             metadata["display_name"] = DisplayName; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(PreferredUsername)) | ||||
|             metadata["preferred_username"] = PreferredUsername; | ||||
|  | ||||
|         if (!string.IsNullOrWhiteSpace(ProfilePictureUrl)) | ||||
|             metadata["profile_picture_url"] = ProfilePictureUrl; | ||||
|  | ||||
|         return metadata; | ||||
|     } | ||||
| } | ||||
| @@ -9,24 +9,27 @@ | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" /> | ||||
|         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> | ||||
|         <PackageReference Include="MagicOnion.Server" Version="7.0.5" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" /> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3"> | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="StackExchange.Redis" Version="2.8.41" /> | ||||
|         <PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" /> | ||||
|         <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0-preview.6.24328.4" /> | ||||
|         <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0-preview1" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> | ||||
|         <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" /> | ||||
|         <PackageReference Include="Quartz" Version="3.14.0" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> | ||||
|     </ItemGroup> | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Startup; | ||||
| using DysonNetwork.Shared.Startup; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| @@ -8,9 +10,15 @@ using Microsoft.Extensions.DependencyInjection; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| // Add services to the container. | ||||
| builder.ConfigureAppKestrel(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppBusinessServices(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddControllers(); | ||||
| builder.Services.AddGrpc(); | ||||
| builder.Services.AddMagicOnion(); | ||||
| builder.Services.AddDbContext<AppDatabase>(options => | ||||
|     options.UseNpgsql(builder.Configuration.GetConnectionString("App"))); | ||||
|  | ||||
| @@ -21,10 +29,10 @@ var app = builder.Build(); | ||||
|  | ||||
| // Configure the HTTP request pipeline. | ||||
| app.UseAuthorization(); | ||||
| app.ConfigureAppMiddleware(builder.Configuration); | ||||
|  | ||||
| app.MapControllers(); | ||||
| app.MapGrpcService<AccountGrpcService>(); | ||||
| app.MapGrpcService<AuthGrpcService>(); | ||||
| app.MapMagicOnionService(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
|   | ||||
							
								
								
									
										186
									
								
								DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| using System.Globalization; | ||||
| using System.Text.Json; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
| using DysonNetwork.Pass.Auth.OpenId; | ||||
| using DysonNetwork.Pass.Localization; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using StackExchange.Redis; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Startup; | ||||
|  | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddLocalization(options => options.ResourcesPath = "Resources"); | ||||
|  | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         // Register MagicOnion services | ||||
|         services.AddScoped<IAccountService, AccountService>(); | ||||
|         services.AddScoped<INotificationService, NotificationService>(); | ||||
|         services.AddScoped<IRelationshipService, RelationshipService>(); | ||||
|         services.AddScoped<IActionLogService, ActionLogService>(); | ||||
|         services.AddScoped<IAccountUsernameService, AccountUsernameService>(); | ||||
|         services.AddScoped<IMagicSpellService, MagicSpellService>(); | ||||
|         services.AddScoped<IAccountEventService, AccountEventService>(); | ||||
|  | ||||
|         // Register OIDC services | ||||
|         services.AddScoped<OidcService, GoogleOidcService>(); | ||||
|         services.AddScoped<OidcService, AppleOidcService>(); | ||||
|         services.AddScoped<OidcService, GitHubOidcService>(); | ||||
|         services.AddScoped<OidcService, MicrosoftOidcService>(); | ||||
|         services.AddScoped<OidcService, DiscordOidcService>(); | ||||
|         services.AddScoped<OidcService, AfdianOidcService>(); | ||||
|         services.AddScoped<GoogleOidcService>(); | ||||
|         services.AddScoped<AppleOidcService>(); | ||||
|         services.AddScoped<GitHubOidcService>(); | ||||
|         services.AddScoped<MicrosoftOidcService>(); | ||||
|         services.AddScoped<DiscordOidcService>(); | ||||
|         services.AddScoped<AfdianOidcService>(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }).AddDataAnnotationsLocalization(options => | ||||
|         { | ||||
|             options.DataAnnotationLocalizerProvider = (type, factory) => | ||||
|                 factory.Create(typeof(SharedResource)); | ||||
|         }); | ||||
|         services.AddRazorPages(); | ||||
|  | ||||
|         services.Configure<RequestLocalizationOptions>(options => | ||||
|         { | ||||
|             var supportedCultures = new[] | ||||
|             { | ||||
|                 new CultureInfo("en-US"), | ||||
|                 new CultureInfo("zh-Hans"), | ||||
|             }; | ||||
|  | ||||
|             options.SupportedCultures = supportedCultures; | ||||
|             options.SupportedUICultures = supportedCultures; | ||||
|         }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => | ||||
|         { | ||||
|             opts.Window = TimeSpan.FromMinutes(1); | ||||
|             opts.PermitLimit = 120; | ||||
|             opts.QueueLimit = 2; | ||||
|             opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; | ||||
|         })); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddCors(); | ||||
|         services.AddAuthorization(); | ||||
|         services.AddAuthentication(options => | ||||
|             { | ||||
|                 options.DefaultAuthenticateScheme = AuthConstants.SchemeName; | ||||
|                 options.DefaultChallengeScheme = AuthConstants.SchemeName; | ||||
|             }) | ||||
|             .AddScheme<DysonTokenAuthOptions, DysonTokenAuthHandler>(AuthConstants.SchemeName, _ => { }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppSwagger(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddEndpointsApiExplorer(); | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Solar Network API", | ||||
|                 Description = "An open-source social network", | ||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), | ||||
|                 License = new OpenApiLicense | ||||
|                 { | ||||
|                     Name = "APGLv3", | ||||
|                     Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html") | ||||
|                 } | ||||
|             }); | ||||
|             options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | ||||
|             { | ||||
|                 In = ParameterLocation.Header, | ||||
|                 Description = "Please enter a valid token", | ||||
|                 Name = "Authorization", | ||||
|                 Type = SecuritySchemeType.Http, | ||||
|                 BearerFormat = "JWT", | ||||
|                 Scheme = "Bearer" | ||||
|             }); | ||||
|             options.AddSecurityRequirement(new OpenApiSecurityRequirement | ||||
|             { | ||||
|                 { | ||||
|                     new OpenApiSecurityScheme | ||||
|                     { | ||||
|                         Reference = new OpenApiReference | ||||
|                         { | ||||
|                             Type = ReferenceType.SecurityScheme, | ||||
|                             Id = "Bearer" | ||||
|                         } | ||||
|                     }, | ||||
|                     [] | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|         services.AddOpenApi(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services, | ||||
|         IConfiguration configuration) | ||||
|     { | ||||
|         services.AddScoped<CompactTokenService>(); | ||||
|         services.AddScoped<PermissionService>(); | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<AccountService>(); | ||||
|         services.AddScoped<AccountEventService>(); | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<RelationshipService>(); | ||||
|         services.AddScoped<MagicSpellService>(); | ||||
|         services.AddScoped<NotificationService>(); | ||||
|         services.AddScoped<AuthService>(); | ||||
|         services.AddScoped<AccountUsernameService>(); | ||||
|          | ||||
|         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); | ||||
|         services.AddScoped<OidcProviderService>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user