♻️ No idea, but errors all gone
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Safety;
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
[ApiController]
|
||||
[Route("/safety/reports")]
|
||||
@@ -85,7 +86,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AbuseReport>> GetReportById(Guid id)
|
||||
@@ -122,7 +123,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpPost("{id}/resolve")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("safety", "reports.resolve")]
|
||||
[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)
|
||||
@@ -144,7 +145,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpGet("count")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<object>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<object>> GetReportsCount()
|
||||
{
|
||||
61
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
61
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
public class SafetyService(AppDatabase db, IAccountService accountService, ILogger<SafetyService> logger)
|
||||
{
|
||||
public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId)
|
||||
{
|
||||
// Check if a similar report already exists from this user
|
||||
var existingReport = await db.AbuseReports
|
||||
.Where(r => r.ResourceIdentifier == resourceIdentifier &&
|
||||
r.AccountId == accountId &&
|
||||
r.DeletedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
throw new InvalidOperationException("You have already reported this content.");
|
||||
}
|
||||
|
||||
return await accountService.CreateAbuseReport(resourceIdentifier, type, reason, accountId);
|
||||
}
|
||||
|
||||
public async Task<int> CountReports(bool includeResolved = false)
|
||||
{
|
||||
return await accountService.CountAbuseReports(includeResolved);
|
||||
}
|
||||
|
||||
public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.CountUserAbuseReports(accountId, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.GetAbuseReports(skip, take, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.GetUserAbuseReports(accountId, skip, take, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport?> GetReportById(Guid id)
|
||||
{
|
||||
return await accountService.GetAbuseReport(id);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> ResolveReport(Guid id, string resolution)
|
||||
{
|
||||
return await accountService.ResolveAbuseReport(id, resolution);
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingReportsCount()
|
||||
{
|
||||
return await accountService.GetPendingAbuseReportsCount();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IAccountUsernameService, AccountUsernameService>();
|
||||
services.AddScoped<IMagicSpellService, MagicSpellService>();
|
||||
services.AddScoped<IAccountEventService, AccountEventService>();
|
||||
services.AddScoped<IAccountProfileService, AccountProfileService>();
|
||||
|
||||
// Register OIDC services
|
||||
services.AddScoped<OidcService, GoogleOidcService>();
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
18
DysonNetwork.Shared/Localization/CultureInfoService.cs
Normal file
18
DysonNetwork.Shared/Localization/CultureInfoService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public abstract class CultureInfoService
|
||||
{
|
||||
public static void SetCultureInfo(Shared.Models.Account account)
|
||||
{
|
||||
SetCultureInfo(account.Language);
|
||||
}
|
||||
|
||||
public static void SetCultureInfo(string? languageCode)
|
||||
{
|
||||
var info = new CultureInfo(languageCode ?? "en-us", false);
|
||||
CultureInfo.CurrentCulture = info;
|
||||
CultureInfo.CurrentUICulture = info;
|
||||
}
|
||||
}
|
||||
5
DysonNetwork.Shared/Localization/EmailResource.cs
Normal file
5
DysonNetwork.Shared/Localization/EmailResource.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public class EmailResource
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
public enum AbuseReportType
|
||||
{
|
||||
@@ -5,10 +5,14 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Grpc.Core;
|
||||
using MagicOnion;
|
||||
using MagicOnion.Server.Hubs;
|
||||
|
||||
namespace DysonNetwork.Shared.Permission;
|
||||
|
||||
public class MagicOnionPermissionFilter : IMagicOnionFilter<RequiredPermissionAttribute>
|
||||
public class MagicOnionPermissionFilter : IMagicOnionServiceFilter
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
@@ -17,9 +21,20 @@ public class MagicOnionPermissionFilter : IMagicOnionFilter<RequiredPermissionAt
|
||||
_permissionService = permissionService;
|
||||
}
|
||||
|
||||
public async ValueTask Invoke(ServiceContext context, RequiredPermissionAttribute attribute, Func<ServiceContext, ValueTask> next)
|
||||
public async ValueTask Invoke(ServiceContext context, Func<ServiceContext, ValueTask> next)
|
||||
{
|
||||
var httpContext = context.GetHttpContext();
|
||||
var attribute = context.MethodInfo.GetCustomAttribute<RequiredPermissionAttribute>();
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
// If no RequiredPermissionAttribute is present, just continue
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Correct way to get HttpContext from ServiceContext
|
||||
var httpContext = context.CallContext.GetHttpContext();
|
||||
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("HttpContext is not available in ServiceContext.");
|
||||
@@ -27,7 +42,7 @@ public class MagicOnionPermissionFilter : IMagicOnionFilter<RequiredPermissionAt
|
||||
|
||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
{
|
||||
throw new ReturnStatusException(MagicOnion.Grpc.StatusCode.PermissionDenied, "Unauthorized: Current user not found.");
|
||||
throw new ReturnStatusException(StatusCode.PermissionDenied, "Unauthorized: Current user not found.");
|
||||
}
|
||||
|
||||
if (currentUser.IsSuperuser)
|
||||
@@ -36,12 +51,11 @@ public class MagicOnionPermissionFilter : IMagicOnionFilter<RequiredPermissionAt
|
||||
return;
|
||||
}
|
||||
|
||||
var actor = $"user:{currentUser.Id}";
|
||||
var hasPermission = await _permissionService.CheckPermission(actor, attribute.Scope, attribute.Permission);
|
||||
var hasPermission = await _permissionService.CheckPermission(attribute.Scope, attribute.Permission);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
throw new ReturnStatusException(MagicOnion.Grpc.StatusCode.PermissionDenied, $"Permission {attribute.Scope}/{attribute.Permission} was required.");
|
||||
throw new ReturnStatusException(StatusCode.PermissionDenied, $"Permission {attribute.Scope}/{attribute.Permission} was required.");
|
||||
}
|
||||
|
||||
await next(context);
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
using System;
|
||||
using MagicOnion.Server.Filters;
|
||||
|
||||
namespace DysonNetwork.Shared.Permission;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||
public class RequiredPermissionAttribute : MagicOnionFilterAttribute
|
||||
public class RequiredPermissionAttribute : Attribute
|
||||
{
|
||||
public string Scope { get; }
|
||||
public string Permission { get; }
|
||||
|
||||
public RequiredPermissionAttribute(string scope, string permission) : base(typeof(MagicOnionPermissionFilter))
|
||||
public RequiredPermissionAttribute(string scope, string permission)
|
||||
{
|
||||
Scope = scope;
|
||||
Permission = permission;
|
||||
Order = 999; // Ensure this runs after authentication filters
|
||||
}
|
||||
}
|
||||
78
DysonNetwork.Shared/Services/EmailService.cs
Normal file
78
DysonNetwork.Shared/Services/EmailService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(
|
||||
string toName,
|
||||
string toEmail,
|
||||
string subject,
|
||||
string body,
|
||||
Dictionary<string, string>? headers = null
|
||||
)
|
||||
{
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(
|
||||
_configuration["Email:SenderName"],
|
||||
_configuration["Email:SenderEmail"]
|
||||
));
|
||||
message.To.Add(new MailboxAddress(toName, toEmail));
|
||||
message.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder { HtmlBody = body };
|
||||
message.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
message.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
using var client = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(
|
||||
_configuration["Email:SmtpHost"],
|
||||
int.Parse(_configuration["Email:SmtpPort"]),
|
||||
MailKit.Security.SecureSocketOptions.StartTls
|
||||
);
|
||||
await client.AuthenticateAsync(
|
||||
_configuration["Email:SmtpUser"],
|
||||
_configuration["Email:SmtpPass"]
|
||||
);
|
||||
await client.SendAsync(message);
|
||||
_logger.LogInformation("Email sent to {ToEmail}", toEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send email to {ToEmail}", toEmail);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendTemplatedEmailAsync<T>(string toName, string toEmail, string subject, string htmlBody)
|
||||
{
|
||||
await SendEmailAsync(toName, toEmail, subject, htmlBody);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using NodaTime;
|
||||
@@ -10,17 +11,22 @@ public interface IAccountEventService : IService<IAccountEventService>
|
||||
/// Purges the status cache for a user
|
||||
/// </summary>
|
||||
void PurgeStatusCache(Guid userId);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a user
|
||||
/// </summary>
|
||||
Task<Status> GetStatus(Guid userId);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statuses of a list of users
|
||||
/// </summary>
|
||||
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a daily check-in for a user
|
||||
/// </summary>
|
||||
Task<CheckInResult> CheckInDaily(Account user);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the check-in streak for a user
|
||||
/// </summary>
|
||||
|
||||
54
DysonNetwork.Shared/Services/IAccountProfileService.cs
Normal file
54
DysonNetwork.Shared/Services/IAccountProfileService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services
|
||||
{
|
||||
public interface IAccountProfileService : IService<IAccountProfileService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an account profile by account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account profile if found, otherwise null.</returns>
|
||||
Task<Profile?> GetAccountProfileByIdAsync(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the StellarMembership of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="subscription">The subscription to set as the StellarMembership.</param>
|
||||
/// <returns>The updated account profile.</returns>
|
||||
Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all account profiles that have a non-null StellarMembership.
|
||||
/// </summary>
|
||||
/// <returns>A list of account profiles with StellarMembership.</returns>
|
||||
Task<List<Profile>> GetAccountsWithStellarMembershipAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the StellarMembership for a list of account IDs.
|
||||
/// </summary>
|
||||
/// <param name="accountIds">The list of account IDs for which to clear the StellarMembership.</param>
|
||||
/// <returns>The number of accounts updated.</returns>
|
||||
Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the profile picture of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="picture">The new profile picture reference object.</param>
|
||||
/// <returns>The updated profile.</returns>
|
||||
Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the profile background of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="background">The new profile background reference object.</param>
|
||||
/// <returns>The updated profile.</returns>
|
||||
Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
@@ -59,4 +62,247 @@ public interface IAccountService : IService<IAccountService>
|
||||
/// <param name="userInfo">The OpenID Connect user information</param>
|
||||
/// <returns>The newly created account</returns>
|
||||
Task<Account> CreateAccount(OidcUserInfo userInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account by its ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="withProfile">Join the profile table or not.</param>
|
||||
/// <returns>The account if found, otherwise null.</returns>
|
||||
Task<Account?> GetAccountById(Guid accountId, bool withProfile = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account profile by account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account profile if found, otherwise null.</returns>
|
||||
Task<Profile?> GetAccountProfile(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication challenge by its ID.
|
||||
/// </summary>
|
||||
/// <param name="challengeId">The ID of the challenge.</param>
|
||||
/// <returns>The authentication challenge if found, otherwise null.</returns>
|
||||
Task<Challenge?> GetAuthChallenge(Guid challengeId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication challenge by account ID, IP address, and user agent.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="ipAddress">The IP address.</param>
|
||||
/// <param name="userAgent">The user agent.</param>
|
||||
/// <param name="now">The current instant.</param>
|
||||
/// <returns>The authentication challenge if found, otherwise null.</returns>
|
||||
Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, NodaTime.Instant now);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new authentication challenge.
|
||||
/// </summary>
|
||||
/// <param name="challenge">The challenge to create.</param>
|
||||
/// <returns>The created challenge.</returns>
|
||||
Task<Challenge> CreateAuthChallenge(Challenge challenge);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account authentication factor by its ID and account ID.
|
||||
/// </summary>
|
||||
/// <param name="factorId">The ID of the factor.</param>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account authentication factor if found, otherwise null.</returns>
|
||||
Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of account authentication factors for a given account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>A list of account authentication factors.</returns>
|
||||
Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication session by its ID.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The ID of the session.</param>
|
||||
/// <returns>The authentication session if found, otherwise null.</returns>
|
||||
Task<Session?> GetAuthSession(Guid sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its ID.
|
||||
/// </summary>
|
||||
/// <param name="spellId">The ID of the magic spell.</param>
|
||||
/// <returns>The magic spell if found, otherwise null.</returns>
|
||||
Task<MagicSpell?> GetMagicSpell(Guid spellId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an abuse report by its ID.
|
||||
/// </summary>
|
||||
/// <param name="reportId">The ID of the abuse report.</param>
|
||||
/// <returns>The abuse report if found, otherwise null.</returns>
|
||||
Task<AbuseReport?> GetAbuseReport(Guid reportId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new abuse report.
|
||||
/// </summary>
|
||||
/// <param name="resourceIdentifier">The identifier of the resource being reported.</param>
|
||||
/// <param name="type">The type of abuse report.</param>
|
||||
/// <param name="reason">The reason for the report.</param>
|
||||
/// <param name="accountId">The ID of the account making the report.</param>
|
||||
/// <returns>The created abuse report.</returns>
|
||||
Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Counts abuse reports.
|
||||
/// </summary>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>The count of abuse reports.</returns>
|
||||
Task<int> CountAbuseReports(bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Counts abuse reports by a specific user.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>The count of abuse reports by the user.</returns>
|
||||
Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of abuse reports.
|
||||
/// </summary>
|
||||
/// <param name="skip">Number of reports to skip.</param>
|
||||
/// <param name="take">Number of reports to take.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>A list of abuse reports.</returns>
|
||||
Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of abuse reports by a specific user.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="skip">Number of reports to skip.</param>
|
||||
/// <param name="take">Number of reports to take.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>A list of abuse reports by the user.</returns>
|
||||
Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an abuse report.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the report to resolve.</param>
|
||||
/// <param name="resolution">The resolution message.</param>
|
||||
/// <returns>The resolved abuse report.</returns>
|
||||
Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of pending abuse reports.
|
||||
/// </summary>
|
||||
/// <returns>The count of pending abuse reports.</returns>
|
||||
Task<int> GetPendingAbuseReportsCount();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a relationship with a specific status exists between two accounts.
|
||||
/// </summary>
|
||||
/// <param name="accountId1">The ID of the first account.</param>
|
||||
/// <param name="accountId2">The ID of the second account.</param>
|
||||
/// <param name="status">The relationship status to check for.</param>
|
||||
/// <returns>True if the relationship exists, otherwise false.</returns>
|
||||
Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statuses for a list of account IDs.
|
||||
/// </summary>
|
||||
/// <param name="accountIds">A list of account IDs.</param>
|
||||
/// <returns>A dictionary where the key is the account ID and the value is the status.</returns>
|
||||
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> accountIds);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to an account.
|
||||
/// </summary>
|
||||
/// <param name="account">The target account.</param>
|
||||
/// <param name="topic">The notification topic.</param>
|
||||
/// <param name="title">The notification title.</param>
|
||||
/// <param name="subtitle">The notification subtitle.</param>
|
||||
/// <param name="body">The notification body.</param>
|
||||
/// <param name="actionUri">The action URI for the notification.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
Task SendNotification(Account account, string topic, string title, string? subtitle, string body, string? actionUri = null);
|
||||
|
||||
/// <summary>
|
||||
/// Lists the friends of an account.
|
||||
/// </summary>
|
||||
/// <param name="account">The account.</param>
|
||||
/// <returns>A list of friend accounts.</returns>
|
||||
Task<List<Account>> ListAccountFriends(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies an authentication factor code.
|
||||
/// </summary>
|
||||
/// <param name="factor">The authentication factor.</param>
|
||||
/// <param name="code">The code to verify.</param>
|
||||
/// <returns>True if the code is valid, otherwise false.</returns>
|
||||
Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code);
|
||||
|
||||
/// <summary>
|
||||
/// Send the auth factor verification code to users, for factors like in-app code and email.
|
||||
/// Sometimes it requires a hint, like a part of the user's email address to ensure the user is who own the account.
|
||||
/// </summary>
|
||||
/// <param name="account">The owner of the auth factor</param>
|
||||
/// <param name="factor">The auth factor needed to send code</param>
|
||||
/// <param name="hint">The part of the contact method for verification</param>
|
||||
Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action log entry.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of action log.</param>
|
||||
/// <param name="meta">Additional metadata for the action log.</param>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <param name="account">The account associated with the action.</param>
|
||||
/// <returns>The created action log.</returns>
|
||||
Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Account? account = null);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new session.
|
||||
/// </summary>
|
||||
/// <param name="lastGrantedAt">The last granted instant.</param>
|
||||
/// <param name="expiredAt">The expiration instant.</param>
|
||||
/// <param name="account">The associated account.</param>
|
||||
/// <param name="challenge">The associated challenge.</param>
|
||||
/// <returns>The created session.</returns>
|
||||
Task<Session> CreateSession(NodaTime.Instant lastGrantedAt, NodaTime.Instant expiredAt, Account account, Challenge challenge);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the LastGrantedAt for a session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The ID of the session.</param>
|
||||
/// <param name="lastGrantedAt">The new LastGrantedAt instant.</param>
|
||||
Task UpdateSessionLastGrantedAt(Guid sessionId, NodaTime.Instant lastGrantedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the LastSeenAt for an account profile.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="lastSeenAt">The new LastSeenAt instant.</param>
|
||||
Task UpdateAccountProfileLastSeenAt(Guid accountId, NodaTime.Instant lastSeenAt);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a token for a session.
|
||||
/// </summary>
|
||||
/// <param name="session">The session.</param>
|
||||
/// <returns>The token string.</returns>
|
||||
string CreateToken(Session session);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AuthConstants.CookieTokenName.
|
||||
/// </summary>
|
||||
/// <returns>The cookie token name.</returns>
|
||||
string GetAuthCookieTokenName();
|
||||
|
||||
/// <summary>
|
||||
/// Searches for accounts by a search term.
|
||||
/// </summary>
|
||||
/// <param name="searchTerm">The term to search for.</param>
|
||||
/// <returns>A list of matching accounts.</returns>
|
||||
Task<List<Account>> SearchAccountsAsync(string searchTerm);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
@@ -15,10 +14,11 @@ public interface IActionLogService : IService<IActionLogService>
|
||||
/// <summary>
|
||||
/// Creates an action log entry from an HTTP request
|
||||
/// </summary>
|
||||
void CreateActionLogFromRequest(
|
||||
string action,
|
||||
Task<ActionLog> CreateActionLogFromRequest(
|
||||
string type,
|
||||
Dictionary<string, object> meta,
|
||||
HttpRequest request,
|
||||
string? ipAddress,
|
||||
string? userAgent,
|
||||
Account? account = null
|
||||
);
|
||||
}
|
||||
}
|
||||
13
DysonNetwork.Shared/Services/ICustomAppService.cs
Normal file
13
DysonNetwork.Shared/Services/ICustomAppService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using MagicOnion;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface ICustomAppService : IService<ICustomAppService>
|
||||
{
|
||||
Task<CustomApp?> FindClientByIdAsync(Guid clientId);
|
||||
Task<int> CountCustomAppsByPublisherId(Guid publisherId);
|
||||
}
|
||||
@@ -22,7 +22,21 @@ public interface IMagicSpellService : IService<IMagicSpellService>
|
||||
/// Gets a magic spell by its token
|
||||
/// </summary>
|
||||
Task<MagicSpell?> GetMagicSpellAsync(string token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its ID.
|
||||
/// </summary>
|
||||
/// <param name="spellId">The ID of the magic spell.</param>
|
||||
/// <returns>The magic spell if found, otherwise null.</returns>
|
||||
Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a password reset magic spell.
|
||||
/// </summary>
|
||||
/// <param name="spell">The magic spell object.</param>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
Task ApplyPasswordReset(MagicSpell spell, string newPassword);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes a magic spell
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
@@ -20,4 +22,25 @@ public interface INotificationService : IService<INotificationService>
|
||||
string deviceId,
|
||||
string deviceToken
|
||||
);
|
||||
|
||||
Task<Notification> SendNotification(
|
||||
Account account,
|
||||
string topic,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
string? content = null,
|
||||
Dictionary<string, object>? meta = null,
|
||||
string? actionUri = null,
|
||||
bool isSilent = false,
|
||||
bool save = true
|
||||
);
|
||||
|
||||
Task DeliveryNotification(Notification notification);
|
||||
|
||||
Task MarkNotificationsViewed(ICollection<Notification> notifications);
|
||||
|
||||
Task BroadcastNotification(Notification notification, bool save = false);
|
||||
|
||||
Task SendNotificationBatch(Notification notification, List<Account> accounts,
|
||||
bool save = false);
|
||||
}
|
||||
|
||||
15
DysonNetwork.Shared/Services/IPublisherService.cs
Normal file
15
DysonNetwork.Shared/Services/IPublisherService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using MagicOnion;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IPublisherService : IService<IPublisherService>
|
||||
{
|
||||
Task<Publisher?> GetPublisherByName(string name);
|
||||
Task<List<Publisher>> GetUserPublishers(Guid accountId);
|
||||
Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role);
|
||||
Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
@@ -9,7 +12,7 @@ public interface IRelationshipService : IService<IRelationshipService>
|
||||
/// Checks if a relationship exists between two accounts
|
||||
/// </summary>
|
||||
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a relationship between two accounts
|
||||
/// </summary>
|
||||
@@ -19,9 +22,58 @@ public interface IRelationshipService : IService<IRelationshipService>
|
||||
RelationshipStatus? status = null,
|
||||
bool ignoreExpired = false
|
||||
);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new relationship between two accounts
|
||||
/// </summary>
|
||||
Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Blocks a user
|
||||
/// </summary>
|
||||
Task<Relationship> BlockAccount(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Unblocks a user
|
||||
/// </summary>
|
||||
Task<Relationship> UnblockAccount(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a friend request to a user
|
||||
/// </summary>
|
||||
Task<Relationship> SendFriendRequest(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a friend request
|
||||
/// </summary>
|
||||
Task DeleteFriendRequest(Guid accountId, Guid relatedId);
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a friend request
|
||||
/// </summary>
|
||||
Task<Relationship> AcceptFriendRelationship(
|
||||
Relationship relationship,
|
||||
RelationshipStatus status = RelationshipStatus.Friends
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a relationship between two users
|
||||
/// </summary>
|
||||
Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all friends of an account
|
||||
/// </summary>
|
||||
Task<List<Account>> ListAccountFriends(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all blocked users of an account
|
||||
/// </summary>
|
||||
Task<List<Guid>> ListAccountBlocked(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a relationship with a specific status exists between two accounts
|
||||
/// </summary>
|
||||
Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
||||
RelationshipStatus status = RelationshipStatus.Friends);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Sphere.Activity;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Activity;
|
||||
public class ActivityService(
|
||||
AppDatabase db,
|
||||
PublisherService pub,
|
||||
DysonNetwork.Shared.Services.IRelationshipService rels,
|
||||
Shared.Services.IRelationshipService rels,
|
||||
PostService ps,
|
||||
DiscoveryService ds
|
||||
)
|
||||
@@ -125,7 +125,7 @@ public class ActivityService(
|
||||
)
|
||||
{
|
||||
var activities = new List<Activity>();
|
||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||
var userFriends = (await rels.ListAccountFriends(currentUser)).Select(x => x.Id).ToList();
|
||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||
debugInclude ??= [];
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ using Microsoft.EntityFrameworkCore.Query;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Sphere;
|
||||
|
||||
public class AppDatabase(
|
||||
DbContextOptions<AppDatabase> options,
|
||||
IConfiguration configuration
|
||||
@@ -18,18 +20,18 @@ public class AppDatabase(
|
||||
public DbSet<CloudFile> Files { get; set; }
|
||||
public DbSet<CloudFileReference> FileReferences { get; set; }
|
||||
|
||||
public DbSet<Publisher> Publishers { get; set; }
|
||||
public DbSet<Shared.Models.Publisher> Publishers { get; set; }
|
||||
public DbSet<PublisherMember> PublisherMembers { get; set; }
|
||||
public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; }
|
||||
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
|
||||
|
||||
public DbSet<Post> Posts { get; set; }
|
||||
public DbSet<Post.Post> Posts { get; set; }
|
||||
public DbSet<PostReaction> PostReactions { get; set; }
|
||||
public DbSet<PostTag> PostTags { get; set; }
|
||||
public DbSet<PostCategory> PostCategories { get; set; }
|
||||
public DbSet<PostCollection> PostCollections { get; set; }
|
||||
|
||||
public DbSet<Realm> Realms { get; set; }
|
||||
public DbSet<Shared.Models.Realm> Realms { get; set; }
|
||||
public DbSet<RealmMember> RealmMembers { get; set; }
|
||||
|
||||
public DbSet<ChatRoom> ChatRooms { get; set; }
|
||||
@@ -38,10 +40,10 @@ public class AppDatabase(
|
||||
public DbSet<RealtimeCall> ChatRealtimeCall { get; set; }
|
||||
public DbSet<MessageReaction> ChatReactions { get; set; }
|
||||
|
||||
public DbSet<Sticker> Stickers { get; set; }
|
||||
public DbSet<Sticker.Sticker> Stickers { get; set; }
|
||||
public DbSet<StickerPack> StickerPacks { get; set; }
|
||||
|
||||
public DbSet<Wallet> Wallets { get; set; }
|
||||
public DbSet<Shared.Models.Wallet> Wallets { get; set; }
|
||||
public DbSet<WalletPocket> WalletPockets { get; set; }
|
||||
public DbSet<Order> PaymentOrders { get; set; }
|
||||
public DbSet<Transaction> PaymentTransactions { get; set; }
|
||||
@@ -95,7 +97,7 @@ public class AppDatabase(
|
||||
.HasForeignKey(ps => ps.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content })
|
||||
.HasIndex(p => p.SearchVector)
|
||||
.HasMethod("GIN");
|
||||
@@ -110,25 +112,25 @@ public class AppDatabase(
|
||||
.HasForeignKey(s => s.AppId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasOne(p => p.RepliedPost)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.RepliedPostId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasOne(p => p.ForwardedPost)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.ForwardedPostId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasMany(p => p.Tags)
|
||||
.WithMany(t => t.Posts)
|
||||
.UsingEntity(j => j.ToTable("post_tag_links"));
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasMany(p => p.Categories)
|
||||
.WithMany(c => c.Posts)
|
||||
.UsingEntity(j => j.ToTable("post_category_links"));
|
||||
modelBuilder.Entity<Post>()
|
||||
modelBuilder.Entity<Post.Post>()
|
||||
.HasMany(p => p.Collections)
|
||||
.WithMany(c => c.Posts)
|
||||
.UsingEntity(j => j.ToTable("post_collection_links"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -137,7 +137,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
|
||||
[HttpPost("{roomId:guid}/messages")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("global", "chat.messages.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "chat.messages.create")]
|
||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -20,10 +20,12 @@ public class ChatRoomController(
|
||||
FileReferenceService fileRefService,
|
||||
ChatRoomService crs,
|
||||
RealmService rs,
|
||||
DysonNetwork.Shared.Services.IActionLogService als,
|
||||
DysonNetwork.Shared.Services.INotificationService nty,
|
||||
DysonNetwork.Shared.Services.IRelationshipService rels,
|
||||
DysonNetwork.Shared.Services.IAccountEventService aes
|
||||
IAccountService accounts,
|
||||
IActionLogService als,
|
||||
INotificationService nty,
|
||||
IRelationshipService rels,
|
||||
IAccountEventService aes,
|
||||
IStringLocalizer<NotificationResource> localizer
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -46,7 +48,7 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
@@ -72,10 +74,10 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
var relatedUser = await accounts.GetAccountById(request.RelatedUserId);
|
||||
if (relatedUser is null)
|
||||
return BadRequest("Related user was not found");
|
||||
|
||||
@@ -104,7 +106,7 @@ public class ChatRoomController(
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
Role = ChatMemberRole.Owner,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
},
|
||||
new()
|
||||
{
|
||||
@@ -118,9 +120,12 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Add(dmRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId);
|
||||
@@ -161,7 +166,7 @@ public class ChatRoomController(
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("global", "chat.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "chat.create")]
|
||||
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
@@ -224,9 +229,12 @@ public class ChatRoomController(
|
||||
chatRoomResourceId
|
||||
);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(chatRoom);
|
||||
@@ -310,9 +318,12 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Update(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomUpdate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(chatRoom);
|
||||
@@ -344,9 +355,12 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Remove(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomDelete,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -435,7 +449,6 @@ public class ChatRoomController(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class ChatMemberRequest
|
||||
{
|
||||
@@ -451,7 +464,7 @@ public class ChatRoomController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
var relatedUser = await accounts.GetAccountById(request.RelatedUserId);
|
||||
if (relatedUser is null) return BadRequest("Related user was not found");
|
||||
|
||||
if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked))
|
||||
@@ -507,9 +520,12 @@ public class ChatRoomController(
|
||||
newMember.ChatRoom = chatRoom;
|
||||
await _SendInviteNotify(newMember, currentUser);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomInvite,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(newMember);
|
||||
@@ -559,9 +575,12 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
_ = crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomJoin,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
@@ -675,7 +694,9 @@ public class ChatRoomController(
|
||||
ActionLogType.RealmAdjustRole,
|
||||
new Dictionary<string, object>
|
||||
{ { "chatroom_id", roomId }, { "account_id", memberId }, { "new_role", newRole } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(targetMember);
|
||||
@@ -722,7 +743,10 @@ public class ChatRoomController(
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomKick,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -762,9 +786,12 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
_ = crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomJoin,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(chatRoom);
|
||||
@@ -799,15 +826,18 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
await crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomLeave,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task _SendInviteNotify(ChatMember member, Shared.Models.Account sender)
|
||||
private async Task _SendInviteNotify(ChatMember member, Account sender)
|
||||
{
|
||||
string title = localizer["ChatInviteTitle"];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
@@ -204,7 +205,7 @@ public partial class ChatService(
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>();
|
||||
var scopedNty = scope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
var scopedNty = scope.ServiceProvider.GetRequiredService<INotificationService>();
|
||||
var scopedCrs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||
|
||||
var roomSubject = room is { Type: ChatRoomType.DirectMessage, Name: null } ? "DM" :
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -6,7 +7,7 @@ namespace DysonNetwork.Sphere.Connection;
|
||||
|
||||
[ApiController]
|
||||
[Route("completion")]
|
||||
public class AutoCompletionController(AppDatabase db)
|
||||
public class AutoCompletionController(IAccountService accounts, AppDatabase db)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
@@ -38,19 +39,15 @@ public class AutoCompletionController(AppDatabase db)
|
||||
|
||||
private async Task<List<CompletionItem>> GetAccountCompletions(string searchTerm)
|
||||
{
|
||||
return await db.Accounts
|
||||
.Where(a => EF.Functions.ILike(a.Name, $"%{searchTerm}%"))
|
||||
.OrderBy(a => a.Name)
|
||||
.Take(10)
|
||||
.Select(a => new CompletionItem
|
||||
{
|
||||
Id = a.Id.ToString(),
|
||||
DisplayName = a.Name,
|
||||
SecondaryText = a.Nick,
|
||||
Type = "account",
|
||||
Data = a
|
||||
})
|
||||
.ToListAsync();
|
||||
var data = await accounts.SearchAccountsAsync(searchTerm);
|
||||
return data.Select(a => new CompletionItem
|
||||
{
|
||||
Id = a.Id.ToString(),
|
||||
DisplayName = a.Name,
|
||||
SecondaryText = a.Nick,
|
||||
Type = "account",
|
||||
Data = a
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<CompletionItem>> GetStickerCompletions(string searchTerm)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Sphere.Connection.WebReader;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
@@ -59,7 +59,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr
|
||||
/// </summary>
|
||||
[HttpDelete("link/cache")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "cache.scrap")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "cache.scrap")]
|
||||
public async Task<IActionResult> InvalidateCache([FromQuery] string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
@@ -76,7 +76,7 @@ public class WebReaderController(WebReaderService reader, ILogger<WebReaderContr
|
||||
/// </summary>
|
||||
[HttpDelete("cache/all")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "cache.scrap")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "cache.scrap")]
|
||||
public async Task<IActionResult> InvalidateAllCache()
|
||||
{
|
||||
await reader.InvalidateAllCachedPreviewsAsync();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -90,7 +90,7 @@ public class DeveloperController(
|
||||
|
||||
[HttpPost("{name}/enroll")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("global", "developers.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "developers.create")]
|
||||
public async Task<ActionResult<Shared.Models.Publisher>> EnrollDeveloperProgram(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
<PackageReference Include="Livekit.Server.Sdk.Dotnet" Version="1.0.8" />
|
||||
<PackageReference Include="MagicOnion.Client" Version="7.0.5" />
|
||||
<PackageReference Include="MagicOnion.Server" Version="7.0.5" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
@@ -153,6 +155,7 @@
|
||||
<DependentUpon>NotificationResource.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Remove="Auth\AppleAuthController.cs" />
|
||||
<Compile Remove="Permission\RequiredPermissionAttribute.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MimeKit;
|
||||
|
||||
namespace DysonNetwork.Sphere.Email;
|
||||
|
||||
public class EmailServiceConfiguration
|
||||
{
|
||||
public string Server { get; set; } = null!;
|
||||
public int Port { get; set; }
|
||||
public bool UseSsl { get; set; }
|
||||
public string Username { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
public string FromAddress { get; set; } = null!;
|
||||
public string FromName { get; set; } = null!;
|
||||
public string SubjectPrefix { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly EmailServiceConfiguration _configuration;
|
||||
private readonly RazorViewRenderer _viewRenderer;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger<EmailService> logger)
|
||||
{
|
||||
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
||||
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
||||
_viewRenderer = viewRenderer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
||||
{
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null);
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody,
|
||||
string? htmlBody)
|
||||
{
|
||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||
|
||||
var emailMessage = new MimeMessage();
|
||||
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||
emailMessage.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
TextBody = textBody
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(htmlBody))
|
||||
bodyBuilder.HtmlBody = htmlBody;
|
||||
|
||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl);
|
||||
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
||||
await client.SendAsync(emailMessage);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
private static string _ConvertHtmlToPlainText(string html)
|
||||
{
|
||||
// Remove style tags and their contents
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<style[^>]*>.*?</style>", "",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline);
|
||||
|
||||
// Replace header tags with text + newlines
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<h[1-6][^>]*>(.*?)</h[1-6]>", "$1\n\n",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
// Replace line breaks
|
||||
html = html.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n");
|
||||
|
||||
// Remove all remaining HTML tags
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "");
|
||||
|
||||
// Decode HTML entities
|
||||
html = System.Net.WebUtility.HtmlDecode(html);
|
||||
|
||||
// Remove excess whitespace
|
||||
html = System.Text.RegularExpressions.Regex.Replace(html, @"\s+", " ").Trim();
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail,
|
||||
string subject, TModel model)
|
||||
where TComponent : IComponent
|
||||
{
|
||||
try
|
||||
{
|
||||
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
|
||||
var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody);
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
_logger.LogError(err, "Failed to render email template...");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Auth;
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{
|
||||
public class LoginModel(
|
||||
AppDatabase db,
|
||||
DysonNetwork.Shared.Services.IAccountService accounts,
|
||||
DysonNetwork.Pass.Auth.AuthService auth,
|
||||
GeoIpService geo,
|
||||
DysonNetwork.Shared.Services.IActionLogService als
|
||||
) : PageModel
|
||||
{
|
||||
|
||||
[BindProperty] [Required] public string Username { get; set; } = string.Empty;
|
||||
|
||||
[BindProperty]
|
||||
@@ -52,13 +52,7 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
||||
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
|
||||
var existingChallenge = await db.AuthChallenges
|
||||
.Where(e => e.Account == account)
|
||||
.Where(e => e.IpAddress == ipAddress)
|
||||
.Where(e => e.UserAgent == userAgent)
|
||||
.Where(e => e.StepRemain > 0)
|
||||
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var existingChallenge = await accounts.GetAuthChallenge(account.Id, ipAddress, userAgent, now);
|
||||
|
||||
if (existingChallenge is not null)
|
||||
{
|
||||
@@ -79,8 +73,7 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
AccountId = account.Id
|
||||
}.Normalize();
|
||||
|
||||
await db.AuthChallenges.AddAsync(challenge);
|
||||
await db.SaveChangesAsync();
|
||||
await accounts.CreateAuthChallenge(challenge);
|
||||
|
||||
// If we have a return URL, pass it to the verify page
|
||||
if (TempData.TryGetValue("ReturnUrl", out var returnUrl) && returnUrl is string url)
|
||||
|
||||
@@ -8,10 +8,8 @@ using DysonNetwork.Pass.Account;
|
||||
namespace DysonNetwork.Sphere.Pages.Auth;
|
||||
|
||||
public class SelectFactorModel(
|
||||
AppDatabase db,
|
||||
AccountService accounts
|
||||
)
|
||||
: PageModel
|
||||
DysonNetwork.Shared.Services.IAccountService accounts
|
||||
) : PageModel
|
||||
{
|
||||
[BindProperty(SupportsGet = true)] public Guid Id { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; }
|
||||
@@ -31,13 +29,11 @@ public class SelectFactorModel(
|
||||
|
||||
public async Task<IActionResult> OnPostSelectFactorAsync()
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
var challenge = await accounts.GetAuthChallenge(Id);
|
||||
|
||||
if (challenge == null) return NotFound();
|
||||
|
||||
var factor = await db.AccountAuthFactors.FindAsync(SelectedFactorId);
|
||||
var factor = await accounts.GetAccountAuthFactor(SelectedFactorId, challenge.Account.Id);
|
||||
if (factor?.EnabledAt == null || factor.Trustworthy <= 0)
|
||||
return BadRequest("Invalid authentication method.");
|
||||
|
||||
@@ -81,16 +77,11 @@ public class SelectFactorModel(
|
||||
|
||||
private async Task LoadChallengeAndFactors()
|
||||
{
|
||||
AuthChallenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
AuthChallenge = await accounts.GetAuthChallenge(Id);
|
||||
|
||||
if (AuthChallenge != null)
|
||||
{
|
||||
AuthFactors = await db.AccountAuthFactors
|
||||
.Where(e => e.AccountId == AuthChallenge.Account.Id)
|
||||
.Where(e => e.EnabledAt != null && e.Trustworthy >= 1)
|
||||
.ToListAsync();
|
||||
AuthFactors = await accounts.GetAccountAuthFactors(AuthChallenge.Account.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@page "/web/auth/challenge/{id:guid}/verify/{factorId:guid}"
|
||||
@using DysonNetwork.Shared.Models
|
||||
@using DysonNetwork.Shared.Models
|
||||
@model DysonNetwork.Sphere.Pages.Auth.VerifyFactorModel
|
||||
@{
|
||||
ViewData["Title"] = "Verify Your Identity";
|
||||
|
||||
@@ -4,20 +4,17 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{
|
||||
public class VerifyFactorModel(
|
||||
AppDatabase db,
|
||||
DysonNetwork.Shared.Services.IAccountService accountService,
|
||||
IAccountService accountService,
|
||||
DysonNetwork.Pass.Auth.AuthService authService,
|
||||
DysonNetwork.Shared.Services.IActionLogService actionLogService,
|
||||
IConfiguration configuration,
|
||||
IHttpClientFactory httpClientFactory
|
||||
)
|
||||
: PageModel
|
||||
IActionLogService actionLogService,
|
||||
IConfiguration configuration
|
||||
) : PageModel
|
||||
{
|
||||
[BindProperty(SupportsGet = true)] public Guid Id { get; set; }
|
||||
|
||||
@@ -55,30 +52,36 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
|
||||
try
|
||||
{
|
||||
if (await accounts.VerifyFactorCode(Factor, Code))
|
||||
if (await accountService.VerifyFactorCode(Factor, Code))
|
||||
{
|
||||
AuthChallenge.StepRemain -= Factor.Trustworthy;
|
||||
AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain);
|
||||
AuthChallenge.BlacklistFactors.Add(Factor.Id);
|
||||
db.Update(AuthChallenge);
|
||||
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
await actionLogService.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
{ "factor_id", Factor?.Id.ToString() ?? string.Empty }
|
||||
}, Request, AuthChallenge.Account);
|
||||
},
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
AuthChallenge.Account
|
||||
);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (AuthChallenge.StepRemain == 0)
|
||||
{
|
||||
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
await actionLogService.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
{ "account_id", AuthChallenge.AccountId }
|
||||
}, Request, AuthChallenge.Account);
|
||||
},
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
AuthChallenge.Account
|
||||
);
|
||||
|
||||
return await ExchangeTokenAndRedirect();
|
||||
}
|
||||
@@ -98,16 +101,18 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
{
|
||||
if (AuthChallenge != null)
|
||||
{
|
||||
AuthChallenge.FailedAttempts++;
|
||||
db.Update(AuthChallenge);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
await actionLogService.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "challenge_id", AuthChallenge.Id },
|
||||
{ "factor_id", Factor?.Id.ToString() ?? string.Empty }
|
||||
}, Request, AuthChallenge.Account);
|
||||
},
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
AuthChallenge.Account
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,47 +123,30 @@ namespace DysonNetwork.Sphere.Pages.Auth
|
||||
|
||||
private async Task LoadChallengeAndFactor()
|
||||
{
|
||||
AuthChallenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
AuthChallenge = await accountService.GetAuthChallenge(Id);
|
||||
|
||||
if (AuthChallenge?.Account != null)
|
||||
{
|
||||
Factor = await db.AccountAuthFactors
|
||||
.FirstOrDefaultAsync(e => e.Id == FactorId &&
|
||||
e.AccountId == AuthChallenge.Account.Id &&
|
||||
e.EnabledAt != null &&
|
||||
e.Trustworthy > 0);
|
||||
Factor = await accountService.GetAccountAuthFactor(FactorId, AuthChallenge.Account.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ExchangeTokenAndRedirect()
|
||||
{
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync(e => e.Id == Id);
|
||||
var challenge = await accountService.GetAuthChallenge(Id);
|
||||
|
||||
if (challenge == null) return BadRequest("Authorization code not found or expired.");
|
||||
if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed.");
|
||||
|
||||
var session = await db.AuthSessions
|
||||
.FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id);
|
||||
var session = await accountService.CreateSession(
|
||||
Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||
Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||
challenge.Account,
|
||||
challenge
|
||||
);
|
||||
|
||||
if (session == null)
|
||||
{
|
||||
session = new Session
|
||||
{
|
||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||
Account = challenge.Account,
|
||||
Challenge = challenge,
|
||||
};
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var token = auth.CreateToken(session);
|
||||
Response.Cookies.Append(AuthConstants.CookieTokenName, token, new CookieOptions
|
||||
var token = authService.CreateToken(session);
|
||||
Response.Cookies.Append(accountService.GetAuthCookieTokenName(), token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = !configuration.GetValue<bool>("Debug"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DysonNetwork.Pass.Auth;
|
||||
@using DysonNetwork.Pass.Auth
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
|
||||
@@ -6,7 +6,7 @@ using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Pages.Spell;
|
||||
|
||||
public class MagicSpellPage(AppDatabase db, DysonNetwork.Shared.Services.IMagicSpellService magicSpellService) : PageModel
|
||||
public class MagicSpellPage(DysonNetwork.Shared.Services.IMagicSpellService magicSpellService) : PageModel
|
||||
{
|
||||
[BindProperty] public MagicSpell? CurrentSpell { get; set; }
|
||||
[BindProperty] public string? NewPassword { get; set; }
|
||||
@@ -17,12 +17,7 @@ public class MagicSpellPage(AppDatabase db, DysonNetwork.Shared.Services.IMa
|
||||
{
|
||||
spellWord = Uri.UnescapeDataString(spellWord);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
CurrentSpell = await db.MagicSpells
|
||||
.Where(e => e.Spell == spellWord)
|
||||
.Where(e => e.ExpiresAt == null || now < e.ExpiresAt)
|
||||
.Where(e => e.AffectedAt == null || now >= e.AffectedAt)
|
||||
.Include(e => e.Account)
|
||||
.FirstOrDefaultAsync();
|
||||
CurrentSpell = await magicSpellService.GetMagicSpellAsync(spellWord);
|
||||
|
||||
return Page();
|
||||
}
|
||||
@@ -33,19 +28,15 @@ public class MagicSpellPage(AppDatabase db, DysonNetwork.Shared.Services.IMa
|
||||
return Page();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var spell = await db.MagicSpells
|
||||
.Where(e => e.Id == CurrentSpell.Id)
|
||||
.Where(e => e.ExpiresAt == null || now < e.ExpiresAt)
|
||||
.Where(e => e.AffectedAt == null || now >= e.AffectedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
var spell = await magicSpellService.GetMagicSpellByIdAsync(CurrentSpell.Id);
|
||||
|
||||
if (spell == null || spell.Type == MagicSpellType.AuthPasswordReset && string.IsNullOrWhiteSpace(NewPassword))
|
||||
return Page();
|
||||
|
||||
if (spell.Type == MagicSpellType.AuthPasswordReset)
|
||||
await spells.ApplyPasswordReset(spell, NewPassword!);
|
||||
await magicSpellService.ApplyPasswordReset(spell, NewPassword!);
|
||||
else
|
||||
await spells.ApplyMagicSpell(spell);
|
||||
await magicSpellService.ApplyMagicSpell(spell.Spell);
|
||||
IsSuccess = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Sphere.Permission;
|
||||
|
||||
public class RequiredPermissionAttribute : TypeFilterAttribute
|
||||
{
|
||||
public RequiredPermissionAttribute(string scope, string permission) : base(typeof(RequiredPermissionFilter))
|
||||
{
|
||||
Arguments = new object[] { scope, permission };
|
||||
}
|
||||
|
||||
private class RequiredPermissionFilter : IAsyncActionFilter
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly string _scope;
|
||||
private readonly string _permission;
|
||||
|
||||
public RequiredPermissionFilter(IPermissionService permissionService, string scope, string permission)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
_scope = scope;
|
||||
_permission = permission;
|
||||
}
|
||||
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
// Assuming the actor is always "user:current" for client-side checks
|
||||
// You might need to adjust this based on how your client identifies itself
|
||||
var hasPermission = await _permissionService.CheckPermission(_scope, _permission);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -33,7 +33,9 @@ public class PostController(
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Shared.Models.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
var userFriends = currentUser is null
|
||||
? []
|
||||
: (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList();
|
||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName);
|
||||
@@ -67,8 +69,10 @@ public class PostController(
|
||||
public async Task<ActionResult<Post>> GetPost(Guid id)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Shared.Models.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
var currentUser = currentUserValue as Account;
|
||||
var userFriends = currentUser is null
|
||||
? []
|
||||
: (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList();
|
||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
var post = await db.Posts
|
||||
@@ -99,8 +103,10 @@ public class PostController(
|
||||
return BadRequest("Search query cannot be empty");
|
||||
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Shared.Models.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
var currentUser = currentUserValue as Account;
|
||||
var userFriends = currentUser is null
|
||||
? []
|
||||
: (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList();
|
||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
var queryable = db.Posts
|
||||
@@ -136,8 +142,10 @@ public class PostController(
|
||||
[FromQuery] int take = 20)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Shared.Models.Account;
|
||||
var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser);
|
||||
var currentUser = currentUserValue as Account;
|
||||
var userFriends = currentUser is null
|
||||
? []
|
||||
: (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList();
|
||||
var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
var parent = await db.Posts
|
||||
@@ -264,9 +272,12 @@ public class PostController(
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostCreate,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
new Dictionary<string, object> { { "post_id", post.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return post;
|
||||
@@ -280,12 +291,12 @@ public class PostController(
|
||||
|
||||
[HttpPost("{id:guid}/reactions")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("global", "posts.react")]
|
||||
[RequiredPermission("global", "posts.react")]
|
||||
public async Task<ActionResult<PostReaction>> ReactPost(Guid id, [FromBody] PostReactionRequest request)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
var userFriends = (await rels.ListAccountFriends(currentUser)).Select(e => e.Id).ToList();
|
||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||
|
||||
var post = await db.Posts
|
||||
@@ -319,9 +330,12 @@ public class PostController(
|
||||
|
||||
if (isRemoving) return NoContent();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostReact,
|
||||
new Dictionary<string, object> { { "post_id", post.Id }, { "reaction", request.Symbol } }, Request
|
||||
new Dictionary<string, object> { { "post_id", post.Id }, { "reaction", request.Symbol } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(reaction);
|
||||
@@ -368,9 +382,12 @@ public class PostController(
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostUpdate,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
new Dictionary<string, object> { { "post_id", post.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(post);
|
||||
@@ -392,9 +409,12 @@ public class PostController(
|
||||
|
||||
await ps.DeletePostAsync(post);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PostDelete,
|
||||
new Dictionary<string, object> { { "post_id", post.Id } }, Request
|
||||
new Dictionary<string, object> { { "post_id", post.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Connection.WebReader;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
@@ -158,14 +159,13 @@ public partial class PostService(
|
||||
var sender = post.Publisher;
|
||||
using var scope = factory.CreateScope();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<INotificationService>();
|
||||
try
|
||||
{
|
||||
var members = await pub.GetPublisherMembers(post.RepliedPost.PublisherId);
|
||||
foreach (var member in members)
|
||||
{
|
||||
AccountService.SetCultureInfo(member.Account);
|
||||
CultureInfoService.SetCultureInfo(member.Account);
|
||||
var (_, content) = ChopPostForNotification(post);
|
||||
await nty.SendNotification(
|
||||
member.Account,
|
||||
@@ -439,14 +439,14 @@ public partial class PostService(
|
||||
{
|
||||
using var scope = factory.CreateScope();
|
||||
var pub = scope.ServiceProvider.GetRequiredService<PublisherService>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
var nty = scope.ServiceProvider.GetRequiredService<INotificationService>();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<PostService>>();
|
||||
try
|
||||
{
|
||||
var members = await pub.GetPublisherMembers(post.PublisherId);
|
||||
foreach (var member in members)
|
||||
{
|
||||
AccountService.SetCultureInfo(member.Account);
|
||||
CultureInfoService.SetCultureInfo(member.Account);
|
||||
await nty.SendNotification(
|
||||
member.Account,
|
||||
"posts.reactions.new",
|
||||
|
||||
@@ -2,6 +2,9 @@ using DysonNetwork.Sphere;
|
||||
using DysonNetwork.Sphere.Startup;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using tusdotnet.Stores;
|
||||
using MagicOnion.Client;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Grpc.Net.Client;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -20,6 +23,42 @@ builder.Services.AddAppSwagger();
|
||||
// Add gRPC services
|
||||
builder.Services.AddGrpc();
|
||||
|
||||
// Configure MagicOnion client for IAccountService
|
||||
builder.Services.AddSingleton<IAccountService>(provider =>
|
||||
{
|
||||
var passServiceUrl = builder.Configuration["PassService:Url"];
|
||||
if (string.IsNullOrEmpty(passServiceUrl))
|
||||
{
|
||||
throw new InvalidOperationException("PassService:Url configuration is missing.");
|
||||
}
|
||||
var channel = GrpcChannel.ForAddress(passServiceUrl);
|
||||
return MagicOnionClient.Create<IAccountService>(channel);
|
||||
});
|
||||
|
||||
// Configure MagicOnion client for IPublisherService
|
||||
builder.Services.AddSingleton<IPublisherService>(provider =>
|
||||
{
|
||||
var passServiceUrl = builder.Configuration["PassService:Url"];
|
||||
if (string.IsNullOrEmpty(passServiceUrl))
|
||||
{
|
||||
throw new InvalidOperationException("PassService:Url configuration is missing.");
|
||||
}
|
||||
var channel = GrpcChannel.ForAddress(passServiceUrl);
|
||||
return MagicOnionClient.Create<IPublisherService>(channel);
|
||||
});
|
||||
|
||||
// Configure MagicOnion client for ICustomAppService
|
||||
builder.Services.AddSingleton<ICustomAppService>(provider =>
|
||||
{
|
||||
var passServiceUrl = builder.Configuration["PassService:Url"];
|
||||
if (string.IsNullOrEmpty(passServiceUrl))
|
||||
{
|
||||
throw new InvalidOperationException("PassService:Url configuration is missing.");
|
||||
}
|
||||
var channel = GrpcChannel.ForAddress(passServiceUrl);
|
||||
return MagicOnionClient.Create<ICustomAppService>(channel);
|
||||
});
|
||||
|
||||
// Add file storage
|
||||
builder.Services.AddAppFileStorage(builder.Configuration);
|
||||
|
||||
@@ -47,8 +86,8 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
||||
// Configure application middleware pipeline
|
||||
app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore);
|
||||
|
||||
// Map gRPC services
|
||||
app.MapGrpcService<DysonNetwork.Pass.Auth.AuthGrpcService>();
|
||||
app.MapGrpcService<DysonNetwork.Pass.Account.AccountGrpcService>();
|
||||
// Remove direct gRPC service mappings for Pass services
|
||||
// app.MapGrpcService<DysonNetwork.Pass.Auth.AuthGrpcService>();
|
||||
// app.MapGrpcService<DysonNetwork.Pass.Account.AccountGrpcService>();
|
||||
|
||||
app.Run();
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -16,7 +17,9 @@ public class PublisherController(
|
||||
AppDatabase db,
|
||||
PublisherService ps,
|
||||
FileReferenceService fileRefService,
|
||||
DysonNetwork.Shared.Services.IActionLogService als)
|
||||
IAccountService accounts,
|
||||
IActionLogService als
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
@@ -28,10 +31,7 @@ public class PublisherController(
|
||||
if (publisher is null) return NotFound();
|
||||
if (publisher.AccountId is null) return Ok(publisher);
|
||||
|
||||
var account = await db.Accounts
|
||||
.Where(a => a.Id == publisher.AccountId)
|
||||
.Include(a => a.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
var account = await accounts.GetAccountById(publisher.AccountId.Value, true);
|
||||
publisher.Account = account;
|
||||
|
||||
return Ok(publisher);
|
||||
@@ -79,7 +79,7 @@ public class PublisherController(
|
||||
|
||||
public class PublisherMemberRequest
|
||||
{
|
||||
[Required] public long RelatedUserId { get; set; }
|
||||
[Required] public Guid RelatedUserId { get; set; }
|
||||
[Required] public PublisherMemberRole Role { get; set; }
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public class PublisherController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
var relatedUser = await accounts.GetAccountById(request.RelatedUserId);
|
||||
if (relatedUser is null) return BadRequest("Related user was not found");
|
||||
|
||||
var publisher = await db.Publishers
|
||||
@@ -112,13 +112,16 @@ public class PublisherController(
|
||||
db.PublisherMembers.Add(newMember);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberInvite,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "publisher_id", publisher.Id },
|
||||
{ "account_id", relatedUser.Id }
|
||||
}, Request
|
||||
},
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(newMember);
|
||||
@@ -142,9 +145,12 @@ public class PublisherController(
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberJoin,
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } }, Request
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
@@ -167,9 +173,12 @@ public class PublisherController(
|
||||
db.PublisherMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberLeave,
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } }, Request
|
||||
new Dictionary<string, object> { { "account_id", member.AccountId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -197,13 +206,16 @@ public class PublisherController(
|
||||
db.PublisherMembers.Remove(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherMemberKick,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "publisher_id", publisher.Id },
|
||||
{ "account_id", memberId }
|
||||
}, Request
|
||||
},
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -221,8 +233,9 @@ public class PublisherController(
|
||||
|
||||
[HttpPost("individual")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "publishers.create")]
|
||||
public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherIndividual([FromBody] PublisherRequest request)
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "publishers.create")]
|
||||
public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherIndividual(
|
||||
[FromBody] PublisherRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
@@ -260,9 +273,12 @@ public class PublisherController(
|
||||
background
|
||||
);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherCreate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
@@ -270,7 +286,7 @@ public class PublisherController(
|
||||
|
||||
[HttpPost("organization/{realmSlug}")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "publishers.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "publishers.create")]
|
||||
public async Task<ActionResult<Shared.Models.Publisher>> CreatePublisherOrganization(string realmSlug,
|
||||
[FromBody] PublisherRequest request)
|
||||
{
|
||||
@@ -315,9 +331,12 @@ public class PublisherController(
|
||||
background
|
||||
);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherCreate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
@@ -393,9 +412,12 @@ public class PublisherController(
|
||||
db.Update(publisher);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherUpdate,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(publisher);
|
||||
@@ -431,9 +453,12 @@ public class PublisherController(
|
||||
db.Publishers.Remove(publisher);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.PublisherDelete,
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } }, Request
|
||||
new Dictionary<string, object> { { "publisher_id", publisher.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -530,7 +555,7 @@ public class PublisherController(
|
||||
|
||||
[HttpPost("{name}/features")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "publishers.features")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "publishers.features")]
|
||||
public async Task<ActionResult<PublisherFeature>> AddPublisherFeature(string name,
|
||||
[FromBody] PublisherFeatureRequest request)
|
||||
{
|
||||
@@ -554,7 +579,7 @@ public class PublisherController(
|
||||
|
||||
[HttpDelete("{name}/features/{flag}")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "publishers.features")]
|
||||
[RequiredPermission("maintenance", "publishers.features")]
|
||||
public async Task<ActionResult> RemovePublisherFeature(string name, string flag)
|
||||
{
|
||||
var publisher = await db.Publishers
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -15,9 +16,10 @@ public class RealmController(
|
||||
AppDatabase db,
|
||||
RealmService rs,
|
||||
FileReferenceService fileRefService,
|
||||
RelationshipService rels,
|
||||
ActionLogService als,
|
||||
AccountEventService aes
|
||||
IRelationshipService rels,
|
||||
IActionLogService als,
|
||||
IAccountEventService aes,
|
||||
IAccountService accounts
|
||||
) : Controller
|
||||
{
|
||||
[HttpGet("{slug}")]
|
||||
@@ -79,7 +81,7 @@ public class RealmController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
var relatedUser = await accounts.GetAccountById(request.RelatedUserId);
|
||||
if (relatedUser is null) return BadRequest("Related user was not found");
|
||||
|
||||
if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked))
|
||||
@@ -111,9 +113,12 @@ public class RealmController(
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmInvite,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", member.AccountId } }, Request
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", member.AccountId } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
member.Account = relatedUser;
|
||||
@@ -141,10 +146,12 @@ public class RealmController(
|
||||
db.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmJoin,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
@@ -167,10 +174,12 @@ public class RealmController(
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmLeave,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -245,7 +254,6 @@ public class RealmController(
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("{slug}/members/me")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealmMember>> GetCurrentIdentity(string slug)
|
||||
@@ -284,10 +292,12 @@ public class RealmController(
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmLeave,
|
||||
new Dictionary<string, object> { { "realm_id", member.RealmId }, { "account_id", member.AccountId } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -349,9 +359,12 @@ public class RealmController(
|
||||
db.Realms.Add(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmCreate,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
var realmResourceId = $"realm:{realm.Id}";
|
||||
@@ -455,9 +468,12 @@ public class RealmController(
|
||||
db.Realms.Update(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmUpdate,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(realm);
|
||||
@@ -494,10 +510,12 @@ public class RealmController(
|
||||
db.RealmMembers.Add(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmJoin,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", currentUser.Id } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
@@ -525,10 +543,12 @@ public class RealmController(
|
||||
member.LeaveAt = SystemClock.Instance.GetCurrentInstant();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomKick,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id }, { "account_id", memberId } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return NoContent();
|
||||
@@ -559,11 +579,13 @@ public class RealmController(
|
||||
db.RealmMembers.Update(member);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmAdjustRole,
|
||||
new Dictionary<string, object>
|
||||
{ { "realm_id", realm.Id }, { "account_id", memberId }, { "new_role", newRole } },
|
||||
Request
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(member);
|
||||
@@ -588,9 +610,12 @@ public class RealmController(
|
||||
db.Realms.Remove(realm);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
await als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmDelete,
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } }, Request
|
||||
new Dictionary<string, object> { { "realm_id", realm.Id } },
|
||||
Request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||
Request.Headers.UserAgent.ToString(),
|
||||
currentUser
|
||||
);
|
||||
|
||||
// Delete all file references for this realm
|
||||
@@ -599,4 +624,4 @@ public class RealmController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Realm;
|
||||
|
||||
@@ -9,7 +10,7 @@ public class RealmService(AppDatabase db, DysonNetwork.Shared.Services.INoti
|
||||
{
|
||||
public async Task SendInviteNotify(RealmMember member)
|
||||
{
|
||||
AccountService.SetCultureInfo(member.Account);
|
||||
CultureInfoService.SetCultureInfo(member.Account);
|
||||
await nty.SendNotification(
|
||||
member.Account,
|
||||
"invites.realms",
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
using DysonNetwork.Pass.Account;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Safety;
|
||||
|
||||
public class SafetyService(AppDatabase db, ILogger<SafetyService> logger)
|
||||
{
|
||||
public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId)
|
||||
{
|
||||
// Check if a similar report already exists from this user
|
||||
var existingReport = await db.AbuseReports
|
||||
.Where(r => r.ResourceIdentifier == resourceIdentifier &&
|
||||
r.AccountId == accountId &&
|
||||
r.DeletedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
throw new InvalidOperationException("You have already reported this content.");
|
||||
}
|
||||
|
||||
var report = new AbuseReport
|
||||
{
|
||||
ResourceIdentifier = resourceIdentifier,
|
||||
Type = type,
|
||||
Reason = reason,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.AbuseReports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogInformation("New abuse report created: {ReportId} for resource {ResourceId}",
|
||||
report.Id, resourceIdentifier);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> CountReports(bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.Include(r => r.Account)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AbuseReport?> GetReportById(Guid id)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Include(r => r.Account)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> ResolveReport(Guid id, string resolution)
|
||||
{
|
||||
var report = await db.AbuseReports.FindAsync(id);
|
||||
if (report == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Report not found");
|
||||
}
|
||||
|
||||
report.ResolvedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
report.Resolution = resolution;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingReportsCount()
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,11 @@ using NodaTime.Serialization.SystemTextJson;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Pass.Safety;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Sphere.Connection.WebReader;
|
||||
using DysonNetwork.Sphere.Developer;
|
||||
using DysonNetwork.Sphere.Discovery;
|
||||
using DysonNetwork.Sphere.Safety;
|
||||
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||
using tusdotnet.Stores;
|
||||
using DysonNetwork.Shared.Etcd;
|
||||
@@ -189,7 +189,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<EmailService>();
|
||||
services.AddScoped<FileService>();
|
||||
services.AddScoped<FileReferenceService>();
|
||||
services.AddScoped<FileReferenceMigrationService>();
|
||||
services.AddScoped<PublisherService>();
|
||||
services.AddScoped<PublisherSubscriptionService>();
|
||||
services.AddScoped<ActivityService>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using DysonNetwork.Sphere.Post;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
@@ -76,7 +76,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequiredPermission("global", "stickers.packs.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "stickers.packs.create")]
|
||||
public async Task<ActionResult<StickerPack>> CreateStickerPack([FromBody] StickerPackRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
@@ -271,7 +271,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa
|
||||
public const int MaxStickersPerPack = 24;
|
||||
|
||||
[HttpPost("{packId:guid}/content")]
|
||||
[RequiredPermissionAttribute("global", "stickers.create")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "stickers.create")]
|
||||
public async Task<IActionResult> CreateSticker(Guid packId, [FromBody] StickerRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -13,8 +13,7 @@ public class FileController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
IConfiguration configuration,
|
||||
IWebHostEnvironment env,
|
||||
FileReferenceMigrationService rms
|
||||
IWebHostEnvironment env
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
@@ -108,13 +107,4 @@ public class FileController(
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("/maintenance/migrateReferences")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "files.references")]
|
||||
public async Task<ActionResult> MigrateFileReferences()
|
||||
{
|
||||
await rms.ScanAndMigrateReferences();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
public class FileReferenceMigrationService(AppDatabase db)
|
||||
{
|
||||
public async Task ScanAndMigrateReferences()
|
||||
{
|
||||
// Scan Posts for file references
|
||||
await ScanPosts();
|
||||
|
||||
// Scan Messages for file references
|
||||
await ScanMessages();
|
||||
|
||||
// Scan Profiles for file references
|
||||
await ScanProfiles();
|
||||
|
||||
// Scan Chat entities for file references
|
||||
await ScanChatRooms();
|
||||
|
||||
// Scan Realms for file references
|
||||
await ScanRealms();
|
||||
|
||||
// Scan Publishers for file references
|
||||
await ScanPublishers();
|
||||
|
||||
// Scan Stickers for file references
|
||||
await ScanStickers();
|
||||
}
|
||||
|
||||
private async Task ScanPosts()
|
||||
{
|
||||
var posts = await db.Posts
|
||||
.Include(p => p.OutdatedAttachments)
|
||||
.Where(p => p.OutdatedAttachments.Any())
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var post in posts)
|
||||
{
|
||||
var updatedAttachments = new List<CloudFileReferenceObject>();
|
||||
|
||||
foreach (var attachment in post.OutdatedAttachments)
|
||||
{
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == attachment.Id);
|
||||
if (file != null)
|
||||
{
|
||||
// Create a reference for the file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = file.Id,
|
||||
File = file,
|
||||
Usage = "post",
|
||||
ResourceId = post.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
updatedAttachments.Add(file.ToReferenceObject());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep the existing reference object if file not found
|
||||
updatedAttachments.Add(attachment.ToReferenceObject());
|
||||
}
|
||||
}
|
||||
|
||||
post.Attachments = updatedAttachments;
|
||||
db.Posts.Update(post);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanMessages()
|
||||
{
|
||||
var messages = await db.ChatMessages
|
||||
.Include(m => m.OutdatedAttachments)
|
||||
.Where(m => m.OutdatedAttachments.Any())
|
||||
.ToListAsync();
|
||||
|
||||
var fileReferences = messages.SelectMany(message => message.OutdatedAttachments.Select(attachment =>
|
||||
new CloudFileReference
|
||||
{
|
||||
FileId = attachment.Id,
|
||||
File = attachment,
|
||||
Usage = "chat",
|
||||
ResourceId = message.ResourceIdentifier,
|
||||
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
UpdatedAt = SystemClock.Instance.GetCurrentInstant()
|
||||
})
|
||||
).ToList();
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
message.Attachments = message.OutdatedAttachments.Select(a => a.ToReferenceObject()).ToList();
|
||||
db.ChatMessages.Update(message);
|
||||
}
|
||||
|
||||
await db.BulkInsertAsync(fileReferences);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanProfiles()
|
||||
{
|
||||
var profiles = await db.AccountProfiles
|
||||
.Where(p => p.PictureId != null || p.BackgroundId != null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
if (profile is { PictureId: not null, Picture: null })
|
||||
{
|
||||
var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.PictureId);
|
||||
if (avatarFile != null)
|
||||
{
|
||||
// Create a reference for the avatar file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = avatarFile.Id,
|
||||
File = avatarFile,
|
||||
Usage = "profile.picture",
|
||||
ResourceId = profile.Id.ToString()
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
profile.Picture = avatarFile.ToReferenceObject();
|
||||
db.AccountProfiles.Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for the banner if it exists
|
||||
if (profile is not { BackgroundId: not null, Background: null }) continue;
|
||||
var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == profile.BackgroundId);
|
||||
if (bannerFile == null) continue;
|
||||
{
|
||||
// Create a reference for the banner file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = bannerFile.Id,
|
||||
File = bannerFile,
|
||||
Usage = "profile.background",
|
||||
ResourceId = profile.Id.ToString()
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
profile.Background = bannerFile.ToReferenceObject();
|
||||
db.AccountProfiles.Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanChatRooms()
|
||||
{
|
||||
var chatRooms = await db.ChatRooms
|
||||
.Where(c => c.PictureId != null || c.BackgroundId != null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var chatRoom in chatRooms)
|
||||
{
|
||||
if (chatRoom is { PictureId: not null, Picture: null })
|
||||
{
|
||||
var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == chatRoom.PictureId);
|
||||
if (avatarFile != null)
|
||||
{
|
||||
// Create a reference for the avatar file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = avatarFile.Id,
|
||||
File = avatarFile,
|
||||
Usage = "chatroom.picture",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
chatRoom.Picture = avatarFile.ToReferenceObject();
|
||||
db.ChatRooms.Update(chatRoom);
|
||||
}
|
||||
}
|
||||
|
||||
if (chatRoom is not { BackgroundId: not null, Background: null }) continue;
|
||||
var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == chatRoom.BackgroundId);
|
||||
if (bannerFile == null) continue;
|
||||
{
|
||||
// Create a reference for the banner file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = bannerFile.Id,
|
||||
File = bannerFile,
|
||||
Usage = "chatroom.background",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
chatRoom.Background = bannerFile.ToReferenceObject();
|
||||
db.ChatRooms.Update(chatRoom);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanRealms()
|
||||
{
|
||||
var realms = await db.Realms
|
||||
.Where(r => r.PictureId != null && r.BackgroundId != null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var realm in realms)
|
||||
{
|
||||
// Process avatar if it exists
|
||||
if (realm is { PictureId: not null, Picture: null })
|
||||
{
|
||||
var avatarFile = await db.Files.FirstOrDefaultAsync(f => f.Id == realm.PictureId);
|
||||
if (avatarFile != null)
|
||||
{
|
||||
// Create a reference for the avatar file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = avatarFile.Id,
|
||||
File = avatarFile,
|
||||
Usage = "realm.picture",
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
realm.Picture = avatarFile.ToReferenceObject();
|
||||
}
|
||||
}
|
||||
|
||||
// Process banner if it exists
|
||||
if (realm is { BackgroundId: not null, Background: null })
|
||||
{
|
||||
var bannerFile = await db.Files.FirstOrDefaultAsync(f => f.Id == realm.BackgroundId);
|
||||
if (bannerFile != null)
|
||||
{
|
||||
// Create a reference for the banner file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = bannerFile.Id,
|
||||
File = bannerFile,
|
||||
Usage = "realm.background",
|
||||
ResourceId = realm.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
realm.Background = bannerFile.ToReferenceObject();
|
||||
}
|
||||
}
|
||||
|
||||
db.Realms.Update(realm);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanPublishers()
|
||||
{
|
||||
var publishers = await db.Publishers
|
||||
.Where(p => p.PictureId != null || p.BackgroundId != null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var publisher in publishers)
|
||||
{
|
||||
if (publisher is { PictureId: not null, Picture: null })
|
||||
{
|
||||
var pictureFile = await db.Files.FirstOrDefaultAsync(f => f.Id == publisher.PictureId);
|
||||
if (pictureFile != null)
|
||||
{
|
||||
// Create a reference for the picture file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = pictureFile.Id,
|
||||
File = pictureFile,
|
||||
Usage = "publisher.picture",
|
||||
ResourceId = publisher.Id.ToString()
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
publisher.Picture = pictureFile.ToReferenceObject();
|
||||
}
|
||||
}
|
||||
|
||||
if (publisher is { BackgroundId: not null, Background: null })
|
||||
{
|
||||
var backgroundFile = await db.Files.FirstOrDefaultAsync(f => f.Id == publisher.BackgroundId);
|
||||
if (backgroundFile != null)
|
||||
{
|
||||
// Create a reference for the background file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = backgroundFile.Id,
|
||||
File = backgroundFile,
|
||||
Usage = "publisher.background",
|
||||
ResourceId = publisher.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
publisher.Background = backgroundFile.ToReferenceObject();
|
||||
}
|
||||
}
|
||||
|
||||
db.Publishers.Update(publisher);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ScanStickers()
|
||||
{
|
||||
var stickers = await db.Stickers
|
||||
.Where(s => s.ImageId != null && s.Image == null)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var sticker in stickers)
|
||||
{
|
||||
var imageFile = await db.Files.FirstOrDefaultAsync(f => f.Id == sticker.ImageId);
|
||||
if (imageFile != null)
|
||||
{
|
||||
// Create a reference for the sticker image file
|
||||
var reference = new CloudFileReference
|
||||
{
|
||||
FileId = imageFile.Id,
|
||||
File = imageFile,
|
||||
Usage = "sticker.image",
|
||||
ResourceId = sticker.ResourceIdentifier
|
||||
};
|
||||
|
||||
await db.FileReferences.AddAsync(reference);
|
||||
sticker.Image = imageFile.ToReferenceObject();
|
||||
db.Stickers.Update(sticker);
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using DysonNetwork.Shared.Services;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage.Handlers;
|
||||
|
||||
public class LastActiveInfo
|
||||
{
|
||||
public Session Session { get; set; } = null!;
|
||||
public Shared.Models.Account Account { get; set; } = null!;
|
||||
public Account Account { get; set; } = null!;
|
||||
public Instant SeenAt { get; set; }
|
||||
}
|
||||
|
||||
public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<LastActiveInfo>
|
||||
public class LastActiveFlushHandler(DysonNetwork.Shared.Services.IAccountService accounts, DysonNetwork.Shared.Services.IAccountProfileService profiles) : IFlushHandler<LastActiveInfo>
|
||||
{
|
||||
public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
|
||||
// Remove duplicates by grouping on (sessionId, accountId), taking the most recent SeenAt
|
||||
var distinctItems = items
|
||||
.GroupBy(x => (SessionId: x.Session.Id, AccountId: x.Account.Id))
|
||||
@@ -36,19 +33,11 @@ public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHa
|
||||
|
||||
// Update sessions using native EF Core ExecuteUpdateAsync
|
||||
foreach (var kvp in sessionIdMap)
|
||||
{
|
||||
await db.AuthSessions
|
||||
.Where(s => s.Id == kvp.Key)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, kvp.Value));
|
||||
}
|
||||
await accounts.UpdateSessionLastGrantedAt(kvp.Key, kvp.Value);
|
||||
|
||||
// Update account profiles using native EF Core ExecuteUpdateAsync
|
||||
foreach (var kvp in accountIdMap)
|
||||
{
|
||||
await db.AccountProfiles
|
||||
.Where(a => a.AccountId == kvp.Key)
|
||||
.ExecuteUpdateAsync(a => a.SetProperty(x => x.LastSeenAt, kvp.Value));
|
||||
}
|
||||
await accounts.UpdateAccountProfileLastSeenAt(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
@@ -11,7 +12,8 @@ namespace DysonNetwork.Sphere.Wallet;
|
||||
public class PaymentService(
|
||||
AppDatabase db,
|
||||
WalletService wat,
|
||||
DysonNetwork.Shared.Services.INotificationService nty,
|
||||
INotificationService nty,
|
||||
IAccountService acc,
|
||||
IStringLocalizer<NotificationResource> localizer
|
||||
)
|
||||
{
|
||||
@@ -196,10 +198,10 @@ public class PaymentService(
|
||||
private async Task NotifyOrderPaid(Order order)
|
||||
{
|
||||
if (order.PayeeWallet is null) return;
|
||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId);
|
||||
var account = await acc.GetAccountById(order.PayeeWallet.AccountId);
|
||||
if (account is null) return;
|
||||
|
||||
AccountService.SetCultureInfo(account);
|
||||
// AccountService.SetCultureInfo(account);
|
||||
|
||||
// Due to ID is uuid, it longer than 8 words for sure
|
||||
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
||||
|
||||
@@ -5,11 +5,14 @@ using Quartz;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
using DysonNetwork.Shared.Services;
|
||||
|
||||
public class SubscriptionRenewalJob(
|
||||
AppDatabase db,
|
||||
SubscriptionService subscriptionService,
|
||||
PaymentService paymentService,
|
||||
WalletService walletService,
|
||||
IAccountProfileService accountProfileService,
|
||||
ILogger<SubscriptionRenewalJob> logger
|
||||
) : IJob
|
||||
{
|
||||
@@ -138,10 +141,7 @@ public class SubscriptionRenewalJob(
|
||||
logger.LogInformation("Validating user stellar memberships...");
|
||||
|
||||
// Get all account IDs with StellarMembership
|
||||
var accountsWithMemberships = await db.AccountProfiles
|
||||
.Where(a => a.StellarMembership != null)
|
||||
.Select(a => new { a.Id, a.StellarMembership })
|
||||
.ToListAsync();
|
||||
var accountsWithMemberships = await accountProfileService.GetAccountsWithStellarMembershipAsync();
|
||||
|
||||
logger.LogInformation("Found {Count} accounts with stellar memberships to validate",
|
||||
accountsWithMemberships.Count);
|
||||
@@ -187,11 +187,7 @@ public class SubscriptionRenewalJob(
|
||||
}
|
||||
|
||||
// Update all accounts in a single batch operation
|
||||
var updatedCount = await db.AccountProfiles
|
||||
.Where(a => accountIdsToUpdate.Contains(a.Id))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(a => a.StellarMembership, p => null)
|
||||
);
|
||||
var updatedCount = await accountProfileService.ClearStellarMembershipsAsync(accountIdsToUpdate);
|
||||
|
||||
logger.LogInformation("Updated {Count} accounts with expired/invalid stellar memberships", updatedCount);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
@@ -14,8 +15,9 @@ namespace DysonNetwork.Sphere.Wallet;
|
||||
public class SubscriptionService(
|
||||
AppDatabase db,
|
||||
PaymentService payment,
|
||||
DysonNetwork.Shared.Services.IAccountService account,
|
||||
DysonNetwork.Shared.Services.INotificationService nty,
|
||||
IAccountService accounts,
|
||||
IAccountProfileService profiles,
|
||||
INotificationService nty,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
IConfiguration configuration,
|
||||
ICacheService cache,
|
||||
@@ -23,7 +25,7 @@ public class SubscriptionService(
|
||||
)
|
||||
{
|
||||
public async Task<Subscription> CreateSubscriptionAsync(
|
||||
Shared.Models.Account account,
|
||||
Account account,
|
||||
string identifier,
|
||||
string paymentMethod,
|
||||
PaymentDetails paymentDetails,
|
||||
@@ -57,9 +59,7 @@ public class SubscriptionService(
|
||||
|
||||
if (subscriptionInfo.RequiredLevel > 0)
|
||||
{
|
||||
var profile = await db.AccountProfiles
|
||||
.Where(p => p.AccountId == account.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
var profile = await profiles.GetAccountProfileByIdAsync(account.Id);
|
||||
if (profile is null) throw new InvalidOperationException("Account profile was not found.");
|
||||
if (profile.Level < subscriptionInfo.RequiredLevel)
|
||||
throw new InvalidOperationException(
|
||||
@@ -141,7 +141,7 @@ public class SubscriptionService(
|
||||
if (!string.IsNullOrEmpty(provider))
|
||||
account = await accounts.LookupAccountByConnection(order.AccountId, provider);
|
||||
else if (Guid.TryParse(order.AccountId, out var accountId))
|
||||
account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
account = await accounts.GetAccountById(accountId);
|
||||
|
||||
if (account is null)
|
||||
throw new InvalidOperationException($"Account was not found with identifier {order.AccountId}");
|
||||
@@ -302,9 +302,7 @@ public class SubscriptionService(
|
||||
|
||||
if (subscription.Identifier.StartsWith(SubscriptionType.StellarProgram))
|
||||
{
|
||||
await db.AccountProfiles
|
||||
.Where(a => a.AccountId == subscription.AccountId)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference()));
|
||||
await profiles.UpdateStellarMembershipAsync(subscription.AccountId, subscription.ToReference());
|
||||
}
|
||||
|
||||
await NotifySubscriptionBegun(subscription);
|
||||
@@ -348,10 +346,10 @@ public class SubscriptionService(
|
||||
|
||||
private async Task NotifySubscriptionBegun(Subscription subscription)
|
||||
{
|
||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId);
|
||||
var account = await accounts.GetAccountById(subscription.AccountId);
|
||||
if (account is null) return;
|
||||
|
||||
AccountService.SetCultureInfo(account);
|
||||
CultureInfoService.SetCultureInfo(account);
|
||||
|
||||
var humanReadableName =
|
||||
SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -75,7 +75,7 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
||||
|
||||
[HttpPost("balance")]
|
||||
[Authorize]
|
||||
[RequiredPermissionAttribute("maintenance", "wallets.balance.modify")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("maintenance", "wallets.balance.modify")]
|
||||
public async Task<ActionResult<Transaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
|
||||
{
|
||||
var wallet = await ws.GetWalletAsync(request.AccountId);
|
||||
|
||||
Reference in New Issue
Block a user