Compare commits
	
		
			3 Commits
		
	
	
		
			8d2f4a4c47
			...
			63b2b989ba
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 63b2b989ba | |||
| 2c67472894 | |||
| 0d47716713 | 
| @@ -1,6 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Pass.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Pass.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Http; | using Microsoft.AspNetCore.Http; | ||||||
|   | |||||||
| @@ -84,16 +84,16 @@ public class AccountEventService( | |||||||
|         foreach (var userId in userIds) |         foreach (var userId in userIds) | ||||||
|         { |         { | ||||||
|             var cacheKey = $"{StatusCacheKey}{userId}"; |             var cacheKey = $"{StatusCacheKey}{userId}"; | ||||||
|             // var cachedStatus = await cache.GetAsync<Status>(cacheKey); |             var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||||
|             // if (cachedStatus != null) |             if (cachedStatus != null) | ||||||
|             // { |             { | ||||||
|             //     cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); |                 cachedStatus.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/; | ||||||
|             //     results[userId] = cachedStatus; |                 results[userId] = cachedStatus; | ||||||
|             // } |             } | ||||||
|             // else |             else | ||||||
|             // { |             { | ||||||
|                 cacheMissUserIds.Add(userId); |                 cacheMissUserIds.Add(userId); | ||||||
|             // } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (cacheMissUserIds.Any()) |         if (cacheMissUserIds.Any()) | ||||||
| @@ -115,7 +115,7 @@ public class AccountEventService( | |||||||
|                 status.IsOnline = !status.IsInvisible && isOnline; |                 status.IsOnline = !status.IsInvisible && isOnline; | ||||||
|                 results[status.AccountId] = status; |                 results[status.AccountId] = status; | ||||||
|                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; |                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; | ||||||
|                 // await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); |                 await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); | ||||||
|                 foundUserIds.Add(status.AccountId); |                 foundUserIds.Add(status.AccountId); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -170,12 +170,12 @@ public class AccountEventService( | |||||||
|     public async Task<bool> CheckInDailyDoAskCaptcha(Shared.Models.Account user) |     public async Task<bool> CheckInDailyDoAskCaptcha(Shared.Models.Account user) | ||||||
|     { |     { | ||||||
|         var cacheKey = $"{CaptchaCacheKey}{user.Id}"; |         var cacheKey = $"{CaptchaCacheKey}{user.Id}"; | ||||||
|         // var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); |         var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); | ||||||
|         // if (needsCaptcha is not null) |         if (needsCaptcha is not null) | ||||||
|         //     return needsCaptcha!.Value; |             return needsCaptcha!.Value; | ||||||
|  |  | ||||||
|         var result = Random.Next(100) < CaptchaProbabilityPercent; |         var result = Random.Next(100) < CaptchaProbabilityPercent; | ||||||
|         // await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); |         await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								DysonNetwork.Pass/Account/AccountProfileService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								DysonNetwork.Pass/Account/AccountProfileService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using MagicOnion.Server; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Account; | ||||||
|  |  | ||||||
|  | public class AccountProfileService(AppDatabase db) : ServiceBase<IAccountProfileService>, IAccountProfileService | ||||||
|  | { | ||||||
|  |     public async Task<Profile?> GetAccountProfileByIdAsync(Guid accountId) | ||||||
|  |     { | ||||||
|  |         return await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription) | ||||||
|  |     { | ||||||
|  |         var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); | ||||||
|  |         if (profile == null) | ||||||
|  |         { | ||||||
|  |             profile = new Profile { AccountId = accountId }; | ||||||
|  |             db.AccountProfiles.Add(profile); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         profile.StellarMembership = subscription; | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return profile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<Profile>> GetAccountsWithStellarMembershipAsync() | ||||||
|  |     { | ||||||
|  |         return await db.AccountProfiles | ||||||
|  |             .Where(a => a.StellarMembership != null) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds) | ||||||
|  |     { | ||||||
|  |         return await db.AccountProfiles | ||||||
|  |             .Where(a => accountIds.Contains(a.Id)) | ||||||
|  |             .ExecuteUpdateAsync(s => s | ||||||
|  |                 .SetProperty(a => a.StellarMembership, p => null) | ||||||
|  |             ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture) | ||||||
|  |     { | ||||||
|  |         var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); | ||||||
|  |         if (profile == null) | ||||||
|  |         { | ||||||
|  |             profile = new Profile { AccountId = accountId }; | ||||||
|  |             db.AccountProfiles.Add(profile); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         profile.Picture = picture; | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return profile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background) | ||||||
|  |     { | ||||||
|  |         var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); | ||||||
|  |         if (profile == null) | ||||||
|  |         { | ||||||
|  |             profile = new Profile { AccountId = accountId }; | ||||||
|  |             db.AccountProfiles.Add(profile); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         profile.Background = background; | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return profile; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -11,18 +11,27 @@ using OtpNet; | |||||||
| using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||||
| using EFCore.BulkExtensions; | using EFCore.BulkExtensions; | ||||||
| using MagicOnion.Server; | using MagicOnion.Server; | ||||||
|  | using Grpc.Core; | ||||||
|  | using DysonNetwork.Pass.Account; | ||||||
|  | using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||||
|  | using DysonNetwork.Pass.Localization; | ||||||
|  | using DysonNetwork.Shared.Localization; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Account; | namespace DysonNetwork.Pass.Account; | ||||||
|  |  | ||||||
| public class AccountService( | public class AccountService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     // MagicSpellService spells, |     MagicSpellService spells, | ||||||
|     // AccountUsernameService uname, |     AccountUsernameService uname, | ||||||
|     // NotificationService nty, |     NotificationService nty, | ||||||
|     // EmailService mailer, |     // EmailService mailer, // Commented out for now | ||||||
|     // IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
|     ILogger<AccountService> logger |     ILogger<AccountService> logger, | ||||||
|  |     AuthService authService, | ||||||
|  |     ActionLogService actionLogService, | ||||||
|  |     RelationshipService relationshipService | ||||||
| ) : ServiceBase<IAccountService>, IAccountService | ) : ServiceBase<IAccountService>, IAccountService | ||||||
| { | { | ||||||
|     public static void SetCultureInfo(Shared.Models.Account account) |     public static void SetCultureInfo(Shared.Models.Account account) | ||||||
| @@ -134,15 +143,15 @@ public class AccountService( | |||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 // var spell = await spells.CreateMagicSpell( |                 var spell = await spells.CreateMagicSpell( | ||||||
|                 //     account, |                     account, | ||||||
|                 //     MagicSpellType.AccountActivation, |                     MagicSpellType.AccountActivation, | ||||||
|                 //     new Dictionary<string, object> |                     new Dictionary<string, object> | ||||||
|                 //     { |                     { | ||||||
|                 //         { "contact_method", account.Contacts.First().Content } |                         { "contact_method", account.Contacts.First().Content } | ||||||
|                 //     } |                     } | ||||||
|                 // ); |                 ); | ||||||
|                 // await spells.NotifyMagicSpell(spell, true); |                 await spells.NotifyMagicSpell(spell, true); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             db.Accounts.Add(account); |             db.Accounts.Add(account); | ||||||
| @@ -167,9 +176,7 @@ public class AccountService( | |||||||
|             ? userInfo.DisplayName |             ? userInfo.DisplayName | ||||||
|             : $"{userInfo.FirstName} {userInfo.LastName}".Trim(); |             : $"{userInfo.FirstName} {userInfo.LastName}".Trim(); | ||||||
|  |  | ||||||
|         // Generate username from email |         var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); | ||||||
|         // var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); |  | ||||||
|         var username = userInfo.Email.Split('@')[0]; // Placeholder |  | ||||||
|  |  | ||||||
|         return await CreateAccount( |         return await CreateAccount( | ||||||
|             username, |             username, | ||||||
| @@ -184,26 +191,26 @@ public class AccountService( | |||||||
|  |  | ||||||
|     public async Task RequestAccountDeletion(Shared.Models.Account account) |     public async Task RequestAccountDeletion(Shared.Models.Account account) | ||||||
|     { |     { | ||||||
|         // var spell = await spells.CreateMagicSpell( |         var spell = await spells.CreateMagicSpell( | ||||||
|         //     account, |             account, | ||||||
|         //     MagicSpellType.AccountRemoval, |             MagicSpellType.AccountRemoval, | ||||||
|         //     new Dictionary<string, object>(), |             new Dictionary<string, object>(), | ||||||
|         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|         //     preventRepeat: true |             preventRepeat: true | ||||||
|         // ); |         ); | ||||||
|         // await spells.NotifyMagicSpell(spell); |         await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task RequestPasswordReset(Shared.Models.Account account) |     public async Task RequestPasswordReset(Shared.Models.Account account) | ||||||
|     { |     { | ||||||
|         // var spell = await spells.CreateMagicSpell( |         var spell = await spells.CreateMagicSpell( | ||||||
|         //     account, |             account, | ||||||
|         //     MagicSpellType.AuthPasswordReset, |             MagicSpellType.AuthPasswordReset, | ||||||
|         //     new Dictionary<string, object>(), |             new Dictionary<string, object>(), | ||||||
|         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|         //     preventRepeat: true |             preventRepeat: true | ||||||
|         // ); |         ); | ||||||
|         // await spells.NotifyMagicSpell(spell); |         await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type) |     public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type) | ||||||
| @@ -329,7 +336,6 @@ public class AccountService( | |||||||
|     { |     { | ||||||
|         var count = await db.AccountAuthFactors |         var count = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == factor.AccountId) |             .Where(f => f.AccountId == factor.AccountId) | ||||||
|             // .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null)) |  | ||||||
|             .CountAsync(); |             .CountAsync(); | ||||||
|         if (count <= 1) |         if (count <= 1) | ||||||
|             throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor."); |             throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor."); | ||||||
| @@ -355,14 +361,14 @@ public class AccountService( | |||||||
|                 if (await _GetFactorCode(factor) is not null) |                 if (await _GetFactorCode(factor) is not null) | ||||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); |                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||||
|  |  | ||||||
|                 // await nty.SendNotification( |                 await nty.SendNotification( | ||||||
|                 //     account, |                     account, | ||||||
|                 //     "auth.verification", |                     "auth.verification", | ||||||
|                 //     localizer["AuthCodeTitle"], |                     localizer["AuthCodeTitle"], | ||||||
|                 //     null, |                     null, | ||||||
|                 //     localizer["AuthCodeBody", code], |                     localizer["AuthCodeBody", code], | ||||||
|                 //     save: true |                     save: true | ||||||
|                 // ); |                 ); | ||||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); |                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); | ||||||
|                 break; |                 break; | ||||||
|             case AccountAuthFactorType.EmailCode: |             case AccountAuthFactorType.EmailCode: | ||||||
| @@ -397,11 +403,11 @@ public class AccountService( | |||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 // await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( |                 // await mailer.SendTemplatedEmailAsync<DysonNetwork.Pass.Pages.Emails.VerificationEmail, DysonNetwork.Pass.Pages.Emails.VerificationEmailModel>( | ||||||
|                 //     account.Nick, |                 //     account.Nick, | ||||||
|                 //     contact.Content, |                 //     contact.Content, | ||||||
|                 //     localizer["VerificationEmail"], |                 //     localizer["VerificationEmail"], | ||||||
|                 //     new VerificationEmailModel |                 //     new DysonNetwork.Pass.Pages.Emails.VerificationEmailModel | ||||||
|                 //     { |                 //     { | ||||||
|                 //         Name = account.Name, |                 //         Name = account.Name, | ||||||
|                 //         Code = code |                 //         Code = code | ||||||
| @@ -454,7 +460,7 @@ public class AccountService( | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label) |     public async Task<Shared.Models.Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label) | ||||||
|     { |     { | ||||||
|         var session = await db.AuthSessions |         var session = await db.AuthSessions | ||||||
|             .Include(s => s.Challenge) |             .Include(s => s.Challenge) | ||||||
| @@ -491,7 +497,7 @@ public class AccountService( | |||||||
|             .ToListAsync(); |             .ToListAsync(); | ||||||
|  |  | ||||||
|         if (session.Challenge.DeviceId is not null) |         if (session.Challenge.DeviceId is not null) | ||||||
|             // await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); |             await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); | ||||||
|  |  | ||||||
|         // The current session should be included in the sessions' list |         // The current session should be included in the sessions' list | ||||||
|         await db.AuthSessions |         await db.AuthSessions | ||||||
| @@ -520,14 +526,14 @@ public class AccountService( | |||||||
|  |  | ||||||
|     public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact) |     public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact) | ||||||
|     { |     { | ||||||
|         // var spell = await spells.CreateMagicSpell( |         var spell = await spells.CreateMagicSpell( | ||||||
|         //     account, |             account, | ||||||
|         //     MagicSpellType.ContactVerification, |             MagicSpellType.ContactVerification, | ||||||
|         //     new Dictionary<string, object> { { "contact_method", contact.Content } }, |             new Dictionary<string, object> { { "contact_method", contact.Content } }, | ||||||
|         //     expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |             expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|         //     preventRepeat: true |             preventRepeat: true | ||||||
|         // ); |         ); | ||||||
|         // await spells.NotifyMagicSpell(spell); |         await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact) |     public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact) | ||||||
| @@ -611,7 +617,7 @@ public class AccountService( | |||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var badge = await db.AccountBadges |             var badge = await db.AccountBadges | ||||||
|                 .Where(b => b.AccountId == account.Id && b.Id == badgeId) |                 .Where(b => b.AccountId == account.Id && b.Id != badgeId) | ||||||
|                 .OrderByDescending(b => b.CreatedAt) |                 .OrderByDescending(b => b.CreatedAt) | ||||||
|                 .FirstOrDefaultAsync(); |                 .FirstOrDefaultAsync(); | ||||||
|             if (badge is null) throw new InvalidOperationException("Badge was not found."); |             if (badge is null) throw new InvalidOperationException("Badge was not found."); | ||||||
| @@ -654,4 +660,246 @@ public class AccountService( | |||||||
|             await db.BulkInsertAsync(newProfiles); |             await db.BulkInsertAsync(newProfiles); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public async Task<Shared.Models.Account?> GetAccountById(Guid accountId, bool withProfile = false) | ||||||
|  |     { | ||||||
|  |         return await db.Accounts | ||||||
|  |             .Where(a => a.Id == accountId) | ||||||
|  |             .If(withProfile, q => q.Include(a => a.Profile)) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public async Task<Profile?> GetAccountProfile(Guid accountId) | ||||||
|  |     { | ||||||
|  |         return await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Challenge?> GetAuthChallenge(Guid challengeId) | ||||||
|  |     { | ||||||
|  |         return await db.AuthChallenges.FindAsync(challengeId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, Instant now) | ||||||
|  |     { | ||||||
|  |         return await db.AuthChallenges | ||||||
|  |             .Where(e => e.AccountId == accountId) | ||||||
|  |             .Where(e => e.IpAddress == ipAddress) | ||||||
|  |             .Where(e => e.UserAgent == userAgent) | ||||||
|  |             .Where(e => e.StepRemain > 0) | ||||||
|  |             .Where(e => e.ExpiredAt != null && now < e.ExpiredAt) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Challenge> CreateAuthChallenge(Challenge challenge) | ||||||
|  |     { | ||||||
|  |         db.AuthChallenges.Add(challenge); | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return challenge; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId) | ||||||
|  |     { | ||||||
|  |         return await db.AccountAuthFactors.FirstOrDefaultAsync(f => f.Id == factorId && f.AccountId == accountId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId) | ||||||
|  |     { | ||||||
|  |         return await db.AccountAuthFactors | ||||||
|  |             .Where(e => e.AccountId == accountId) | ||||||
|  |             .Where(e => e.EnabledAt != null && e.Trustworthy >= 1) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Session?> GetAuthSession(Guid sessionId) | ||||||
|  |     { | ||||||
|  |         return await db.AuthSessions.FindAsync(sessionId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<MagicSpell?> GetMagicSpell(Guid spellId) | ||||||
|  |     { | ||||||
|  |         return await db.MagicSpells.FindAsync(spellId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AbuseReport?> GetAbuseReport(Guid reportId) | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports.FindAsync(reportId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId) | ||||||
|  |     { | ||||||
|  |         var existingReport = await db.AbuseReports | ||||||
|  |             .Where(r => r.ResourceIdentifier == resourceIdentifier &&  | ||||||
|  |                        r.AccountId == accountId &&  | ||||||
|  |                        r.DeletedAt == null) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |  | ||||||
|  |         if (existingReport != null) | ||||||
|  |         { | ||||||
|  |             throw new InvalidOperationException("You have already reported this content."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var report = new AbuseReport | ||||||
|  |         { | ||||||
|  |             ResourceIdentifier = resourceIdentifier, | ||||||
|  |             Type = type, | ||||||
|  |             Reason = reason, | ||||||
|  |             AccountId = accountId | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         db.AbuseReports.Add(report); | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         logger.LogInformation("New abuse report created: {ReportId} for resource {ResourceId}",  | ||||||
|  |             report.Id, resourceIdentifier); | ||||||
|  |  | ||||||
|  |         return report; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> CountAbuseReports(bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports | ||||||
|  |             .Where(r => includeResolved || r.ResolvedAt == null) | ||||||
|  |             .CountAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports | ||||||
|  |             .Where(r => r.AccountId == accountId) | ||||||
|  |             .Where(r => includeResolved || r.ResolvedAt == null) | ||||||
|  |             .CountAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports | ||||||
|  |             .Where(r => includeResolved || r.ResolvedAt == null) | ||||||
|  |             .OrderByDescending(r => r.CreatedAt) | ||||||
|  |             .Skip(skip) | ||||||
|  |             .Take(take) | ||||||
|  |             .Include(r => r.Account) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports | ||||||
|  |             .Where(r => r.AccountId == accountId) | ||||||
|  |             .Where(r => includeResolved || r.ResolvedAt == null) | ||||||
|  |             .OrderByDescending(r => r.CreatedAt) | ||||||
|  |             .Skip(skip) | ||||||
|  |             .Take(take) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution) | ||||||
|  |     { | ||||||
|  |         var report = await db.AbuseReports.FindAsync(id); | ||||||
|  |         if (report == null) | ||||||
|  |         { | ||||||
|  |             throw new KeyNotFoundException("Report not found"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         report.ResolvedAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|  |         report.Resolution = resolution; | ||||||
|  |  | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return report; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> GetPendingAbuseReportsCount() | ||||||
|  |     { | ||||||
|  |         return await db.AbuseReports | ||||||
|  |             .Where(r => r.ResolvedAt == null) | ||||||
|  |             .CountAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, Shared.Models.RelationshipStatus status) | ||||||
|  |     { | ||||||
|  |         return await db.AccountRelationships.AnyAsync(r => | ||||||
|  |             (r.AccountId == accountId1 && r.RelatedId == accountId2 && r.Status == status) || | ||||||
|  |             (r.AccountId == accountId2 && r.RelatedId == accountId1 && r.Status == status) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Dictionary<Guid, Shared.Models.Status>> GetStatuses(List<Guid> accountIds) | ||||||
|  |     { | ||||||
|  |         return await db.AccountStatuses | ||||||
|  |             .Where(s => accountIds.Contains(s.AccountId)) | ||||||
|  |             .GroupBy(s => s.AccountId) | ||||||
|  |             .ToDictionaryAsync(g => g.Key, g => g.OrderByDescending(s => s.CreatedAt).First()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task SendNotification(Shared.Models.Account account, string topic, string title, string? subtitle, string body, string? actionUri = null) | ||||||
|  |     { | ||||||
|  |         await nty.SendNotification(account, topic, title, subtitle, body, actionUri: actionUri); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<Shared.Models.Account>> ListAccountFriends(Shared.Models.Account account) | ||||||
|  |     { | ||||||
|  |         return await relationshipService.ListAccountFriends(account); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public string CreateToken(Shared.Models.Session session) | ||||||
|  |     { | ||||||
|  |         return authService.CreateToken(session); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public string GetAuthCookieTokenName() | ||||||
|  |     { | ||||||
|  |         return AuthConstants.CookieTokenName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Shared.Models.Account? account = null) | ||||||
|  |     { | ||||||
|  |         return await actionLogService.CreateActionLogFromRequest(type, meta, ipAddress, userAgent, account); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Challenge> UpdateAuthChallenge(Challenge challenge) | ||||||
|  |     { | ||||||
|  |         db.AuthChallenges.Update(challenge); | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return challenge; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Session> CreateSession(Instant lastGrantedAt, Instant expiredAt, Shared.Models.Account account, Challenge challenge) | ||||||
|  |     { | ||||||
|  |         var session = new Session | ||||||
|  |         { | ||||||
|  |             LastGrantedAt = lastGrantedAt, | ||||||
|  |             ExpiredAt = expiredAt, | ||||||
|  |             Account = account, | ||||||
|  |             Challenge = challenge, | ||||||
|  |         }; | ||||||
|  |         db.AuthSessions.Add(session); | ||||||
|  |         await db.SaveChangesAsync(); | ||||||
|  |         return session; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task UpdateSessionLastGrantedAt(Guid sessionId, Instant lastGrantedAt) | ||||||
|  |     { | ||||||
|  |         await db.AuthSessions | ||||||
|  |             .Where(s => s.Id == sessionId) | ||||||
|  |             .ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, lastGrantedAt)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task UpdateAccountProfileLastSeenAt(Guid accountId, Instant lastSeenAt) | ||||||
|  |     { | ||||||
|  |         await db.AccountProfiles | ||||||
|  |             .Where(a => a.AccountId == accountId) | ||||||
|  |             .ExecuteUpdateAsync(a => a.SetProperty(x => x.LastSeenAt, lastSeenAt)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<Shared.Models.Account>> SearchAccountsAsync(string searchTerm) | ||||||
|  |     { | ||||||
|  |         return await db.Accounts | ||||||
|  |             .Where(a => EF.Functions.ILike(a.Name, $"%{searchTerm}%")) | ||||||
|  |             .OrderBy(a => a.Name) | ||||||
|  |             .Take(10) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
|  |      | ||||||
| } | } | ||||||
| @@ -31,28 +31,28 @@ public class ActionLogService : ServiceBase<IActionLogService>, IActionLogServic | |||||||
|         // fbs.Enqueue(log); |         // fbs.Enqueue(log); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, |     public async Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Shared.Models.Account? account = null) | ||||||
|         Shared.Models.Account? account = null) |  | ||||||
|     { |     { | ||||||
|         var log = new ActionLog |         var log = new ActionLog | ||||||
|         { |         { | ||||||
|             Action = action, |             Action = type, | ||||||
|             Meta = meta, |             Meta = meta, | ||||||
|             UserAgent = request.Headers.UserAgent, |             UserAgent = userAgent, | ||||||
|             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), |             IpAddress = ipAddress, | ||||||
|             // Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) |             // Location = geo.GetPointFromIp(ipAddress) | ||||||
|         }; |         }; | ||||||
|          |          | ||||||
|         if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser) |         if (account != null) | ||||||
|             log.AccountId = currentUser.Id; |  | ||||||
|         else if (account != null) |  | ||||||
|             log.AccountId = account.Id; |             log.AccountId = account.Id; | ||||||
|         else |         else | ||||||
|             throw new ArgumentException("No user context was found"); |             throw new ArgumentException("No user context was found"); | ||||||
|          |          | ||||||
|         if (request.HttpContext.Items["CurrentSession"] is Session currentSession) |         // For MagicOnion, HttpContext.Items["CurrentSession"] is not directly available. | ||||||
|             log.SessionId = currentSession.Id; |         // You might need to pass session ID explicitly if needed. | ||||||
|  |         // if (request.HttpContext.Items["CurrentSession"] is Session currentSession) | ||||||
|  |         //    log.SessionId = currentSession.Id; | ||||||
|  |  | ||||||
|         // fbs.Enqueue(log); |         // fbs.Enqueue(log); | ||||||
|  |         return log; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -70,6 +70,11 @@ public class MagicSpellService( | |||||||
|         return spell; |         return spell; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public async Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId) | ||||||
|  |     { | ||||||
|  |         return await db.MagicSpells.FirstOrDefaultAsync(s => s.Id == spellId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) |     public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) | ||||||
|     { |     { | ||||||
|         var contact = await db.AccountContacts |         var contact = await db.AccountContacts | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Pass.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Pass.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -141,7 +141,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | |||||||
|      |      | ||||||
|     [HttpPost("send")] |     [HttpPost("send")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "notifications.send")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "notifications.send")] | ||||||
|     public async Task<ActionResult> SendNotification( |     public async Task<ActionResult> SendNotification( | ||||||
|         [FromBody] NotificationWithAimRequest request, |         [FromBody] NotificationWithAimRequest request, | ||||||
|         [FromQuery] bool save = false |         [FromQuery] bool save = false | ||||||
|   | |||||||
| @@ -66,8 +66,8 @@ public class NotificationService( | |||||||
|             AccountId = account.Id, |             AccountId = account.Id, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // db.NotificationPushSubscriptions.Add(subscription); |         db.NotificationPushSubscriptions.Add(subscription); | ||||||
|         // await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         return subscription; |         return subscription; | ||||||
|     } |     } | ||||||
| @@ -107,7 +107,7 @@ public class NotificationService( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!isSilent) |         if (!isSilent) | ||||||
|             Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification); |             _ = DeliveryNotification(notification); | ||||||
|  |  | ||||||
|         return notification; |         return notification; | ||||||
|     } |     } | ||||||
| @@ -134,10 +134,10 @@ public class NotificationService( | |||||||
|         var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); |         var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); | ||||||
|         if (id.Count == 0) return; |         if (id.Count == 0) return; | ||||||
|  |  | ||||||
|         // await db.Notifications |         await db.Notifications | ||||||
|         //     .Where(n => id.Contains(n.Id)) |             .Where(n => id.Contains(n.Id)) | ||||||
|         //     .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) |             .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) | ||||||
|         //     ); |             ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task BroadcastNotification(Notification notification, bool save = false) |     public async Task BroadcastNotification(Notification notification, bool save = false) | ||||||
| @@ -161,7 +161,7 @@ public class NotificationService( | |||||||
|                 }; |                 }; | ||||||
|                 return newNotification; |                 return newNotification; | ||||||
|             }).ToList(); |             }).ToList(); | ||||||
|             // await db.BulkInsertAsync(notifications); |             await db.BulkInsertAsync(notifications); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         foreach (var account in accounts) |         foreach (var account in accounts) | ||||||
|   | |||||||
| @@ -155,19 +155,23 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB | |||||||
|         return relationship; |         return relationship; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account) |     public async Task<List<Shared.Models.Account>> ListAccountFriends(Shared.Models.Account account) | ||||||
|     { |     { | ||||||
|         var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; |         var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; | ||||||
|         var friends = await cache.GetAsync<List<Guid>>(cacheKey); |         var friends = await cache.GetAsync<List<Shared.Models.Account>>(cacheKey); | ||||||
|          |          | ||||||
|         if (friends == null) |         if (friends == null) | ||||||
|         { |         { | ||||||
|             friends = await db.AccountRelationships |             var friendIds = await db.AccountRelationships | ||||||
|                 .Where(r => r.RelatedId == account.Id) |                 .Where(r => r.RelatedId == account.Id) | ||||||
|                 .Where(r => r.Status == RelationshipStatus.Friends) |                 .Where(r => r.Status == RelationshipStatus.Friends) | ||||||
|                 .Select(r => r.AccountId) |                 .Select(r => r.AccountId) | ||||||
|                 .ToListAsync(); |                 .ToListAsync(); | ||||||
|  |  | ||||||
|  |             friends = await db.Accounts | ||||||
|  |                 .Where(a => friendIds.Contains(a.Id)) | ||||||
|  |                 .ToListAsync(); | ||||||
|  |                  | ||||||
|             await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1)); |             await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,6 +50,13 @@ public class AppDatabase( | |||||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } |     public DbSet<MagicSpell> MagicSpells { get; set; } | ||||||
|     public DbSet<AbuseReport> AbuseReports { get; set; } |     public DbSet<AbuseReport> AbuseReports { get; set; } | ||||||
|  |  | ||||||
|  |     public DbSet<CustomApp> CustomApps { get; set; } | ||||||
|  |     public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } | ||||||
|  |  | ||||||
|  |     public DbSet<DysonNetwork.Shared.Models.Publisher> Publishers { get; set; } | ||||||
|  |     public DbSet<PublisherMember> PublisherMembers { get; set; } | ||||||
|  |     public DbSet<PublisherFeature> PublisherFeatures { get; set; } | ||||||
|  |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|     { |     { | ||||||
|         optionsBuilder.UseNpgsql( |         optionsBuilder.UseNpgsql( | ||||||
|   | |||||||
| @@ -189,8 +189,6 @@ public class DysonTokenAuthHandler( | |||||||
|                     { |                     { | ||||||
|                         return false; |                         return false; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     break; |  | ||||||
|                 default: |                 default: | ||||||
|                     return false; |                     return false; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ namespace DysonNetwork.Pass.Auth; | |||||||
|  |  | ||||||
| public class AuthService( | public class AuthService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     IConfiguration config |     IConfiguration config, | ||||||
|     // IHttpClientFactory httpClientFactory, |     IHttpClientFactory httpClientFactory | ||||||
|     // IHttpContextAccessor httpContextAccessor, |     // IHttpContextAccessor httpContextAccessor, | ||||||
|     // ICacheService cache |     // ICacheService cache | ||||||
| ) | ) | ||||||
| @@ -105,55 +105,56 @@ public class AuthService( | |||||||
|  |  | ||||||
|     public async Task<bool> ValidateCaptcha(string token) |     public async Task<bool> ValidateCaptcha(string token) | ||||||
|     { |     { | ||||||
|  |         await Task.CompletedTask; | ||||||
|         if (string.IsNullOrWhiteSpace(token)) return false; |         if (string.IsNullOrWhiteSpace(token)) return false; | ||||||
|  |  | ||||||
|         // var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); |         var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); | ||||||
|         // var apiSecret = config.GetSection("Captcha")["ApiSecret"]; |         var apiSecret = config.GetSection("Captcha")["ApiSecret"]; | ||||||
|  |  | ||||||
|         // var client = httpClientFactory.CreateClient(); |         var client = httpClientFactory.CreateClient(); | ||||||
|  |  | ||||||
|         // var jsonOpts = new JsonSerializerOptions |         var jsonOpts = new JsonSerializerOptions | ||||||
|         // { |         { | ||||||
|         //     PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|         //     DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower |             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||||
|         // }; |         }; | ||||||
|  |  | ||||||
|         // switch (provider) |         switch (provider) | ||||||
|         // { |         { | ||||||
|         //     case "cloudflare": |             case "cloudflare": | ||||||
|         //         var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |                 var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|         //             "application/x-www-form-urlencoded"); |                     "application/x-www-form-urlencoded"); | ||||||
|         //         var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", |                 var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", | ||||||
|         //             content); |                     content); | ||||||
|         //         response.EnsureSuccessStatusCode(); |                 response.EnsureSuccessStatusCode(); | ||||||
|  |  | ||||||
|         //         var json = await response.Content.ReadAsStringAsync(); |                 var json = await response.Content.ReadAsStringAsync(); | ||||||
|         //         var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |                 var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
|  |  | ||||||
|         //         return result?.Success == true; |                 return result?.Success == true; | ||||||
|         //     case "google": |             case "google": | ||||||
|         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|         //             "application/x-www-form-urlencoded"); |                     "application/x-www-form-urlencoded"); | ||||||
|         //         response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); |                 response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); | ||||||
|         //         response.EnsureSuccessStatusCode(); |                 response.EnsureSuccessStatusCode(); | ||||||
|  |  | ||||||
|         //         json = await response.Content.ReadAsStringAsync(); |                 json = await response.Content.ReadAsStringAsync(); | ||||||
|         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
|  |  | ||||||
|         //         return result?.Success == true; |                 return result?.Success == true; | ||||||
|         //     case "hcaptcha": |             case "hcaptcha": | ||||||
|         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|         //             "application/x-www-form-urlencoded"); |                     "application/x-www-form-urlencoded"); | ||||||
|         //         response = await client.PostAsync("https://hcaptcha.com/siteverify", content); |                 response = await client.PostAsync("https://hcaptcha.com/siteverify", content); | ||||||
|         //         response.EnsureSuccessStatusCode(); |                 response.EnsureSuccessStatusCode(); | ||||||
|  |  | ||||||
|         //         json = await response.Content.ReadAsStringAsync(); |                 json = await response.Content.ReadAsStringAsync(); | ||||||
|         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
|  |  | ||||||
|         //         return result?.Success == true; |                 return result?.Success == true; | ||||||
|         //     default: |             default: | ||||||
|         //         throw new ArgumentException("The server misconfigured for the captcha."); |                 throw new ArgumentException("The server misconfigured for the captcha."); | ||||||
|         // } |         } | ||||||
|         return true; // Placeholder for captcha validation |         return true; // Placeholder for captcha validation | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,8 +21,7 @@ public class OidcProviderController( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     OidcProviderService oidcService, |     OidcProviderService oidcService, | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     IOptions<OidcProviderOptions> options, |     IOptions<OidcProviderOptions> options | ||||||
|     ILogger<OidcProviderController> logger |  | ||||||
| ) | ) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| { | { | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ public class OidcProviderService( | |||||||
|  |  | ||||||
|     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) |     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) | ||||||
|     { |     { | ||||||
|  |         await Task.CompletedTask; | ||||||
|         return null; |         return null; | ||||||
|         // return await db.CustomApps |         // return await db.CustomApps | ||||||
|         //     .Include(c => c.Secrets) |         //     .Include(c => c.Secrets) | ||||||
| @@ -35,6 +36,7 @@ public class OidcProviderService( | |||||||
|  |  | ||||||
|     public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId) |     public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId) | ||||||
|     { |     { | ||||||
|  |         await Task.CompletedTask; | ||||||
|         return null; |         return null; | ||||||
|         // return await db.CustomApps |         // return await db.CustomApps | ||||||
|         //     .Include(c => c.Secrets) |         //     .Include(c => c.Secrets) | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								DysonNetwork.Pass/Developer/CustomAppService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								DysonNetwork.Pass/Developer/CustomAppService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | using MagicOnion.Server; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using System; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Developer; | ||||||
|  |  | ||||||
|  | public class CustomAppService : ServiceBase<ICustomAppService>, ICustomAppService | ||||||
|  | { | ||||||
|  |     private readonly AppDatabase _db; | ||||||
|  |  | ||||||
|  |     public CustomAppService(AppDatabase db) | ||||||
|  |     { | ||||||
|  |         _db = db; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) | ||||||
|  |     { | ||||||
|  |         return await _db.CustomApps.FirstOrDefaultAsync(app => app.Id == clientId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> CountCustomAppsByPublisherId(Guid publisherId) | ||||||
|  |     { | ||||||
|  |         return await _db.CustomApps.CountAsync(app => app.PublisherId == publisherId); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| namespace DysonNetwork.Pass.Localization; |  | ||||||
|  |  | ||||||
| public class EmailResource |  | ||||||
| { |  | ||||||
| } |  | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| @using DysonNetwork.Pass.Localization | @using DysonNetwork.Pass.Localization | ||||||
|  | @using DysonNetwork.Shared.Localization | ||||||
| @using Microsoft.Extensions.Localization | @using Microsoft.Extensions.Localization | ||||||
|  |  | ||||||
| <EmailLayout> | <EmailLayout> | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| @using DysonNetwork.Pass.Localization | @using DysonNetwork.Shared.Localization | ||||||
| @using Microsoft.Extensions.Localization | @using Microsoft.Extensions.Localization | ||||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource |  | ||||||
|  |  | ||||||
| <EmailLayout> | <EmailLayout> | ||||||
|     <tr> |     <tr> | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| @using DysonNetwork.Pass.Localization | @using DysonNetwork.Shared.Localization | ||||||
| @using Microsoft.Extensions.Localization | @using Microsoft.Extensions.Localization | ||||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource |  | ||||||
|  |  | ||||||
| <EmailLayout> | <EmailLayout> | ||||||
|     <tr> |     <tr> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| @using DysonNetwork.Pass.Localization | @using DysonNetwork.Pass.Localization | ||||||
|  | @using DysonNetwork.Shared.Localization | ||||||
| @using Microsoft.Extensions.Localization | @using Microsoft.Extensions.Localization | ||||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource |  | ||||||
|  |  | ||||||
| <EmailLayout> | <EmailLayout> | ||||||
|     <tr> |     <tr> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| @using DysonNetwork.Pass.Localization | @using DysonNetwork.Pass.Localization | ||||||
|  | @using DysonNetwork.Shared.Localization | ||||||
| @using Microsoft.Extensions.Localization | @using Microsoft.Extensions.Localization | ||||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource |  | ||||||
|  |  | ||||||
| <EmailLayout> | <EmailLayout> | ||||||
|     <tr> |     <tr> | ||||||
|   | |||||||
| @@ -1,51 +0,0 @@ | |||||||
| using Microsoft.AspNetCore.Http; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Permission; |  | ||||||
|  |  | ||||||
| [AttributeUsage(AttributeTargets.Method, Inherited = 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 pm) |  | ||||||
|     { |  | ||||||
|         var endpoint = httpContext.GetEndpoint(); |  | ||||||
|  |  | ||||||
|         var attr = endpoint?.Metadata |  | ||||||
|             .OfType<RequiredPermissionAttribute>() |  | ||||||
|             .FirstOrDefault(); |  | ||||||
|  |  | ||||||
|         if (attr != null) |  | ||||||
|         { |  | ||||||
|             if (httpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) |  | ||||||
|             { |  | ||||||
|                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; |  | ||||||
|                 await httpContext.Response.WriteAsync("Unauthorized"); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (currentUser.IsSuperuser) |  | ||||||
|             { |  | ||||||
|                 // Bypass the permission check for performance |  | ||||||
|                 await next(httpContext); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var actor = $"user:{currentUser.Id}"; |  | ||||||
|             var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Area, attr.Key); |  | ||||||
|  |  | ||||||
|             if (!permNode) |  | ||||||
|             { |  | ||||||
|                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; |  | ||||||
|                 await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} = {true} was required."); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await next(httpContext); |  | ||||||
|     }  |  | ||||||
| } |  | ||||||
| @@ -1,6 +1,9 @@ | |||||||
|  | using MagicOnion; | ||||||
|  | using MagicOnion.Server; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  |  | ||||||
| @@ -9,7 +12,7 @@ namespace DysonNetwork.Pass.Permission; | |||||||
| public class PermissionService( | public class PermissionService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     ICacheService cache |     ICacheService cache | ||||||
| ) | ) : ServiceBase<IPermissionService>, IPermissionService | ||||||
| { | { | ||||||
|     private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1); |     private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1); | ||||||
|  |  | ||||||
| @@ -195,4 +198,11 @@ public class PermissionService( | |||||||
|             Value = _SerializePermissionValue(value), |             Value = _SerializePermissionValue(value), | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public async UnaryResult<bool> CheckPermission(string scope, string permission) | ||||||
|  |     { | ||||||
|  |         // Assuming the actor is always "user:current" for client-side checks | ||||||
|  |         // You might need to adjust this based on how your client identifies itself | ||||||
|  |         return await HasPermissionAsync("user:current", scope, permission); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Configuration; | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
|  | using MagicOnion.Server; | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
| @@ -24,6 +25,8 @@ builder.Services.AddDbContext<AppDatabase>(options => | |||||||
|  |  | ||||||
| builder.Services.AddScoped<AccountService>(); | builder.Services.AddScoped<AccountService>(); | ||||||
| builder.Services.AddScoped<AuthService>(); | builder.Services.AddScoped<AuthService>(); | ||||||
|  | builder.Services.AddScoped<DysonNetwork.Pass.Publisher.PublisherService>(); | ||||||
|  | builder.Services.AddScoped<DysonNetwork.Pass.Developer.CustomAppService>(); | ||||||
|  |  | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								DysonNetwork.Pass/Publisher/PublisherService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Pass/Publisher/PublisherService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | using MagicOnion.Server; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using NodaTime; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Publisher; | ||||||
|  |  | ||||||
|  | public class PublisherService : ServiceBase<IPublisherService>, IPublisherService | ||||||
|  | { | ||||||
|  |     private readonly AppDatabase _db; | ||||||
|  |  | ||||||
|  |     public PublisherService(AppDatabase db) | ||||||
|  |     { | ||||||
|  |         _db = db; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Shared.Models.Publisher?> GetPublisherByName(string name) | ||||||
|  |     { | ||||||
|  |         return await _db.Publishers.FirstOrDefaultAsync(p => p.Name == name); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<Shared.Models.Publisher>> GetUserPublishers(Guid accountId) | ||||||
|  |     { | ||||||
|  |         var publisherIds = await _db.PublisherMembers | ||||||
|  |             .Where(m => m.AccountId == accountId) | ||||||
|  |             .Select(m => m.PublisherId) | ||||||
|  |             .ToListAsync(); | ||||||
|  |  | ||||||
|  |         return await _db.Publishers | ||||||
|  |             .Where(p => publisherIds.Contains(p.Id)) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role) | ||||||
|  |     { | ||||||
|  |         return await _db.PublisherMembers.AnyAsync(m => | ||||||
|  |             m.PublisherId == publisherId && | ||||||
|  |             m.AccountId == accountId && | ||||||
|  |             m.Role >= role); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId) | ||||||
|  |     { | ||||||
|  |         return await _db.PublisherFeatures | ||||||
|  |             .Where(f => f.PublisherId == publisherId) | ||||||
|  |             .ToListAsync(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,10 +1,11 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Safety; | namespace DysonNetwork.Pass.Safety; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/safety/reports")] | [Route("/safety/reports")] | ||||||
| @@ -85,7 +86,7 @@ public class AbuseReportController( | |||||||
| 
 | 
 | ||||||
|     [HttpGet("{id}")] |     [HttpGet("{id}")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("safety", "reports.view")] |     [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")] | ||||||
|     [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)] |     [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)] | ||||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] |     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|     public async Task<ActionResult<AbuseReport>> GetReportById(Guid id) |     public async Task<ActionResult<AbuseReport>> GetReportById(Guid id) | ||||||
| @@ -122,7 +123,7 @@ public class AbuseReportController( | |||||||
| 
 | 
 | ||||||
|     [HttpPost("{id}/resolve")] |     [HttpPost("{id}/resolve")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("safety", "reports.resolve")] |     [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.resolve")] | ||||||
|     [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)] |     [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)] | ||||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] |     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||||
|     public async Task<ActionResult<AbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request) |     public async Task<ActionResult<AbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request) | ||||||
| @@ -144,7 +145,7 @@ public class AbuseReportController( | |||||||
| 
 | 
 | ||||||
|     [HttpGet("count")] |     [HttpGet("count")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("safety", "reports.view")] |     [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")] | ||||||
|     [ProducesResponseType<object>(StatusCodes.Status200OK)] |     [ProducesResponseType<object>(StatusCodes.Status200OK)] | ||||||
|     public async Task<ActionResult<object>> GetReportsCount() |     public async Task<ActionResult<object>> GetReportsCount() | ||||||
|     { |     { | ||||||
							
								
								
									
										61
									
								
								DysonNetwork.Pass/Safety/SafetyService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								DysonNetwork.Pass/Safety/SafetyService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Safety; | ||||||
|  |  | ||||||
|  | public class SafetyService(AppDatabase db, IAccountService accountService, ILogger<SafetyService> logger) | ||||||
|  | { | ||||||
|  |     public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId) | ||||||
|  |     { | ||||||
|  |         // Check if a similar report already exists from this user | ||||||
|  |         var existingReport = await db.AbuseReports | ||||||
|  |             .Where(r => r.ResourceIdentifier == resourceIdentifier &&  | ||||||
|  |                        r.AccountId == accountId &&  | ||||||
|  |                        r.DeletedAt == null) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |  | ||||||
|  |         if (existingReport != null) | ||||||
|  |         { | ||||||
|  |             throw new InvalidOperationException("You have already reported this content."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return await accountService.CreateAbuseReport(resourceIdentifier, type, reason, accountId); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public async Task<int> CountReports(bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await accountService.CountAbuseReports(includeResolved); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await accountService.CountUserAbuseReports(accountId, includeResolved); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await accountService.GetAbuseReports(skip, take, includeResolved); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false) | ||||||
|  |     { | ||||||
|  |         return await accountService.GetUserAbuseReports(accountId, skip, take, includeResolved); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AbuseReport?> GetReportById(Guid id) | ||||||
|  |     { | ||||||
|  |         return await accountService.GetAbuseReport(id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<AbuseReport> ResolveReport(Guid id, string resolution) | ||||||
|  |     { | ||||||
|  |         return await accountService.ResolveAbuseReport(id, resolution); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<int> GetPendingReportsCount() | ||||||
|  |     { | ||||||
|  |         return await accountService.GetPendingAbuseReportsCount(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -47,6 +47,7 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<IAccountUsernameService, AccountUsernameService>(); |         services.AddScoped<IAccountUsernameService, AccountUsernameService>(); | ||||||
|         services.AddScoped<IMagicSpellService, MagicSpellService>(); |         services.AddScoped<IMagicSpellService, MagicSpellService>(); | ||||||
|         services.AddScoped<IAccountEventService, AccountEventService>(); |         services.AddScoped<IAccountEventService, AccountEventService>(); | ||||||
|  |         services.AddScoped<IAccountProfileService, AccountProfileService>(); | ||||||
|  |  | ||||||
|         // Register OIDC services |         // Register OIDC services | ||||||
|         services.AddScoped<OidcService, GoogleOidcService>(); |         services.AddScoped<OidcService, GoogleOidcService>(); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|     <PropertyGroup> |     <PropertyGroup> | ||||||
|         <TargetFramework>net9.0</TargetFramework> |         <TargetFramework>net9.0</TargetFramework> | ||||||
| @@ -12,10 +12,11 @@ | |||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> |         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||||
|         <PackageReference Include="Google.Protobuf" Version="3.27.2" /> |         <PackageReference Include="dotnet-etcd" Version="8.0.1" /> | ||||||
|         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> |         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" /> |         <PackageReference Include="MagicOnion.Server" Version="7.0.5" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" /> |          | ||||||
|  |          | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" /> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> |         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> | ||||||
|         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" /> |         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" /> | ||||||
| @@ -24,6 +25,10 @@ | |||||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> |         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> |         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||||
|         <PackageReference Include="Otp.NET" Version="1.4.0" /> |         <PackageReference Include="Otp.NET" Version="1.4.0" /> | ||||||
|  |         <PackageReference Include="MailKit" Version="4.11.0" /> | ||||||
|  |         <PackageReference Include="MimeKit" Version="4.11.0" /> | ||||||
|  |         <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" /> | ||||||
|  |         <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" /> | ||||||
|         <PackageReference Include="StackExchange.Redis" Version="2.8.41" /> |         <PackageReference Include="StackExchange.Redis" Version="2.8.41" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
| @@ -33,4 +38,6 @@ | |||||||
|       </Reference> |       </Reference> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								DysonNetwork.Shared/Etcd/EtcdService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								DysonNetwork.Shared/Etcd/EtcdService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | using dotnet_etcd; | ||||||
|  | using Etcdserverpb; | ||||||
|  | using Grpc.Core; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Etcd; | ||||||
|  |  | ||||||
|  | public class EtcdService(string connectionString) : IEtcdService | ||||||
|  | { | ||||||
|  |     private readonly EtcdClient _etcdClient = new(connectionString); | ||||||
|  |     private long _leaseId; | ||||||
|  |     private string? _serviceKey; | ||||||
|  |     private readonly CancellationTokenSource _cts = new(); | ||||||
|  |  | ||||||
|  |     public async Task RegisterServiceAsync(string serviceName, string serviceAddress, int ttl = 15) | ||||||
|  |     { | ||||||
|  |         _serviceKey = $"/services/{serviceName}/{Guid.NewGuid()}"; | ||||||
|  |         var leaseGrantResponse = await _etcdClient.LeaseGrantAsync(new LeaseGrantRequest { TTL = ttl }); | ||||||
|  |         _leaseId = leaseGrantResponse.ID; | ||||||
|  |  | ||||||
|  |         await _etcdClient.PutAsync(new PutRequest | ||||||
|  |         { | ||||||
|  |             Key = Google.Protobuf.ByteString.CopyFromUtf8(_serviceKey), | ||||||
|  |             Value = Google.Protobuf.ByteString.CopyFromUtf8(serviceAddress), | ||||||
|  |             Lease = _leaseId | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         _ = Task.Run(async () => | ||||||
|  |         { | ||||||
|  |             while (!_cts.Token.IsCancellationRequested) | ||||||
|  |             { | ||||||
|  |                 try | ||||||
|  |                 { | ||||||
|  |                     await _etcdClient.LeaseKeepAlive(new LeaseKeepAliveRequest { ID = _leaseId }, | ||||||
|  |                         _ => { }, _cts.Token); | ||||||
|  |                     await Task.Delay(TimeSpan.FromSeconds(ttl / 3), _cts.Token); | ||||||
|  |                 } | ||||||
|  |                 catch (RpcException) | ||||||
|  |                 { | ||||||
|  |                     // Ignored | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, _cts.Token); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task UnregisterServiceAsync() | ||||||
|  |     { | ||||||
|  |         if (!string.IsNullOrEmpty(_serviceKey)) | ||||||
|  |         { | ||||||
|  |             await _etcdClient.DeleteRangeAsync(_serviceKey); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<List<string>> DiscoverServicesAsync(string serviceName) | ||||||
|  |     { | ||||||
|  |         var prefix = $"/services/{serviceName}/"; | ||||||
|  |         var rangeResponse = await _etcdClient.GetRangeAsync(prefix); | ||||||
|  |         return rangeResponse.Kvs.Select(kv => kv.Value.ToStringUtf8()).ToList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void Dispose() | ||||||
|  |     { | ||||||
|  |         _cts.Cancel(); | ||||||
|  |         if (_leaseId != 0) | ||||||
|  |         { | ||||||
|  |             _etcdClient.LeaseRevoke(new LeaseRevokeRequest { ID = _leaseId }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         _etcdClient.Dispose(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								DysonNetwork.Shared/Etcd/EtcdServiceExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								DysonNetwork.Shared/Etcd/EtcdServiceExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | using Microsoft.Extensions.DependencyInjection; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Grpc.Net.Client; | ||||||
|  | using MagicOnion.Client; | ||||||
|  | using System; | ||||||
|  | using System.Linq; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Etcd | ||||||
|  | { | ||||||
|  |     public static class EtcdServiceExtensions | ||||||
|  |     { | ||||||
|  |         public static IServiceCollection AddEtcdService(this IServiceCollection services, IConfiguration configuration) | ||||||
|  |         { | ||||||
|  |             var etcdConnectionString = configuration.GetConnectionString("Etcd"); | ||||||
|  |             services.AddSingleton<IEtcdService>(new EtcdService(etcdConnectionString!)); | ||||||
|  |             return services; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public static IServiceCollection AddMagicOnionService<TService>(this IServiceCollection services) | ||||||
|  |             where TService : class, MagicOnion.IService<TService> | ||||||
|  |         { | ||||||
|  |             services.AddSingleton(serviceProvider => | ||||||
|  |             { | ||||||
|  |                 var etcdService = serviceProvider.GetRequiredService<IEtcdService>(); | ||||||
|  |                 var serviceName = typeof(TService).Name.TrimStart('I'); // Convention: IMyService -> MyService | ||||||
|  |  | ||||||
|  |                 // Synchronously wait for service discovery (or handle asynchronously if preferred) | ||||||
|  |                 var endpoints = etcdService.DiscoverServicesAsync(serviceName).GetAwaiter().GetResult(); | ||||||
|  |  | ||||||
|  |                 if (!endpoints.Any()) | ||||||
|  |                 { | ||||||
|  |                     throw new InvalidOperationException($"No endpoints found for MagicOnion service: {serviceName}"); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // For simplicity, use the first discovered endpoint | ||||||
|  |                 var endpoint = endpoints.First(); | ||||||
|  |  | ||||||
|  |                 var channel = GrpcChannel.ForAddress(endpoint); | ||||||
|  |                 return MagicOnionClient.Create<TService>(channel); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             return services; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								DysonNetwork.Shared/Etcd/IEtcdService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Shared/Etcd/IEtcdService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Etcd | ||||||
|  | { | ||||||
|  |     public interface IEtcdService : IDisposable | ||||||
|  |     { | ||||||
|  |         Task RegisterServiceAsync(string serviceName, string serviceAddress, int ttl = 15); | ||||||
|  |         Task UnregisterServiceAsync(); | ||||||
|  |         Task<List<string>> DiscoverServicesAsync(string serviceName); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								DysonNetwork.Shared/Localization/CultureInfoService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								DysonNetwork.Shared/Localization/CultureInfoService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | using System.Globalization; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Localization; | ||||||
|  |  | ||||||
|  | public abstract class CultureInfoService | ||||||
|  | { | ||||||
|  |     public static void SetCultureInfo(Shared.Models.Account account) | ||||||
|  |     { | ||||||
|  |         SetCultureInfo(account.Language); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static void SetCultureInfo(string? languageCode) | ||||||
|  |     { | ||||||
|  |         var info = new CultureInfo(languageCode ?? "en-us", false); | ||||||
|  |         CultureInfo.CurrentCulture = info; | ||||||
|  |         CultureInfo.CurrentUICulture = info; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								DysonNetwork.Shared/Localization/EmailResource.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								DysonNetwork.Shared/Localization/EmailResource.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | namespace DysonNetwork.Shared.Localization; | ||||||
|  |  | ||||||
|  | public class EmailResource | ||||||
|  | { | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pass.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum AbuseReportType | public enum AbuseReportType | ||||||
| { | { | ||||||
							
								
								
									
										63
									
								
								DysonNetwork.Shared/Permission/MagicOnionPermissionFilter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								DysonNetwork.Shared/Permission/MagicOnionPermissionFilter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | using MagicOnion.Server.Filters; | ||||||
|  | using MagicOnion.Server; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using Microsoft.Extensions.DependencyInjection; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System; | ||||||
|  | using System.Reflection; | ||||||
|  | using Grpc.Core; | ||||||
|  | using MagicOnion; | ||||||
|  | using MagicOnion.Server.Hubs; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Permission; | ||||||
|  |  | ||||||
|  | public class MagicOnionPermissionFilter : IMagicOnionServiceFilter | ||||||
|  | { | ||||||
|  |     private readonly IPermissionService _permissionService; | ||||||
|  |  | ||||||
|  |     public MagicOnionPermissionFilter(IPermissionService permissionService) | ||||||
|  |     { | ||||||
|  |         _permissionService = permissionService; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async ValueTask Invoke(ServiceContext context, Func<ServiceContext, ValueTask> next) | ||||||
|  |     { | ||||||
|  |         var attribute = context.MethodInfo.GetCustomAttribute<RequiredPermissionAttribute>(); | ||||||
|  |  | ||||||
|  |         if (attribute == null) | ||||||
|  |         { | ||||||
|  |             // If no RequiredPermissionAttribute is present, just continue | ||||||
|  |             await next(context); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Correct way to get HttpContext from ServiceContext | ||||||
|  |         var httpContext = context.CallContext.GetHttpContext(); | ||||||
|  |  | ||||||
|  |         if (httpContext == null) | ||||||
|  |         { | ||||||
|  |             throw new InvalidOperationException("HttpContext is not available in ServiceContext."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (httpContext.Items["CurrentUser"] is not Account currentUser) | ||||||
|  |         { | ||||||
|  |             throw new ReturnStatusException(StatusCode.PermissionDenied, "Unauthorized: Current user not found."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (currentUser.IsSuperuser) | ||||||
|  |         { | ||||||
|  |             await next(context); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var hasPermission = await _permissionService.CheckPermission(attribute.Scope, attribute.Permission); | ||||||
|  |  | ||||||
|  |         if (!hasPermission) | ||||||
|  |         { | ||||||
|  |             throw new ReturnStatusException(StatusCode.PermissionDenied, $"Permission {attribute.Scope}/{attribute.Permission} was required."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await next(context); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | using System; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Permission; | ||||||
|  |  | ||||||
|  | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] | ||||||
|  | public class RequiredPermissionAttribute : Attribute | ||||||
|  | { | ||||||
|  |     public string Scope { get; } | ||||||
|  |     public string Permission { get; } | ||||||
|  |  | ||||||
|  |     public RequiredPermissionAttribute(string scope, string permission) | ||||||
|  |     { | ||||||
|  |         Scope = scope; | ||||||
|  |         Permission = permission; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								DysonNetwork.Shared/Services/EmailService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								DysonNetwork.Shared/Services/EmailService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | using MailKit.Net.Smtp; | ||||||
|  | using MimeKit; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Collections.Generic; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
|  | public class EmailService | ||||||
|  | { | ||||||
|  |     private readonly IConfiguration _configuration; | ||||||
|  |     private readonly ILogger<EmailService> _logger; | ||||||
|  |  | ||||||
|  |     public EmailService(IConfiguration configuration, ILogger<EmailService> logger) | ||||||
|  |     { | ||||||
|  |         _configuration = configuration; | ||||||
|  |         _logger = logger; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task SendEmailAsync( | ||||||
|  |         string toName, | ||||||
|  |         string toEmail, | ||||||
|  |         string subject, | ||||||
|  |         string body, | ||||||
|  |         Dictionary<string, string>? headers = null | ||||||
|  |     ) | ||||||
|  |     { | ||||||
|  |         var message = new MimeMessage(); | ||||||
|  |         message.From.Add(new MailboxAddress( | ||||||
|  |             _configuration["Email:SenderName"], | ||||||
|  |             _configuration["Email:SenderEmail"] | ||||||
|  |         )); | ||||||
|  |         message.To.Add(new MailboxAddress(toName, toEmail)); | ||||||
|  |         message.Subject = subject; | ||||||
|  |  | ||||||
|  |         var bodyBuilder = new BodyBuilder { HtmlBody = body }; | ||||||
|  |         message.Body = bodyBuilder.ToMessageBody(); | ||||||
|  |  | ||||||
|  |         if (headers != null) | ||||||
|  |         { | ||||||
|  |             foreach (var header in headers) | ||||||
|  |             { | ||||||
|  |                 message.Headers.Add(header.Key, header.Value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         using var client = new SmtpClient(); | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             await client.ConnectAsync( | ||||||
|  |                 _configuration["Email:SmtpHost"], | ||||||
|  |                 int.Parse(_configuration["Email:SmtpPort"]), | ||||||
|  |                 MailKit.Security.SecureSocketOptions.StartTls | ||||||
|  |             ); | ||||||
|  |             await client.AuthenticateAsync( | ||||||
|  |                 _configuration["Email:SmtpUser"], | ||||||
|  |                 _configuration["Email:SmtpPass"] | ||||||
|  |             ); | ||||||
|  |             await client.SendAsync(message); | ||||||
|  |             _logger.LogInformation("Email sent to {ToEmail}", toEmail); | ||||||
|  |         } | ||||||
|  |         catch (Exception ex) | ||||||
|  |         { | ||||||
|  |             _logger.LogError(ex, "Failed to send email to {ToEmail}", toEmail); | ||||||
|  |             throw; | ||||||
|  |         } | ||||||
|  |         finally | ||||||
|  |         { | ||||||
|  |             await client.DisconnectAsync(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task SendTemplatedEmailAsync<T>(string toName, string toEmail, string subject, string htmlBody) | ||||||
|  |     { | ||||||
|  |         await SendEmailAsync(toName, toEmail, subject, htmlBody); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using MagicOnion; | using MagicOnion; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| @@ -16,6 +17,11 @@ public interface IAccountEventService : IService<IAccountEventService> | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     Task<Status> GetStatus(Guid userId); |     Task<Status> GetStatus(Guid userId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets the statuses of a list of users | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds); | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Performs a daily check-in for a user |     /// Performs a daily check-in for a user | ||||||
|     /// </summary> |     /// </summary> | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								DysonNetwork.Shared/Services/IAccountProfileService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								DysonNetwork.Shared/Services/IAccountProfileService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using MagicOnion; | ||||||
|  | using System; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Services | ||||||
|  | { | ||||||
|  |     public interface IAccountProfileService : IService<IAccountProfileService> | ||||||
|  |     { | ||||||
|  |         /// <summary> | ||||||
|  |         /// Gets an account profile by account ID. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="accountId">The ID of the account.</param> | ||||||
|  |         /// <returns>The account profile if found, otherwise null.</returns> | ||||||
|  |         Task<Profile?> GetAccountProfileByIdAsync(Guid accountId); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Updates the StellarMembership of an account. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="accountId">The ID of the account.</param> | ||||||
|  |         /// <param name="subscription">The subscription to set as the StellarMembership.</param> | ||||||
|  |         /// <returns>The updated account profile.</returns> | ||||||
|  |         Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Gets all account profiles that have a non-null StellarMembership. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <returns>A list of account profiles with StellarMembership.</returns> | ||||||
|  |         Task<List<Profile>> GetAccountsWithStellarMembershipAsync(); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Clears the StellarMembership for a list of account IDs. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="accountIds">The list of account IDs for which to clear the StellarMembership.</param> | ||||||
|  |         /// <returns>The number of accounts updated.</returns> | ||||||
|  |         Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Updates the profile picture of an account. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="accountId">The ID of the account.</param> | ||||||
|  |         /// <param name="picture">The new profile picture reference object.</param> | ||||||
|  |         /// <returns>The updated profile.</returns> | ||||||
|  |         Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture); | ||||||
|  |  | ||||||
|  |         /// <summary> | ||||||
|  |         /// Updates the profile background of an account. | ||||||
|  |         /// </summary> | ||||||
|  |         /// <param name="accountId">The ID of the account.</param> | ||||||
|  |         /// <param name="background">The new profile background reference object.</param> | ||||||
|  |         /// <returns>The updated profile.</returns> | ||||||
|  |         Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using MagicOnion; | using MagicOnion; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Services; | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| @@ -59,4 +62,247 @@ public interface IAccountService : IService<IAccountService> | |||||||
|     /// <param name="userInfo">The OpenID Connect user information</param> |     /// <param name="userInfo">The OpenID Connect user information</param> | ||||||
|     /// <returns>The newly created account</returns> |     /// <returns>The newly created account</returns> | ||||||
|     Task<Account> CreateAccount(OidcUserInfo userInfo); |     Task<Account> CreateAccount(OidcUserInfo userInfo); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an account by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <param name="withProfile">Join the profile table or not.</param> | ||||||
|  |     /// <returns>The account if found, otherwise null.</returns> | ||||||
|  |     Task<Account?> GetAccountById(Guid accountId, bool withProfile = false); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an account profile by account ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <returns>The account profile if found, otherwise null.</returns> | ||||||
|  |     Task<Profile?> GetAccountProfile(Guid accountId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an authentication challenge by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="challengeId">The ID of the challenge.</param> | ||||||
|  |     /// <returns>The authentication challenge if found, otherwise null.</returns> | ||||||
|  |     Task<Challenge?> GetAuthChallenge(Guid challengeId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an authentication challenge by account ID, IP address, and user agent. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <param name="ipAddress">The IP address.</param> | ||||||
|  |     /// <param name="userAgent">The user agent.</param> | ||||||
|  |     /// <param name="now">The current instant.</param> | ||||||
|  |     /// <returns>The authentication challenge if found, otherwise null.</returns> | ||||||
|  |     Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, NodaTime.Instant now); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a new authentication challenge. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="challenge">The challenge to create.</param> | ||||||
|  |     /// <returns>The created challenge.</returns> | ||||||
|  |     Task<Challenge> CreateAuthChallenge(Challenge challenge); | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an account authentication factor by its ID and account ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="factorId">The ID of the factor.</param> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <returns>The account authentication factor if found, otherwise null.</returns> | ||||||
|  |     Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets a list of account authentication factors for a given account ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <returns>A list of account authentication factors.</returns> | ||||||
|  |     Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an authentication session by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="sessionId">The ID of the session.</param> | ||||||
|  |     /// <returns>The authentication session if found, otherwise null.</returns> | ||||||
|  |     Task<Session?> GetAuthSession(Guid sessionId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets a magic spell by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="spellId">The ID of the magic spell.</param> | ||||||
|  |     /// <returns>The magic spell if found, otherwise null.</returns> | ||||||
|  |     Task<MagicSpell?> GetMagicSpell(Guid spellId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets an abuse report by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="reportId">The ID of the abuse report.</param> | ||||||
|  |     /// <returns>The abuse report if found, otherwise null.</returns> | ||||||
|  |     Task<AbuseReport?> GetAbuseReport(Guid reportId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a new abuse report. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="resourceIdentifier">The identifier of the resource being reported.</param> | ||||||
|  |     /// <param name="type">The type of abuse report.</param> | ||||||
|  |     /// <param name="reason">The reason for the report.</param> | ||||||
|  |     /// <param name="accountId">The ID of the account making the report.</param> | ||||||
|  |     /// <returns>The created abuse report.</returns> | ||||||
|  |     Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Counts abuse reports. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="includeResolved">Whether to include resolved reports.</param> | ||||||
|  |     /// <returns>The count of abuse reports.</returns> | ||||||
|  |     Task<int> CountAbuseReports(bool includeResolved = false); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Counts abuse reports by a specific user. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <param name="includeResolved">Whether to include resolved reports.</param> | ||||||
|  |     /// <returns>The count of abuse reports by the user.</returns> | ||||||
|  |     Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets a list of abuse reports. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="skip">Number of reports to skip.</param> | ||||||
|  |     /// <param name="take">Number of reports to take.</param> | ||||||
|  |     /// <param name="includeResolved">Whether to include resolved reports.</param> | ||||||
|  |     /// <returns>A list of abuse reports.</returns> | ||||||
|  |     Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets a list of abuse reports by a specific user. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <param name="skip">Number of reports to skip.</param> | ||||||
|  |     /// <param name="take">Number of reports to take.</param> | ||||||
|  |     /// <param name="includeResolved">Whether to include resolved reports.</param> | ||||||
|  |     /// <returns>A list of abuse reports by the user.</returns> | ||||||
|  |     Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Resolves an abuse report. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="id">The ID of the report to resolve.</param> | ||||||
|  |     /// <param name="resolution">The resolution message.</param> | ||||||
|  |     /// <returns>The resolved abuse report.</returns> | ||||||
|  |     Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets the count of pending abuse reports. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <returns>The count of pending abuse reports.</returns> | ||||||
|  |     Task<int> GetPendingAbuseReportsCount(); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Checks if a relationship with a specific status exists between two accounts. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId1">The ID of the first account.</param> | ||||||
|  |     /// <param name="accountId2">The ID of the second account.</param> | ||||||
|  |     /// <param name="status">The relationship status to check for.</param> | ||||||
|  |     /// <returns>True if the relationship exists, otherwise false.</returns> | ||||||
|  |     Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, RelationshipStatus status); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets the statuses for a list of account IDs. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountIds">A list of account IDs.</param> | ||||||
|  |     /// <returns>A dictionary where the key is the account ID and the value is the status.</returns> | ||||||
|  |     Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> accountIds); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Sends a notification to an account. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="account">The target account.</param> | ||||||
|  |     /// <param name="topic">The notification topic.</param> | ||||||
|  |     /// <param name="title">The notification title.</param> | ||||||
|  |     /// <param name="subtitle">The notification subtitle.</param> | ||||||
|  |     /// <param name="body">The notification body.</param> | ||||||
|  |     /// <param name="actionUri">The action URI for the notification.</param> | ||||||
|  |     /// <returns>A task representing the asynchronous operation.</returns> | ||||||
|  |     Task SendNotification(Account account, string topic, string title, string? subtitle, string body, string? actionUri = null); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Lists the friends of an account. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="account">The account.</param> | ||||||
|  |     /// <returns>A list of friend accounts.</returns> | ||||||
|  |     Task<List<Account>> ListAccountFriends(Account account); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Verifies an authentication factor code. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="factor">The authentication factor.</param> | ||||||
|  |     /// <param name="code">The code to verify.</param> | ||||||
|  |     /// <returns>True if the code is valid, otherwise false.</returns> | ||||||
|  |     Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Send the auth factor verification code to users, for factors like in-app code and email. | ||||||
|  |     /// Sometimes it requires a hint, like a part of the user's email address to ensure the user is who own the account. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="account">The owner of the auth factor</param> | ||||||
|  |     /// <param name="factor">The auth factor needed to send code</param> | ||||||
|  |     /// <param name="hint">The part of the contact method for verification</param> | ||||||
|  |     Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates an action log entry. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="type">The type of action log.</param> | ||||||
|  |     /// <param name="meta">Additional metadata for the action log.</param> | ||||||
|  |     /// <param name="request">The HTTP request.</param> | ||||||
|  |     /// <param name="account">The account associated with the action.</param> | ||||||
|  |     /// <returns>The created action log.</returns> | ||||||
|  |     Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Account? account = null); | ||||||
|  |  | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a new session. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="lastGrantedAt">The last granted instant.</param> | ||||||
|  |     /// <param name="expiredAt">The expiration instant.</param> | ||||||
|  |     /// <param name="account">The associated account.</param> | ||||||
|  |     /// <param name="challenge">The associated challenge.</param> | ||||||
|  |     /// <returns>The created session.</returns> | ||||||
|  |     Task<Session> CreateSession(NodaTime.Instant lastGrantedAt, NodaTime.Instant expiredAt, Account account, Challenge challenge); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Updates the LastGrantedAt for a session. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="sessionId">The ID of the session.</param> | ||||||
|  |     /// <param name="lastGrantedAt">The new LastGrantedAt instant.</param> | ||||||
|  |     Task UpdateSessionLastGrantedAt(Guid sessionId, NodaTime.Instant lastGrantedAt); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Updates the LastSeenAt for an account profile. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="accountId">The ID of the account.</param> | ||||||
|  |     /// <param name="lastSeenAt">The new LastSeenAt instant.</param> | ||||||
|  |     Task UpdateAccountProfileLastSeenAt(Guid accountId, NodaTime.Instant lastSeenAt); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Creates a token for a session. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="session">The session.</param> | ||||||
|  |     /// <returns>The token string.</returns> | ||||||
|  |     string CreateToken(Session session); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets the AuthConstants.CookieTokenName. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <returns>The cookie token name.</returns> | ||||||
|  |     string GetAuthCookieTokenName(); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Searches for accounts by a search term. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="searchTerm">The term to search for.</param> | ||||||
|  |     /// <returns>A list of matching accounts.</returns> | ||||||
|  |     Task<List<Account>> SearchAccountsAsync(string searchTerm); | ||||||
| } | } | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using MagicOnion; | using MagicOnion; | ||||||
| using Microsoft.AspNetCore.Http; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Services; | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| @@ -15,10 +14,11 @@ public interface IActionLogService : IService<IActionLogService> | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Creates an action log entry from an HTTP request |     /// Creates an action log entry from an HTTP request | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     void CreateActionLogFromRequest( |     Task<ActionLog> CreateActionLogFromRequest( | ||||||
|         string action,  |         string type,  | ||||||
|         Dictionary<string, object> meta,  |         Dictionary<string, object> meta,  | ||||||
|         HttpRequest request, |         string? ipAddress,  | ||||||
|  |         string? userAgent,  | ||||||
|         Account? account = null |         Account? account = null | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
							
								
								
									
										13
									
								
								DysonNetwork.Shared/Services/ICustomAppService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Shared/Services/ICustomAppService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | using MagicOnion; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
|  | public interface ICustomAppService : IService<ICustomAppService> | ||||||
|  | { | ||||||
|  |     Task<CustomApp?> FindClientByIdAsync(Guid clientId); | ||||||
|  |     Task<int> CountCustomAppsByPublisherId(Guid publisherId); | ||||||
|  | } | ||||||
| @@ -23,6 +23,20 @@ public interface IMagicSpellService : IService<IMagicSpellService> | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     Task<MagicSpell?> GetMagicSpellAsync(string token); |     Task<MagicSpell?> GetMagicSpellAsync(string token); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Gets a magic spell by its ID. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="spellId">The ID of the magic spell.</param> | ||||||
|  |     /// <returns>The magic spell if found, otherwise null.</returns> | ||||||
|  |     Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Applies a password reset magic spell. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="spell">The magic spell object.</param> | ||||||
|  |     /// <param name="newPassword">The new password.</param> | ||||||
|  |     Task ApplyPasswordReset(MagicSpell spell, string newPassword); | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Consumes a magic spell |     /// Consumes a magic spell | ||||||
|     /// </summary> |     /// </summary> | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using MagicOnion; | using MagicOnion; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Shared.Services; | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| @@ -20,4 +22,25 @@ public interface INotificationService : IService<INotificationService> | |||||||
|         string deviceId, |         string deviceId, | ||||||
|         string deviceToken |         string deviceToken | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     Task DeliveryNotification(Notification notification); | ||||||
|  |  | ||||||
|  |     Task MarkNotificationsViewed(ICollection<Notification> notifications); | ||||||
|  |  | ||||||
|  |     Task BroadcastNotification(Notification notification, bool save = false); | ||||||
|  |  | ||||||
|  |     Task SendNotificationBatch(Notification notification, List<Account> accounts, | ||||||
|  |         bool save = false); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								DysonNetwork.Shared/Services/IPermissionService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								DysonNetwork.Shared/Services/IPermissionService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | using MagicOnion; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
|  | public interface IPermissionService : IService<IPermissionService> | ||||||
|  | { | ||||||
|  |     UnaryResult<bool> CheckPermission(string scope, string permission); | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								DysonNetwork.Shared/Services/IPublisherService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								DysonNetwork.Shared/Services/IPublisherService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | using MagicOnion; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
|  | public interface IPublisherService : IService<IPublisherService> | ||||||
|  | { | ||||||
|  |     Task<Publisher?> GetPublisherByName(string name); | ||||||
|  |     Task<List<Publisher>> GetUserPublishers(Guid accountId); | ||||||
|  |     Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role); | ||||||
|  |     Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId); | ||||||
|  | } | ||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using MagicOnion; | using MagicOnion; | ||||||
|  |  | ||||||
| @@ -24,4 +27,53 @@ public interface IRelationshipService : IService<IRelationshipService> | |||||||
|     /// Creates a new relationship between two accounts |     /// Creates a new relationship between two accounts | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status); |     Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Blocks a user | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Relationship> BlockAccount(Account sender, Account target); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Unblocks a user | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Relationship> UnblockAccount(Account sender, Account target); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Sends a friend request to a user | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Relationship> SendFriendRequest(Account sender, Account target); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Deletes a friend request | ||||||
|  |     /// </summary> | ||||||
|  |     Task DeleteFriendRequest(Guid accountId, Guid relatedId); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Accepts a friend request | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Relationship> AcceptFriendRelationship( | ||||||
|  |         Relationship relationship, | ||||||
|  |         RelationshipStatus status = RelationshipStatus.Friends | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Updates a relationship between two users | ||||||
|  |     /// </summary> | ||||||
|  |     Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Lists all friends of an account | ||||||
|  |     /// </summary> | ||||||
|  |     Task<List<Account>> ListAccountFriends(Account account); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Lists all blocked users of an account | ||||||
|  |     /// </summary> | ||||||
|  |     Task<List<Guid>> ListAccountBlocked(Account account); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Checks if a relationship with a specific status exists between two accounts | ||||||
|  |     /// </summary> | ||||||
|  |     Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, | ||||||
|  |         RelationshipStatus status = RelationshipStatus.Friends); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Activity; | namespace DysonNetwork.Sphere.Activity; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Connection.WebReader; | using DysonNetwork.Sphere.Connection.WebReader; | ||||||
| using DysonNetwork.Sphere.Discovery; | using DysonNetwork.Sphere.Discovery; | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| @@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Activity; | |||||||
| public class ActivityService( | public class ActivityService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PublisherService pub, |     PublisherService pub, | ||||||
|     RelationshipService rels, |     Shared.Services.IRelationshipService rels, | ||||||
|     PostService ps, |     PostService ps, | ||||||
|     DiscoveryService ds |     DiscoveryService ds | ||||||
| ) | ) | ||||||
| @@ -125,7 +125,7 @@ public class ActivityService( | |||||||
|     ) |     ) | ||||||
|     { |     { | ||||||
|         var activities = new List<Activity>(); |         var activities = new List<Activity>(); | ||||||
|         var userFriends = await rels.ListAccountFriends(currentUser); |         var userFriends = (await rels.ListAccountFriends(currentUser)).Select(x => x.Id).ToList(); | ||||||
|         var userPublishers = await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = await pub.GetUserPublishers(currentUser.Id); | ||||||
|         debugInclude ??= []; |         debugInclude ??= []; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using System.Linq.Expressions; | using System.Linq.Expressions; | ||||||
| using System.Reflection; | using System.Reflection; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Sphere.Connection.WebReader; | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| using DysonNetwork.Sphere.Sticker; | using DysonNetwork.Sphere.Sticker; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -11,13 +12,6 @@ using Quartz; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere; | namespace DysonNetwork.Sphere; | ||||||
|  |  | ||||||
| public abstract class ModelBase |  | ||||||
| { |  | ||||||
|     public Instant CreatedAt { get; set; } |  | ||||||
|     public Instant UpdatedAt { get; set; } |  | ||||||
|     public Instant? DeletedAt { get; set; } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class AppDatabase( | public class AppDatabase( | ||||||
|     DbContextOptions<AppDatabase> options, |     DbContextOptions<AppDatabase> options, | ||||||
|     IConfiguration configuration |     IConfiguration configuration | ||||||
| @@ -59,8 +53,8 @@ public class AppDatabase( | |||||||
|  |  | ||||||
|     public DbSet<Subscription> WalletSubscriptions { get; set; } |     public DbSet<Subscription> WalletSubscriptions { get; set; } | ||||||
|     public DbSet<Coupon> WalletCoupons { get; set; } |     public DbSet<Coupon> WalletCoupons { get; set; } | ||||||
|     public DbSet<Connection.WebReader.WebArticle> WebArticles { get; set; } |     public DbSet<WebArticle> WebArticles { get; set; } | ||||||
|     public DbSet<Connection.WebReader.WebFeed> WebFeeds { get; set; } |     public DbSet<WebFeed> WebFeeds { get; set; } | ||||||
|  |  | ||||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||||
|     { |     { | ||||||
| @@ -189,11 +183,11 @@ public class AppDatabase( | |||||||
|             .HasForeignKey(m => m.SenderId) |             .HasForeignKey(m => m.SenderId) | ||||||
|             .OnDelete(DeleteBehavior.Cascade); |             .OnDelete(DeleteBehavior.Cascade); | ||||||
|  |  | ||||||
|         modelBuilder.Entity<Connection.WebReader.WebFeed>() |         modelBuilder.Entity<WebFeed>() | ||||||
|             .HasIndex(f => f.Url) |             .HasIndex(f => f.Url) | ||||||
|             .IsUnique(); |             .IsUnique(); | ||||||
|  |  | ||||||
|         modelBuilder.Entity<Connection.WebReader.WebArticle>() |         modelBuilder.Entity<WebArticle>() | ||||||
|             .HasIndex(a => a.Url) |             .HasIndex(a => a.Url) | ||||||
|             .IsUnique(); |             .IsUnique(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| @@ -137,7 +137,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|  |  | ||||||
|     [HttpPost("{roomId:guid}/messages")] |     [HttpPost("{roomId:guid}/messages")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "chat.messages.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "chat.messages.create")] | ||||||
|     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId) |     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Pass.Account; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Permission; |  | ||||||
| using DysonNetwork.Sphere.Realm; | using DysonNetwork.Sphere.Realm; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -20,11 +20,12 @@ public class ChatRoomController( | |||||||
|     FileReferenceService fileRefService, |     FileReferenceService fileRefService, | ||||||
|     ChatRoomService crs, |     ChatRoomService crs, | ||||||
|     RealmService rs, |     RealmService rs, | ||||||
|     ActionLogService als, |     IAccountService accounts, | ||||||
|     NotificationService nty, |     IActionLogService als, | ||||||
|     RelationshipService rels, |     INotificationService nty, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IRelationshipService rels, | ||||||
|     AccountEventService aes |     IAccountEventService aes, | ||||||
|  |     IStringLocalizer<NotificationResource> localizer | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|     [HttpGet("{id:guid}")] |     [HttpGet("{id:guid}")] | ||||||
| @@ -47,7 +48,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms() |     public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms() | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||||
|             return Unauthorized(); |             return Unauthorized(); | ||||||
|         var userId = currentUser.Id; |         var userId = currentUser.Id; | ||||||
|  |  | ||||||
| @@ -73,10 +74,10 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request) |     public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||||
|             return Unauthorized(); |             return Unauthorized(); | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await accounts.GetAccountById(request.RelatedUserId); | ||||||
|         if (relatedUser is null) |         if (relatedUser is null) | ||||||
|             return BadRequest("Related user was not found"); |             return BadRequest("Related user was not found"); | ||||||
|  |  | ||||||
| @@ -105,7 +106,7 @@ public class ChatRoomController( | |||||||
|                 { |                 { | ||||||
|                     AccountId = currentUser.Id, |                     AccountId = currentUser.Id, | ||||||
|                     Role = ChatMemberRole.Owner, |                     Role = ChatMemberRole.Owner, | ||||||
|                     JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow) |                     JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow) | ||||||
|                 }, |                 }, | ||||||
|                 new() |                 new() | ||||||
|                 { |                 { | ||||||
| @@ -119,9 +120,12 @@ public class ChatRoomController( | |||||||
|         db.ChatRooms.Add(dmRoom); |         db.ChatRooms.Add(dmRoom); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomCreate, |             ActionLogType.ChatroomCreate, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } }, Request |             new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId); |         var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId); | ||||||
| @@ -162,7 +166,7 @@ public class ChatRoomController( | |||||||
|  |  | ||||||
|     [HttpPost] |     [HttpPost] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "chat.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "chat.create")] | ||||||
|     public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request) |     public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
| @@ -225,9 +229,12 @@ public class ChatRoomController( | |||||||
|                 chatRoomResourceId |                 chatRoomResourceId | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomCreate, |             ActionLogType.ChatroomCreate, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request |             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(chatRoom); |         return Ok(chatRoom); | ||||||
| @@ -311,9 +318,12 @@ public class ChatRoomController( | |||||||
|         db.ChatRooms.Update(chatRoom); |         db.ChatRooms.Update(chatRoom); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomUpdate, |             ActionLogType.ChatroomUpdate, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request |             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(chatRoom); |         return Ok(chatRoom); | ||||||
| @@ -345,9 +355,12 @@ public class ChatRoomController( | |||||||
|         db.ChatRooms.Remove(chatRoom); |         db.ChatRooms.Remove(chatRoom); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomDelete, |             ActionLogType.ChatroomDelete, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request |             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -437,7 +450,6 @@ public class ChatRoomController( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     public class ChatMemberRequest |     public class ChatMemberRequest | ||||||
|     { |     { | ||||||
|         [Required] public Guid RelatedUserId { get; set; } |         [Required] public Guid RelatedUserId { get; set; } | ||||||
| @@ -452,7 +464,7 @@ public class ChatRoomController( | |||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|         var userId = currentUser.Id; |         var userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await accounts.GetAccountById(request.RelatedUserId); | ||||||
|         if (relatedUser is null) return BadRequest("Related user was not found"); |         if (relatedUser is null) return BadRequest("Related user was not found"); | ||||||
|  |  | ||||||
|         if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) |         if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) | ||||||
| @@ -508,9 +520,12 @@ public class ChatRoomController( | |||||||
|         newMember.ChatRoom = chatRoom; |         newMember.ChatRoom = chatRoom; | ||||||
|         await _SendInviteNotify(newMember, currentUser); |         await _SendInviteNotify(newMember, currentUser); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomInvite, |             ActionLogType.ChatroomInvite, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } }, Request |             new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(newMember); |         return Ok(newMember); | ||||||
| @@ -560,9 +575,12 @@ public class ChatRoomController( | |||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|         _ = crs.PurgeRoomMembersCache(roomId); |         _ = crs.PurgeRoomMembersCache(roomId); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomJoin, |             ActionLogType.ChatroomJoin, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", roomId } }, Request |             new Dictionary<string, object> { { "chatroom_id", roomId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
| @@ -676,7 +694,9 @@ public class ChatRoomController( | |||||||
|                 ActionLogType.RealmAdjustRole, |                 ActionLogType.RealmAdjustRole, | ||||||
|                 new Dictionary<string, object> |                 new Dictionary<string, object> | ||||||
|                     { { "chatroom_id", roomId }, { "account_id", memberId }, { "new_role", newRole } }, |                     { { "chatroom_id", roomId }, { "account_id", memberId }, { "new_role", newRole } }, | ||||||
|                 Request |                 Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |                 Request.Headers.UserAgent.ToString(), | ||||||
|  |                 currentUser | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|             return Ok(targetMember); |             return Ok(targetMember); | ||||||
| @@ -723,7 +743,10 @@ public class ChatRoomController( | |||||||
|  |  | ||||||
|             als.CreateActionLogFromRequest( |             als.CreateActionLogFromRequest( | ||||||
|                 ActionLogType.ChatroomKick, |                 ActionLogType.ChatroomKick, | ||||||
|                 new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } }, Request |                 new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } }, | ||||||
|  |                 Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |                 Request.Headers.UserAgent.ToString(), | ||||||
|  |                 currentUser | ||||||
|             ); |             ); | ||||||
|  |  | ||||||
|             return NoContent(); |             return NoContent(); | ||||||
| @@ -763,9 +786,12 @@ public class ChatRoomController( | |||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|         _ = crs.PurgeRoomMembersCache(roomId); |         _ = crs.PurgeRoomMembersCache(roomId); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomJoin, |             ActionLogType.ChatroomJoin, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", roomId } }, Request |             new Dictionary<string, object> { { "chatroom_id", roomId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(chatRoom); |         return Ok(chatRoom); | ||||||
| @@ -800,15 +826,18 @@ public class ChatRoomController( | |||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|         await crs.PurgeRoomMembersCache(roomId); |         await crs.PurgeRoomMembersCache(roomId); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomLeave, |             ActionLogType.ChatroomLeave, | ||||||
|             new Dictionary<string, object> { { "chatroom_id", roomId } }, Request |             new Dictionary<string, object> { { "chatroom_id", roomId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task _SendInviteNotify(ChatMember member, Shared.Models.Account sender) |     private async Task _SendInviteNotify(ChatMember member, Account sender) | ||||||
|     { |     { | ||||||
|         string title = localizer["ChatInviteTitle"]; |         string title = localizer["ChatInviteTitle"]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Chat.Realtime; | using DysonNetwork.Sphere.Chat.Realtime; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Sphere.Connection; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| @@ -205,7 +205,7 @@ public partial class ChatService( | |||||||
|  |  | ||||||
|         using var scope = scopeFactory.CreateScope(); |         using var scope = scopeFactory.CreateScope(); | ||||||
|         var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>(); |         var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>(); | ||||||
|         var scopedNty = scope.ServiceProvider.GetRequiredService<NotificationService>(); |         var scopedNty = scope.ServiceProvider.GetRequiredService<INotificationService>(); | ||||||
|         var scopedCrs = scope.ServiceProvider.GetRequiredService<ChatRoomService>(); |         var scopedCrs = scope.ServiceProvider.GetRequiredService<ChatRoomService>(); | ||||||
|  |  | ||||||
|         var roomSubject = room is { Type: ChatRoomType.DirectMessage, Name: null } ? "DM" : |         var roomSubject = room is { Type: ChatRoomType.DirectMessage, Name: null } ? "DM" : | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
| @@ -6,7 +7,7 @@ namespace DysonNetwork.Sphere.Connection; | |||||||
|  |  | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("completion")] | [Route("completion")] | ||||||
| public class AutoCompletionController(AppDatabase db) | public class AutoCompletionController(IAccountService accounts, AppDatabase db) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| { | { | ||||||
|     [HttpPost] |     [HttpPost] | ||||||
| @@ -38,19 +39,15 @@ public class AutoCompletionController(AppDatabase db) | |||||||
|  |  | ||||||
|     private async Task<List<CompletionItem>> GetAccountCompletions(string searchTerm) |     private async Task<List<CompletionItem>> GetAccountCompletions(string searchTerm) | ||||||
|     { |     { | ||||||
|         return await db.Accounts |         var data = await accounts.SearchAccountsAsync(searchTerm); | ||||||
|             .Where(a => EF.Functions.ILike(a.Name, $"%{searchTerm}%")) |         return data.Select(a => new CompletionItem | ||||||
|             .OrderBy(a => a.Name) |  | ||||||
|             .Take(10) |  | ||||||
|             .Select(a => new CompletionItem |  | ||||||
|         { |         { | ||||||
|             Id = a.Id.ToString(), |             Id = a.Id.ToString(), | ||||||
|             DisplayName = a.Name, |             DisplayName = a.Name, | ||||||
|             SecondaryText = a.Nick, |             SecondaryText = a.Nick, | ||||||
|             Type = "account", |             Type = "account", | ||||||
|             Data = a |             Data = a | ||||||
|             }) |         }).ToList(); | ||||||
|             .ToListAsync(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task<List<CompletionItem>> GetStickerCompletions(string searchTerm) |     private async Task<List<CompletionItem>> GetStickerCompletions(string searchTerm) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Connection.WebReader; | namespace DysonNetwork.Sphere.Connection.WebReader; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.RateLimiting; | using Microsoft.AspNetCore.RateLimiting; | ||||||
| @@ -59,7 +59,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     [HttpDelete("link/cache")] |     [HttpDelete("link/cache")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("maintenance", "cache.scrap")] |     [DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "cache.scrap")] | ||||||
|     public async Task<IActionResult> InvalidateCache([FromQuery] string url) |     public async Task<IActionResult> InvalidateCache([FromQuery] string url) | ||||||
|     { |     { | ||||||
|         if (string.IsNullOrEmpty(url)) |         if (string.IsNullOrEmpty(url)) | ||||||
| @@ -76,7 +76,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     [HttpDelete("cache/all")] |     [HttpDelete("cache/all")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("maintenance", "cache.scrap")] |     [DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "cache.scrap")] | ||||||
|     public async Task<IActionResult> InvalidateAllCache() |     public async Task<IActionResult> InvalidateAllCache() | ||||||
|     { |     { | ||||||
|         await reader.InvalidateAllCachedPreviewsAsync(); |         await reader.InvalidateAllCachedPreviewsAsync(); | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using DysonNetwork.Sphere.Publisher; | using DysonNetwork.Sphere.Publisher; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Sphere.Permission; |  | ||||||
| using DysonNetwork.Sphere.Publisher; | using DysonNetwork.Sphere.Publisher; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| @@ -14,7 +13,7 @@ namespace DysonNetwork.Sphere.Developer; | |||||||
| public class DeveloperController( | public class DeveloperController( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PublisherService ps, |     PublisherService ps, | ||||||
|     ActionLogService als |     DysonNetwork.Shared.Services.IActionLogService als | ||||||
| ) | ) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| { | { | ||||||
| @@ -91,7 +90,7 @@ public class DeveloperController( | |||||||
|  |  | ||||||
|     [HttpPost("{name}/enroll")] |     [HttpPost("{name}/enroll")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "developers.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "developers.create")] | ||||||
|     public async Task<ActionResult<Shared.Models.Publisher>> EnrollDeveloperProgram(string name) |     public async Task<ActionResult<Shared.Models.Publisher>> EnrollDeveloperProgram(string name) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ | |||||||
|         <PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" /> |         <PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" /> | ||||||
|         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> |         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> | ||||||
|         <PackageReference Include="MagicOnion.Server" Version="7.0.5" /> |         <PackageReference Include="MagicOnion.Server" Version="7.0.5" /> | ||||||
|  |         <PackageReference Include="Grpc.Net.Client" Version="2.71.0" /> | ||||||
|  |         <PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" /> | ||||||
|         <PackageReference Include="MailKit" Version="4.11.0" /> |         <PackageReference Include="MailKit" Version="4.11.0" /> | ||||||
|         <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> |         <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> |         <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> | ||||||
| @@ -153,6 +155,7 @@ | |||||||
|             <DependentUpon>NotificationResource.resx</DependentUpon> |             <DependentUpon>NotificationResource.resx</DependentUpon> | ||||||
|         </Compile> |         </Compile> | ||||||
|         <Compile Remove="Auth\AppleAuthController.cs" /> |         <Compile Remove="Auth\AppleAuthController.cs" /> | ||||||
|  |         <Compile Remove="Permission\RequiredPermissionAttribute.cs" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
| @@ -172,6 +175,7 @@ | |||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|  |       <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -1,106 +0,0 @@ | |||||||
| using MailKit.Net.Smtp; |  | ||||||
| using Microsoft.AspNetCore.Components; |  | ||||||
| using MimeKit; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Email; |  | ||||||
|  |  | ||||||
| public class EmailServiceConfiguration |  | ||||||
| { |  | ||||||
|     public string Server { get; set; } = null!; |  | ||||||
|     public int Port { get; set; } |  | ||||||
|     public bool UseSsl { get; set; } |  | ||||||
|     public string Username { get; set; } = null!; |  | ||||||
|     public string Password { get; set; } = null!; |  | ||||||
|     public string FromAddress { get; set; } = null!; |  | ||||||
|     public string FromName { get; set; } = null!; |  | ||||||
|     public string SubjectPrefix { get; set; } = null!; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public class EmailService |  | ||||||
| { |  | ||||||
|     private readonly EmailServiceConfiguration _configuration; |  | ||||||
|     private readonly RazorViewRenderer _viewRenderer; |  | ||||||
|     private readonly ILogger<EmailService> _logger; |  | ||||||
|  |  | ||||||
|     public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger<EmailService> logger) |  | ||||||
|     { |  | ||||||
|         var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>(); |  | ||||||
|         _configuration = cfg ?? throw new ArgumentException("Email service was not configured."); |  | ||||||
|         _viewRenderer = viewRenderer; |  | ||||||
|         _logger = logger; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody) |  | ||||||
|     { |  | ||||||
|         await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody, |  | ||||||
|         string? htmlBody) |  | ||||||
|     { |  | ||||||
|         subject = $"[{_configuration.SubjectPrefix}] {subject}"; |  | ||||||
|  |  | ||||||
|         var emailMessage = new MimeMessage(); |  | ||||||
|         emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress)); |  | ||||||
|         emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail)); |  | ||||||
|         emailMessage.Subject = subject; |  | ||||||
|  |  | ||||||
|         var bodyBuilder = new BodyBuilder |  | ||||||
|         { |  | ||||||
|             TextBody = textBody |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (!string.IsNullOrEmpty(htmlBody)) |  | ||||||
|             bodyBuilder.HtmlBody = htmlBody; |  | ||||||
|  |  | ||||||
|         emailMessage.Body = bodyBuilder.ToMessageBody(); |  | ||||||
|  |  | ||||||
|         using var client = new SmtpClient(); |  | ||||||
|         await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl); |  | ||||||
|         await client.AuthenticateAsync(_configuration.Username, _configuration.Password); |  | ||||||
|         await client.SendAsync(emailMessage); |  | ||||||
|         await client.DisconnectAsync(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static string _ConvertHtmlToPlainText(string html) |  | ||||||
|     { |  | ||||||
|         // Remove style tags and their contents |  | ||||||
|         html = System.Text.RegularExpressions.Regex.Replace(html, "<style[^>]*>.*?</style>", "",  |  | ||||||
|             System.Text.RegularExpressions.RegexOptions.Singleline); |  | ||||||
|      |  | ||||||
|         // Replace header tags with text + newlines |  | ||||||
|         html = System.Text.RegularExpressions.Regex.Replace(html, "<h[1-6][^>]*>(.*?)</h[1-6]>", "$1\n\n", |  | ||||||
|             System.Text.RegularExpressions.RegexOptions.IgnoreCase); |  | ||||||
|      |  | ||||||
|         // Replace line breaks |  | ||||||
|         html = html.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n"); |  | ||||||
|      |  | ||||||
|         // Remove all remaining HTML tags |  | ||||||
|         html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", ""); |  | ||||||
|      |  | ||||||
|         // Decode HTML entities |  | ||||||
|         html = System.Net.WebUtility.HtmlDecode(html); |  | ||||||
|      |  | ||||||
|         // Remove excess whitespace |  | ||||||
|         html = System.Text.RegularExpressions.Regex.Replace(html, @"\s+", " ").Trim(); |  | ||||||
|      |  | ||||||
|         return html; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail, |  | ||||||
|         string subject, TModel model) |  | ||||||
|         where TComponent : IComponent |  | ||||||
|     { |  | ||||||
|         try |  | ||||||
|         { |  | ||||||
|             var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model); |  | ||||||
|             var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody); |  | ||||||
|             await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody); |  | ||||||
|         } |  | ||||||
|         catch (Exception err) |  | ||||||
|         { |  | ||||||
|             _logger.LogError(err, "Failed to render email template..."); |  | ||||||
|             throw; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Services; |  | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Responses; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Sphere.Developer; | using DysonNetwork.Sphere.Developer; | ||||||
|  | using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Pages.Auth; | namespace DysonNetwork.Sphere.Pages.Auth; | ||||||
|  |  | ||||||
| public class AuthorizeModel(OidcProviderService oidcService, IConfiguration configuration) : PageModel | public class AuthorizeModel(    DysonNetwork.Pass.Auth.OidcProvider.Services.OidcProviderService oidcService, IConfiguration configuration) : PageModel | ||||||
| { | { | ||||||
|     [BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; } |     [BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ using Microsoft.AspNetCore.Mvc; | |||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Sphere.Connection; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -11,13 +11,13 @@ using Microsoft.EntityFrameworkCore; | |||||||
| namespace DysonNetwork.Sphere.Pages.Auth | namespace DysonNetwork.Sphere.Pages.Auth | ||||||
| { | { | ||||||
|     public class LoginModel( |     public class LoginModel( | ||||||
|         AppDatabase db, |         DysonNetwork.Shared.Services.IAccountService accounts, | ||||||
|         AccountService accounts, |         DysonNetwork.Pass.Auth.AuthService auth, | ||||||
|         AuthService auth, |  | ||||||
|         GeoIpService geo, |         GeoIpService geo, | ||||||
|         ActionLogService als |         DysonNetwork.Shared.Services.IActionLogService als | ||||||
|     ) : PageModel |     ) : PageModel | ||||||
|     { |     { | ||||||
|  |          | ||||||
|         [BindProperty] [Required] public string Username { get; set; } = string.Empty; |         [BindProperty] [Required] public string Username { get; set; } = string.Empty; | ||||||
|          |          | ||||||
|         [BindProperty] |         [BindProperty] | ||||||
| @@ -52,13 +52,7 @@ namespace DysonNetwork.Sphere.Pages.Auth | |||||||
|             var userAgent = HttpContext.Request.Headers.UserAgent.ToString(); |             var userAgent = HttpContext.Request.Headers.UserAgent.ToString(); | ||||||
|             var now = Instant.FromDateTimeUtc(DateTime.UtcNow); |             var now = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||||
|  |  | ||||||
|             var existingChallenge = await db.AuthChallenges |             var existingChallenge = await accounts.GetAuthChallenge(account.Id, ipAddress, userAgent, now); | ||||||
|                 .Where(e => e.Account == account) |  | ||||||
|                 .Where(e => e.IpAddress == ipAddress) |  | ||||||
|                 .Where(e => e.UserAgent == userAgent) |  | ||||||
|                 .Where(e => e.StepRemain > 0) |  | ||||||
|                 .Where(e => e.ExpiredAt != null && now < e.ExpiredAt) |  | ||||||
|                 .FirstOrDefaultAsync(); |  | ||||||
|  |  | ||||||
|             if (existingChallenge is not null) |             if (existingChallenge is not null) | ||||||
|             { |             { | ||||||
| @@ -79,8 +73,7 @@ namespace DysonNetwork.Sphere.Pages.Auth | |||||||
|                 AccountId = account.Id |                 AccountId = account.Id | ||||||
|             }.Normalize(); |             }.Normalize(); | ||||||
|  |  | ||||||
|             await db.AuthChallenges.AddAsync(challenge); |             await accounts.CreateAuthChallenge(challenge); | ||||||
|             await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|             // If we have a return URL, pass it to the verify page |             // If we have a return URL, pass it to the verify page | ||||||
|             if (TempData.TryGetValue("ReturnUrl", out var returnUrl) && returnUrl is string url) |             if (TempData.TryGetValue("ReturnUrl", out var returnUrl) && returnUrl is string url) | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| @page "/web/auth/challenge/{id:guid}/select-factor" | @page "/web/auth/challenge/{id:guid}/select-factor" | ||||||
| @using DysonNetwork.Shared.Models | @using DysonNetwork.Shared.Models | ||||||
| @using DysonNetwork.Sphere.Account |  | ||||||
| @model DysonNetwork.Sphere.Pages.Auth.SelectFactorModel | @model DysonNetwork.Sphere.Pages.Auth.SelectFactorModel | ||||||
| @{ | @{ | ||||||
|     ViewData["Title"] = "Select Authentication Method"; |     ViewData["Title"] = "Select Authentication Method"; | ||||||
|   | |||||||
| @@ -2,16 +2,14 @@ using DysonNetwork.Shared.Models; | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Pass.Account; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Pages.Auth; | namespace DysonNetwork.Sphere.Pages.Auth; | ||||||
|  |  | ||||||
| public class SelectFactorModel( | public class SelectFactorModel( | ||||||
|     AppDatabase db, |     DysonNetwork.Shared.Services.IAccountService accounts | ||||||
|     AccountService accounts | ) : PageModel | ||||||
| ) |  | ||||||
|     : PageModel |  | ||||||
| { | { | ||||||
|     [BindProperty(SupportsGet = true)] public Guid Id { get; set; } |     [BindProperty(SupportsGet = true)] public Guid Id { get; set; } | ||||||
|     [BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; } |     [BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; } | ||||||
| @@ -31,13 +29,11 @@ public class SelectFactorModel( | |||||||
|  |  | ||||||
|     public async Task<IActionResult> OnPostSelectFactorAsync() |     public async Task<IActionResult> OnPostSelectFactorAsync() | ||||||
|     { |     { | ||||||
|         var challenge = await db.AuthChallenges |         var challenge = await accounts.GetAuthChallenge(Id); | ||||||
|             .Include(e => e.Account) |  | ||||||
|             .FirstOrDefaultAsync(e => e.Id == Id); |  | ||||||
|  |  | ||||||
|         if (challenge == null) return NotFound(); |         if (challenge == null) return NotFound(); | ||||||
|  |  | ||||||
|         var factor = await db.AccountAuthFactors.FindAsync(SelectedFactorId); |         var factor = await accounts.GetAccountAuthFactor(SelectedFactorId, challenge.Account.Id); | ||||||
|         if (factor?.EnabledAt == null || factor.Trustworthy <= 0) |         if (factor?.EnabledAt == null || factor.Trustworthy <= 0) | ||||||
|             return BadRequest("Invalid authentication method."); |             return BadRequest("Invalid authentication method."); | ||||||
|  |  | ||||||
| @@ -81,16 +77,11 @@ public class SelectFactorModel( | |||||||
|  |  | ||||||
|     private async Task LoadChallengeAndFactors() |     private async Task LoadChallengeAndFactors() | ||||||
|     { |     { | ||||||
|         AuthChallenge = await db.AuthChallenges |         AuthChallenge = await accounts.GetAuthChallenge(Id); | ||||||
|             .Include(e => e.Account) |  | ||||||
|             .FirstOrDefaultAsync(e => e.Id == Id); |  | ||||||
|  |  | ||||||
|         if (AuthChallenge != null) |         if (AuthChallenge != null) | ||||||
|         { |         { | ||||||
|             AuthFactors = await db.AccountAuthFactors |             AuthFactors = await accounts.GetAccountAuthFactors(AuthChallenge.Account.Id); | ||||||
|                 .Where(e => e.AccountId == AuthChallenge.Account.Id) |  | ||||||
|                 .Where(e => e.EnabledAt != null && e.Trustworthy >= 1) |  | ||||||
|                 .ToListAsync(); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| @page "/web/auth/challenge/{id:guid}/verify/{factorId:guid}" | @page "/web/auth/challenge/{id:guid}/verify/{factorId:guid}" | ||||||
| @using DysonNetwork.Shared.Models | @using DysonNetwork.Shared.Models | ||||||
| @using DysonNetwork.Sphere.Account |  | ||||||
| @model DysonNetwork.Sphere.Pages.Auth.VerifyFactorModel | @model DysonNetwork.Sphere.Pages.Auth.VerifyFactorModel | ||||||
| @{ | @{ | ||||||
|     ViewData["Title"] = "Verify Your Identity"; |     ViewData["Title"] = "Verify Your Identity"; | ||||||
|   | |||||||
| @@ -3,21 +3,18 @@ using DysonNetwork.Shared.Models; | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Pages.Auth | namespace DysonNetwork.Sphere.Pages.Auth | ||||||
| { | { | ||||||
|     public class VerifyFactorModel( |     public class VerifyFactorModel( | ||||||
|         AppDatabase db, |         AppDatabase db, | ||||||
|         AccountService accounts, |         IAccountService accountService, | ||||||
|         AuthService auth, |         DysonNetwork.Pass.Auth.AuthService authService, | ||||||
|         ActionLogService als, |         IActionLogService actionLogService, | ||||||
|         IConfiguration configuration, |         IConfiguration configuration | ||||||
|         IHttpClientFactory httpClientFactory |     ) : PageModel | ||||||
|     ) |  | ||||||
|         : PageModel |  | ||||||
|     { |     { | ||||||
|         [BindProperty(SupportsGet = true)] public Guid Id { get; set; } |         [BindProperty(SupportsGet = true)] public Guid Id { get; set; } | ||||||
|  |  | ||||||
| @@ -55,30 +52,36 @@ namespace DysonNetwork.Sphere.Pages.Auth | |||||||
|  |  | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                 if (await accounts.VerifyFactorCode(Factor, Code)) |                 if (await accountService.VerifyFactorCode(Factor, Code)) | ||||||
|                 { |                 { | ||||||
|                     AuthChallenge.StepRemain -= Factor.Trustworthy; |                     AuthChallenge.StepRemain -= Factor.Trustworthy; | ||||||
|                     AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain); |                     AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain); | ||||||
|                     AuthChallenge.BlacklistFactors.Add(Factor.Id); |  | ||||||
|                     db.Update(AuthChallenge); |  | ||||||
|  |  | ||||||
|                     als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, |                     await actionLogService.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, | ||||||
|                         new Dictionary<string, object> |                         new Dictionary<string, object> | ||||||
|                         { |                         { | ||||||
|                             { "challenge_id", AuthChallenge.Id }, |                             { "challenge_id", AuthChallenge.Id }, | ||||||
|                             { "factor_id", Factor?.Id.ToString() ?? string.Empty } |                             { "factor_id", Factor?.Id.ToString() ?? string.Empty } | ||||||
|                         }, Request, AuthChallenge.Account); |                         }, | ||||||
|  |                         Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |                         Request.Headers.UserAgent.ToString(), | ||||||
|  |                         AuthChallenge.Account | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|                     await db.SaveChangesAsync(); |                     await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|                     if (AuthChallenge.StepRemain == 0) |                     if (AuthChallenge.StepRemain == 0) | ||||||
|                     { |                     { | ||||||
|                         als.CreateActionLogFromRequest(ActionLogType.NewLogin, |                         await actionLogService.CreateActionLogFromRequest(ActionLogType.NewLogin, | ||||||
|                             new Dictionary<string, object> |                             new Dictionary<string, object> | ||||||
|                             { |                             { | ||||||
|                                 { "challenge_id", AuthChallenge.Id }, |                                 { "challenge_id", AuthChallenge.Id }, | ||||||
|                                 { "account_id", AuthChallenge.AccountId } |                                 { "account_id", AuthChallenge.AccountId } | ||||||
|                             }, Request, AuthChallenge.Account); |                             }, | ||||||
|  |                             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |                             Request.Headers.UserAgent.ToString(), | ||||||
|  |                             AuthChallenge.Account | ||||||
|  |                         ); | ||||||
|  |  | ||||||
|                         return await ExchangeTokenAndRedirect(); |                         return await ExchangeTokenAndRedirect(); | ||||||
|                     } |                     } | ||||||
| @@ -98,16 +101,18 @@ namespace DysonNetwork.Sphere.Pages.Auth | |||||||
|             { |             { | ||||||
|                 if (AuthChallenge != null) |                 if (AuthChallenge != null) | ||||||
|                 { |                 { | ||||||
|                     AuthChallenge.FailedAttempts++; |  | ||||||
|                     db.Update(AuthChallenge); |  | ||||||
|                     await db.SaveChangesAsync(); |                     await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|                     als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, |                     await actionLogService.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, | ||||||
|                         new Dictionary<string, object> |                         new Dictionary<string, object> | ||||||
|                         { |                         { | ||||||
|                             { "challenge_id", AuthChallenge.Id }, |                             { "challenge_id", AuthChallenge.Id }, | ||||||
|                             { "factor_id", Factor?.Id.ToString() ?? string.Empty } |                             { "factor_id", Factor?.Id.ToString() ?? string.Empty } | ||||||
|                         }, Request, AuthChallenge.Account); |                         }, | ||||||
|  |                         Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |                         Request.Headers.UserAgent.ToString(), | ||||||
|  |                         AuthChallenge.Account | ||||||
|  |                     ); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -118,47 +123,30 @@ namespace DysonNetwork.Sphere.Pages.Auth | |||||||
|  |  | ||||||
|         private async Task LoadChallengeAndFactor() |         private async Task LoadChallengeAndFactor() | ||||||
|         { |         { | ||||||
|             AuthChallenge = await db.AuthChallenges |             AuthChallenge = await accountService.GetAuthChallenge(Id); | ||||||
|                 .Include(e => e.Account) |  | ||||||
|                 .FirstOrDefaultAsync(e => e.Id == Id); |  | ||||||
|  |  | ||||||
|             if (AuthChallenge?.Account != null) |             if (AuthChallenge?.Account != null) | ||||||
|             { |             { | ||||||
|                 Factor = await db.AccountAuthFactors |                 Factor = await accountService.GetAccountAuthFactor(FactorId, AuthChallenge.Account.Id); | ||||||
|                     .FirstOrDefaultAsync(e => e.Id == FactorId && |  | ||||||
|                                               e.AccountId == AuthChallenge.Account.Id && |  | ||||||
|                                               e.EnabledAt != null && |  | ||||||
|                                               e.Trustworthy > 0); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         private async Task<IActionResult> ExchangeTokenAndRedirect() |         private async Task<IActionResult> ExchangeTokenAndRedirect() | ||||||
|         { |         { | ||||||
|             var challenge = await db.AuthChallenges |             var challenge = await accountService.GetAuthChallenge(Id); | ||||||
|                 .Include(e => e.Account) |  | ||||||
|                 .FirstOrDefaultAsync(e => e.Id == Id); |  | ||||||
|  |  | ||||||
|             if (challenge == null) return BadRequest("Authorization code not found or expired."); |             if (challenge == null) return BadRequest("Authorization code not found or expired."); | ||||||
|             if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed."); |             if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed."); | ||||||
|  |  | ||||||
|             var session = await db.AuthSessions |             var session = await accountService.CreateSession( | ||||||
|                 .FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id); |                 Instant.FromDateTimeUtc(DateTime.UtcNow), | ||||||
|  |                 Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), | ||||||
|  |                 challenge.Account, | ||||||
|  |                 challenge | ||||||
|  |             ); | ||||||
|  |  | ||||||
|             if (session == null) |             var token = authService.CreateToken(session); | ||||||
|             { |             Response.Cookies.Append(accountService.GetAuthCookieTokenName(), token, new CookieOptions | ||||||
|                 session = new Session |  | ||||||
|                 { |  | ||||||
|                     LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), |  | ||||||
|                     ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), |  | ||||||
|                     Account = challenge.Account, |  | ||||||
|                     Challenge = challenge, |  | ||||||
|                 }; |  | ||||||
|                 db.AuthSessions.Add(session); |  | ||||||
|                 await db.SaveChangesAsync(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var token = auth.CreateToken(session); |  | ||||||
|             Response.Cookies.Append(AuthConstants.CookieTokenName, token, new CookieOptions |  | ||||||
|             { |             { | ||||||
|                 HttpOnly = true, |                 HttpOnly = true, | ||||||
|                 Secure = !configuration.GetValue<bool>("Debug"), |                 Secure = !configuration.GetValue<bool>("Debug"), | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| @using DysonNetwork.Sphere.Auth | @using DysonNetwork.Pass.Auth | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html lang="en" class="h-full"> | <html lang="en" class="h-full"> | ||||||
| <head> | <head> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| @page "/spells/{spellWord}" | @page "/spells/{spellWord}" | ||||||
| @using DysonNetwork.Sphere.Account | @using DysonNetwork.Shared.Models | ||||||
| @model DysonNetwork.Sphere.Pages.Spell.MagicSpellPage | @model DysonNetwork.Sphere.Pages.Spell.MagicSpellPage | ||||||
|  |  | ||||||
| @{ | @{ | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.RazorPages; | using Microsoft.AspNetCore.Mvc.RazorPages; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -6,7 +6,7 @@ using NodaTime; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Pages.Spell; | namespace DysonNetwork.Sphere.Pages.Spell; | ||||||
|  |  | ||||||
| public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageModel | public class MagicSpellPage(DysonNetwork.Shared.Services.IMagicSpellService magicSpellService) : PageModel | ||||||
| { | { | ||||||
|     [BindProperty] public MagicSpell? CurrentSpell { get; set; } |     [BindProperty] public MagicSpell? CurrentSpell { get; set; } | ||||||
|     [BindProperty] public string? NewPassword { get; set; } |     [BindProperty] public string? NewPassword { get; set; } | ||||||
| @@ -17,12 +17,7 @@ public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageMode | |||||||
|     { |     { | ||||||
|         spellWord = Uri.UnescapeDataString(spellWord); |         spellWord = Uri.UnescapeDataString(spellWord); | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         CurrentSpell = await db.MagicSpells |         CurrentSpell = await magicSpellService.GetMagicSpellAsync(spellWord); | ||||||
|             .Where(e => e.Spell == spellWord) |  | ||||||
|             .Where(e => e.ExpiresAt == null || now < e.ExpiresAt) |  | ||||||
|             .Where(e => e.AffectedAt == null || now >= e.AffectedAt) |  | ||||||
|             .Include(e => e.Account) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|  |  | ||||||
|         return Page(); |         return Page(); | ||||||
|     } |     } | ||||||
| @@ -33,19 +28,15 @@ public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageMode | |||||||
|             return Page(); |             return Page(); | ||||||
|  |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var spell = await db.MagicSpells |         var spell = await magicSpellService.GetMagicSpellByIdAsync(CurrentSpell.Id); | ||||||
|             .Where(e => e.Id == CurrentSpell.Id) |  | ||||||
|             .Where(e => e.ExpiresAt == null || now < e.ExpiresAt) |  | ||||||
|             .Where(e => e.AffectedAt == null || now >= e.AffectedAt) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|  |  | ||||||
|         if (spell == null || spell.Type == MagicSpellType.AuthPasswordReset && string.IsNullOrWhiteSpace(NewPassword)) |         if (spell == null || spell.Type == MagicSpellType.AuthPasswordReset && string.IsNullOrWhiteSpace(NewPassword)) | ||||||
|             return Page(); |             return Page(); | ||||||
|  |  | ||||||
|         if (spell.Type == MagicSpellType.AuthPasswordReset) |         if (spell.Type == MagicSpellType.AuthPasswordReset) | ||||||
|             await spells.ApplyPasswordReset(spell, NewPassword!); |             await magicSpellService.ApplyPasswordReset(spell, NewPassword!); | ||||||
|         else |         else | ||||||
|             await spells.ApplyMagicSpell(spell); |             await magicSpellService.ApplyMagicSpell(spell.Spell); | ||||||
|         IsSuccess = true; |         IsSuccess = true; | ||||||
|         return Page(); |         return Page(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Sphere.Publisher; | using DysonNetwork.Sphere.Publisher; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -19,8 +19,8 @@ public class PostController( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PostService ps, |     PostService ps, | ||||||
|     PublisherService pub, |     PublisherService pub, | ||||||
|     RelationshipService rels, |     DysonNetwork.Shared.Services.IRelationshipService rels, | ||||||
|     ActionLogService als |     DysonNetwork.Shared.Services.IActionLogService als | ||||||
| ) | ) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| { | { | ||||||
| @@ -33,7 +33,9 @@ public class PostController( | |||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Shared.Models.Account; |         var currentUser = currentUserValue as Shared.Models.Account; | ||||||
|         var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); |         var userFriends = currentUser is null | ||||||
|  |             ? [] | ||||||
|  |             : (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList(); | ||||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); | ||||||
|  |  | ||||||
|         var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName); |         var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName); | ||||||
| @@ -67,8 +69,10 @@ public class PostController( | |||||||
|     public async Task<ActionResult<Post>> GetPost(Guid id) |     public async Task<ActionResult<Post>> GetPost(Guid id) | ||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Shared.Models.Account; |         var currentUser = currentUserValue as Account; | ||||||
|         var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); |         var userFriends = currentUser is null | ||||||
|  |             ? [] | ||||||
|  |             : (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList(); | ||||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); | ||||||
|  |  | ||||||
|         var post = await db.Posts |         var post = await db.Posts | ||||||
| @@ -99,8 +103,10 @@ public class PostController( | |||||||
|             return BadRequest("Search query cannot be empty"); |             return BadRequest("Search query cannot be empty"); | ||||||
|  |  | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Shared.Models.Account; |         var currentUser = currentUserValue as Account; | ||||||
|         var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); |         var userFriends = currentUser is null | ||||||
|  |             ? [] | ||||||
|  |             : (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList(); | ||||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); | ||||||
|  |  | ||||||
|         var queryable = db.Posts |         var queryable = db.Posts | ||||||
| @@ -136,8 +142,10 @@ public class PostController( | |||||||
|         [FromQuery] int take = 20) |         [FromQuery] int take = 20) | ||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Shared.Models.Account; |         var currentUser = currentUserValue as Account; | ||||||
|         var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); |         var userFriends = currentUser is null | ||||||
|  |             ? [] | ||||||
|  |             : (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList(); | ||||||
|         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id); | ||||||
|  |  | ||||||
|         var parent = await db.Posts |         var parent = await db.Posts | ||||||
| @@ -264,9 +272,12 @@ public class PostController( | |||||||
|             return BadRequest(err.Message); |             return BadRequest(err.Message); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PostCreate, |             ActionLogType.PostCreate, | ||||||
|             new Dictionary<string, object> { { "post_id", post.Id } }, Request |             new Dictionary<string, object> { { "post_id", post.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return post; |         return post; | ||||||
| @@ -284,8 +295,8 @@ public class PostController( | |||||||
|     public async Task<ActionResult<PostReaction>> ReactPost(Guid id, [FromBody] PostReactionRequest request) |     public async Task<ActionResult<PostReaction>> ReactPost(Guid id, [FromBody] PostReactionRequest request) | ||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized(); |         if (currentUserValue is not Account currentUser) return Unauthorized(); | ||||||
|         var userFriends = await rels.ListAccountFriends(currentUser); |         var userFriends = (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList(); | ||||||
|         var userPublishers = await pub.GetUserPublishers(currentUser.Id); |         var userPublishers = await pub.GetUserPublishers(currentUser.Id); | ||||||
|  |  | ||||||
|         var post = await db.Posts |         var post = await db.Posts | ||||||
| @@ -319,9 +330,12 @@ public class PostController( | |||||||
|  |  | ||||||
|         if (isRemoving) return NoContent(); |         if (isRemoving) return NoContent(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PostReact, |             ActionLogType.PostReact, | ||||||
|             new Dictionary<string, object> { { "post_id", post.Id }, { "reaction", request.Symbol } }, Request |             new Dictionary<string, object> { { "post_id", post.Id }, { "reaction", request.Symbol } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(reaction); |         return Ok(reaction); | ||||||
| @@ -368,9 +382,12 @@ public class PostController( | |||||||
|             return BadRequest(err.Message); |             return BadRequest(err.Message); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PostUpdate, |             ActionLogType.PostUpdate, | ||||||
|             new Dictionary<string, object> { { "post_id", post.Id } }, Request |             new Dictionary<string, object> { { "post_id", post.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(post); |         return Ok(post); | ||||||
| @@ -392,9 +409,12 @@ public class PostController( | |||||||
|  |  | ||||||
|         await ps.DeletePostAsync(post); |         await ps.DeletePostAsync(post); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PostDelete, |             ActionLogType.PostDelete, | ||||||
|             new Dictionary<string, object> { { "post_id", post.Id } }, Request |             new Dictionary<string, object> { { "post_id", post.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Localization; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Connection.WebReader; | using DysonNetwork.Sphere.Connection.WebReader; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Publisher; | using DysonNetwork.Sphere.Publisher; | ||||||
| @@ -158,14 +159,13 @@ public partial class PostService( | |||||||
|                 var sender = post.Publisher; |                 var sender = post.Publisher; | ||||||
|                 using var scope = factory.CreateScope(); |                 using var scope = factory.CreateScope(); | ||||||
|                 var pub = scope.ServiceProvider.GetRequiredService<PublisherService>(); |                 var pub = scope.ServiceProvider.GetRequiredService<PublisherService>(); | ||||||
|                 var nty = scope.ServiceProvider.GetRequiredService<NotificationService>(); |                 var nty = scope.ServiceProvider.GetRequiredService<INotificationService>(); | ||||||
|                 var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>(); |  | ||||||
|                 try |                 try | ||||||
|                 { |                 { | ||||||
|                     var members = await pub.GetPublisherMembers(post.RepliedPost.PublisherId); |                     var members = await pub.GetPublisherMembers(post.RepliedPost.PublisherId); | ||||||
|                     foreach (var member in members) |                     foreach (var member in members) | ||||||
|                     { |                     { | ||||||
|                         AccountService.SetCultureInfo(member.Account); |                         CultureInfoService.SetCultureInfo(member.Account); | ||||||
|                         var (_, content) = ChopPostForNotification(post); |                         var (_, content) = ChopPostForNotification(post); | ||||||
|                         await nty.SendNotification( |                         await nty.SendNotification( | ||||||
|                             member.Account, |                             member.Account, | ||||||
| @@ -439,14 +439,14 @@ public partial class PostService( | |||||||
|             { |             { | ||||||
|                 using var scope = factory.CreateScope(); |                 using var scope = factory.CreateScope(); | ||||||
|                 var pub = scope.ServiceProvider.GetRequiredService<PublisherService>(); |                 var pub = scope.ServiceProvider.GetRequiredService<PublisherService>(); | ||||||
|                 var nty = scope.ServiceProvider.GetRequiredService<NotificationService>(); |                 var nty = scope.ServiceProvider.GetRequiredService<INotificationService>(); | ||||||
|                 var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>(); |                 var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>(); | ||||||
|                 try |                 try | ||||||
|                 { |                 { | ||||||
|                     var members = await pub.GetPublisherMembers(post.PublisherId); |                     var members = await pub.GetPublisherMembers(post.PublisherId); | ||||||
|                     foreach (var member in members) |                     foreach (var member in members) | ||||||
|                     { |                     { | ||||||
|                         AccountService.SetCultureInfo(member.Account); |                         CultureInfoService.SetCultureInfo(member.Account); | ||||||
|                         await nty.SendNotification( |                         await nty.SendNotification( | ||||||
|                             member.Account, |                             member.Account, | ||||||
|                             "posts.reactions.new", |                             "posts.reactions.new", | ||||||
|   | |||||||
| @@ -2,6 +2,9 @@ using DysonNetwork.Sphere; | |||||||
| using DysonNetwork.Sphere.Startup; | using DysonNetwork.Sphere.Startup; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using tusdotnet.Stores; | using tusdotnet.Stores; | ||||||
|  | using MagicOnion.Client; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using Grpc.Net.Client; | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
| @@ -20,6 +23,42 @@ builder.Services.AddAppSwagger(); | |||||||
| // Add gRPC services | // Add gRPC services | ||||||
| builder.Services.AddGrpc(); | builder.Services.AddGrpc(); | ||||||
|  |  | ||||||
|  | // Configure MagicOnion client for IAccountService | ||||||
|  | builder.Services.AddSingleton<IAccountService>(provider => | ||||||
|  | { | ||||||
|  |     var passServiceUrl = builder.Configuration["PassService:Url"]; | ||||||
|  |     if (string.IsNullOrEmpty(passServiceUrl)) | ||||||
|  |     { | ||||||
|  |         throw new InvalidOperationException("PassService:Url configuration is missing."); | ||||||
|  |     } | ||||||
|  |     var channel = GrpcChannel.ForAddress(passServiceUrl); | ||||||
|  |     return MagicOnionClient.Create<IAccountService>(channel); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Configure MagicOnion client for IPublisherService | ||||||
|  | builder.Services.AddSingleton<IPublisherService>(provider => | ||||||
|  | { | ||||||
|  |     var passServiceUrl = builder.Configuration["PassService:Url"]; | ||||||
|  |     if (string.IsNullOrEmpty(passServiceUrl)) | ||||||
|  |     { | ||||||
|  |         throw new InvalidOperationException("PassService:Url configuration is missing."); | ||||||
|  |     } | ||||||
|  |     var channel = GrpcChannel.ForAddress(passServiceUrl); | ||||||
|  |     return MagicOnionClient.Create<IPublisherService>(channel); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // Configure MagicOnion client for ICustomAppService | ||||||
|  | builder.Services.AddSingleton<ICustomAppService>(provider => | ||||||
|  | { | ||||||
|  |     var passServiceUrl = builder.Configuration["PassService:Url"]; | ||||||
|  |     if (string.IsNullOrEmpty(passServiceUrl)) | ||||||
|  |     { | ||||||
|  |         throw new InvalidOperationException("PassService:Url configuration is missing."); | ||||||
|  |     } | ||||||
|  |     var channel = GrpcChannel.ForAddress(passServiceUrl); | ||||||
|  |     return MagicOnionClient.Create<ICustomAppService>(channel); | ||||||
|  | }); | ||||||
|  |  | ||||||
| // Add file storage | // Add file storage | ||||||
| builder.Services.AddAppFileStorage(builder.Configuration); | builder.Services.AddAppFileStorage(builder.Configuration); | ||||||
|  |  | ||||||
| @@ -47,8 +86,8 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | |||||||
| // Configure application middleware pipeline | // Configure application middleware pipeline | ||||||
| app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); | app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); | ||||||
|  |  | ||||||
| // Map gRPC services | // Remove direct gRPC service mappings for Pass services | ||||||
| app.MapGrpcService<DysonNetwork.Sphere.Auth.AuthGrpcService>(); | // app.MapGrpcService<DysonNetwork.Pass.Auth.AuthGrpcService>(); | ||||||
| app.MapGrpcService<DysonNetwork.Sphere.Account.AccountGrpcService>(); | // app.MapGrpcService<DysonNetwork.Pass.Account.AccountGrpcService>(); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Realm; | using DysonNetwork.Sphere.Realm; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -17,7 +17,9 @@ public class PublisherController( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PublisherService ps, |     PublisherService ps, | ||||||
|     FileReferenceService fileRefService, |     FileReferenceService fileRefService, | ||||||
|     ActionLogService als) |     IAccountService accounts, | ||||||
|  |     IActionLogService als | ||||||
|  | ) | ||||||
|     : ControllerBase |     : ControllerBase | ||||||
| { | { | ||||||
|     [HttpGet("{name}")] |     [HttpGet("{name}")] | ||||||
| @@ -29,10 +31,7 @@ public class PublisherController( | |||||||
|         if (publisher is null) return NotFound(); |         if (publisher is null) return NotFound(); | ||||||
|         if (publisher.AccountId is null) return Ok(publisher); |         if (publisher.AccountId is null) return Ok(publisher); | ||||||
|  |  | ||||||
|         var account = await db.Accounts |         var account = await accounts.GetAccountById(publisher.AccountId.Value, true); | ||||||
|             .Where(a => a.Id == publisher.AccountId) |  | ||||||
|             .Include(a => a.Profile) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|         publisher.Account = account; |         publisher.Account = account; | ||||||
|  |  | ||||||
|         return Ok(publisher); |         return Ok(publisher); | ||||||
| @@ -80,7 +79,7 @@ public class PublisherController( | |||||||
|  |  | ||||||
|     public class PublisherMemberRequest |     public class PublisherMemberRequest | ||||||
|     { |     { | ||||||
|         [Required] public long RelatedUserId { get; set; } |         [Required] public Guid RelatedUserId { get; set; } | ||||||
|         [Required] public PublisherMemberRole Role { get; set; } |         [Required] public PublisherMemberRole Role { get; set; } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -92,7 +91,7 @@ public class PublisherController( | |||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|         var userId = currentUser.Id; |         var userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await accounts.GetAccountById(request.RelatedUserId); | ||||||
|         if (relatedUser is null) return BadRequest("Related user was not found"); |         if (relatedUser is null) return BadRequest("Related user was not found"); | ||||||
|  |  | ||||||
|         var publisher = await db.Publishers |         var publisher = await db.Publishers | ||||||
| @@ -113,13 +112,16 @@ public class PublisherController( | |||||||
|         db.PublisherMembers.Add(newMember); |         db.PublisherMembers.Add(newMember); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherMemberInvite, |             ActionLogType.PublisherMemberInvite, | ||||||
|             new Dictionary<string, object> |             new Dictionary<string, object> | ||||||
|             { |             { | ||||||
|                 { "publisher_id", publisher.Id }, |                 { "publisher_id", publisher.Id }, | ||||||
|                 { "account_id", relatedUser.Id } |                 { "account_id", relatedUser.Id } | ||||||
|             }, Request |             }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(newMember); |         return Ok(newMember); | ||||||
| @@ -143,9 +145,12 @@ public class PublisherController( | |||||||
|         db.Update(member); |         db.Update(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherMemberJoin, |             ActionLogType.PublisherMemberJoin, | ||||||
|             new Dictionary<string, object> { { "account_id", member.AccountId } }, Request |             new Dictionary<string, object> { { "account_id", member.AccountId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
| @@ -168,9 +173,12 @@ public class PublisherController( | |||||||
|         db.PublisherMembers.Remove(member); |         db.PublisherMembers.Remove(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherMemberLeave, |             ActionLogType.PublisherMemberLeave, | ||||||
|             new Dictionary<string, object> { { "account_id", member.AccountId } }, Request |             new Dictionary<string, object> { { "account_id", member.AccountId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -198,13 +206,16 @@ public class PublisherController( | |||||||
|         db.PublisherMembers.Remove(member); |         db.PublisherMembers.Remove(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherMemberKick, |             ActionLogType.PublisherMemberKick, | ||||||
|             new Dictionary<string, object> |             new Dictionary<string, object> | ||||||
|             { |             { | ||||||
|                 { "publisher_id", publisher.Id }, |                 { "publisher_id", publisher.Id }, | ||||||
|                 { "account_id", memberId } |                 { "account_id", memberId } | ||||||
|             }, Request |             }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -222,8 +233,9 @@ public class PublisherController( | |||||||
|  |  | ||||||
|     [HttpPost("individual")] |     [HttpPost("individual")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "publishers.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "publishers.create")] | ||||||
|     public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherIndividual([FromBody] PublisherRequest request) |     public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherIndividual( | ||||||
|  |         [FromBody] PublisherRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
| @@ -261,9 +273,12 @@ public class PublisherController( | |||||||
|             background |             background | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherCreate, |             ActionLogType.PublisherCreate, | ||||||
|             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request |             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(publisher); |         return Ok(publisher); | ||||||
| @@ -271,7 +286,7 @@ public class PublisherController( | |||||||
|  |  | ||||||
|     [HttpPost("organization/{realmSlug}")] |     [HttpPost("organization/{realmSlug}")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("global", "publishers.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "publishers.create")] | ||||||
|     public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherOrganization(string realmSlug, |     public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherOrganization(string realmSlug, | ||||||
|         [FromBody] PublisherRequest request) |         [FromBody] PublisherRequest request) | ||||||
|     { |     { | ||||||
| @@ -316,9 +331,12 @@ public class PublisherController( | |||||||
|             background |             background | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherCreate, |             ActionLogType.PublisherCreate, | ||||||
|             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request |             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(publisher); |         return Ok(publisher); | ||||||
| @@ -394,9 +412,12 @@ public class PublisherController( | |||||||
|         db.Update(publisher); |         db.Update(publisher); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherUpdate, |             ActionLogType.PublisherUpdate, | ||||||
|             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request |             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(publisher); |         return Ok(publisher); | ||||||
| @@ -432,9 +453,12 @@ public class PublisherController( | |||||||
|         db.Publishers.Remove(publisher); |         db.Publishers.Remove(publisher); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.PublisherDelete, |             ActionLogType.PublisherDelete, | ||||||
|             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request |             new Dictionary<string, object> { { "publisher_id", publisher.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -531,7 +555,7 @@ public class PublisherController( | |||||||
|  |  | ||||||
|     [HttpPost("{name}/features")] |     [HttpPost("{name}/features")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("maintenance", "publishers.features")] |     [DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "publishers.features")] | ||||||
|     public async Task<ActionResult<PublisherFeature>> AddPublisherFeature(string name, |     public async Task<ActionResult<PublisherFeature>> AddPublisherFeature(string name, | ||||||
|         [FromBody] PublisherFeatureRequest request) |         [FromBody] PublisherFeatureRequest request) | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Pass.Account; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Pass.Account; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -15,9 +16,10 @@ public class RealmController( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     RealmService rs, |     RealmService rs, | ||||||
|     FileReferenceService fileRefService, |     FileReferenceService fileRefService, | ||||||
|     RelationshipService rels, |     IRelationshipService rels, | ||||||
|     ActionLogService als, |     IActionLogService als, | ||||||
|     AccountEventService aes |     IAccountEventService aes, | ||||||
|  |     IAccountService accounts | ||||||
| ) : Controller | ) : Controller | ||||||
| { | { | ||||||
|     [HttpGet("{slug}")] |     [HttpGet("{slug}")] | ||||||
| @@ -79,7 +81,7 @@ public class RealmController( | |||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|         var userId = currentUser.Id; |         var userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await accounts.GetAccountById(request.RelatedUserId); | ||||||
|         if (relatedUser is null) return BadRequest("Related user was not found"); |         if (relatedUser is null) return BadRequest("Related user was not found"); | ||||||
|  |  | ||||||
|         if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) |         if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) | ||||||
| @@ -111,9 +113,12 @@ public class RealmController( | |||||||
|         db.RealmMembers.Add(member); |         db.RealmMembers.Add(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmInvite, |             ActionLogType.RealmInvite, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", member.AccountId } }, Request |             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", member.AccountId } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         member.Account = relatedUser; |         member.Account = relatedUser; | ||||||
| @@ -141,10 +146,12 @@ public class RealmController( | |||||||
|         db.Update(member); |         db.Update(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmJoin, |             ActionLogType.RealmJoin, | ||||||
|             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, |             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
| @@ -167,10 +174,12 @@ public class RealmController( | |||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmLeave, |             ActionLogType.RealmLeave, | ||||||
|             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, |             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -245,7 +254,6 @@ public class RealmController( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     [HttpGet("{slug}/members/me")] |     [HttpGet("{slug}/members/me")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug) |     public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug) | ||||||
| @@ -284,10 +292,12 @@ public class RealmController( | |||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmLeave, |             ActionLogType.RealmLeave, | ||||||
|             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, |             new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -349,9 +359,12 @@ public class RealmController( | |||||||
|         db.Realms.Add(realm); |         db.Realms.Add(realm); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmCreate, |             ActionLogType.RealmCreate, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id } }, Request |             new Dictionary<string, object> { { "realm_id", realm.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         var realmResourceId = $"realm:{realm.Id}"; |         var realmResourceId = $"realm:{realm.Id}"; | ||||||
| @@ -455,9 +468,12 @@ public class RealmController( | |||||||
|         db.Realms.Update(realm); |         db.Realms.Update(realm); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmUpdate, |             ActionLogType.RealmUpdate, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id } }, Request |             new Dictionary<string, object> { { "realm_id", realm.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(realm); |         return Ok(realm); | ||||||
| @@ -494,10 +510,12 @@ public class RealmController( | |||||||
|         db.RealmMembers.Add(member); |         db.RealmMembers.Add(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmJoin, |             ActionLogType.RealmJoin, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", currentUser.Id } }, |             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", currentUser.Id } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
| @@ -525,10 +543,12 @@ public class RealmController( | |||||||
|         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); |         member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.ChatroomKick, |             ActionLogType.ChatroomKick, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", memberId } }, |             new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", memberId } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
| @@ -559,11 +579,13 @@ public class RealmController( | |||||||
|         db.RealmMembers.Update(member); |         db.RealmMembers.Update(member); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmAdjustRole, |             ActionLogType.RealmAdjustRole, | ||||||
|             new Dictionary<string, object> |             new Dictionary<string, object> | ||||||
|                 { { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } }, |                 { { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } }, | ||||||
|             Request |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Ok(member); |         return Ok(member); | ||||||
| @@ -588,9 +610,12 @@ public class RealmController( | |||||||
|         db.Realms.Remove(realm); |         db.Realms.Remove(realm); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         als.CreateActionLogFromRequest( |         await als.CreateActionLogFromRequest( | ||||||
|             ActionLogType.RealmDelete, |             ActionLogType.RealmDelete, | ||||||
|             new Dictionary<string, object> { { "realm_id", realm.Id } }, Request |             new Dictionary<string, object> { { "realm_id", realm.Id } }, | ||||||
|  |             Request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|  |             Request.Headers.UserAgent.ToString(), | ||||||
|  |             currentUser | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         // Delete all file references for this realm |         // Delete all file references for this realm | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
|  | using DysonNetwork.Shared.Localization; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Realm; | namespace DysonNetwork.Sphere.Realm; | ||||||
|  |  | ||||||
| public class RealmService(AppDatabase db, NotificationService nty, IStringLocalizer<NotificationResource> localizer) | public class RealmService(AppDatabase db,     DysonNetwork.Shared.Services.INotificationService nty, IStringLocalizer<NotificationResource> localizer) | ||||||
| { | { | ||||||
|     public async Task SendInviteNotify(RealmMember member) |     public async Task SendInviteNotify(RealmMember member) | ||||||
|     { |     { | ||||||
|         AccountService.SetCultureInfo(member.Account); |         CultureInfoService.SetCultureInfo(member.Account); | ||||||
|         await nty.SendNotification( |         await nty.SendNotification( | ||||||
|             member.Account, |             member.Account, | ||||||
|             "invites.realms", |             "invites.realms", | ||||||
|   | |||||||
| @@ -1,105 +0,0 @@ | |||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Safety; |  | ||||||
|  |  | ||||||
| public class SafetyService(AppDatabase db, ILogger<SafetyService> logger) |  | ||||||
| { |  | ||||||
|     public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId) |  | ||||||
|     { |  | ||||||
|         // Check if a similar report already exists from this user |  | ||||||
|         var existingReport = await db.AbuseReports |  | ||||||
|             .Where(r => r.ResourceIdentifier == resourceIdentifier &&  |  | ||||||
|                        r.AccountId == accountId &&  |  | ||||||
|                        r.DeletedAt == null) |  | ||||||
|             .FirstOrDefaultAsync(); |  | ||||||
|  |  | ||||||
|         if (existingReport != null) |  | ||||||
|         { |  | ||||||
|             throw new InvalidOperationException("You have already reported this content."); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var report = new AbuseReport |  | ||||||
|         { |  | ||||||
|             ResourceIdentifier = resourceIdentifier, |  | ||||||
|             Type = type, |  | ||||||
|             Reason = reason, |  | ||||||
|             AccountId = accountId |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         db.AbuseReports.Add(report); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("New abuse report created: {ReportId} for resource {ResourceId}",  |  | ||||||
|             report.Id, resourceIdentifier); |  | ||||||
|  |  | ||||||
|         return report; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public async Task<int> CountReports(bool includeResolved = false) |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Where(r => includeResolved || r.ResolvedAt == null) |  | ||||||
|             .CountAsync(); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false) |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Where(r => r.AccountId == accountId) |  | ||||||
|             .Where(r => includeResolved || r.ResolvedAt == null) |  | ||||||
|             .CountAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false) |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Where(r => includeResolved || r.ResolvedAt == null) |  | ||||||
|             .OrderByDescending(r => r.CreatedAt) |  | ||||||
|             .Skip(skip) |  | ||||||
|             .Take(take) |  | ||||||
|             .Include(r => r.Account) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false) |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Where(r => r.AccountId == accountId) |  | ||||||
|             .Where(r => includeResolved || r.ResolvedAt == null) |  | ||||||
|             .OrderByDescending(r => r.CreatedAt) |  | ||||||
|             .Skip(skip) |  | ||||||
|             .Take(take) |  | ||||||
|             .ToListAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<AbuseReport?> GetReportById(Guid id) |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Include(r => r.Account) |  | ||||||
|             .FirstOrDefaultAsync(r => r.Id == id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<AbuseReport> ResolveReport(Guid id, string resolution) |  | ||||||
|     { |  | ||||||
|         var report = await db.AbuseReports.FindAsync(id); |  | ||||||
|         if (report == null) |  | ||||||
|         { |  | ||||||
|             throw new KeyNotFoundException("Report not found"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         report.ResolvedAt = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|         report.Resolution = resolution; |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|         return report; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async Task<int> GetPendingReportsCount() |  | ||||||
|     { |  | ||||||
|         return await db.AbuseReports |  | ||||||
|             .Where(r => r.ResolvedAt == null) |  | ||||||
|             .CountAsync(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -20,13 +20,15 @@ using NodaTime.Serialization.SystemTextJson; | |||||||
| using StackExchange.Redis; | using StackExchange.Redis; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Threading.RateLimiting; | using System.Threading.RateLimiting; | ||||||
|  | using DysonNetwork.Pass.Safety; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Sphere.Connection.WebReader; | using DysonNetwork.Sphere.Connection.WebReader; | ||||||
| using DysonNetwork.Sphere.Developer; | using DysonNetwork.Sphere.Developer; | ||||||
| using DysonNetwork.Sphere.Discovery; | using DysonNetwork.Sphere.Discovery; | ||||||
| using DysonNetwork.Sphere.Safety; |  | ||||||
| using DysonNetwork.Sphere.Wallet.PaymentHandlers; | using DysonNetwork.Sphere.Wallet.PaymentHandlers; | ||||||
| using tusdotnet.Stores; | using tusdotnet.Stores; | ||||||
|  | using DysonNetwork.Shared.Etcd; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Startup; | namespace DysonNetwork.Sphere.Startup; | ||||||
|  |  | ||||||
| @@ -187,7 +189,6 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<EmailService>(); |         services.AddScoped<EmailService>(); | ||||||
|         services.AddScoped<FileService>(); |         services.AddScoped<FileService>(); | ||||||
|         services.AddScoped<FileReferenceService>(); |         services.AddScoped<FileReferenceService>(); | ||||||
|         services.AddScoped<FileReferenceMigrationService>(); |  | ||||||
|         services.AddScoped<PublisherService>(); |         services.AddScoped<PublisherService>(); | ||||||
|         services.AddScoped<PublisherSubscriptionService>(); |         services.AddScoped<PublisherSubscriptionService>(); | ||||||
|         services.AddScoped<ActivityService>(); |         services.AddScoped<ActivityService>(); | ||||||
| @@ -207,6 +208,15 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<DiscoveryService>(); |         services.AddScoped<DiscoveryService>(); | ||||||
|         services.AddScoped<CustomAppService>(); |         services.AddScoped<CustomAppService>(); | ||||||
|  |  | ||||||
|  |         // Add MagicOnion services | ||||||
|  |         services.AddMagicOnionService<IAccountService>(); | ||||||
|  |         services.AddMagicOnionService<IAccountEventService>(); | ||||||
|  |         services.AddMagicOnionService<IAccountUsernameService>(); | ||||||
|  |         services.AddMagicOnionService<IActionLogService>(); | ||||||
|  |         services.AddMagicOnionService<IMagicSpellService>(); | ||||||
|  |         services.AddMagicOnionService<INotificationService>(); | ||||||
|  |         services.AddMagicOnionService<IRelationshipService>(); | ||||||
|  |          | ||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| using DysonNetwork.Sphere.Publisher; | using DysonNetwork.Sphere.Publisher; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| @@ -76,7 +76,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     [HttpPost] |     [HttpPost] | ||||||
|     [RequiredPermission("global", "stickers.packs.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "stickers.packs.create")] | ||||||
|     public async Task<ActionResult<StickerPack>> CreateStickerPack([FromBody] StickerPackRequest request) |     public async Task<ActionResult<StickerPack>> CreateStickerPack([FromBody] StickerPackRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
| @@ -271,7 +271,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa | |||||||
|     public const int MaxStickersPerPack = 24; |     public const int MaxStickersPerPack = 24; | ||||||
|  |  | ||||||
|     [HttpPost("{packId:guid}/content")] |     [HttpPost("{packId:guid}/content")] | ||||||
|     [RequiredPermission("global", "stickers.create")] |     [DysonNetwork.Shared.Permission.RequiredPermission("global", "stickers.create")] | ||||||
|     public async Task<IActionResult> CreateSticker(Guid packId, [FromBody] StickerRequest request) |     public async Task<IActionResult> CreateSticker(Guid packId, [FromBody] StickerRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -13,8 +13,7 @@ public class FileController( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     FileService fs, |     FileService fs, | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     IWebHostEnvironment env, |     IWebHostEnvironment env | ||||||
|     FileReferenceMigrationService rms |  | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|     [HttpGet("{id}")] |     [HttpGet("{id}")] | ||||||
| @@ -108,13 +107,4 @@ public class FileController( | |||||||
|  |  | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     [HttpPost("/maintenance/migrateReferences")] |  | ||||||
|     [Authorize] |  | ||||||
|     [RequiredPermission("maintenance", "files.references")] |  | ||||||
|     public async Task<ActionResult> MigrateFileReferences() |  | ||||||
|     { |  | ||||||
|         await rms.ScanAndMigrateReferences(); |  | ||||||
|         return Ok(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| @@ -1,340 +0,0 @@ | |||||||
| using DysonNetwork.Shared.Models; |  | ||||||
| using EFCore.BulkExtensions; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; |  | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Storage; |  | ||||||
|  |  | ||||||
| public class FileReferenceMigrationService(AppDatabase db) |  | ||||||
| { |  | ||||||
|     public async Task ScanAndMigrateReferences() |  | ||||||
|     { |  | ||||||
|         // Scan Posts for file references |  | ||||||
|         await ScanPosts(); |  | ||||||
|  |  | ||||||
|         // Scan Messages for file references |  | ||||||
|         await ScanMessages(); |  | ||||||
|  |  | ||||||
|         // Scan Profiles for file references |  | ||||||
|         await ScanProfiles(); |  | ||||||
|  |  | ||||||
|         // Scan Chat entities for file references |  | ||||||
|         await ScanChatRooms(); |  | ||||||
|  |  | ||||||
|         // Scan Realms for file references |  | ||||||
|         await ScanRealms(); |  | ||||||
|  |  | ||||||
|         // Scan Publishers for file references |  | ||||||
|         await ScanPublishers(); |  | ||||||
|  |  | ||||||
|         // Scan Stickers for file references |  | ||||||
|         await ScanStickers(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanPosts() |  | ||||||
|     { |  | ||||||
|         var posts = await db.Posts |  | ||||||
|             .Include(p => p.OutdatedAttachments) |  | ||||||
|             .Where(p => p.OutdatedAttachments.Any()) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var post in posts) |  | ||||||
|         { |  | ||||||
|             var updatedAttachments = new List<CloudFileReferenceObject>(); |  | ||||||
|  |  | ||||||
|             foreach (var attachment in post.OutdatedAttachments) |  | ||||||
|             { |  | ||||||
|                 var file = await db.Files.FirstOrDefaultAsync(f => f.Id == attachment.Id); |  | ||||||
|                 if (file != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = file.Id, |  | ||||||
|                         File = file, |  | ||||||
|                         Usage = "post", |  | ||||||
|                         ResourceId = post.ResourceIdentifier |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     updatedAttachments.Add(file.ToReferenceObject()); |  | ||||||
|                 } |  | ||||||
|                 else |  | ||||||
|                 { |  | ||||||
|                     // Keep the existing reference object if file not found |  | ||||||
|                     updatedAttachments.Add(attachment.ToReferenceObject()); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             post.Attachments = updatedAttachments; |  | ||||||
|             db.Posts.Update(post); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanMessages() |  | ||||||
|     { |  | ||||||
|         var messages = await db.ChatMessages |  | ||||||
|             .Include(m => m.OutdatedAttachments) |  | ||||||
|             .Where(m => m.OutdatedAttachments.Any()) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         var fileReferences = messages.SelectMany(message => message.OutdatedAttachments.Select(attachment => |  | ||||||
|             new CloudFileReference |  | ||||||
|             { |  | ||||||
|                 FileId = attachment.Id, |  | ||||||
|                 File = attachment, |  | ||||||
|                 Usage = "chat", |  | ||||||
|                 ResourceId = message.ResourceIdentifier, |  | ||||||
|                 CreatedAt = SystemClock.Instance.GetCurrentInstant(), |  | ||||||
|                 UpdatedAt = SystemClock.Instance.GetCurrentInstant() |  | ||||||
|             }) |  | ||||||
|         ).ToList(); |  | ||||||
|  |  | ||||||
|         foreach (var message in messages) |  | ||||||
|         { |  | ||||||
|             message.Attachments = message.OutdatedAttachments.Select(a => a.ToReferenceObject()).ToList(); |  | ||||||
|             db.ChatMessages.Update(message); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.BulkInsertAsync(fileReferences); |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanProfiles() |  | ||||||
|     { |  | ||||||
|         var profiles = await db.AccountProfiles |  | ||||||
|             .Where(p => p.PictureId != null || p.BackgroundId != null) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var profile in profiles) |  | ||||||
|         { |  | ||||||
|             if (profile is { PictureId: not null, Picture: null }) |  | ||||||
|             { |  | ||||||
|                 var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.PictureId); |  | ||||||
|                 if (avatarFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the avatar file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = avatarFile.Id, |  | ||||||
|                         File = avatarFile, |  | ||||||
|                         Usage = "profile.picture", |  | ||||||
|                         ResourceId = profile.Id.ToString() |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     profile.Picture = avatarFile.ToReferenceObject(); |  | ||||||
|                     db.AccountProfiles.Update(profile); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Also check for the banner if it exists |  | ||||||
|             if (profile is not { BackgroundId: not null, Background: null }) continue; |  | ||||||
|             var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.BackgroundId); |  | ||||||
|             if (bannerFile == null) continue; |  | ||||||
|             { |  | ||||||
|                 // Create a reference for the banner file |  | ||||||
|                 var reference = new CloudFileReference |  | ||||||
|                 { |  | ||||||
|                     FileId = bannerFile.Id, |  | ||||||
|                     File = bannerFile, |  | ||||||
|                     Usage = "profile.background", |  | ||||||
|                     ResourceId = profile.Id.ToString() |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 await db.FileReferences.AddAsync(reference); |  | ||||||
|                 profile.Background = bannerFile.ToReferenceObject(); |  | ||||||
|                 db.AccountProfiles.Update(profile); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanChatRooms() |  | ||||||
|     { |  | ||||||
|         var chatRooms = await db.ChatRooms |  | ||||||
|             .Where(c => c.PictureId != null || c.BackgroundId != null) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var chatRoom in chatRooms) |  | ||||||
|         { |  | ||||||
|             if (chatRoom is { PictureId: not null, Picture: null }) |  | ||||||
|             { |  | ||||||
|                 var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == chatRoom.PictureId); |  | ||||||
|                 if (avatarFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the avatar file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = avatarFile.Id, |  | ||||||
|                         File = avatarFile, |  | ||||||
|                         Usage = "chatroom.picture", |  | ||||||
|                         ResourceId = chatRoom.ResourceIdentifier |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     chatRoom.Picture = avatarFile.ToReferenceObject(); |  | ||||||
|                     db.ChatRooms.Update(chatRoom); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (chatRoom is not { BackgroundId: not null, Background: null }) continue; |  | ||||||
|             var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == chatRoom.BackgroundId); |  | ||||||
|             if (bannerFile == null) continue; |  | ||||||
|             { |  | ||||||
|                 // Create a reference for the banner file |  | ||||||
|                 var reference = new CloudFileReference |  | ||||||
|                 { |  | ||||||
|                     FileId = bannerFile.Id, |  | ||||||
|                     File = bannerFile, |  | ||||||
|                     Usage = "chatroom.background", |  | ||||||
|                     ResourceId = chatRoom.ResourceIdentifier |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 await db.FileReferences.AddAsync(reference); |  | ||||||
|                 chatRoom.Background = bannerFile.ToReferenceObject(); |  | ||||||
|                 db.ChatRooms.Update(chatRoom); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanRealms() |  | ||||||
|     { |  | ||||||
|         var realms = await db.Realms |  | ||||||
|             .Where(r => r.PictureId != null && r.BackgroundId != null) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var realm in realms) |  | ||||||
|         { |  | ||||||
|             // Process avatar if it exists |  | ||||||
|             if (realm is { PictureId: not null, Picture: null }) |  | ||||||
|             { |  | ||||||
|                 var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == realm.PictureId); |  | ||||||
|                 if (avatarFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the avatar file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = avatarFile.Id, |  | ||||||
|                         File = avatarFile, |  | ||||||
|                         Usage = "realm.picture", |  | ||||||
|                         ResourceId = realm.ResourceIdentifier |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     realm.Picture = avatarFile.ToReferenceObject(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Process banner if it exists |  | ||||||
|             if (realm is { BackgroundId: not null, Background: null }) |  | ||||||
|             { |  | ||||||
|                 var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == realm.BackgroundId); |  | ||||||
|                 if (bannerFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the banner file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = bannerFile.Id, |  | ||||||
|                         File = bannerFile, |  | ||||||
|                         Usage = "realm.background", |  | ||||||
|                         ResourceId = realm.ResourceIdentifier |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     realm.Background = bannerFile.ToReferenceObject(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             db.Realms.Update(realm); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanPublishers() |  | ||||||
|     { |  | ||||||
|         var publishers = await db.Publishers |  | ||||||
|             .Where(p => p.PictureId != null || p.BackgroundId != null) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var publisher in publishers) |  | ||||||
|         { |  | ||||||
|             if (publisher is { PictureId: not null, Picture: null }) |  | ||||||
|             { |  | ||||||
|                 var pictureFile = await db.Files.FirstOrDefaultAsync(f => f.Id == publisher.PictureId); |  | ||||||
|                 if (pictureFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the picture file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = pictureFile.Id, |  | ||||||
|                         File = pictureFile, |  | ||||||
|                         Usage = "publisher.picture", |  | ||||||
|                         ResourceId = publisher.Id.ToString() |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     publisher.Picture = pictureFile.ToReferenceObject(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (publisher is { BackgroundId: not null, Background: null }) |  | ||||||
|             { |  | ||||||
|                 var backgroundFile = await db.Files.FirstOrDefaultAsync(f => f.Id == publisher.BackgroundId); |  | ||||||
|                 if (backgroundFile != null) |  | ||||||
|                 { |  | ||||||
|                     // Create a reference for the background file |  | ||||||
|                     var reference = new CloudFileReference |  | ||||||
|                     { |  | ||||||
|                         FileId = backgroundFile.Id, |  | ||||||
|                         File = backgroundFile, |  | ||||||
|                         Usage = "publisher.background", |  | ||||||
|                         ResourceId = publisher.ResourceIdentifier |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     await db.FileReferences.AddAsync(reference); |  | ||||||
|                     publisher.Background = backgroundFile.ToReferenceObject(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             db.Publishers.Update(publisher); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async Task ScanStickers() |  | ||||||
|     { |  | ||||||
|         var stickers = await db.Stickers |  | ||||||
|             .Where(s => s.ImageId != null && s.Image == null) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         foreach (var sticker in stickers) |  | ||||||
|         { |  | ||||||
|             var imageFile = await db.Files.FirstOrDefaultAsync(f => f.Id == sticker.ImageId); |  | ||||||
|             if (imageFile != null) |  | ||||||
|             { |  | ||||||
|                 // Create a reference for the sticker image file |  | ||||||
|                 var reference = new CloudFileReference |  | ||||||
|                 { |  | ||||||
|                     FileId = imageFile.Id, |  | ||||||
|                     File = imageFile, |  | ||||||
|                     Usage = "sticker.image", |  | ||||||
|                     ResourceId = sticker.ResourceIdentifier |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 await db.FileReferences.AddAsync(reference); |  | ||||||
|                 sticker.Image = imageFile.ToReferenceObject(); |  | ||||||
|                 db.Stickers.Update(sticker); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         await db.SaveChangesAsync(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,4 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using EFCore.BulkExtensions; | using EFCore.BulkExtensions; | ||||||
| using Quartz; | using Quartz; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,24 +1,21 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Quartz; | using Quartz; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Storage.Handlers; | namespace DysonNetwork.Sphere.Storage.Handlers; | ||||||
|  |  | ||||||
| public class LastActiveInfo | public class LastActiveInfo | ||||||
| { | { | ||||||
|     public Session Session { get; set; } = null!; |     public Session Session { get; set; } = null!; | ||||||
|     public Shared.Models.Account Account { get; set; } = null!; |     public Account Account { get; set; } = null!; | ||||||
|     public Instant SeenAt { get; set; } |     public Instant SeenAt { get; set; } | ||||||
| } | } | ||||||
|  |  | ||||||
| public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<LastActiveInfo> | public class LastActiveFlushHandler(DysonNetwork.Shared.Services.IAccountService accounts, DysonNetwork.Shared.Services.IAccountProfileService profiles) : IFlushHandler<LastActiveInfo> | ||||||
| { | { | ||||||
|     public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items) |     public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items) | ||||||
|     { |     { | ||||||
|         using var scope = serviceProvider.CreateScope(); |  | ||||||
|         var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |  | ||||||
|  |  | ||||||
|         // Remove duplicates by grouping on (sessionId, accountId), taking the most recent SeenAt |         // Remove duplicates by grouping on (sessionId, accountId), taking the most recent SeenAt | ||||||
|         var distinctItems = items |         var distinctItems = items | ||||||
|             .GroupBy(x => (SessionId: x.Session.Id, AccountId: x.Account.Id)) |             .GroupBy(x => (SessionId: x.Session.Id, AccountId: x.Account.Id)) | ||||||
| @@ -36,19 +33,11 @@ public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHa | |||||||
|  |  | ||||||
|         // Update sessions using native EF Core ExecuteUpdateAsync |         // Update sessions using native EF Core ExecuteUpdateAsync | ||||||
|         foreach (var kvp in sessionIdMap) |         foreach (var kvp in sessionIdMap) | ||||||
|         { |             await accounts.UpdateSessionLastGrantedAt(kvp.Key, kvp.Value); | ||||||
|             await db.AuthSessions |  | ||||||
|                 .Where(s => s.Id == kvp.Key) |  | ||||||
|                 .ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, kvp.Value)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Update account profiles using native EF Core ExecuteUpdateAsync |         // Update account profiles using native EF Core ExecuteUpdateAsync | ||||||
|         foreach (var kvp in accountIdMap) |         foreach (var kvp in accountIdMap) | ||||||
|         { |             await accounts.UpdateAccountProfileLastSeenAt(kvp.Key, kvp.Value); | ||||||
|             await db.AccountProfiles |  | ||||||
|                 .Where(a => a.AccountId == kvp.Key) |  | ||||||
|                 .ExecuteUpdateAsync(a => a.SetProperty(x => x.LastSeenAt, kvp.Value)); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.Net; | using System.Net; | ||||||
| using System.Text; | using System.Text; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Pass.Permission; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.Extensions.Options; | using Microsoft.Extensions.Options; | ||||||
| using tusdotnet.Interfaces; | using tusdotnet.Interfaces; | ||||||
| @@ -10,7 +10,7 @@ using tusdotnet.Models.Configuration; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Storage; | namespace DysonNetwork.Sphere.Storage; | ||||||
|  |  | ||||||
| public abstract class TusService | public class TusService(DefaultTusConfiguration config, ITusStore store) | ||||||
| { | { | ||||||
|     public static DefaultTusConfiguration BuildConfiguration(ITusStore store) => new() |     public static DefaultTusConfiguration BuildConfiguration(ITusStore store) => new() | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Storage; | using Microsoft.EntityFrameworkCore.Storage; | ||||||
| @@ -12,7 +12,8 @@ namespace DysonNetwork.Sphere.Wallet; | |||||||
| public class PaymentService( | public class PaymentService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     WalletService wat, |     WalletService wat, | ||||||
|     NotificationService nty, |     INotificationService nty, | ||||||
|  |     IAccountService acc, | ||||||
|     IStringLocalizer<NotificationResource> localizer |     IStringLocalizer<NotificationResource> localizer | ||||||
| ) | ) | ||||||
| { | { | ||||||
| @@ -197,10 +198,10 @@ public class PaymentService( | |||||||
|     private async Task NotifyOrderPaid(Order order) |     private async Task NotifyOrderPaid(Order order) | ||||||
|     { |     { | ||||||
|         if (order.PayeeWallet is null) return; |         if (order.PayeeWallet is null) return; | ||||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId); |         var account = await acc.GetAccountById(order.PayeeWallet.AccountId); | ||||||
|         if (account is null) return; |         if (account is null) return; | ||||||
|          |          | ||||||
|         AccountService.SetCultureInfo(account); |         // AccountService.SetCultureInfo(account); | ||||||
|  |  | ||||||
|         // Due to ID is uuid, it longer than 8 words for sure |         // Due to ID is uuid, it longer than 8 words for sure | ||||||
|         var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; |         var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; | ||||||
|   | |||||||
| @@ -5,11 +5,14 @@ using Quartz; | |||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Wallet; | namespace DysonNetwork.Sphere.Wallet; | ||||||
|  |  | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  |  | ||||||
| public class SubscriptionRenewalJob( | public class SubscriptionRenewalJob( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     SubscriptionService subscriptionService, |     SubscriptionService subscriptionService, | ||||||
|     PaymentService paymentService, |     PaymentService paymentService, | ||||||
|     WalletService walletService, |     WalletService walletService, | ||||||
|  |     IAccountProfileService accountProfileService, | ||||||
|     ILogger<SubscriptionRenewalJob> logger |     ILogger<SubscriptionRenewalJob> logger | ||||||
| ) : IJob | ) : IJob | ||||||
| { | { | ||||||
| @@ -138,10 +141,7 @@ public class SubscriptionRenewalJob( | |||||||
|         logger.LogInformation("Validating user stellar memberships..."); |         logger.LogInformation("Validating user stellar memberships..."); | ||||||
|  |  | ||||||
|         // Get all account IDs with StellarMembership |         // Get all account IDs with StellarMembership | ||||||
|         var accountsWithMemberships = await db.AccountProfiles |         var accountsWithMemberships = await accountProfileService.GetAccountsWithStellarMembershipAsync(); | ||||||
|             .Where(a => a.StellarMembership != null) |  | ||||||
|             .Select(a => new { a.Id, a.StellarMembership }) |  | ||||||
|             .ToListAsync(); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Found {Count} accounts with stellar memberships to validate", |         logger.LogInformation("Found {Count} accounts with stellar memberships to validate", | ||||||
|             accountsWithMemberships.Count); |             accountsWithMemberships.Count); | ||||||
| @@ -187,11 +187,7 @@ public class SubscriptionRenewalJob( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Update all accounts in a single batch operation |         // Update all accounts in a single batch operation | ||||||
|         var updatedCount = await db.AccountProfiles |         var updatedCount = await accountProfileService.ClearStellarMembershipsAsync(accountIdsToUpdate); | ||||||
|             .Where(a => accountIdsToUpdate.Contains(a.Id)) |  | ||||||
|             .ExecuteUpdateAsync(s => s |  | ||||||
|                 .SetProperty(a => a.StellarMembership, p => null) |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Updated {Count} accounts with expired/invalid stellar memberships", updatedCount); |         logger.LogInformation("Updated {Count} accounts with expired/invalid stellar memberships", updatedCount); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Localization; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using DysonNetwork.Sphere.Wallet.PaymentHandlers; | using DysonNetwork.Sphere.Wallet.PaymentHandlers; | ||||||
| @@ -14,8 +15,9 @@ namespace DysonNetwork.Sphere.Wallet; | |||||||
| public class SubscriptionService( | public class SubscriptionService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PaymentService payment, |     PaymentService payment, | ||||||
|     AccountService accounts, |     IAccountService accounts, | ||||||
|     NotificationService nty, |     IAccountProfileService profiles, | ||||||
|  |     INotificationService nty, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
| @@ -23,7 +25,7 @@ public class SubscriptionService( | |||||||
| ) | ) | ||||||
| { | { | ||||||
|     public async Task<Subscription> CreateSubscriptionAsync( |     public async Task<Subscription> CreateSubscriptionAsync( | ||||||
|         Shared.Models.Account account, |         Account account, | ||||||
|         string identifier, |         string identifier, | ||||||
|         string paymentMethod, |         string paymentMethod, | ||||||
|         PaymentDetails paymentDetails, |         PaymentDetails paymentDetails, | ||||||
| @@ -57,9 +59,7 @@ public class SubscriptionService( | |||||||
|  |  | ||||||
|         if (subscriptionInfo.RequiredLevel > 0) |         if (subscriptionInfo.RequiredLevel > 0) | ||||||
|         { |         { | ||||||
|             var profile = await db.AccountProfiles |             var profile = await profiles.GetAccountProfileByIdAsync(account.Id); | ||||||
|                 .Where(p => p.AccountId == account.Id) |  | ||||||
|                 .FirstOrDefaultAsync(); |  | ||||||
|             if (profile is null) throw new InvalidOperationException("Account profile was not found."); |             if (profile is null) throw new InvalidOperationException("Account profile was not found."); | ||||||
|             if (profile.Level < subscriptionInfo.RequiredLevel) |             if (profile.Level < subscriptionInfo.RequiredLevel) | ||||||
|                 throw new InvalidOperationException( |                 throw new InvalidOperationException( | ||||||
| @@ -141,7 +141,7 @@ public class SubscriptionService( | |||||||
|         if (!string.IsNullOrEmpty(provider)) |         if (!string.IsNullOrEmpty(provider)) | ||||||
|             account = await accounts.LookupAccountByConnection(order.AccountId, provider); |             account = await accounts.LookupAccountByConnection(order.AccountId, provider); | ||||||
|         else if (Guid.TryParse(order.AccountId, out var accountId)) |         else if (Guid.TryParse(order.AccountId, out var accountId)) | ||||||
|             account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == accountId); |             account = await accounts.GetAccountById(accountId); | ||||||
|  |  | ||||||
|         if (account is null) |         if (account is null) | ||||||
|             throw new InvalidOperationException($"Account was not found with identifier {order.AccountId}"); |             throw new InvalidOperationException($"Account was not found with identifier {order.AccountId}"); | ||||||
| @@ -302,9 +302,7 @@ public class SubscriptionService( | |||||||
|  |  | ||||||
|         if (subscription.Identifier.StartsWith(SubscriptionType.StellarProgram)) |         if (subscription.Identifier.StartsWith(SubscriptionType.StellarProgram)) | ||||||
|         { |         { | ||||||
|             await db.AccountProfiles |             await profiles.UpdateStellarMembershipAsync(subscription.AccountId, subscription.ToReference()); | ||||||
|                 .Where(a => a.AccountId == subscription.AccountId) |  | ||||||
|                 .ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference())); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         await NotifySubscriptionBegun(subscription); |         await NotifySubscriptionBegun(subscription); | ||||||
| @@ -348,10 +346,10 @@ public class SubscriptionService( | |||||||
|  |  | ||||||
|     private async Task NotifySubscriptionBegun(Subscription subscription) |     private async Task NotifySubscriptionBegun(Subscription subscription) | ||||||
|     { |     { | ||||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId); |         var account = await accounts.GetAccountById(subscription.AccountId); | ||||||
|         if (account is null) return; |         if (account is null) return; | ||||||
|  |  | ||||||
|         AccountService.SetCultureInfo(account); |         CultureInfoService.SetCultureInfo(account); | ||||||
|  |  | ||||||
|         var humanReadableName = |         var humanReadableName = | ||||||
|             SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) |             SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Shared.Models; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Shared.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -75,7 +75,7 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p | |||||||
|  |  | ||||||
|     [HttpPost("balance")] |     [HttpPost("balance")] | ||||||
|     [Authorize] |     [Authorize] | ||||||
|     [RequiredPermission("maintenance", "wallets.balance.modify")] |     [DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "wallets.balance.modify")] | ||||||
|     public async Task<ActionResult<Transaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request) |     public async Task<ActionResult<Transaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request) | ||||||
|     { |     { | ||||||
|         var wallet = await ws.GetWalletAsync(request.AccountId); |         var wallet = await ws.GetWalletAsync(request.AccountId); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user