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