♻️ Moved services & controllers to Pass
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public enum AbuseReportType
|
public enum AbuseReportType
|
||||||
{
|
{
|
@ -1,15 +1,12 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Pass.Auth;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Extensions;
|
using Microsoft.AspNetCore.Http;
|
||||||
using System.Collections.Generic;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/accounts")]
|
[Route("/accounts")]
|
@ -1,15 +1,15 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using DysonNetwork.Pass.Auth;
|
||||||
|
using DysonNetwork.Pass.Permission;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Auth;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Org.BouncyCastle.Utilities;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@ -17,7 +17,6 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
public class AccountCurrentController(
|
public class AccountCurrentController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
FileReferenceService fileRefService,
|
|
||||||
AccountEventService events,
|
AccountEventService events,
|
||||||
AuthService auth
|
AuthService auth
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
@ -96,61 +95,61 @@ public class AccountCurrentController(
|
|||||||
if (request.Location is not null) profile.Location = request.Location;
|
if (request.Location is not null) profile.Location = request.Location;
|
||||||
if (request.TimeZone is not null) profile.TimeZone = request.TimeZone;
|
if (request.TimeZone is not null) profile.TimeZone = request.TimeZone;
|
||||||
|
|
||||||
if (request.PictureId is not null)
|
// if (request.PictureId is not null)
|
||||||
{
|
// {
|
||||||
var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
// var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync();
|
||||||
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
// if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||||
|
//
|
||||||
var profileResourceId = $"profile:{profile.Id}";
|
// var profileResourceId = $"profile:{profile.Id}";
|
||||||
|
//
|
||||||
// Remove old references for the profile picture
|
// // Remove old references for the profile picture
|
||||||
if (profile.Picture is not null)
|
// if (profile.Picture is not null)
|
||||||
{
|
// {
|
||||||
var oldPictureRefs =
|
// var oldPictureRefs =
|
||||||
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
|
||||||
foreach (var oldRef in oldPictureRefs)
|
// foreach (var oldRef in oldPictureRefs)
|
||||||
{
|
// {
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
profile.Picture = picture.ToReferenceObject();
|
// profile.Picture = picture.ToReferenceObject();
|
||||||
|
//
|
||||||
// Create new reference
|
// // Create new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
// await fileRefService.CreateReferenceAsync(
|
||||||
picture.Id,
|
// picture.Id,
|
||||||
"profile.picture",
|
// "profile.picture",
|
||||||
profileResourceId
|
// profileResourceId
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (request.BackgroundId is not null)
|
// if (request.BackgroundId is not null)
|
||||||
{
|
// {
|
||||||
var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
// var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync();
|
||||||
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
// if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||||
|
//
|
||||||
var profileResourceId = $"profile:{profile.Id}";
|
// var profileResourceId = $"profile:{profile.Id}";
|
||||||
|
//
|
||||||
// Remove old references for the profile background
|
// // Remove old references for the profile background
|
||||||
if (profile.Background is not null)
|
// if (profile.Background is not null)
|
||||||
{
|
// {
|
||||||
var oldBackgroundRefs =
|
// var oldBackgroundRefs =
|
||||||
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
|
||||||
foreach (var oldRef in oldBackgroundRefs)
|
// foreach (var oldRef in oldBackgroundRefs)
|
||||||
{
|
// {
|
||||||
await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
profile.Background = background.ToReferenceObject();
|
// profile.Background = background.ToReferenceObject();
|
||||||
|
//
|
||||||
// Create new reference
|
// // Create new reference
|
||||||
await fileRefService.CreateReferenceAsync(
|
// await fileRefService.CreateReferenceAsync(
|
||||||
background.Id,
|
// background.Id,
|
||||||
"profile.background",
|
// "profile.background",
|
||||||
profileResourceId
|
// profileResourceId
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
db.Update(profile);
|
db.Update(profile);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -679,7 +678,7 @@ public class AccountCurrentController(
|
|||||||
{
|
{
|
||||||
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 badges = await db.Badges
|
var badges = await db.AccountBadges
|
||||||
.Where(b => b.AccountId == currentUser.Id)
|
.Where(b => b.AccountId == currentUser.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(badges);
|
return Ok(badges);
|
@ -1,22 +1,17 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Activity;
|
|
||||||
using DysonNetwork.Sphere.Connection;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using DysonNetwork.Sphere.Wallet;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Org.BouncyCastle.Asn1.X509;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public class AccountEventService(
|
public class AccountEventService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
WebSocketService ws,
|
// WebSocketService ws,
|
||||||
ICacheService cache,
|
// ICacheService cache,
|
||||||
PaymentService payment,
|
// PaymentService payment,
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -26,18 +21,18 @@ public class AccountEventService(
|
|||||||
public void PurgeStatusCache(Guid userId)
|
public void PurgeStatusCache(Guid userId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||||
cache.RemoveAsync(cacheKey);
|
// cache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Status> GetStatus(Guid userId)
|
public async Task<Status> GetStatus(Guid userId)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{StatusCacheKey}{userId}";
|
var cacheKey = $"{StatusCacheKey}{userId}";
|
||||||
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
// var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||||
if (cachedStatus is not null)
|
// if (cachedStatus is not null)
|
||||||
{
|
// {
|
||||||
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
// cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||||
return cachedStatus;
|
// return cachedStatus;
|
||||||
}
|
// }
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@ -45,12 +40,13 @@ public class AccountEventService(
|
|||||||
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
var isOnline = ws.GetAccountIsConnected(userId);
|
// var isOnline = ws.GetAccountIsConnected(userId);
|
||||||
|
var isOnline = false; // Placeholder
|
||||||
if (status is not null)
|
if (status is not null)
|
||||||
{
|
{
|
||||||
status.IsOnline = !status.IsInvisible && isOnline;
|
status.IsOnline = !status.IsInvisible && isOnline;
|
||||||
await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
|
// await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
|
||||||
TimeSpan.FromMinutes(5));
|
// TimeSpan.FromMinutes(5));
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,16 +80,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())
|
||||||
@ -110,11 +106,12 @@ public class AccountEventService(
|
|||||||
|
|
||||||
foreach (var status in statusesFromDb)
|
foreach (var status in statusesFromDb)
|
||||||
{
|
{
|
||||||
var isOnline = ws.GetAccountIsConnected(status.AccountId);
|
// var isOnline = ws.GetAccountIsConnected(status.AccountId);
|
||||||
|
var isOnline = false; // Placeholder
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +120,8 @@ public class AccountEventService(
|
|||||||
{
|
{
|
||||||
foreach (var userId in usersWithoutStatus)
|
foreach (var userId in usersWithoutStatus)
|
||||||
{
|
{
|
||||||
var isOnline = ws.GetAccountIsConnected(userId);
|
// var isOnline = ws.GetAccountIsConnected(userId);
|
||||||
|
var isOnline = false; // Placeholder
|
||||||
var defaultStatus = new Status
|
var defaultStatus = new Status
|
||||||
{
|
{
|
||||||
Attitude = StatusAttitude.Neutral,
|
Attitude = StatusAttitude.Neutral,
|
||||||
@ -168,12 +166,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,10 +200,10 @@ public class AccountEventService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(100));
|
// var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(100));
|
||||||
|
|
||||||
if (lk != null)
|
// if (lk != null)
|
||||||
await lk.ReleaseAsync();
|
// await lk.ReleaseAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -213,8 +211,8 @@ public class AccountEventService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now try to acquire the lock properly
|
// Now try to acquire the lock properly
|
||||||
await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
// await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
||||||
if (lockObj is null) throw new InvalidOperationException("Check-in was in progress.");
|
// if (lockObj is null) throw new InvalidOperationException("Check-in was in progress.");
|
||||||
|
|
||||||
var cultureInfo = new CultureInfo(user.Language, false);
|
var cultureInfo = new CultureInfo(user.Language, false);
|
||||||
CultureInfo.CurrentCulture = cultureInfo;
|
CultureInfo.CurrentCulture = cultureInfo;
|
||||||
@ -256,13 +254,14 @@ public class AccountEventService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (result.RewardPoints.HasValue)
|
if (result.RewardPoints.HasValue)
|
||||||
await payment.CreateTransactionWithAccountAsync(
|
// await payment.CreateTransactionWithAccountAsync(
|
||||||
null,
|
// null,
|
||||||
user.Id,
|
// user.Id,
|
||||||
WalletCurrency.SourcePoint,
|
// WalletCurrency.SourcePoint,
|
||||||
result.RewardPoints.Value,
|
// result.RewardPoints.Value,
|
||||||
$"Check-in reward on {now:yyyy/MM/dd}"
|
// $"Check-in reward on {now:yyyy/MM/dd}"
|
||||||
);
|
// );
|
||||||
|
Console.WriteLine($"Simulating transaction for {result.RewardPoints.Value} points");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
61
DysonNetwork.Pass/Account/AccountGrpcService.cs
Normal file
61
DysonNetwork.Pass/Account/AccountGrpcService.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Shared.Protos.Account;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
public class AccountGrpcService(AppDatabase db, AuthService auth)
|
||||||
|
: DysonNetwork.Shared.Protos.Account.AccountService.AccountServiceBase
|
||||||
|
{
|
||||||
|
public override async Task<AccountResponse> GetAccount(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var account = await GetAccountFromContext(context);
|
||||||
|
return ToAccountResponse(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AccountResponse> UpdateAccount(UpdateAccountRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var account = await GetAccountFromContext(context);
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return ToAccountResponse(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Shared.Models.Account> GetAccountFromContext(ServerCallContext context)
|
||||||
|
{
|
||||||
|
var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization");
|
||||||
|
if (authorizationHeader == null)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Missing authorization header."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = authorizationHeader.Value.Replace("Bearer ", "");
|
||||||
|
if (!auth.ValidateToken(token, out var sessionId))
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid token."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await db.AuthSessions.Include(s => s.Account).ThenInclude(a => a.Contacts)
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Session not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.Account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccountResponse ToAccountResponse(Shared.Models.Account account)
|
||||||
|
{
|
||||||
|
// TODO: implement
|
||||||
|
return new AccountResponse
|
||||||
|
{
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,24 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using DysonNetwork.Pass.Auth;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Auth;
|
|
||||||
using DysonNetwork.Sphere.Auth.OpenId;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Email;
|
|
||||||
|
|
||||||
using DysonNetwork.Sphere.Localization;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using EFCore.BulkExtensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Org.BouncyCastle.Utilities;
|
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using EFCore.BulkExtensions;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.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,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
// IStringLocalizer<NotificationResource> localizer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<AccountService> logger
|
ILogger<AccountService> logger
|
||||||
)
|
)
|
||||||
@ -136,15 +132,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);
|
||||||
@ -170,7 +166,8 @@ public class AccountService(
|
|||||||
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
||||||
|
|
||||||
// Generate username from email
|
// 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,
|
||||||
@ -185,26 +182,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)
|
||||||
@ -330,7 +327,7 @@ 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))
|
// .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.");
|
||||||
@ -356,14 +353,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:
|
||||||
@ -398,16 +395,16 @@ public class AccountService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
// await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
||||||
account.Nick,
|
// account.Nick,
|
||||||
contact.Content,
|
// contact.Content,
|
||||||
localizer["VerificationEmail"],
|
// localizer["VerificationEmail"],
|
||||||
new VerificationEmailModel
|
// new VerificationEmailModel
|
||||||
{
|
// {
|
||||||
Name = account.Name,
|
// Name = account.Name,
|
||||||
Code = code
|
// Code = code
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
||||||
break;
|
break;
|
||||||
@ -492,7 +489,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
|
||||||
@ -521,14 +518,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)
|
||||||
@ -578,7 +575,7 @@ public class AccountService(
|
|||||||
public async Task<Badge> GrantBadge(Shared.Models.Account account, Badge badge)
|
public async Task<Badge> GrantBadge(Shared.Models.Account account, Badge badge)
|
||||||
{
|
{
|
||||||
badge.AccountId = account.Id;
|
badge.AccountId = account.Id;
|
||||||
db.Badges.Add(badge);
|
db.AccountBadges.Add(badge);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
@ -589,7 +586,7 @@ public class AccountService(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task RevokeBadge(Shared.Models.Account account, Guid badgeId)
|
public async Task RevokeBadge(Shared.Models.Account account, Guid badgeId)
|
||||||
{
|
{
|
||||||
var badge = await db.Badges
|
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();
|
||||||
@ -611,13 +608,13 @@ public class AccountService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var badge = await db.Badges
|
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.");
|
||||||
|
|
||||||
await db.Badges
|
await db.AccountBadges
|
||||||
.Where(b => b.AccountId == account.Id && b.Id != badgeId)
|
.Where(b => b.AccountId == account.Id && b.Id != badgeId)
|
||||||
.ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null));
|
.ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null));
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for handling username generation and validation
|
/// Service for handling username generation and validation
|
@ -1,12 +1,12 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Quartz;
|
using Microsoft.AspNetCore.Http;
|
||||||
using DysonNetwork.Sphere.Connection;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using DysonNetwork.Sphere.Storage.Handlers;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
|
public class ActionLogService(
|
||||||
|
// GeoIpService geo,
|
||||||
|
// FlushBufferService fbs
|
||||||
|
)
|
||||||
{
|
{
|
||||||
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
|
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
|
||||||
{
|
{
|
||||||
@ -17,7 +17,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
|
|||||||
Meta = meta,
|
Meta = meta,
|
||||||
};
|
};
|
||||||
|
|
||||||
fbs.Enqueue(log);
|
// fbs.Enqueue(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
|
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
|
||||||
@ -29,7 +29,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
|
|||||||
Meta = meta,
|
Meta = meta,
|
||||||
UserAgent = request.Headers.UserAgent,
|
UserAgent = request.Headers.UserAgent,
|
||||||
IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
|
// Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
|
if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
|
||||||
@ -42,6 +42,6 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
|
|||||||
if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
|
if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
|
||||||
log.SessionId = currentSession.Id;
|
log.SessionId = currentSession.Id;
|
||||||
|
|
||||||
fbs.Enqueue(log);
|
// fbs.Enqueue(log);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public enum MagicSpellType
|
public enum MagicSpellType
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/spells")]
|
[Route("/spells")]
|
@ -2,23 +2,20 @@ using System.Globalization;
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Email;
|
|
||||||
using DysonNetwork.Sphere.Pages.Emails;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Resources.Localization;
|
|
||||||
using DysonNetwork.Sphere.Resources.Pages.Emails;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public class MagicSpellService(
|
public class MagicSpellService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
EmailService email,
|
// EmailService email,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger,
|
ILogger<MagicSpellService> logger
|
||||||
IStringLocalizer<Localization.EmailResource> localizer
|
// IStringLocalizer<Localization.EmailResource> localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<MagicSpell> CreateMagicSpell(
|
public async Task<MagicSpell> CreateMagicSpell(
|
||||||
@ -88,54 +85,54 @@ public class MagicSpellService(
|
|||||||
switch (spell.Type)
|
switch (spell.Type)
|
||||||
{
|
{
|
||||||
case MagicSpellType.AccountActivation:
|
case MagicSpellType.AccountActivation:
|
||||||
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
// await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
||||||
contact.Account.Nick,
|
// contact.Account.Nick,
|
||||||
contact.Content,
|
// contact.Content,
|
||||||
localizer["EmailLandingTitle"],
|
// localizer["EmailLandingTitle"],
|
||||||
new LandingEmailModel
|
// new LandingEmailModel
|
||||||
{
|
// {
|
||||||
Name = contact.Account.Name,
|
// Name = contact.Account.Name,
|
||||||
Link = link
|
// Link = link
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.AccountRemoval:
|
case MagicSpellType.AccountRemoval:
|
||||||
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
// await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
||||||
contact.Account.Nick,
|
// contact.Account.Nick,
|
||||||
contact.Content,
|
// contact.Content,
|
||||||
localizer["EmailAccountDeletionTitle"],
|
// localizer["EmailAccountDeletionTitle"],
|
||||||
new AccountDeletionEmailModel
|
// new AccountDeletionEmailModel
|
||||||
{
|
// {
|
||||||
Name = contact.Account.Name,
|
// Name = contact.Account.Name,
|
||||||
Link = link
|
// Link = link
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.AuthPasswordReset:
|
case MagicSpellType.AuthPasswordReset:
|
||||||
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
// await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||||
contact.Account.Nick,
|
// contact.Account.Nick,
|
||||||
contact.Content,
|
// contact.Content,
|
||||||
localizer["EmailAccountDeletionTitle"],
|
// localizer["EmailAccountDeletionTitle"],
|
||||||
new PasswordResetEmailModel
|
// new PasswordResetEmailModel
|
||||||
{
|
// {
|
||||||
Name = contact.Account.Name,
|
// Name = contact.Account.Name,
|
||||||
Link = link
|
// Link = link
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
break;
|
break;
|
||||||
case MagicSpellType.ContactVerification:
|
case MagicSpellType.ContactVerification:
|
||||||
if (spell.Meta["contact_method"] is not string contactMethod)
|
if (spell.Meta["contact_method"] is not string contactMethod)
|
||||||
throw new InvalidOperationException("Contact method is not found.");
|
throw new InvalidOperationException("Contact method is not found.");
|
||||||
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
// await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
||||||
contact.Account.Nick,
|
// contact.Account.Nick,
|
||||||
contactMethod!,
|
// contactMethod!,
|
||||||
localizer["EmailContactVerificationTitle"],
|
// localizer["EmailContactVerificationTitle"],
|
||||||
new ContactVerificationEmailModel
|
// new ContactVerificationEmailModel
|
||||||
{
|
// {
|
||||||
Name = contact.Account.Name,
|
// Name = contact.Account.Name,
|
||||||
Link = link
|
// Link = link
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
@ -1,13 +1,13 @@
|
|||||||
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.Permission;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/notifications")]
|
[Route("/notifications")]
|
||||||
@ -49,7 +49,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
Response.Headers["X-Total"] = totalCount.ToString();
|
Response.Headers["X-Total"] = totalCount.ToString();
|
||||||
await nty.MarkNotificationsViewed(notifications);
|
// await nty.MarkNotificationsViewed(notifications);
|
||||||
|
|
||||||
return Ok(notifications);
|
return Ok(notifications);
|
||||||
}
|
}
|
||||||
@ -73,11 +73,11 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
var currentSession = currentSessionValue as Session;
|
var currentSession = currentSessionValue as Session;
|
||||||
if (currentSession == null) return Unauthorized();
|
if (currentSession == null) return Unauthorized();
|
||||||
|
|
||||||
var result =
|
// var result =
|
||||||
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
// await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
|
||||||
request.DeviceToken);
|
// request.DeviceToken);
|
||||||
|
|
||||||
return Ok(result);
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("subscription")]
|
[HttpDelete("subscription")]
|
||||||
@ -111,26 +111,26 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
|
|
||||||
[HttpPost("broadcast")]
|
[HttpPost("broadcast")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "notifications.broadcast")]
|
// [RequiredPermission("global", "notifications.broadcast")]
|
||||||
public async Task<ActionResult> BroadcastNotification(
|
public async Task<ActionResult> BroadcastNotification(
|
||||||
[FromBody] NotificationRequest request,
|
[FromBody] NotificationRequest request,
|
||||||
[FromQuery] bool save = false
|
[FromQuery] bool save = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
await nty.BroadcastNotification(
|
// await nty.BroadcastNotification(
|
||||||
new Notification
|
// new Notification
|
||||||
{
|
// {
|
||||||
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
// CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
// UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
Topic = request.Topic,
|
// Topic = request.Topic,
|
||||||
Title = request.Title,
|
// Title = request.Title,
|
||||||
Subtitle = request.Subtitle,
|
// Subtitle = request.Subtitle,
|
||||||
Content = request.Content,
|
// Content = request.Content,
|
||||||
Meta = request.Meta,
|
// Meta = request.Meta,
|
||||||
Priority = request.Priority,
|
// Priority = request.Priority,
|
||||||
},
|
// },
|
||||||
save
|
// save
|
||||||
);
|
// );
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,27 +141,27 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
|||||||
|
|
||||||
[HttpPost("send")]
|
[HttpPost("send")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "notifications.send")]
|
// [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
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var accounts = await db.Accounts.Where(a => request.AccountId.Contains(a.Id)).ToListAsync();
|
var accounts = await db.Accounts.Where(a => request.AccountId.Contains(a.Id)).ToListAsync();
|
||||||
await nty.SendNotificationBatch(
|
// await nty.SendNotificationBatch(
|
||||||
new Notification
|
// new Notification
|
||||||
{
|
// {
|
||||||
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
// CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
// UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
Topic = request.Topic,
|
// Topic = request.Topic,
|
||||||
Title = request.Title,
|
// Title = request.Title,
|
||||||
Subtitle = request.Subtitle,
|
// Subtitle = request.Subtitle,
|
||||||
Content = request.Content,
|
// Content = request.Content,
|
||||||
Meta = request.Meta,
|
// Meta = request.Meta,
|
||||||
},
|
// },
|
||||||
accounts,
|
// accounts,
|
||||||
save
|
// save
|
||||||
);
|
// );
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
311
DysonNetwork.Pass/Account/NotificationService.cs
Normal file
311
DysonNetwork.Pass/Account/NotificationService.cs
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using EFCore.BulkExtensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
public class NotificationService(
|
||||||
|
AppDatabase db
|
||||||
|
// WebSocketService ws,
|
||||||
|
// IHttpClientFactory httpFactory,
|
||||||
|
// IConfiguration config
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
||||||
|
// private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
||||||
|
|
||||||
|
public async Task UnsubscribePushNotifications(string deviceId)
|
||||||
|
{
|
||||||
|
// await db.NotificationPushSubscriptions
|
||||||
|
// .Where(s => s.DeviceId == deviceId)
|
||||||
|
// .ExecuteDeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
||||||
|
Shared.Models.Account account,
|
||||||
|
NotificationPushProvider provider,
|
||||||
|
string deviceId,
|
||||||
|
string deviceToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
// First check if a matching subscription exists
|
||||||
|
// var existingSubscription = await db.NotificationPushSubscriptions
|
||||||
|
// .Where(s => s.AccountId == account.Id)
|
||||||
|
// .Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
|
||||||
|
// .FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
// if (existingSubscription is not null)
|
||||||
|
// {
|
||||||
|
// // Update the existing subscription directly in the database
|
||||||
|
// await db.NotificationPushSubscriptions
|
||||||
|
// .Where(s => s.Id == existingSubscription.Id)
|
||||||
|
// .ExecuteUpdateAsync(setters => setters
|
||||||
|
// .SetProperty(s => s.DeviceId, deviceId)
|
||||||
|
// .SetProperty(s => s.DeviceToken, deviceToken)
|
||||||
|
// .SetProperty(s => s.UpdatedAt, now));
|
||||||
|
|
||||||
|
// // Return the updated subscription
|
||||||
|
// existingSubscription.DeviceId = deviceId;
|
||||||
|
// existingSubscription.DeviceToken = deviceToken;
|
||||||
|
// existingSubscription.UpdatedAt = now;
|
||||||
|
// return existingSubscription;
|
||||||
|
// }
|
||||||
|
|
||||||
|
var subscription = new NotificationPushSubscription
|
||||||
|
{
|
||||||
|
DeviceId = deviceId,
|
||||||
|
DeviceToken = deviceToken,
|
||||||
|
Provider = provider,
|
||||||
|
AccountId = account.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// db.NotificationPushSubscriptions.Add(subscription);
|
||||||
|
// await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Notification> SendNotification(
|
||||||
|
Shared.Models.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
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (title is null && subtitle is null && content is null)
|
||||||
|
throw new ArgumentException("Unable to send notification that completely empty.");
|
||||||
|
|
||||||
|
meta ??= new Dictionary<string, object>();
|
||||||
|
if (actionUri is not null) meta["action_uri"] = actionUri;
|
||||||
|
|
||||||
|
var notification = new Notification
|
||||||
|
{
|
||||||
|
Topic = topic,
|
||||||
|
Title = title,
|
||||||
|
Subtitle = subtitle,
|
||||||
|
Content = content,
|
||||||
|
Meta = meta,
|
||||||
|
AccountId = account.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (save)
|
||||||
|
{
|
||||||
|
// db.Add(notification);
|
||||||
|
// await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSilent) Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification);
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeliveryNotification(Notification notification)
|
||||||
|
{
|
||||||
|
// ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
||||||
|
// {
|
||||||
|
// Type = "notifications.new",
|
||||||
|
// Data = notification
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Pushing the notification
|
||||||
|
// var subscribers = await db.NotificationPushSubscriptions
|
||||||
|
// .Where(s => s.AccountId == notification.AccountId)
|
||||||
|
// .ToListAsync();
|
||||||
|
|
||||||
|
// await _PushNotification(notification, subscribers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkNotificationsViewed(ICollection<Notification> notifications)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
|
||||||
|
if (id.Count == 0) return;
|
||||||
|
|
||||||
|
// await db.Notifications
|
||||||
|
// .Where(n => id.Contains(n.Id))
|
||||||
|
// .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now)
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BroadcastNotification(Notification notification, bool save = false)
|
||||||
|
{
|
||||||
|
var accounts = new List<Shared.Models.Account>(); // await db.Accounts.ToListAsync();
|
||||||
|
|
||||||
|
if (save)
|
||||||
|
{
|
||||||
|
var notifications = accounts.Select(x =>
|
||||||
|
{
|
||||||
|
var newNotification = new Notification
|
||||||
|
{
|
||||||
|
Topic = notification.Topic,
|
||||||
|
Title = notification.Title,
|
||||||
|
Subtitle = notification.Subtitle,
|
||||||
|
Content = notification.Content,
|
||||||
|
Meta = notification.Meta,
|
||||||
|
Priority = notification.Priority,
|
||||||
|
Account = x,
|
||||||
|
AccountId = x.Id
|
||||||
|
};
|
||||||
|
return newNotification;
|
||||||
|
}).ToList();
|
||||||
|
// await db.BulkInsertAsync(notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var account in accounts)
|
||||||
|
{
|
||||||
|
notification.Account = account;
|
||||||
|
notification.AccountId = account.Id;
|
||||||
|
// ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||||
|
// {
|
||||||
|
// Type = "notifications.new",
|
||||||
|
// Data = notification
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// var subscribers = await db.NotificationPushSubscriptions
|
||||||
|
// .ToListAsync();
|
||||||
|
// await _PushNotification(notification, subscribers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts, bool save = false)
|
||||||
|
{
|
||||||
|
if (save)
|
||||||
|
{
|
||||||
|
var notifications = accounts.Select(x =>
|
||||||
|
{
|
||||||
|
var newNotification = new Notification
|
||||||
|
{
|
||||||
|
Topic = notification.Topic,
|
||||||
|
Title = notification.Title,
|
||||||
|
Subtitle = notification.Subtitle,
|
||||||
|
Content = notification.Content,
|
||||||
|
Meta = notification.Meta,
|
||||||
|
Priority = notification.Priority,
|
||||||
|
Account = x,
|
||||||
|
AccountId = x.Id
|
||||||
|
};
|
||||||
|
return newNotification;
|
||||||
|
}).ToList();
|
||||||
|
// await db.BulkInsertAsync(notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var account in accounts)
|
||||||
|
{
|
||||||
|
notification.Account = account;
|
||||||
|
notification.AccountId = account.Id;
|
||||||
|
// ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||||
|
// {
|
||||||
|
// Type = "notifications.new",
|
||||||
|
// Data = notification
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// var accountsId = accounts.Select(x => x.Id).ToList();
|
||||||
|
// var subscribers = await db.NotificationPushSubscriptions
|
||||||
|
// .Where(s => accountsId.Contains(s.AccountId))
|
||||||
|
// .ToListAsync();
|
||||||
|
// await _PushNotification(notification, subscribers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
|
||||||
|
// IEnumerable<NotificationPushSubscription> subscriptions)
|
||||||
|
// {
|
||||||
|
// var subDict = subscriptions
|
||||||
|
// .GroupBy(x => x.Provider)
|
||||||
|
// .ToDictionary(x => x.Key, x => x.ToList());
|
||||||
|
|
||||||
|
// var notifications = subDict.Select(value =>
|
||||||
|
// {
|
||||||
|
// var platformCode = value.Key switch
|
||||||
|
// {
|
||||||
|
// NotificationPushProvider.Apple => 1,
|
||||||
|
// NotificationPushProvider.Google => 2,
|
||||||
|
// _ => throw new InvalidOperationException($"Unknown push provider: {value.Key}")
|
||||||
|
// };
|
||||||
|
|
||||||
|
// var tokens = value.Value.Select(x => x.DeviceToken).ToList();
|
||||||
|
// return _BuildNotificationPayload(notification, platformCode, tokens);
|
||||||
|
// }).ToList();
|
||||||
|
|
||||||
|
// return notifications.ToList();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private Dictionary<string, object> _BuildNotificationPayload(Notification notification, int platformCode,
|
||||||
|
// IEnumerable<string> deviceTokens)
|
||||||
|
// {
|
||||||
|
// var alertDict = new Dictionary<string, object>();
|
||||||
|
// var dict = new Dictionary<string, object>
|
||||||
|
// {
|
||||||
|
// ["notif_id"] = notification.Id.ToString(),
|
||||||
|
// ["apns_id"] = notification.Id.ToString(),
|
||||||
|
// ["topic"] = _notifyTopic,
|
||||||
|
// ["tokens"] = deviceTokens,
|
||||||
|
// ["data"] = new Dictionary<string, object>
|
||||||
|
// {
|
||||||
|
// ["type"] = notification.Topic,
|
||||||
|
// ["meta"] = notification.Meta ?? new Dictionary<string, object>(),
|
||||||
|
// },
|
||||||
|
// ["mutable_content"] = true,
|
||||||
|
// ["priority"] = notification.Priority >= 5 ? "high" : "normal",
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (!string.IsNullOrWhiteSpace(notification.Title))
|
||||||
|
// {
|
||||||
|
// dict["title"] = notification.Title;
|
||||||
|
// alertDict["title"] = notification.Title;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!string.IsNullOrWhiteSpace(notification.Content))
|
||||||
|
// {
|
||||||
|
// dict["message"] = notification.Content;
|
||||||
|
// alertDict["body"] = notification.Content;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!string.IsNullOrWhiteSpace(notification.Subtitle))
|
||||||
|
// {
|
||||||
|
// dict["message"] = $"{notification.Subtitle}\n{dict["message"]}";
|
||||||
|
// alertDict["subtitle"] = notification.Subtitle;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (notification.Priority >= 5)
|
||||||
|
// dict["name"] = "default";
|
||||||
|
|
||||||
|
// dict["platform"] = platformCode;
|
||||||
|
// dict["alert"] = alertDict;
|
||||||
|
|
||||||
|
// return dict;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private async Task _PushNotification(Notification notification,
|
||||||
|
// IEnumerable<NotificationPushSubscription> subscriptions)
|
||||||
|
// {
|
||||||
|
// var subList = subscriptions.ToList();
|
||||||
|
// if (subList.Count == 0) return;
|
||||||
|
|
||||||
|
// var requestDict = new Dictionary<string, object>
|
||||||
|
// {
|
||||||
|
// ["notifications"] = _BuildNotificationPayload(notification, subList)
|
||||||
|
// };
|
||||||
|
|
||||||
|
// var client = httpFactory.CreateClient();
|
||||||
|
// client.BaseAddress = _notifyEndpoint;
|
||||||
|
// var request = await client.PostAsync("/push", new StringContent(
|
||||||
|
// JsonSerializer.Serialize(requestDict),
|
||||||
|
// Encoding.UTF8,
|
||||||
|
// "application/json"
|
||||||
|
// ));
|
||||||
|
// request.EnsureSuccessStatusCode();
|
||||||
|
// }
|
||||||
|
}
|
@ -4,8 +4,9 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/relationships")]
|
[Route("/relationships")]
|
@ -1,11 +1,13 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
public class RelationshipService(AppDatabase db, ICacheService cache)
|
public class RelationshipService(
|
||||||
|
AppDatabase db
|
||||||
|
// ICacheService cache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||||
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
|
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
|
||||||
@ -53,7 +55,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
db.AccountRelationships.Add(relationship);
|
db.AccountRelationships.Add(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await PurgeRelationshipCache(sender.Id, target.Id);
|
// await PurgeRelationshipCache(sender.Id, target.Id);
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
@ -72,7 +74,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
db.Remove(relationship);
|
db.Remove(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await PurgeRelationshipCache(sender.Id, target.Id);
|
// await PurgeRelationshipCache(sender.Id, target.Id);
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
@ -105,7 +107,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
|
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Relationship> AcceptFriendRelationship(
|
public async Task<Relationship> AcceptFriendRelationship(
|
||||||
@ -134,7 +136,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
|
||||||
|
|
||||||
return relationshipBackward;
|
return relationshipBackward;
|
||||||
}
|
}
|
||||||
@ -148,7 +150,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
db.Update(relationship);
|
db.Update(relationship);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await PurgeRelationshipCache(accountId, relatedId);
|
// await PurgeRelationshipCache(accountId, relatedId);
|
||||||
|
|
||||||
return relationship;
|
return relationship;
|
||||||
}
|
}
|
||||||
@ -156,7 +158,8 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account)
|
public async Task<List<Guid>> 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<Guid>>(cacheKey);
|
||||||
|
var friends = new List<Guid>(); // Placeholder
|
||||||
|
|
||||||
if (friends == null)
|
if (friends == null)
|
||||||
{
|
{
|
||||||
@ -166,7 +169,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
.Select(r => r.AccountId)
|
.Select(r => r.AccountId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
// await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return friends ?? [];
|
return friends ?? [];
|
||||||
@ -175,7 +178,8 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
public async Task<List<Guid>> ListAccountBlocked(Shared.Models.Account account)
|
public async Task<List<Guid>> ListAccountBlocked(Shared.Models.Account account)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}";
|
var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}";
|
||||||
var blocked = await cache.GetAsync<List<Guid>>(cacheKey);
|
// var blocked = await cache.GetAsync<List<Guid>>(cacheKey);
|
||||||
|
var blocked = new List<Guid>(); // Placeholder
|
||||||
|
|
||||||
if (blocked == null)
|
if (blocked == null)
|
||||||
{
|
{
|
||||||
@ -185,7 +189,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
.Select(r => r.AccountId)
|
.Select(r => r.AccountId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
// await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocked ?? [];
|
return blocked ?? [];
|
||||||
@ -200,9 +204,9 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
|||||||
|
|
||||||
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId)
|
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId)
|
||||||
{
|
{
|
||||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
// await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
|
||||||
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
// await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
|
||||||
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
|
// await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
|
||||||
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}");
|
// await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,15 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using DysonNetwork.Pass.Permission;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass;
|
namespace DysonNetwork.Pass;
|
||||||
|
|
||||||
@ -22,23 +29,67 @@ public class AppDatabase(
|
|||||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
||||||
|
|
||||||
public DbSet<Account> Accounts { get; set; }
|
public DbSet<Shared.Models.Account> Accounts { get; set; }
|
||||||
public DbSet<AccountConnection> AccountConnections { get; set; }
|
public DbSet<AccountConnection> AccountConnections { get; set; }
|
||||||
public DbSet<Profile> AccountProfiles { get; set; }
|
public DbSet<Profile> AccountProfiles { get; set; }
|
||||||
public DbSet<AccountContact> AccountContacts { get; set; }
|
public DbSet<AccountContact> AccountContacts { get; set; }
|
||||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
|
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
|
||||||
public DbSet<Relationship> AccountRelationships { get; set; }
|
public DbSet<Relationship> AccountRelationships { get; set; }
|
||||||
|
public DbSet<Status> AccountStatuses { get; set; }
|
||||||
|
public DbSet<CheckInResult> AccountCheckInResults { get; set; }
|
||||||
|
public DbSet<Badge> AccountBadges { get; set; }
|
||||||
|
|
||||||
|
public DbSet<ActionLog> ActionLogs { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Notification> Notifications { get; set; }
|
||||||
|
public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
||||||
|
|
||||||
public DbSet<Session> AuthSessions { get; set; }
|
public DbSet<Session> AuthSessions { get; set; }
|
||||||
public DbSet<Challenge> AuthChallenges { get; set; }
|
public DbSet<Challenge> AuthChallenges { get; set; }
|
||||||
|
|
||||||
|
public DbSet<MagicSpell> MagicSpells { get; set; }
|
||||||
|
public DbSet<AbuseReport> AbuseReports { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder.UseNpgsql(
|
optionsBuilder.UseNpgsql(
|
||||||
configuration.GetConnectionString("App"),
|
configuration.GetConnectionString("App"),
|
||||||
opt => opt
|
opt => opt
|
||||||
|
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
||||||
|
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||||
|
.UseNetTopologySuite()
|
||||||
.UseNodaTime()
|
.UseNodaTime()
|
||||||
).UseSnakeCaseNamingConvention();
|
).UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
|
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var defaultPermissionGroup = await context.Set<PermissionGroup>()
|
||||||
|
.FirstOrDefaultAsync(g => g.Key == "default", cancellationToken);
|
||||||
|
if (defaultPermissionGroup is null)
|
||||||
|
{
|
||||||
|
context.Set<PermissionGroup>().Add(new PermissionGroup
|
||||||
|
{
|
||||||
|
Key = "default",
|
||||||
|
Nodes = new List<string>
|
||||||
|
{
|
||||||
|
"posts.create",
|
||||||
|
"posts.react",
|
||||||
|
"publishers.create",
|
||||||
|
"files.create",
|
||||||
|
"chat.create",
|
||||||
|
"chat.messages.create",
|
||||||
|
"chat.realtime.create",
|
||||||
|
"accounts.statuses.create",
|
||||||
|
"accounts.statuses.update",
|
||||||
|
"stickers.packs.create",
|
||||||
|
"stickers.create"
|
||||||
|
}.Select(permission =>
|
||||||
|
PermissionService.NewPermissionNode("group:default", "global", permission, true))
|
||||||
|
.ToList()
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
}
|
}
|
||||||
@ -49,9 +100,40 @@ public class AppDatabase(
|
|||||||
|
|
||||||
modelBuilder.Entity<PermissionGroupMember>()
|
modelBuilder.Entity<PermissionGroupMember>()
|
||||||
.HasKey(pg => new { pg.GroupId, pg.Actor });
|
.HasKey(pg => new { pg.GroupId, pg.Actor });
|
||||||
|
modelBuilder.Entity<PermissionGroupMember>()
|
||||||
|
.HasOne(pg => pg.Group)
|
||||||
|
.WithMany(g => g.Members)
|
||||||
|
.HasForeignKey(pg => pg.GroupId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.Entity<Relationship>()
|
modelBuilder.Entity<Relationship>()
|
||||||
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
|
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
|
||||||
|
modelBuilder.Entity<Relationship>()
|
||||||
|
.HasOne(r => r.Account)
|
||||||
|
.WithMany(a => a.OutgoingRelationships)
|
||||||
|
.HasForeignKey(r => r.AccountId);
|
||||||
|
modelBuilder.Entity<Relationship>()
|
||||||
|
.HasOne(r => r.Related)
|
||||||
|
.WithMany(a => a.IncomingRelationships)
|
||||||
|
.HasForeignKey(r => r.RelatedId);
|
||||||
|
|
||||||
|
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||||
|
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||||
|
{
|
||||||
|
if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
|
||||||
|
var method = typeof(AppDatabase)
|
||||||
|
.GetMethod(nameof(SetSoftDeleteFilter),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!
|
||||||
|
.MakeGenericMethod(entityType.ClrType);
|
||||||
|
|
||||||
|
method.Invoke(null, [modelBuilder]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
|
||||||
|
where TEntity : ModelBase
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
@ -80,6 +162,66 @@ public class AppDatabase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> logger) : IJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
logger.LogInformation("Cleaning up expired records...");
|
||||||
|
|
||||||
|
// Expired relationships
|
||||||
|
var affectedRows = await db.AccountRelationships
|
||||||
|
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows);
|
||||||
|
// Expired permission group members
|
||||||
|
affectedRows = await db.PermissionGroupMembers
|
||||||
|
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows);
|
||||||
|
|
||||||
|
logger.LogInformation("Deleting soft-deleted records...");
|
||||||
|
|
||||||
|
var threshold = now - Duration.FromDays(7);
|
||||||
|
|
||||||
|
var entityTypes = db.Model.GetEntityTypes()
|
||||||
|
.Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase))
|
||||||
|
.Select(t => t.ClrType);
|
||||||
|
|
||||||
|
foreach (var entityType in entityTypes)
|
||||||
|
{
|
||||||
|
var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
|
||||||
|
.MakeGenericMethod(entityType).Invoke(db, null)!;
|
||||||
|
var parameter = Expression.Parameter(entityType, "e");
|
||||||
|
var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt));
|
||||||
|
var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?)));
|
||||||
|
var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?)));
|
||||||
|
var finalCondition = Expression.AndAlso(notNull, condition);
|
||||||
|
var lambda = Expression.Lambda(finalCondition, parameter);
|
||||||
|
|
||||||
|
var queryable = set.Provider.CreateQuery(
|
||||||
|
Expression.Call(
|
||||||
|
typeof(Queryable),
|
||||||
|
"Where",
|
||||||
|
[entityType],
|
||||||
|
set.Expression,
|
||||||
|
Expression.Quote(lambda)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var toListAsync = typeof(EntityFrameworkQueryableExtensions)
|
||||||
|
.GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))!
|
||||||
|
.MakeGenericMethod(entityType);
|
||||||
|
|
||||||
|
var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!;
|
||||||
|
db.RemoveRange(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||||
{
|
{
|
||||||
public AppDatabase CreateDbContext(string[] args)
|
public AppDatabase CreateDbContext(string[] args)
|
||||||
@ -92,4 +234,36 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|||||||
var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
|
var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
|
||||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class OptionalQueryExtensions
|
||||||
|
{
|
||||||
|
public static IQueryable<T> If<T>(
|
||||||
|
this IQueryable<T> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IQueryable<T>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<T> If<T, TP>(
|
||||||
|
this IIncludableQueryable<T, TP> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IQueryable<T> If<T, TP>(
|
||||||
|
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
||||||
|
bool condition,
|
||||||
|
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
||||||
|
)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return condition ? transform(source) : source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using DysonNetwork.Sphere.Account;
|
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using DysonNetwork.Sphere.Storage.Handlers;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text;
|
using Microsoft.AspNetCore.Http;
|
||||||
using DysonNetwork.Shared.Models;
|
using Microsoft.Extensions.Configuration;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
using Microsoft.Extensions.Logging;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
using SystemClock = Microsoft.Extensions.Internal.SystemClock;
|
||||||
using SystemClock = NodaTime.SystemClock;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
public static class AuthConstants
|
public static class AuthConstants
|
||||||
{
|
{
|
||||||
@ -47,12 +40,11 @@ public class DysonTokenAuthHandler(
|
|||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
AppDatabase database,
|
AppDatabase database
|
||||||
OidcProviderService oidc,
|
// OidcProviderService oidc,
|
||||||
ICacheService cache,
|
// ICacheService cache,
|
||||||
FlushBufferService fbs
|
// FlushBufferService fbs
|
||||||
)
|
) : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
||||||
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
|
|
||||||
{
|
{
|
||||||
public const string AuthCachePrefix = "auth:";
|
public const string AuthCachePrefix = "auth:";
|
||||||
|
|
||||||
@ -65,36 +57,42 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
// Validate token and extract session ID
|
// Validate token and extract session ID
|
||||||
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
if (!ValidateToken(tokenInfo.Token, out var sessionId))
|
||||||
return AuthenticateResult.Fail("Invalid token.");
|
return AuthenticateResult.Fail("Invalid token.");
|
||||||
|
|
||||||
// Try to get session from cache first
|
// Try to get session from cache first
|
||||||
var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
// var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
|
||||||
|
var session = await database.AuthSessions
|
||||||
// If not in cache, load from database
|
|
||||||
if (session is null)
|
|
||||||
{
|
|
||||||
session = await database.AuthSessions
|
|
||||||
.Where(e => e.Id == sessionId)
|
.Where(e => e.Id == sessionId)
|
||||||
.Include(e => e.Challenge)
|
.Include(e => e.Challenge)
|
||||||
.Include(e => e.Account)
|
.Include(e => e.Account)
|
||||||
.ThenInclude(e => e.Profile)
|
.ThenInclude(e => e.Profile)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (session is not null)
|
// If not in cache, load from database
|
||||||
{
|
// if (session is null)
|
||||||
// Store in cache for future requests
|
// {
|
||||||
await cache.SetWithGroupsAsync(
|
// session = await database.AuthSessions
|
||||||
$"auth:{sessionId}",
|
// .Where(e => e.Id == sessionId)
|
||||||
session,
|
// .Include(e => e.Challenge)
|
||||||
[$"{AccountService.AccountCachePrefix}{session.Account.Id}"],
|
// .Include(e => e.Account)
|
||||||
TimeSpan.FromHours(1)
|
// .ThenInclude(e => e.Profile)
|
||||||
);
|
// .FirstOrDefaultAsync();
|
||||||
}
|
|
||||||
}
|
// if (session is not null)
|
||||||
|
// {
|
||||||
|
// // Store in cache for future requests
|
||||||
|
// await cache.SetWithGroupsAsync(
|
||||||
|
// $"auth:{sessionId}",
|
||||||
|
// session,
|
||||||
|
// // [$"{AccountService.AccountCachePrefix}{session.Account.Id}"],
|
||||||
|
// TimeSpan.FromHours(1)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Check if the session exists
|
// Check if the session exists
|
||||||
if (session == null)
|
if (session == null)
|
||||||
@ -130,13 +128,13 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
|
||||||
|
|
||||||
var lastInfo = new LastActiveInfo
|
// var lastInfo = new LastActiveInfo
|
||||||
{
|
// {
|
||||||
Account = session.Account,
|
// Account = session.Account,
|
||||||
Session = session,
|
// Session = session,
|
||||||
SeenAt = SystemClock.Instance.GetCurrentInstant(),
|
// SeenAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
};
|
// };
|
||||||
fbs.Enqueue(lastInfo);
|
// fbs.Enqueue(lastInfo);
|
||||||
|
|
||||||
return AuthenticateResult.Success(ticket);
|
return AuthenticateResult.Success(ticket);
|
||||||
}
|
}
|
||||||
@ -159,12 +157,13 @@ public class DysonTokenAuthHandler(
|
|||||||
// Handle JWT tokens (3 parts)
|
// Handle JWT tokens (3 parts)
|
||||||
case 3:
|
case 3:
|
||||||
{
|
{
|
||||||
var (isValid, jwtResult) = oidc.ValidateToken(token);
|
// var (isValid, jwtResult) = oidc.ValidateToken(token);
|
||||||
if (!isValid) return false;
|
// if (!isValid) return false;
|
||||||
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
// var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
|
||||||
if (jti is null) return false;
|
// if (jti is null) return false;
|
||||||
|
|
||||||
return Guid.TryParse(jti, out sessionId);
|
// return Guid.TryParse(jti, out sessionId);
|
||||||
|
return false; // Placeholder
|
||||||
}
|
}
|
||||||
// Handle compact tokens (2 parts)
|
// Handle compact tokens (2 parts)
|
||||||
case 2:
|
case 2:
|
@ -1,24 +1,23 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Pass.Account;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NodaTime;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using NodaTime;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Connection;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/auth")]
|
[Route("/auth")]
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
AuthService auth,
|
AuthService auth
|
||||||
GeoIpService geo,
|
// GeoIpService geo,
|
||||||
ActionLogService als
|
// ActionLogService als
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
public class ChallengeRequest
|
public class ChallengeRequest
|
||||||
@ -60,7 +59,7 @@ public class AuthController(
|
|||||||
Scopes = request.Scopes,
|
Scopes = request.Scopes,
|
||||||
IpAddress = ipAddress,
|
IpAddress = ipAddress,
|
||||||
UserAgent = userAgent,
|
UserAgent = userAgent,
|
||||||
Location = geo.GetPointFromIp(ipAddress),
|
// Location = geo.GetPointFromIp(ipAddress),
|
||||||
DeviceId = request.DeviceId,
|
DeviceId = request.DeviceId,
|
||||||
AccountId = account.Id
|
AccountId = account.Id
|
||||||
}.Normalize();
|
}.Normalize();
|
||||||
@ -68,9 +67,9 @@ public class AuthController(
|
|||||||
await db.AuthChallenges.AddAsync(challenge);
|
await db.AuthChallenges.AddAsync(challenge);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
// als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
|
||||||
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
// new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
|
||||||
);
|
// );
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
@ -161,13 +160,13 @@ public class AuthController(
|
|||||||
challenge.StepRemain = Math.Max(0, challenge.StepRemain);
|
challenge.StepRemain = Math.Max(0, challenge.StepRemain);
|
||||||
challenge.BlacklistFactors.Add(factor.Id);
|
challenge.BlacklistFactors.Add(factor.Id);
|
||||||
db.Update(challenge);
|
db.Update(challenge);
|
||||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
// als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
|
||||||
new Dictionary<string, object>
|
// new Dictionary<string, object>
|
||||||
{
|
// {
|
||||||
{ "challenge_id", challenge.Id },
|
// { "challenge_id", challenge.Id },
|
||||||
{ "factor_id", factor.Id }
|
// { "factor_id", factor.Id }
|
||||||
}, Request, challenge.Account
|
// }, Request, challenge.Account
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -178,26 +177,26 @@ public class AuthController(
|
|||||||
{
|
{
|
||||||
challenge.FailedAttempts++;
|
challenge.FailedAttempts++;
|
||||||
db.Update(challenge);
|
db.Update(challenge);
|
||||||
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
// als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
|
||||||
new Dictionary<string, object>
|
// new Dictionary<string, object>
|
||||||
{
|
// {
|
||||||
{ "challenge_id", challenge.Id },
|
// { "challenge_id", challenge.Id },
|
||||||
{ "factor_id", factor.Id }
|
// { "factor_id", factor.Id }
|
||||||
}, Request, challenge.Account
|
// }, Request, challenge.Account
|
||||||
);
|
// );
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return BadRequest("Invalid password.");
|
return BadRequest("Invalid password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (challenge.StepRemain == 0)
|
if (challenge.StepRemain == 0)
|
||||||
{
|
{
|
||||||
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
// als.CreateActionLogFromRequest(ActionLogType.NewLogin,
|
||||||
new Dictionary<string, object>
|
// new Dictionary<string, object>
|
||||||
{
|
// {
|
||||||
{ "challenge_id", challenge.Id },
|
// { "challenge_id", challenge.Id },
|
||||||
{ "account_id", challenge.AccountId }
|
// { "account_id", challenge.AccountId }
|
||||||
}, Request, challenge.Account
|
// }, Request, challenge.Account
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
@ -1,16 +1,18 @@
|
|||||||
using DysonNetwork.Sphere.Auth.Proto;
|
using DysonNetwork.Shared.Protos.Auth;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using DysonNetwork.Sphere.Account;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using Challenge = DysonNetwork.Shared.Models.Challenge;
|
||||||
|
using Session = DysonNetwork.Shared.Models.Session;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth)
|
public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth)
|
||||||
: DysonNetwork.Sphere.Auth.Proto.AuthService.AuthServiceBase
|
: DysonNetwork.Shared.Protos.Auth.AuthService.AuthServiceBase
|
||||||
{
|
{
|
||||||
public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
|
public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
@ -31,7 +33,7 @@ public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthServic
|
|||||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
||||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
||||||
Account = account,
|
Account = account,
|
||||||
Challenge = new Challenge() // Create a dummy challenge
|
Challenge = new Challenge()
|
||||||
};
|
};
|
||||||
|
|
||||||
db.AuthSessions.Add(session);
|
db.AuthSessions.Add(session);
|
@ -1,22 +1,23 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
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
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
// private HttpContext HttpContext => httpContextAccessor.HttpContext!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detect the risk of the current request to login
|
/// Detect the risk of the current request to login
|
||||||
@ -79,8 +80,8 @@ public class AuthService(
|
|||||||
var challenge = new Challenge
|
var challenge = new Challenge
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
|
IpAddress = "127.0.0.1", // HttpContext.Connection.RemoteIpAddress?.ToString(),
|
||||||
UserAgent = HttpContext.Request.Headers.UserAgent,
|
UserAgent = "TestAgent", // HttpContext.Request.Headers.UserAgent,
|
||||||
StepRemain = 1,
|
StepRemain = 1,
|
||||||
StepTotal = 1,
|
StepTotal = 1,
|
||||||
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
|
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
|
||||||
@ -106,53 +107,54 @@ public class AuthService(
|
|||||||
{
|
{
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CreateToken(Session session)
|
public string CreateToken(Session session)
|
||||||
@ -184,56 +186,56 @@ public class AuthService(
|
|||||||
return $"{payloadBase64}.{signatureBase64}";
|
return $"{payloadBase64}.{signatureBase64}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
|
// public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
|
||||||
{
|
// {
|
||||||
// Check if the session is already in sudo mode (cached)
|
// // Check if the session is already in sudo mode (cached)
|
||||||
var sudoModeKey = $"accounts:{session.Id}:sudo";
|
// var sudoModeKey = $"accounts:{session.Id}:sudo";
|
||||||
var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
|
// var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
|
||||||
|
|
||||||
if (found)
|
// if (found)
|
||||||
{
|
// {
|
||||||
// Session is already in sudo mode
|
// // Session is already in sudo mode
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check if the user has a pin code
|
// // Check if the user has a pin code
|
||||||
var hasPinCode = await db.AccountAuthFactors
|
// var hasPinCode = await db.AccountAuthFactors
|
||||||
.Where(f => f.AccountId == session.AccountId)
|
// .Where(f => f.AccountId == session.AccountId)
|
||||||
.Where(f => f.EnabledAt != null)
|
// .Where(f => f.EnabledAt != null)
|
||||||
.Where(f => f.Type == AccountAuthFactorType.PinCode)
|
// .Where(f => f.Type == AccountAuthFactorType.PinCode)
|
||||||
.AnyAsync();
|
// .AnyAsync();
|
||||||
|
|
||||||
if (!hasPinCode)
|
// if (!hasPinCode)
|
||||||
{
|
// {
|
||||||
// User doesn't have a pin code, no validation needed
|
// // User doesn't have a pin code, no validation needed
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// If pin code is not provided, we can't validate
|
// // If pin code is not provided, we can't validate
|
||||||
if (string.IsNullOrEmpty(pinCode))
|
// if (string.IsNullOrEmpty(pinCode))
|
||||||
{
|
// {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
// Validate the pin code
|
// // Validate the pin code
|
||||||
var isValid = await ValidatePinCode(session.AccountId, pinCode);
|
// var isValid = await ValidatePinCode(session.AccountId, pinCode);
|
||||||
|
|
||||||
if (isValid)
|
// if (isValid)
|
||||||
{
|
// {
|
||||||
// Set session in sudo mode for 5 minutes
|
// // Set session in sudo mode for 5 minutes
|
||||||
await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
|
// await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
|
||||||
}
|
// }
|
||||||
|
|
||||||
return isValid;
|
// return isValid;
|
||||||
}
|
// }
|
||||||
catch (InvalidOperationException)
|
// catch (InvalidOperationException)
|
||||||
{
|
// {
|
||||||
// No pin code enabled for this account, so validation is successful
|
// // No pin code enabled for this account, so validation is successful
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
|
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
public class CaptchaVerificationResponse
|
public class CaptchaVerificationResponse
|
||||||
{
|
{
|
@ -1,7 +1,8 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
public class CompactTokenService(IConfiguration config)
|
public class CompactTokenService(IConfiguration config)
|
||||||
{
|
{
|
@ -1,19 +1,19 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Account;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers;
|
||||||
|
|
||||||
[Route("/auth/open")]
|
[Route("/auth/open")]
|
||||||
[ApiController]
|
[ApiController]
|
@ -1,8 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Models;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Models;
|
||||||
|
|
||||||
public class AuthorizationCodeInfo
|
public class AuthorizationCodeInfo
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||||
|
|
||||||
public class OidcProviderOptions
|
public class OidcProviderOptions
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||||
|
|
||||||
public class AuthorizationResponse
|
public class AuthorizationResponse
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||||
|
|
||||||
public class ErrorResponse
|
public class ErrorResponse
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||||
|
|
||||||
public class TokenResponse
|
public class TokenResponse
|
||||||
{
|
{
|
@ -2,18 +2,18 @@ using System.IdentityModel.Tokens.Jwt;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using DysonNetwork.Pass.Auth.OidcProvider.Models;
|
||||||
|
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||||
|
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Models;
|
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
|
||||||
using DysonNetwork.Sphere.Developer;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||||
|
|
||||||
public class OidcProviderService(
|
public class OidcProviderService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
@ -27,16 +27,18 @@ public class OidcProviderService(
|
|||||||
|
|
||||||
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
||||||
{
|
{
|
||||||
return await db.CustomApps
|
return null;
|
||||||
.Include(c => c.Secrets)
|
// return await db.CustomApps
|
||||||
.FirstOrDefaultAsync(c => c.Id == clientId);
|
// .Include(c => c.Secrets)
|
||||||
|
// .FirstOrDefaultAsync(c => c.Id == clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId)
|
public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId)
|
||||||
{
|
{
|
||||||
return await db.CustomApps
|
return null;
|
||||||
.Include(c => c.Secrets)
|
// return await db.CustomApps
|
||||||
.FirstOrDefaultAsync(c => c.Id == appId);
|
// .Include(c => c.Secrets)
|
||||||
|
// .FirstOrDefaultAsync(c => c.Id == appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
@ -1,8 +1,10 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
public class AfdianOidcService(
|
public class AfdianOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -3,10 +3,12 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of OpenID Connect service for Apple Sign In
|
/// Implementation of OpenID Connect service for Apple Sign In
|
@ -1,12 +1,14 @@
|
|||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/accounts/me/connections")]
|
[Route("/accounts/me/connections")]
|
@ -1,8 +1,10 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
public class DiscordOidcService(
|
public class DiscordOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,8 +1,10 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
public class GitHubOidcService(
|
public class GitHubOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,11 +1,11 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
using DysonNetwork.Shared.Cache;
|
||||||
using System.Text;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
public class GoogleOidcService(
|
public class GoogleOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,8 +1,10 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
public class MicrosoftOidcService(
|
public class MicrosoftOidcService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
@ -1,12 +1,14 @@
|
|||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/auth/login")]
|
[Route("/auth/login")]
|
@ -1,14 +1,16 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base service for OpenID Connect authentication providers
|
/// Base service for OpenID Connect authentication providers
|
@ -1,95 +0,0 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Data;
|
|
||||||
|
|
||||||
public abstract class ModelBase
|
|
||||||
{
|
|
||||||
public Instant CreatedAt { get; set; }
|
|
||||||
public Instant UpdatedAt { get; set; }
|
|
||||||
public Instant? DeletedAt { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppDatabase(
|
|
||||||
DbContextOptions<AppDatabase> options,
|
|
||||||
IConfiguration configuration
|
|
||||||
) : DbContext(options)
|
|
||||||
{
|
|
||||||
public DbSet<PermissionNode> PermissionNodes { get; set; }
|
|
||||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
|
||||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Account> Accounts { get; set; }
|
|
||||||
public DbSet<AccountConnection> AccountConnections { get; set; }
|
|
||||||
public DbSet<Profile> AccountProfiles { get; set; }
|
|
||||||
public DbSet<AccountContact> AccountContacts { get; set; }
|
|
||||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
|
|
||||||
public DbSet<Relationship> AccountRelationships { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Session> AuthSessions { get; set; }
|
|
||||||
public DbSet<Challenge> AuthChallenges { get; set; }
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
||||||
{
|
|
||||||
optionsBuilder.UseNpgsql(
|
|
||||||
configuration.GetConnectionString("App"),
|
|
||||||
opt => opt
|
|
||||||
.UseNodaTime()
|
|
||||||
).UseSnakeCaseNamingConvention();
|
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<PermissionGroupMember>()
|
|
||||||
.HasKey(pg => new { pg.GroupId, pg.Actor });
|
|
||||||
|
|
||||||
modelBuilder.Entity<Relationship>()
|
|
||||||
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
|
|
||||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
|
||||||
{
|
|
||||||
if (entry.State == EntityState.Added)
|
|
||||||
{
|
|
||||||
entry.Entity.CreatedAt = now;
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
else if (entry.State == EntityState.Modified)
|
|
||||||
{
|
|
||||||
entry.Entity.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
else if (entry.State == EntityState.Deleted)
|
|
||||||
{
|
|
||||||
entry.State = EntityState.Modified;
|
|
||||||
entry.Entity.DeletedAt = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await base.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
|
||||||
{
|
|
||||||
public AppDatabase CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
|
|
||||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@ -16,10 +17,18 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||||
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0-preview.6.24328.4" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0-preview1" />
|
||||||
|
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||||
|
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -32,4 +41,34 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="Pages\Emails\ContactVerificationEmail.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Pages\Emails\EmailLayout.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Pages\Emails\LandingEmail.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Pages\Emails\PasswordResetEmail.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Pages\Emails\VerificationEmail.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Pages\Emails\AccountDeletionEmail.razor">
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="Pages\Emails\AccountDeletionEmail.razor" />
|
||||||
|
<AdditionalFiles Include="Pages\Emails\ContactVerificationEmail.razor" />
|
||||||
|
<AdditionalFiles Include="Pages\Emails\EmailLayout.razor" />
|
||||||
|
<AdditionalFiles Include="Pages\Emails\LandingEmail.razor" />
|
||||||
|
<AdditionalFiles Include="Pages\Emails\PasswordResetEmail.razor" />
|
||||||
|
<AdditionalFiles Include="Pages\Emails\VerificationEmail.razor" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class AccountEventResource
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
5
DysonNetwork.Pass/Localization/EmailResource.cs
Normal file
5
DysonNetwork.Pass/Localization/EmailResource.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class EmailResource
|
||||||
|
{
|
||||||
|
}
|
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class NotificationResource
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Pass.Localization;
|
||||||
|
|
||||||
|
public class SharedResource
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
42
DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
Normal file
42
DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@using DysonNetwork.Pass.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<p class="font-bold">@(Localizer["AccountDeletionHeader"])</p>
|
||||||
|
<p>@(Localizer["AccountDeletionPara1"]) @@@Name,</p>
|
||||||
|
<p>@(Localizer["AccountDeletionPara2"])</p>
|
||||||
|
<p>@(Localizer["AccountDeletionPara3"])</p>
|
||||||
|
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="@Link" target="_blank">
|
||||||
|
@(Localizer["AccountDeletionButton"])
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>@(Localizer["AccountDeletionPara4"])</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Link { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
@using DysonNetwork.Pass.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<p class="font-bold">@(Localizer["ContactVerificationHeader"])</p>
|
||||||
|
<p>@(Localizer["ContactVerificationPara1"]) @Name,</p>
|
||||||
|
<p>@(Localizer["ContactVerificationPara2"])</p>
|
||||||
|
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="@Link" target="_blank">
|
||||||
|
@(Localizer["ContactVerificationButton"])
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>@(Localizer["ContactVerificationPara3"])</p>
|
||||||
|
<p>@(Localizer["ContactVerificationPara4"])</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Link { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
}
|
337
DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
Normal file
337
DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<style media="all" type="text/css">
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f4f5f6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #f4f5f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #eaebed;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
padding-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #9a9ea6;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0867ec;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 100% !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 2px #0867ec;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #0867ec;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #0867ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #0867ec;
|
||||||
|
border-color: #0867ec;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-code
|
||||||
|
{
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media all {
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #ec0867 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #ec0867 !important;
|
||||||
|
border-color: #ec0867 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-link {
|
||||||
|
color: #0867ec !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media only screen and (max-width: 640px) {
|
||||||
|
.main p,
|
||||||
|
.main td,
|
||||||
|
.main span {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
@ChildContent
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block">
|
||||||
|
<span class="apple-link">Solar Network</span>
|
||||||
|
<br> Solsynth LLC © @(DateTime.Now.Year)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by <a href="https://github.com/solsynth/dysonnetwork">Dyson Network</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER --></div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
}
|
43
DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
Normal file
43
DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@using DysonNetwork.Pass.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<p class="font-bold">@(Localizer["LandingHeader1"])</p>
|
||||||
|
<p>@(Localizer["LandingPara1"]) @@@Name,</p>
|
||||||
|
<p>@(Localizer["LandingPara2"])</p>
|
||||||
|
<p>@(Localizer["LandingPara3"])</p>
|
||||||
|
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="@Link" target="_blank">
|
||||||
|
@(Localizer["LandingButton1"])
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>@(Localizer["LandingPara4"])</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Link { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
}
|
44
DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
Normal file
44
DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@using DysonNetwork.Pass.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<p class="font-bold">@(Localizer["PasswordResetHeader"])</p>
|
||||||
|
<p>@(Localizer["PasswordResetPara1"]) @@@Name,</p>
|
||||||
|
<p>@(Localizer["PasswordResetPara2"])</p>
|
||||||
|
<p>@(Localizer["PasswordResetPara3"])</p>
|
||||||
|
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="@Link" target="_blank">
|
||||||
|
@(Localizer["PasswordResetButton"])
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>@(Localizer["PasswordResetPara4"])</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Link { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||||
|
}
|
27
DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
Normal file
27
DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@using DysonNetwork.Pass.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Pass.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<p class="font-bold">@(Localizer["VerificationHeader1"])</p>
|
||||||
|
<p>@(Localizer["VerificationPara1"]) @@@Name,</p>
|
||||||
|
<p>@(Localizer["VerificationPara2"])</p>
|
||||||
|
<p>@(Localizer["VerificationPara3"])</p>
|
||||||
|
|
||||||
|
<p class="verification-code">@Code</p>
|
||||||
|
|
||||||
|
<p>@(Localizer["VerificationPara4"])</p>
|
||||||
|
<p>@(Localizer["VerificationPara5"])</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Code { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
namespace DysonNetwork.Sphere.Permission;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
using System;
|
namespace DysonNetwork.Pass.Permission;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
|
||||||
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
public class RequiredPermissionAttribute(string area, string key) : Attribute
|
@ -1,10 +1,10 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Permission;
|
namespace DysonNetwork.Pass.Permission;
|
||||||
|
|
||||||
public class PermissionService(
|
public class PermissionService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
@ -1,4 +1,6 @@
|
|||||||
using DysonNetwork.Pass.Data;
|
using DysonNetwork.Pass;
|
||||||
|
using DysonNetwork.Pass.Account;
|
||||||
|
using DysonNetwork.Pass.Auth;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@ -8,15 +10,21 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddGrpc();
|
||||||
builder.Services.AddDbContext<AppDatabase>(options =>
|
builder.Services.AddDbContext<AppDatabase>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("App")));
|
options.UseNpgsql(builder.Configuration.GetConnectionString("App")));
|
||||||
|
|
||||||
|
builder.Services.AddScoped<AccountService>();
|
||||||
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
app.MapGrpcService<AccountGrpcService>();
|
||||||
|
app.MapGrpcService<AuthGrpcService>();
|
||||||
|
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
|
@ -4,7 +4,7 @@ using NodaTime;
|
|||||||
using NodaTime.Serialization.JsonNet;
|
using NodaTime.Serialization.JsonNet;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Storage;
|
namespace DysonNetwork.Shared.Cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a distributed lock that can be used to synchronize access across multiple processes
|
/// Represents a distributed lock that can be used to synchronize access across multiple processes
|
@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
|
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -20,7 +20,9 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||||
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<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="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Account.Proto;
|
|
||||||
using Grpc.Core;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using DysonNetwork.Sphere.Auth;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
|
||||||
|
|
||||||
public class AccountGrpcService : DysonNetwork.Sphere.Account.Proto.AccountService.AccountServiceBase
|
|
||||||
{
|
|
||||||
private readonly AppDatabase _db;
|
|
||||||
private readonly AuthService _auth;
|
|
||||||
|
|
||||||
public AccountGrpcService(AppDatabase db, AuthService auth)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_auth = auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<AccountResponse> GetAccount(Empty request, ServerCallContext context)
|
|
||||||
{
|
|
||||||
var account = await GetAccountFromContext(context);
|
|
||||||
return ToAccountResponse(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<AccountResponse> UpdateAccount(UpdateAccountRequest request, ServerCallContext context)
|
|
||||||
{
|
|
||||||
var account = await GetAccountFromContext(context);
|
|
||||||
|
|
||||||
if (request.Email != null)
|
|
||||||
{
|
|
||||||
var emailContact = await _db.AccountContacts.FirstOrDefaultAsync(c => c.AccountId == account.Id && c.Type == AccountContactType.Email);
|
|
||||||
if (emailContact != null)
|
|
||||||
{
|
|
||||||
emailContact.Content = request.Email;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
account.Contacts.Add(new AccountContact { Type = AccountContactType.Email, Content = request.Email });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.DisplayName != null)
|
|
||||||
{
|
|
||||||
account.Nick = request.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return ToAccountResponse(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Shared.Models.Account> GetAccountFromContext(ServerCallContext context)
|
|
||||||
{
|
|
||||||
var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization");
|
|
||||||
if (authorizationHeader == null)
|
|
||||||
{
|
|
||||||
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Missing authorization header."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = authorizationHeader.Value.Replace("Bearer ", "");
|
|
||||||
if (!_auth.ValidateToken(token, out var sessionId))
|
|
||||||
{
|
|
||||||
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid token."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var session = await _db.AuthSessions.Include(s => s.Account).ThenInclude(a => a.Contacts).FirstOrDefaultAsync(s => s.Id == sessionId);
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Session not found."));
|
|
||||||
}
|
|
||||||
|
|
||||||
return session.Account;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AccountResponse ToAccountResponse(Shared.Models.Account account)
|
|
||||||
{
|
|
||||||
var emailContact = account.Contacts.FirstOrDefault(c => c.Type == AccountContactType.Email);
|
|
||||||
return new AccountResponse
|
|
||||||
{
|
|
||||||
Id = account.Id.ToString(),
|
|
||||||
Username = account.Name,
|
|
||||||
Email = emailContact?.Content ?? "",
|
|
||||||
DisplayName = account.Nick
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,309 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using DysonNetwork.Shared.Models;
|
|
||||||
using DysonNetwork.Sphere.Connection;
|
|
||||||
using EFCore.BulkExtensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
|
||||||
|
|
||||||
public class NotificationService(
|
|
||||||
AppDatabase db,
|
|
||||||
WebSocketService ws,
|
|
||||||
IHttpClientFactory httpFactory,
|
|
||||||
IConfiguration config)
|
|
||||||
{
|
|
||||||
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
|
||||||
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
|
||||||
|
|
||||||
public async Task UnsubscribePushNotifications(string deviceId)
|
|
||||||
{
|
|
||||||
await db.NotificationPushSubscriptions
|
|
||||||
.Where(s => s.DeviceId == deviceId)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
|
||||||
Shared.Models.Account account,
|
|
||||||
NotificationPushProvider provider,
|
|
||||||
string deviceId,
|
|
||||||
string deviceToken
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
|
|
||||||
// First check if a matching subscription exists
|
|
||||||
var existingSubscription = await db.NotificationPushSubscriptions
|
|
||||||
.Where(s => s.AccountId == account.Id)
|
|
||||||
.Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (existingSubscription is not null)
|
|
||||||
{
|
|
||||||
// Update the existing subscription directly in the database
|
|
||||||
await db.NotificationPushSubscriptions
|
|
||||||
.Where(s => s.Id == existingSubscription.Id)
|
|
||||||
.ExecuteUpdateAsync(setters => setters
|
|
||||||
.SetProperty(s => s.DeviceId, deviceId)
|
|
||||||
.SetProperty(s => s.DeviceToken, deviceToken)
|
|
||||||
.SetProperty(s => s.UpdatedAt, now));
|
|
||||||
|
|
||||||
// Return the updated subscription
|
|
||||||
existingSubscription.DeviceId = deviceId;
|
|
||||||
existingSubscription.DeviceToken = deviceToken;
|
|
||||||
existingSubscription.UpdatedAt = now;
|
|
||||||
return existingSubscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = new NotificationPushSubscription
|
|
||||||
{
|
|
||||||
DeviceId = deviceId,
|
|
||||||
DeviceToken = deviceToken,
|
|
||||||
Provider = provider,
|
|
||||||
AccountId = account.Id,
|
|
||||||
};
|
|
||||||
|
|
||||||
db.NotificationPushSubscriptions.Add(subscription);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Notification> SendNotification(
|
|
||||||
Shared.Models.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
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (title is null && subtitle is null && content is null)
|
|
||||||
throw new ArgumentException("Unable to send notification that completely empty.");
|
|
||||||
|
|
||||||
meta ??= new Dictionary<string, object>();
|
|
||||||
if (actionUri is not null) meta["action_uri"] = actionUri;
|
|
||||||
|
|
||||||
var notification = new Notification
|
|
||||||
{
|
|
||||||
Topic = topic,
|
|
||||||
Title = title,
|
|
||||||
Subtitle = subtitle,
|
|
||||||
Content = content,
|
|
||||||
Meta = meta,
|
|
||||||
AccountId = account.Id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
db.Add(notification);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSilent) _ = DeliveryNotification(notification);
|
|
||||||
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeliveryNotification(Notification notification)
|
|
||||||
{
|
|
||||||
ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
|
||||||
{
|
|
||||||
Type = "notifications.new",
|
|
||||||
Data = notification
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pushing the notification
|
|
||||||
var subscribers = await db.NotificationPushSubscriptions
|
|
||||||
.Where(s => s.AccountId == notification.AccountId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
await _PushNotification(notification, subscribers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task MarkNotificationsViewed(ICollection<Notification> notifications)
|
|
||||||
{
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
|
|
||||||
if (id.Count == 0) return;
|
|
||||||
|
|
||||||
await db.Notifications
|
|
||||||
.Where(n => id.Contains(n.Id))
|
|
||||||
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BroadcastNotification(Notification notification, bool save = false)
|
|
||||||
{
|
|
||||||
var accounts = await db.Accounts.ToListAsync();
|
|
||||||
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
var notifications = accounts.Select(x =>
|
|
||||||
{
|
|
||||||
var newNotification = new Notification
|
|
||||||
{
|
|
||||||
Topic = notification.Topic,
|
|
||||||
Title = notification.Title,
|
|
||||||
Subtitle = notification.Subtitle,
|
|
||||||
Content = notification.Content,
|
|
||||||
Meta = notification.Meta,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Account = x,
|
|
||||||
AccountId = x.Id
|
|
||||||
};
|
|
||||||
return newNotification;
|
|
||||||
}).ToList();
|
|
||||||
await db.BulkInsertAsync(notifications);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var account in accounts)
|
|
||||||
{
|
|
||||||
notification.Account = account;
|
|
||||||
notification.AccountId = account.Id;
|
|
||||||
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
|
||||||
{
|
|
||||||
Type = "notifications.new",
|
|
||||||
Data = notification
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscribers = await db.NotificationPushSubscriptions
|
|
||||||
.ToListAsync();
|
|
||||||
await _PushNotification(notification, subscribers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts, bool save = false)
|
|
||||||
{
|
|
||||||
if (save)
|
|
||||||
{
|
|
||||||
var notifications = accounts.Select(x =>
|
|
||||||
{
|
|
||||||
var newNotification = new Notification
|
|
||||||
{
|
|
||||||
Topic = notification.Topic,
|
|
||||||
Title = notification.Title,
|
|
||||||
Subtitle = notification.Subtitle,
|
|
||||||
Content = notification.Content,
|
|
||||||
Meta = notification.Meta,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Account = x,
|
|
||||||
AccountId = x.Id
|
|
||||||
};
|
|
||||||
return newNotification;
|
|
||||||
}).ToList();
|
|
||||||
await db.BulkInsertAsync(notifications);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var account in accounts)
|
|
||||||
{
|
|
||||||
notification.Account = account;
|
|
||||||
notification.AccountId = account.Id;
|
|
||||||
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
|
||||||
{
|
|
||||||
Type = "notifications.new",
|
|
||||||
Data = notification
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountsId = accounts.Select(x => x.Id).ToList();
|
|
||||||
var subscribers = await db.NotificationPushSubscriptions
|
|
||||||
.Where(s => accountsId.Contains(s.AccountId))
|
|
||||||
.ToListAsync();
|
|
||||||
await _PushNotification(notification, subscribers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
|
|
||||||
IEnumerable<NotificationPushSubscription> subscriptions)
|
|
||||||
{
|
|
||||||
var subDict = subscriptions
|
|
||||||
.GroupBy(x => x.Provider)
|
|
||||||
.ToDictionary(x => x.Key, x => x.ToList());
|
|
||||||
|
|
||||||
var notifications = subDict.Select(value =>
|
|
||||||
{
|
|
||||||
var platformCode = value.Key switch
|
|
||||||
{
|
|
||||||
NotificationPushProvider.Apple => 1,
|
|
||||||
NotificationPushProvider.Google => 2,
|
|
||||||
_ => throw new InvalidOperationException($"Unknown push provider: {value.Key}")
|
|
||||||
};
|
|
||||||
|
|
||||||
var tokens = value.Value.Select(x => x.DeviceToken).ToList();
|
|
||||||
return _BuildNotificationPayload(notification, platformCode, tokens);
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
return notifications.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, object> _BuildNotificationPayload(Notification notification, int platformCode,
|
|
||||||
IEnumerable<string> deviceTokens)
|
|
||||||
{
|
|
||||||
var alertDict = new Dictionary<string, object>();
|
|
||||||
var dict = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["notif_id"] = notification.Id.ToString(),
|
|
||||||
["apns_id"] = notification.Id.ToString(),
|
|
||||||
["topic"] = _notifyTopic,
|
|
||||||
["tokens"] = deviceTokens,
|
|
||||||
["data"] = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["type"] = notification.Topic,
|
|
||||||
["meta"] = notification.Meta ?? new Dictionary<string, object>(),
|
|
||||||
},
|
|
||||||
["mutable_content"] = true,
|
|
||||||
["priority"] = notification.Priority >= 5 ? "high" : "normal",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(notification.Title))
|
|
||||||
{
|
|
||||||
dict["title"] = notification.Title;
|
|
||||||
alertDict["title"] = notification.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(notification.Content))
|
|
||||||
{
|
|
||||||
dict["message"] = notification.Content;
|
|
||||||
alertDict["body"] = notification.Content;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(notification.Subtitle))
|
|
||||||
{
|
|
||||||
dict["message"] = $"{notification.Subtitle}\n{dict["message"]}";
|
|
||||||
alertDict["subtitle"] = notification.Subtitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.Priority >= 5)
|
|
||||||
dict["name"] = "default";
|
|
||||||
|
|
||||||
dict["platform"] = platformCode;
|
|
||||||
dict["alert"] = alertDict;
|
|
||||||
|
|
||||||
return dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task _PushNotification(Notification notification,
|
|
||||||
IEnumerable<NotificationPushSubscription> subscriptions)
|
|
||||||
{
|
|
||||||
var subList = subscriptions.ToList();
|
|
||||||
if (subList.Count == 0) return;
|
|
||||||
|
|
||||||
var requestDict = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["notifications"] = _BuildNotificationPayload(notification, subList)
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = httpFactory.CreateClient();
|
|
||||||
client.BaseAddress = _notifyEndpoint;
|
|
||||||
var request = await client.PostAsync("/push", new StringContent(
|
|
||||||
JsonSerializer.Serialize(requestDict),
|
|
||||||
Encoding.UTF8,
|
|
||||||
"application/json"
|
|
||||||
));
|
|
||||||
request.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,8 @@ public class ActivityService(
|
|||||||
PublisherService pub,
|
PublisherService pub,
|
||||||
RelationshipService rels,
|
RelationshipService rels,
|
||||||
PostService ps,
|
PostService ps,
|
||||||
DiscoveryService ds)
|
DiscoveryService ds
|
||||||
|
)
|
||||||
{
|
{
|
||||||
private static double CalculateHotRank(Post.Post post, Instant now)
|
private static double CalculateHotRank(Post.Post post, Instant now)
|
||||||
{
|
{
|
||||||
|
@ -1,22 +1,12 @@
|
|||||||
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.Account;
|
|
||||||
using DysonNetwork.Sphere.Auth;
|
|
||||||
using DysonNetwork.Sphere.Chat;
|
|
||||||
using DysonNetwork.Sphere.Developer;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Publisher;
|
|
||||||
using DysonNetwork.Sphere.Realm;
|
|
||||||
using DysonNetwork.Sphere.Sticker;
|
using DysonNetwork.Sphere.Sticker;
|
||||||
using DysonNetwork.Sphere.Storage;
|
|
||||||
using DysonNetwork.Sphere.Wallet;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using Microsoft.EntityFrameworkCore.Query;
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Npgsql;
|
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere;
|
namespace DysonNetwork.Sphere;
|
||||||
@ -33,28 +23,6 @@ public class AppDatabase(
|
|||||||
IConfiguration configuration
|
IConfiguration configuration
|
||||||
) : DbContext(options)
|
) : DbContext(options)
|
||||||
{
|
{
|
||||||
public DbSet<PermissionNode> PermissionNodes { get; set; }
|
|
||||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
|
||||||
public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; }
|
|
||||||
|
|
||||||
public DbSet<MagicSpell> MagicSpells { get; set; }
|
|
||||||
public DbSet<Shared.Models.Account> Accounts { get; set; }
|
|
||||||
public DbSet<AccountConnection> AccountConnections { get; set; }
|
|
||||||
public DbSet<Profile> AccountProfiles { get; set; }
|
|
||||||
public DbSet<AccountContact> AccountContacts { get; set; }
|
|
||||||
public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; }
|
|
||||||
public DbSet<Relationship> AccountRelationships { get; set; }
|
|
||||||
public DbSet<Status> AccountStatuses { get; set; }
|
|
||||||
public DbSet<CheckInResult> AccountCheckInResults { get; set; }
|
|
||||||
public DbSet<Notification> Notifications { get; set; }
|
|
||||||
public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; }
|
|
||||||
public DbSet<Badge> Badges { get; set; }
|
|
||||||
public DbSet<ActionLog> ActionLogs { get; set; }
|
|
||||||
public DbSet<AbuseReport> AbuseReports { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Session> AuthSessions { get; set; }
|
|
||||||
public DbSet<Challenge> AuthChallenges { get; set; }
|
|
||||||
|
|
||||||
public DbSet<CloudFile> Files { get; set; }
|
public DbSet<CloudFile> Files { get; set; }
|
||||||
public DbSet<CloudFileReference> FileReferences { get; set; }
|
public DbSet<CloudFileReference> FileReferences { get; set; }
|
||||||
|
|
||||||
@ -104,39 +72,7 @@ public class AppDatabase(
|
|||||||
.UseNetTopologySuite()
|
.UseNetTopologySuite()
|
||||||
.UseNodaTime()
|
.UseNodaTime()
|
||||||
).UseSnakeCaseNamingConvention();
|
).UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
|
||||||
{
|
|
||||||
var defaultPermissionGroup = await context.Set<PermissionGroup>()
|
|
||||||
.FirstOrDefaultAsync(g => g.Key == "default", cancellationToken);
|
|
||||||
if (defaultPermissionGroup is null)
|
|
||||||
{
|
|
||||||
context.Set<PermissionGroup>().Add(new PermissionGroup
|
|
||||||
{
|
|
||||||
Key = "default",
|
|
||||||
Nodes = new List<string>
|
|
||||||
{
|
|
||||||
"posts.create",
|
|
||||||
"posts.react",
|
|
||||||
"publishers.create",
|
|
||||||
"files.create",
|
|
||||||
"chat.create",
|
|
||||||
"chat.messages.create",
|
|
||||||
"chat.realtime.create",
|
|
||||||
"accounts.statuses.create",
|
|
||||||
"accounts.statuses.update",
|
|
||||||
"stickers.packs.create",
|
|
||||||
"stickers.create"
|
|
||||||
}.Select(permission =>
|
|
||||||
PermissionService.NewPermissionNode("group:default", "global", permission, true))
|
|
||||||
.ToList()
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
optionsBuilder.UseSeeding((context, _) => {});
|
|
||||||
|
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,25 +80,6 @@ public class AppDatabase(
|
|||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity<PermissionGroupMember>()
|
|
||||||
.HasKey(pg => new { pg.GroupId, pg.Actor });
|
|
||||||
modelBuilder.Entity<PermissionGroupMember>()
|
|
||||||
.HasOne(pg => pg.Group)
|
|
||||||
.WithMany(g => g.Members)
|
|
||||||
.HasForeignKey(pg => pg.GroupId)
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Relationship>()
|
|
||||||
.HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId });
|
|
||||||
modelBuilder.Entity<Relationship>()
|
|
||||||
.HasOne(r => r.Account)
|
|
||||||
.WithMany(a => a.OutgoingRelationships)
|
|
||||||
.HasForeignKey(r => r.AccountId);
|
|
||||||
modelBuilder.Entity<Relationship>()
|
|
||||||
.HasOne(r => r.Related)
|
|
||||||
.WithMany(a => a.IncomingRelationships)
|
|
||||||
.HasForeignKey(r => r.RelatedId);
|
|
||||||
|
|
||||||
modelBuilder.Entity<PublisherMember>()
|
modelBuilder.Entity<PublisherMember>()
|
||||||
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
|
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
|
||||||
modelBuilder.Entity<PublisherMember>()
|
modelBuilder.Entity<PublisherMember>()
|
||||||
@ -333,23 +250,9 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
|
|||||||
{
|
{
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
|
|
||||||
logger.LogInformation("Cleaning up expired records...");
|
|
||||||
|
|
||||||
// Expired relationships
|
|
||||||
var affectedRows = await db.AccountRelationships
|
|
||||||
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows);
|
|
||||||
// Expired permission group members
|
|
||||||
affectedRows = await db.PermissionGroupMembers
|
|
||||||
.Where(x => x.ExpiredAt != null && x.ExpiredAt <= now)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows);
|
|
||||||
|
|
||||||
logger.LogInformation("Deleting soft-deleted records...");
|
logger.LogInformation("Deleting soft-deleted records...");
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var threshold = now - Duration.FromDays(7);
|
var threshold = now - Duration.FromDays(7);
|
||||||
|
|
||||||
var entityTypes = db.Model.GetEntityTypes()
|
var entityTypes = db.Model.GetEntityTypes()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -4,6 +4,7 @@ using Livekit.Server.Sdk.Dotnet;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Auth\" />
|
||||||
<Folder Include="Discovery\" />
|
<Folder Include="Discovery\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -164,7 +165,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Grpc.AspNetCore" Version="2.65.0" />
|
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Connection.WebReader;
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
@ -24,6 +24,7 @@ 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.Shared.Cache;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
||||||
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
|
||||||
using DysonNetwork.Sphere.Connection.WebReader;
|
using DysonNetwork.Sphere.Connection.WebReader;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
@ -2,6 +2,7 @@ using System.Globalization;
|
|||||||
using FFMpegCore;
|
using FFMpegCore;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using AngleSharp.Text;
|
using AngleSharp.Text;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Minio;
|
using Minio;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
|
Reference in New Issue
Block a user