♻️ 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