♻️ No idea, but errors all gone
This commit is contained in:
		@@ -1,30 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Account;
 | 
			
		||||
 | 
			
		||||
public enum AbuseReportType
 | 
			
		||||
{
 | 
			
		||||
    Copyright,
 | 
			
		||||
    Harassment,
 | 
			
		||||
    Impersonation,
 | 
			
		||||
    OffensiveContent,
 | 
			
		||||
    Spam,
 | 
			
		||||
    PrivacyViolation,
 | 
			
		||||
    IllegalContent,
 | 
			
		||||
    Other
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AbuseReport : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!;
 | 
			
		||||
    public AbuseReportType Type { get; set; }
 | 
			
		||||
    [MaxLength(8192)] public string Reason { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public Instant? ResolvedAt { get; set; }
 | 
			
		||||
    [MaxLength(8192)] public string? Resolution { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
    public Shared.Models.Account Account { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Pass.Auth;
 | 
			
		||||
using DysonNetwork.Pass.Permission;
 | 
			
		||||
using DysonNetwork.Shared.Permission;
 | 
			
		||||
using DysonNetwork.Shared.Models;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 EFCore.BulkExtensions;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
public class AccountService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    // MagicSpellService spells,
 | 
			
		||||
    // AccountUsernameService uname,
 | 
			
		||||
    // NotificationService nty,
 | 
			
		||||
    // EmailService mailer,
 | 
			
		||||
    // IStringLocalizer<NotificationResource> localizer,
 | 
			
		||||
    MagicSpellService spells,
 | 
			
		||||
    AccountUsernameService uname,
 | 
			
		||||
    NotificationService nty,
 | 
			
		||||
    // EmailService mailer, // Commented out for now
 | 
			
		||||
    IStringLocalizer<NotificationResource> localizer,
 | 
			
		||||
    ICacheService cache,
 | 
			
		||||
    ILogger<AccountService> logger
 | 
			
		||||
    ILogger<AccountService> logger,
 | 
			
		||||
    AuthService authService,
 | 
			
		||||
    ActionLogService actionLogService,
 | 
			
		||||
    RelationshipService relationshipService
 | 
			
		||||
) : ServiceBase<IAccountService>, IAccountService
 | 
			
		||||
{
 | 
			
		||||
    public static void SetCultureInfo(Shared.Models.Account account)
 | 
			
		||||
@@ -134,15 +143,15 @@ public class AccountService(
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // var spell = await spells.CreateMagicSpell(
 | 
			
		||||
                //     account,
 | 
			
		||||
                //     MagicSpellType.AccountActivation,
 | 
			
		||||
                //     new Dictionary<string, object>
 | 
			
		||||
                //     {
 | 
			
		||||
                //         { "contact_method", account.Contacts.First().Content }
 | 
			
		||||
                //     }
 | 
			
		||||
                // );
 | 
			
		||||
                // await spells.NotifyMagicSpell(spell, true);
 | 
			
		||||
                var spell = await spells.CreateMagicSpell(
 | 
			
		||||
                    account,
 | 
			
		||||
                    MagicSpellType.AccountActivation,
 | 
			
		||||
                    new Dictionary<string, object>
 | 
			
		||||
                    {
 | 
			
		||||
                        { "contact_method", account.Contacts.First().Content }
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                await spells.NotifyMagicSpell(spell, true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            db.Accounts.Add(account);
 | 
			
		||||
@@ -167,9 +176,7 @@ public class AccountService(
 | 
			
		||||
            ? userInfo.DisplayName
 | 
			
		||||
            : $"{userInfo.FirstName} {userInfo.LastName}".Trim();
 | 
			
		||||
 | 
			
		||||
        // Generate username from email
 | 
			
		||||
        // var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
 | 
			
		||||
        var username = userInfo.Email.Split('@')[0]; // Placeholder
 | 
			
		||||
        var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
 | 
			
		||||
 | 
			
		||||
        return await CreateAccount(
 | 
			
		||||
            username,
 | 
			
		||||
@@ -184,28 +191,26 @@ public class AccountService(
 | 
			
		||||
 | 
			
		||||
    public async Task RequestAccountDeletion(Shared.Models.Account account)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        // var spell = await spells.CreateMagicSpell(
 | 
			
		||||
        //     account,
 | 
			
		||||
        //     MagicSpellType.AccountRemoval,
 | 
			
		||||
        //     new Dictionary<string, object>(),
 | 
			
		||||
        //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
        //     preventRepeat: true
 | 
			
		||||
        // );
 | 
			
		||||
        // await spells.NotifyMagicSpell(spell);
 | 
			
		||||
        var spell = await spells.CreateMagicSpell(
 | 
			
		||||
            account,
 | 
			
		||||
            MagicSpellType.AccountRemoval,
 | 
			
		||||
            new Dictionary<string, object>(),
 | 
			
		||||
            SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
            preventRepeat: true
 | 
			
		||||
        );
 | 
			
		||||
        await spells.NotifyMagicSpell(spell);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RequestPasswordReset(Shared.Models.Account account)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        // var spell = await spells.CreateMagicSpell(
 | 
			
		||||
        //     account,
 | 
			
		||||
        //     MagicSpellType.AuthPasswordReset,
 | 
			
		||||
        //     new Dictionary<string, object>(),
 | 
			
		||||
        //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
        //     preventRepeat: true
 | 
			
		||||
        // );
 | 
			
		||||
        // await spells.NotifyMagicSpell(spell);
 | 
			
		||||
        var spell = await spells.CreateMagicSpell(
 | 
			
		||||
            account,
 | 
			
		||||
            MagicSpellType.AuthPasswordReset,
 | 
			
		||||
            new Dictionary<string, object>(),
 | 
			
		||||
            SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
            preventRepeat: true
 | 
			
		||||
        );
 | 
			
		||||
        await spells.NotifyMagicSpell(spell);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type)
 | 
			
		||||
@@ -331,7 +336,6 @@ public class AccountService(
 | 
			
		||||
    {
 | 
			
		||||
        var count = await db.AccountAuthFactors
 | 
			
		||||
            .Where(f => f.AccountId == factor.AccountId)
 | 
			
		||||
            // .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
 | 
			
		||||
            .CountAsync();
 | 
			
		||||
        if (count <= 1)
 | 
			
		||||
            throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
 | 
			
		||||
@@ -357,14 +361,14 @@ public class AccountService(
 | 
			
		||||
                if (await _GetFactorCode(factor) is not null)
 | 
			
		||||
                    throw new InvalidOperationException("A factor code has been sent and in active duration.");
 | 
			
		||||
 | 
			
		||||
                // await nty.SendNotification(
 | 
			
		||||
                //     account,
 | 
			
		||||
                //     "auth.verification",
 | 
			
		||||
                //     localizer["AuthCodeTitle"],
 | 
			
		||||
                //     null,
 | 
			
		||||
                //     localizer["AuthCodeBody", code],
 | 
			
		||||
                //     save: true
 | 
			
		||||
                // );
 | 
			
		||||
                await nty.SendNotification(
 | 
			
		||||
                    account,
 | 
			
		||||
                    "auth.verification",
 | 
			
		||||
                    localizer["AuthCodeTitle"],
 | 
			
		||||
                    null,
 | 
			
		||||
                    localizer["AuthCodeBody", code],
 | 
			
		||||
                    save: true
 | 
			
		||||
                );
 | 
			
		||||
                await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
 | 
			
		||||
                break;
 | 
			
		||||
            case AccountAuthFactorType.EmailCode:
 | 
			
		||||
@@ -399,11 +403,11 @@ public class AccountService(
 | 
			
		||||
                    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,
 | 
			
		||||
                //     contact.Content,
 | 
			
		||||
                //     localizer["VerificationEmail"],
 | 
			
		||||
                //     new VerificationEmailModel
 | 
			
		||||
                //     new DysonNetwork.Pass.Pages.Emails.VerificationEmailModel
 | 
			
		||||
                //     {
 | 
			
		||||
                //         Name = account.Name,
 | 
			
		||||
                //         Code = code
 | 
			
		||||
@@ -456,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
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
@@ -493,7 +497,7 @@ public class AccountService(
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        await db.AuthSessions
 | 
			
		||||
@@ -522,15 +526,14 @@ public class AccountService(
 | 
			
		||||
 | 
			
		||||
    public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact)
 | 
			
		||||
    {
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        // var spell = await spells.CreateMagicSpell(
 | 
			
		||||
        //     account,
 | 
			
		||||
        //     MagicSpellType.ContactVerification,
 | 
			
		||||
        //     new Dictionary<string, object> { { "contact_method", contact.Content } },
 | 
			
		||||
        //     expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
        //     preventRepeat: true
 | 
			
		||||
        // );
 | 
			
		||||
        // await spells.NotifyMagicSpell(spell);
 | 
			
		||||
        var spell = await spells.CreateMagicSpell(
 | 
			
		||||
            account,
 | 
			
		||||
            MagicSpellType.ContactVerification,
 | 
			
		||||
            new Dictionary<string, object> { { "contact_method", contact.Content } },
 | 
			
		||||
            expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
 | 
			
		||||
            preventRepeat: true
 | 
			
		||||
        );
 | 
			
		||||
        await spells.NotifyMagicSpell(spell);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact)
 | 
			
		||||
@@ -614,7 +617,7 @@ public class AccountService(
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            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)
 | 
			
		||||
                .FirstOrDefaultAsync();
 | 
			
		||||
            if (badge is null) throw new InvalidOperationException("Badge was not found.");
 | 
			
		||||
@@ -657,4 +660,246 @@ public class AccountService(
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
 | 
			
		||||
        Shared.Models.Account? account = null)
 | 
			
		||||
    public async Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Shared.Models.Account? account = null)
 | 
			
		||||
    {
 | 
			
		||||
        var log = new ActionLog
 | 
			
		||||
        {
 | 
			
		||||
            Action = action,
 | 
			
		||||
            Action = type,
 | 
			
		||||
            Meta = meta,
 | 
			
		||||
            UserAgent = request.Headers.UserAgent,
 | 
			
		||||
            IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
 | 
			
		||||
            // Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
 | 
			
		||||
            UserAgent = userAgent,
 | 
			
		||||
            IpAddress = ipAddress,
 | 
			
		||||
            // Location = geo.GetPointFromIp(ipAddress)
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
 | 
			
		||||
            log.AccountId = currentUser.Id;
 | 
			
		||||
        else if (account != null)
 | 
			
		||||
        if (account != null)
 | 
			
		||||
            log.AccountId = account.Id;
 | 
			
		||||
        else
 | 
			
		||||
            throw new ArgumentException("No user context was found");
 | 
			
		||||
        
 | 
			
		||||
        if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
 | 
			
		||||
            log.SessionId = currentSession.Id;
 | 
			
		||||
        // For MagicOnion, HttpContext.Items["CurrentSession"] is not directly available.
 | 
			
		||||
        // 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);
 | 
			
		||||
        return log;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -70,6 +70,11 @@ public class MagicSpellService(
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
        var contact = await db.AccountContacts
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Shared.Models;
 | 
			
		||||
using DysonNetwork.Pass.Auth;
 | 
			
		||||
using DysonNetwork.Pass.Permission;
 | 
			
		||||
using DysonNetwork.Shared.Permission;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
@@ -141,7 +141,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
 | 
			
		||||
    
 | 
			
		||||
    [HttpPost("send")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [RequiredPermission("global", "notifications.send")]
 | 
			
		||||
    [DysonNetwork.Shared.Permission.RequiredPermission("global", "notifications.send")]
 | 
			
		||||
    public async Task<ActionResult> SendNotification(
 | 
			
		||||
        [FromBody] NotificationWithAimRequest request,
 | 
			
		||||
        [FromQuery] bool save = false
 | 
			
		||||
 
 | 
			
		||||
@@ -155,18 +155,22 @@ public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceB
 | 
			
		||||
        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 friends = await cache.GetAsync<List<Guid>>(cacheKey);
 | 
			
		||||
        var friends = await cache.GetAsync<List<Shared.Models.Account>>(cacheKey);
 | 
			
		||||
        
 | 
			
		||||
        if (friends == null)
 | 
			
		||||
        {
 | 
			
		||||
            friends = await db.AccountRelationships
 | 
			
		||||
            var friendIds = await db.AccountRelationships
 | 
			
		||||
                .Where(r => r.RelatedId == account.Id)
 | 
			
		||||
                .Where(r => r.Status == RelationshipStatus.Friends)
 | 
			
		||||
                .Select(r => r.AccountId)
 | 
			
		||||
                .ToListAsync();
 | 
			
		||||
 | 
			
		||||
            friends = await db.Accounts
 | 
			
		||||
                .Where(a => friendIds.Contains(a.Id))
 | 
			
		||||
                .ToListAsync();
 | 
			
		||||
                
 | 
			
		||||
            await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user