♻️ 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));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,13 @@ public class AppDatabase(
 | 
			
		||||
    public DbSet<MagicSpell> MagicSpells { 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)
 | 
			
		||||
    {
 | 
			
		||||
        optionsBuilder.UseNpgsql(
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ namespace DysonNetwork.Pass.Auth;
 | 
			
		||||
 | 
			
		||||
public class AuthService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    IConfiguration config
 | 
			
		||||
    // IHttpClientFactory httpClientFactory,
 | 
			
		||||
    IConfiguration config,
 | 
			
		||||
    IHttpClientFactory httpClientFactory
 | 
			
		||||
    // IHttpContextAccessor httpContextAccessor,
 | 
			
		||||
    // ICacheService cache
 | 
			
		||||
)
 | 
			
		||||
@@ -108,53 +108,53 @@ public class AuthService(
 | 
			
		||||
        await Task.CompletedTask;
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(token)) return false;
 | 
			
		||||
 | 
			
		||||
        // var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
 | 
			
		||||
        // var apiSecret = config.GetSection("Captcha")["ApiSecret"];
 | 
			
		||||
        var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
 | 
			
		||||
        var apiSecret = config.GetSection("Captcha")["ApiSecret"];
 | 
			
		||||
 | 
			
		||||
        // var client = httpClientFactory.CreateClient();
 | 
			
		||||
        var client = httpClientFactory.CreateClient();
 | 
			
		||||
 | 
			
		||||
        // var jsonOpts = new JsonSerializerOptions
 | 
			
		||||
        // {
 | 
			
		||||
        //     PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
 | 
			
		||||
        //     DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
 | 
			
		||||
        // };
 | 
			
		||||
        var jsonOpts = new JsonSerializerOptions
 | 
			
		||||
        {
 | 
			
		||||
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
 | 
			
		||||
            DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // switch (provider)
 | 
			
		||||
        // {
 | 
			
		||||
        //     case "cloudflare":
 | 
			
		||||
        //         var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
        //             "application/x-www-form-urlencoded");
 | 
			
		||||
        //         var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
 | 
			
		||||
        //             content);
 | 
			
		||||
        //         response.EnsureSuccessStatusCode();
 | 
			
		||||
        switch (provider)
 | 
			
		||||
        {
 | 
			
		||||
            case "cloudflare":
 | 
			
		||||
                var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
                    "application/x-www-form-urlencoded");
 | 
			
		||||
                var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
 | 
			
		||||
                    content);
 | 
			
		||||
                response.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
        //         var json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
        //         var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
                var json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
                var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
 | 
			
		||||
        //         return result?.Success == true;
 | 
			
		||||
        //     case "google":
 | 
			
		||||
        //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
        //             "application/x-www-form-urlencoded");
 | 
			
		||||
        //         response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
 | 
			
		||||
        //         response.EnsureSuccessStatusCode();
 | 
			
		||||
                return result?.Success == true;
 | 
			
		||||
            case "google":
 | 
			
		||||
                content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
                    "application/x-www-form-urlencoded");
 | 
			
		||||
                response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
 | 
			
		||||
                response.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
        //         json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
        //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
                json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
                result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
 | 
			
		||||
        //         return result?.Success == true;
 | 
			
		||||
        //     case "hcaptcha":
 | 
			
		||||
        //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
        //             "application/x-www-form-urlencoded");
 | 
			
		||||
        //         response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
 | 
			
		||||
        //         response.EnsureSuccessStatusCode();
 | 
			
		||||
                return result?.Success == true;
 | 
			
		||||
            case "hcaptcha":
 | 
			
		||||
                content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
 | 
			
		||||
                    "application/x-www-form-urlencoded");
 | 
			
		||||
                response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
 | 
			
		||||
                response.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
        //         json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
        //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
                json = await response.Content.ReadAsStringAsync();
 | 
			
		||||
                result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
 | 
			
		||||
 | 
			
		||||
        //         return result?.Success == true;
 | 
			
		||||
        //     default:
 | 
			
		||||
        //         throw new ArgumentException("The server misconfigured for the captcha.");
 | 
			
		||||
        // }
 | 
			
		||||
                return result?.Success == true;
 | 
			
		||||
            default:
 | 
			
		||||
                throw new ArgumentException("The server misconfigured for the captcha.");
 | 
			
		||||
        }
 | 
			
		||||
        return true; // Placeholder for captcha validation
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.Shared.Localization
 | 
			
		||||
@using Microsoft.Extensions.Localization
 | 
			
		||||
 | 
			
		||||
<EmailLayout>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
@using DysonNetwork.Pass.Localization
 | 
			
		||||
@using DysonNetwork.Shared.Localization
 | 
			
		||||
@using Microsoft.Extensions.Localization
 | 
			
		||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
 | 
			
		||||
 | 
			
		||||
<EmailLayout>
 | 
			
		||||
    <tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
@using DysonNetwork.Pass.Localization
 | 
			
		||||
@using DysonNetwork.Shared.Localization
 | 
			
		||||
@using Microsoft.Extensions.Localization
 | 
			
		||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
 | 
			
		||||
 | 
			
		||||
<EmailLayout>
 | 
			
		||||
    <tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
@using DysonNetwork.Pass.Localization
 | 
			
		||||
@using DysonNetwork.Shared.Localization
 | 
			
		||||
@using Microsoft.Extensions.Localization
 | 
			
		||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
 | 
			
		||||
 | 
			
		||||
<EmailLayout>
 | 
			
		||||
    <tr>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
@using DysonNetwork.Pass.Localization
 | 
			
		||||
@using DysonNetwork.Shared.Localization
 | 
			
		||||
@using Microsoft.Extensions.Localization
 | 
			
		||||
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
 | 
			
		||||
 | 
			
		||||
<EmailLayout>
 | 
			
		||||
    <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);
 | 
			
		||||
    } 
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using MagicOnion.Server;
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
@@ -24,6 +25,8 @@ builder.Services.AddDbContext<AppDatabase>(options =>
 | 
			
		||||
 | 
			
		||||
builder.Services.AddScoped<AccountService>();
 | 
			
		||||
builder.Services.AddScoped<AuthService>();
 | 
			
		||||
builder.Services.AddScoped<DysonNetwork.Pass.Publisher.PublisherService>();
 | 
			
		||||
builder.Services.AddScoped<DysonNetwork.Pass.Developer.CustomAppService>();
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								DysonNetwork.Pass/Safety/AbuseReportController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								DysonNetwork.Pass/Safety/AbuseReportController.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Shared.Models;
 | 
			
		||||
using DysonNetwork.Shared.Permission;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Safety;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/safety/reports")]
 | 
			
		||||
public class AbuseReportController(
 | 
			
		||||
    SafetyService safety
 | 
			
		||||
) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    public class CreateReportRequest
 | 
			
		||||
    {
 | 
			
		||||
        [Required] public string ResourceIdentifier { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
        [Required] public AbuseReportType Type { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Required]
 | 
			
		||||
        [MinLength(10)]
 | 
			
		||||
        [MaxLength(1000)]
 | 
			
		||||
        public string Reason { get; set; } = null!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    public async Task<ActionResult<AbuseReport>> CreateReport([FromBody] CreateReportRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var report = await safety.CreateReport(
 | 
			
		||||
                request.ResourceIdentifier,
 | 
			
		||||
                request.Type,
 | 
			
		||||
                request.Reason,
 | 
			
		||||
                currentUser.Id
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return Ok(report);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [RequiredPermission("safety", "reports.view")]
 | 
			
		||||
    [ProducesResponseType<List<AbuseReport>>(StatusCodes.Status200OK)]
 | 
			
		||||
    public async Task<ActionResult<List<AbuseReport>>> GetReports(
 | 
			
		||||
        [FromQuery] int offset = 0,
 | 
			
		||||
        [FromQuery] int take = 20,
 | 
			
		||||
        [FromQuery] bool includeResolved = false
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        var totalCount = await safety.CountReports(includeResolved);
 | 
			
		||||
        var reports = await safety.GetReports(offset, take, includeResolved);
 | 
			
		||||
        Response.Headers["X-Total"] = totalCount.ToString();
 | 
			
		||||
        return Ok(reports);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("me")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [ProducesResponseType<List<AbuseReport>>(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
    public async Task<ActionResult<List<AbuseReport>>> GetMyReports(
 | 
			
		||||
        [FromQuery] int offset = 0,
 | 
			
		||||
        [FromQuery] int take = 20,
 | 
			
		||||
        [FromQuery] bool includeResolved = false
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved);
 | 
			
		||||
        var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved);
 | 
			
		||||
        Response.Headers["X-Total"] = totalCount.ToString();
 | 
			
		||||
        return Ok(reports);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{id}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
 | 
			
		||||
    [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    public async Task<ActionResult<AbuseReport>> GetReportById(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        var report = await safety.GetReportById(id);
 | 
			
		||||
        return report == null ? NotFound() : Ok(report);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("me/{id}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 | 
			
		||||
    public async Task<ActionResult<AbuseReport>> GetMyReportById(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var report = await safety.GetReportById(id);
 | 
			
		||||
        if (report == null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        // Ensure the user only accesses their own reports
 | 
			
		||||
        if (report.AccountId != currentUser.Id) return Forbid();
 | 
			
		||||
 | 
			
		||||
        return Ok(report);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class ResolveReportRequest
 | 
			
		||||
    {
 | 
			
		||||
        [Required]
 | 
			
		||||
        [MinLength(5)]
 | 
			
		||||
        [MaxLength(1000)]
 | 
			
		||||
        public string Resolution { get; set; } = null!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{id}/resolve")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.resolve")]
 | 
			
		||||
    [ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status404NotFound)]
 | 
			
		||||
    public async Task<ActionResult<AbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var report = await safety.ResolveReport(id, request.Resolution);
 | 
			
		||||
            return Ok(report);
 | 
			
		||||
        }
 | 
			
		||||
        catch (KeyNotFoundException)
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("count")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
 | 
			
		||||
    [ProducesResponseType<object>(StatusCodes.Status200OK)]
 | 
			
		||||
    public async Task<ActionResult<object>> GetReportsCount()
 | 
			
		||||
    {
 | 
			
		||||
        var count = await safety.GetPendingReportsCount();
 | 
			
		||||
        return Ok(new { pendingCount = count });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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<IMagicSpellService, MagicSpellService>();
 | 
			
		||||
        services.AddScoped<IAccountEventService, AccountEventService>();
 | 
			
		||||
        services.AddScoped<IAccountProfileService, AccountProfileService>();
 | 
			
		||||
 | 
			
		||||
        // Register OIDC services
 | 
			
		||||
        services.AddScoped<OidcService, GoogleOidcService>();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user