6 Commits

207 changed files with 2698 additions and 102028 deletions

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
public enum AbuseReportType
{
@ -26,5 +26,5 @@ public class AbuseReport : ModelBase
[MaxLength(8192)] public string? Resolution { get; set; }
public Guid AccountId { get; set; }
public Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
}

View File

@ -1,14 +1,12 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Sphere.Permission;
using Microsoft.AspNetCore.Authorization;
using DysonNetwork.Shared.Models;
using DysonNetwork.Pass.Auth;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Extensions;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
[ApiController]
[Route("/accounts")]
@ -20,9 +18,9 @@ public class AccountController(
) : ControllerBase
{
[HttpGet("{name}")]
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Account?>> GetByName(string name)
public async Task<ActionResult<Shared.Models.Account?>> GetByName(string name)
{
var account = await db.Accounts
.Include(e => e.Badges)
@ -73,9 +71,9 @@ public class AccountController(
}
[HttpPost]
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Account>> CreateAccount([FromBody] AccountCreateRequest request)
public async Task<ActionResult<Shared.Models.Account>> CreateAccount([FromBody] AccountCreateRequest request)
{
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
@ -163,7 +161,7 @@ public class AccountController(
}
[HttpGet("search")]
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
public async Task<List<Shared.Models.Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
{
if (string.IsNullOrWhiteSpace(query))
return [];

View File

@ -1,14 +1,15 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Org.BouncyCastle.Utilities;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
[Authorize]
[ApiController]
@ -16,16 +17,15 @@ namespace DysonNetwork.Sphere.Account;
public class AccountCurrentController(
AppDatabase db,
AccountService accounts,
FileReferenceService fileRefService,
AccountEventService events,
AuthService auth
) : ControllerBase
{
[HttpGet]
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
public async Task<ActionResult<Account>> GetCurrentIdentity()
[ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)]
public async Task<ActionResult<Shared.Models.Account>> GetCurrentIdentity()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var account = await db.Accounts
@ -44,9 +44,9 @@ public class AccountCurrentController(
}
[HttpPatch]
public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
public async Task<ActionResult<Shared.Models.Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
@ -77,7 +77,7 @@ public class AccountCurrentController(
[HttpPatch("profile")]
public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var profile = await db.AccountProfiles
@ -95,61 +95,61 @@ public class AccountCurrentController(
if (request.Location is not null) profile.Location = request.Location;
if (request.TimeZone is not null) profile.TimeZone = request.TimeZone;
if (request.PictureId is not null)
{
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.");
var profileResourceId = $"profile:{profile.Id}";
// Remove old references for the profile picture
if (profile.Picture is not null)
{
var oldPictureRefs =
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
foreach (var oldRef in oldPictureRefs)
{
await fileRefService.DeleteReferenceAsync(oldRef.Id);
}
}
profile.Picture = picture.ToReferenceObject();
// Create new reference
await fileRefService.CreateReferenceAsync(
picture.Id,
"profile.picture",
profileResourceId
);
}
if (request.BackgroundId is not null)
{
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.");
var profileResourceId = $"profile:{profile.Id}";
// Remove old references for the profile background
if (profile.Background is not null)
{
var oldBackgroundRefs =
await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
foreach (var oldRef in oldBackgroundRefs)
{
await fileRefService.DeleteReferenceAsync(oldRef.Id);
}
}
profile.Background = background.ToReferenceObject();
// Create new reference
await fileRefService.CreateReferenceAsync(
background.Id,
"profile.background",
profileResourceId
);
}
// if (request.PictureId is not null)
// {
// 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.");
//
// var profileResourceId = $"profile:{profile.Id}";
//
// // Remove old references for the profile picture
// if (profile.Picture is not null)
// {
// var oldPictureRefs =
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture");
// foreach (var oldRef in oldPictureRefs)
// {
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
// }
// }
//
// profile.Picture = picture.ToReferenceObject();
//
// // Create new reference
// await fileRefService.CreateReferenceAsync(
// picture.Id,
// "profile.picture",
// profileResourceId
// );
// }
//
// if (request.BackgroundId is not null)
// {
// 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.");
//
// var profileResourceId = $"profile:{profile.Id}";
//
// // Remove old references for the profile background
// if (profile.Background is not null)
// {
// var oldBackgroundRefs =
// await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background");
// foreach (var oldRef in oldBackgroundRefs)
// {
// await fileRefService.DeleteReferenceAsync(oldRef.Id);
// }
// }
//
// profile.Background = background.ToReferenceObject();
//
// // Create new reference
// await fileRefService.CreateReferenceAsync(
// background.Id,
// "profile.background",
// profileResourceId
// );
// }
db.Update(profile);
await db.SaveChangesAsync();
@ -162,7 +162,7 @@ public class AccountCurrentController(
[HttpDelete]
public async Task<ActionResult> RequestDeleteAccount()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -179,7 +179,7 @@ public class AccountCurrentController(
[HttpGet("statuses")]
public async Task<ActionResult<Status>> GetCurrentStatus()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var status = await events.GetStatus(currentUser.Id);
return Ok(status);
}
@ -188,7 +188,7 @@ public class AccountCurrentController(
[RequiredPermission("global", "accounts.statuses.update")]
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var now = SystemClock.Instance.GetCurrentInstant();
var status = await db.AccountStatuses
@ -215,7 +215,7 @@ public class AccountCurrentController(
[RequiredPermission("global", "accounts.statuses.create")]
public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var status = new Status
{
@ -233,7 +233,7 @@ public class AccountCurrentController(
[HttpDelete("me/statuses")]
public async Task<ActionResult> DeleteStatus()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var now = SystemClock.Instance.GetCurrentInstant();
var status = await db.AccountStatuses
@ -250,7 +250,7 @@ public class AccountCurrentController(
[HttpGet("check-in")]
public async Task<ActionResult<CheckInResult>> GetCheckInResult()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var now = SystemClock.Instance.GetCurrentInstant();
@ -270,7 +270,7 @@ public class AccountCurrentController(
[HttpPost("check-in")]
public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var isAvailable = await events.CheckInDailyIsAvailable(currentUser);
if (!isAvailable)
@ -297,7 +297,7 @@ public class AccountCurrentController(
public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month,
[FromQuery] int? year)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
month ??= currentDate.Month;
@ -318,7 +318,7 @@ public class AccountCurrentController(
[FromQuery] int offset = 0
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var query = db.ActionLogs
.Where(log => log.AccountId == currentUser.Id)
@ -338,7 +338,7 @@ public class AccountCurrentController(
[HttpGet("factors")]
public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var factors = await db.AccountAuthFactors
.Include(f => f.Account)
@ -358,7 +358,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
if (await accounts.CheckAuthFactorExists(currentUser, request.Type))
return BadRequest($"Auth factor with type {request.Type} is already exists.");
@ -370,7 +370,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var factor = await db.AccountAuthFactors
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
@ -392,7 +392,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var factor = await db.AccountAuthFactors
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
@ -414,7 +414,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var factor = await db.AccountAuthFactors
.Where(f => f.AccountId == currentUser.Id && f.Id == id)
@ -445,7 +445,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
@ -480,7 +480,7 @@ public class AccountCurrentController(
[FromQuery] int offset = 0
)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
var query = db.AuthSessions
@ -505,7 +505,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<Session>> DeleteSession(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -522,7 +522,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<Session>> DeleteCurrentSession()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
try
@ -539,7 +539,7 @@ public class AccountCurrentController(
[HttpPatch("sessions/{id:guid}/label")]
public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -555,7 +555,7 @@ public class AccountCurrentController(
[HttpPatch("sessions/current/label")]
public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
try
@ -573,7 +573,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<List<AccountContact>>> GetContacts()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var contacts = await db.AccountContacts
.Where(c => c.AccountId == currentUser.Id)
@ -592,7 +592,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -609,7 +609,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountContact>> VerifyContact(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var contact = await db.AccountContacts
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
@ -631,7 +631,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var contact = await db.AccountContacts
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
@ -653,7 +653,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<AccountContact>> DeleteContact(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var contact = await db.AccountContacts
.Where(c => c.AccountId == currentUser.Id && c.Id == id)
@ -676,9 +676,9 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<List<Badge>>> GetBadges()
{
if (HttpContext.Items["CurrentUser"] is not 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)
.ToListAsync();
return Ok(badges);
@ -688,7 +688,7 @@ public class AccountCurrentController(
[Authorize]
public async Task<ActionResult<Badge>> ActivateBadge(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{

View File

@ -1,21 +1,17 @@
using System.Globalization;
using DysonNetwork.Sphere.Activity;
using DysonNetwork.Sphere.Connection;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Sphere.Wallet;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization;
using NodaTime;
using Org.BouncyCastle.Asn1.X509;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
public class AccountEventService(
AppDatabase db,
WebSocketService ws,
ICacheService cache,
PaymentService payment,
// WebSocketService ws,
// ICacheService cache,
// PaymentService payment,
IStringLocalizer<Localization.AccountEventResource> localizer
)
{
@ -25,18 +21,18 @@ public class AccountEventService(
public void PurgeStatusCache(Guid userId)
{
var cacheKey = $"{StatusCacheKey}{userId}";
cache.RemoveAsync(cacheKey);
// cache.RemoveAsync(cacheKey);
}
public async Task<Status> GetStatus(Guid userId)
{
var cacheKey = $"{StatusCacheKey}{userId}";
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
if (cachedStatus is not null)
{
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
return cachedStatus;
}
// var cachedStatus = await cache.GetAsync<Status>(cacheKey);
// if (cachedStatus is not null)
// {
// cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
// return cachedStatus;
// }
var now = SystemClock.Instance.GetCurrentInstant();
var status = await db.AccountStatuses
@ -44,12 +40,13 @@ public class AccountEventService(
.Where(e => e.ClearedAt == null || e.ClearedAt > now)
.OrderByDescending(e => e.CreatedAt)
.FirstOrDefaultAsync();
var isOnline = ws.GetAccountIsConnected(userId);
// var isOnline = ws.GetAccountIsConnected(userId);
var isOnline = false; // Placeholder
if (status is not null)
{
status.IsOnline = !status.IsInvisible && isOnline;
await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
TimeSpan.FromMinutes(5));
// await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"],
// TimeSpan.FromMinutes(5));
return status;
}
@ -83,16 +80,16 @@ public class AccountEventService(
foreach (var userId in userIds)
{
var cacheKey = $"{StatusCacheKey}{userId}";
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
if (cachedStatus != null)
{
cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
results[userId] = cachedStatus;
}
else
{
// var cachedStatus = await cache.GetAsync<Status>(cacheKey);
// if (cachedStatus != null)
// {
// cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
// results[userId] = cachedStatus;
// }
// else
// {
cacheMissUserIds.Add(userId);
}
// }
}
if (cacheMissUserIds.Any())
@ -109,11 +106,12 @@ public class AccountEventService(
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;
results[status.AccountId] = status;
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);
}
@ -122,7 +120,8 @@ public class AccountEventService(
{
foreach (var userId in usersWithoutStatus)
{
var isOnline = ws.GetAccountIsConnected(userId);
// var isOnline = ws.GetAccountIsConnected(userId);
var isOnline = false; // Placeholder
var defaultStatus = new Status
{
Attitude = StatusAttitude.Neutral,
@ -139,7 +138,7 @@ public class AccountEventService(
return results;
}
public async Task<Status> CreateStatus(Account user, Status status)
public async Task<Status> CreateStatus(Shared.Models.Account user, Status status)
{
var now = SystemClock.Instance.GetCurrentInstant();
await db.AccountStatuses
@ -152,7 +151,7 @@ public class AccountEventService(
return status;
}
public async Task ClearStatus(Account user, Status status)
public async Task ClearStatus(Shared.Models.Account user, Status status)
{
status.ClearedAt = SystemClock.Instance.GetCurrentInstant();
db.Update(status);
@ -164,19 +163,19 @@ public class AccountEventService(
private const string CaptchaCacheKey = "CheckInCaptcha_";
private const int CaptchaProbabilityPercent = 20;
public async Task<bool> CheckInDailyDoAskCaptcha(Account user)
public async Task<bool> CheckInDailyDoAskCaptcha(Shared.Models.Account user)
{
var cacheKey = $"{CaptchaCacheKey}{user.Id}";
var needsCaptcha = await cache.GetAsync<bool?>(cacheKey);
if (needsCaptcha is not null)
return needsCaptcha!.Value;
// var needsCaptcha = await cache.GetAsync<bool?>(cacheKey);
// if (needsCaptcha is not null)
// return needsCaptcha!.Value;
var result = Random.Next(100) < CaptchaProbabilityPercent;
await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
// await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24));
return result;
}
public async Task<bool> CheckInDailyIsAvailable(Account user)
public async Task<bool> CheckInDailyIsAvailable(Shared.Models.Account user)
{
var now = SystemClock.Instance.GetCurrentInstant();
var lastCheckIn = await db.AccountCheckInResults
@ -195,16 +194,16 @@ public class AccountEventService(
public const string CheckInLockKey = "CheckInLock_";
public async Task<CheckInResult> CheckInDaily(Account user)
public async Task<CheckInResult> CheckInDaily(Shared.Models.Account user)
{
var lockKey = $"{CheckInLockKey}{user.Id}";
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)
await lk.ReleaseAsync();
// if (lk != null)
// await lk.ReleaseAsync();
}
catch
{
@ -212,8 +211,8 @@ public class AccountEventService(
}
// Now try to acquire the lock properly
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.");
// 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.");
var cultureInfo = new CultureInfo(user.Language, false);
CultureInfo.CurrentCulture = cultureInfo;
@ -255,13 +254,14 @@ public class AccountEventService(
try
{
if (result.RewardPoints.HasValue)
await payment.CreateTransactionWithAccountAsync(
null,
user.Id,
WalletCurrency.SourcePoint,
result.RewardPoints.Value,
$"Check-in reward on {now:yyyy/MM/dd}"
);
// await payment.CreateTransactionWithAccountAsync(
// null,
// user.Id,
// WalletCurrency.SourcePoint,
// result.RewardPoints.Value,
// $"Check-in reward on {now:yyyy/MM/dd}"
// );
Console.WriteLine($"Simulating transaction for {result.RewardPoints.Value} points");
}
catch
{
@ -280,7 +280,7 @@ public class AccountEventService(
return result;
}
public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0,
public async Task<List<DailyEventResponse>> GetEventCalendar(Shared.Models.Account user, int month, int year = 0,
bool replaceInvisible = false)
{
if (year == 0)

View 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
{
};
}
}

View File

@ -1,32 +1,29 @@
using System.Globalization;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
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.Extensions.Localization;
using NodaTime;
using Org.BouncyCastle.Utilities;
using OtpNet;
using Microsoft.Extensions.Logging;
using EFCore.BulkExtensions;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
public class AccountService(
AppDatabase db,
MagicSpellService spells,
AccountUsernameService uname,
NotificationService nty,
EmailService mailer,
IStringLocalizer<NotificationResource> localizer,
// MagicSpellService spells,
// AccountUsernameService uname,
// NotificationService nty,
// EmailService mailer,
// IStringLocalizer<NotificationResource> localizer,
ICacheService cache,
ILogger<AccountService> logger
)
{
public static void SetCultureInfo(Account account)
public static void SetCultureInfo(Shared.Models.Account account)
{
SetCultureInfo(account.Language);
}
@ -40,12 +37,12 @@ public class AccountService(
public const string AccountCachePrefix = "account:";
public async Task PurgeAccountCache(Account account)
public async Task PurgeAccountCache(Shared.Models.Account account)
{
await cache.RemoveGroupAsync($"{AccountCachePrefix}{account.Id}");
}
public async Task<Account?> LookupAccount(string probe)
public async Task<Shared.Models.Account?> LookupAccount(string probe)
{
var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync();
if (account is not null) return account;
@ -57,7 +54,7 @@ public class AccountService(
return contact?.Account;
}
public async Task<Account?> LookupAccountByConnection(string identifier, string provider)
public async Task<Shared.Models.Account?> LookupAccountByConnection(string identifier, string provider)
{
var connection = await db.AccountConnections
.Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider)
@ -74,7 +71,7 @@ public class AccountService(
return profile?.Level;
}
public async Task<Account> CreateAccount(
public async Task<Shared.Models.Account> CreateAccount(
string name,
string nick,
string email,
@ -91,7 +88,7 @@ public class AccountService(
if (dupeNameCount > 0)
throw new InvalidOperationException("Account name has already been taken.");
var account = new Account
var account = new Shared.Models.Account
{
Name = name,
Nick = nick,
@ -135,15 +132,15 @@ public class AccountService(
}
else
{
var spell = await spells.CreateMagicSpell(
account,
MagicSpellType.AccountActivation,
new Dictionary<string, object>
{
{ "contact_method", account.Contacts.First().Content }
}
);
await spells.NotifyMagicSpell(spell, true);
// var spell = await spells.CreateMagicSpell(
// account,
// MagicSpellType.AccountActivation,
// new Dictionary<string, object>
// {
// { "contact_method", account.Contacts.First().Content }
// }
// );
// await spells.NotifyMagicSpell(spell, true);
}
db.Accounts.Add(account);
@ -159,7 +156,7 @@ public class AccountService(
}
}
public async Task<Account> CreateAccount(OidcUserInfo userInfo)
public async Task<Shared.Models.Account> CreateAccount(OidcUserInfo userInfo)
{
if (string.IsNullOrEmpty(userInfo.Email))
throw new ArgumentException("Email is required for account creation");
@ -169,7 +166,8 @@ public class AccountService(
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
// Generate username from email
var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
// var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
var username = userInfo.Email.Split('@')[0]; // Placeholder
return await CreateAccount(
username,
@ -182,31 +180,31 @@ public class AccountService(
);
}
public async Task RequestAccountDeletion(Account account)
public async Task RequestAccountDeletion(Shared.Models.Account account)
{
var spell = await spells.CreateMagicSpell(
account,
MagicSpellType.AccountRemoval,
new Dictionary<string, object>(),
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
preventRepeat: true
);
await spells.NotifyMagicSpell(spell);
// var spell = await spells.CreateMagicSpell(
// account,
// MagicSpellType.AccountRemoval,
// new Dictionary<string, object>(),
// SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
// preventRepeat: true
// );
// await spells.NotifyMagicSpell(spell);
}
public async Task RequestPasswordReset(Account account)
public async Task RequestPasswordReset(Shared.Models.Account account)
{
var spell = await spells.CreateMagicSpell(
account,
MagicSpellType.AuthPasswordReset,
new Dictionary<string, object>(),
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
preventRepeat: true
);
await spells.NotifyMagicSpell(spell);
// var spell = await spells.CreateMagicSpell(
// account,
// MagicSpellType.AuthPasswordReset,
// new Dictionary<string, object>(),
// SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
// preventRepeat: true
// );
// await spells.NotifyMagicSpell(spell);
}
public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type)
public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type)
{
var isExists = await db.AccountAuthFactors
.Where(x => x.AccountId == account.Id && x.Type == type)
@ -214,7 +212,7 @@ public class AccountService(
return isExists;
}
public async Task<AccountAuthFactor?> CreateAuthFactor(Account account, AccountAuthFactorType type, string? secret)
public async Task<AccountAuthFactor?> CreateAuthFactor(Shared.Models.Account account, AccountAuthFactorType type, string? secret)
{
AccountAuthFactor? factor = null;
switch (type)
@ -329,7 +327,7 @@ public class AccountService(
{
var count = await db.AccountAuthFactors
.Where(f => f.AccountId == factor.AccountId)
.If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
// .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
.CountAsync();
if (count <= 1)
throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
@ -345,7 +343,7 @@ public class AccountService(
/// <param name="account">The owner of the auth factor</param>
/// <param name="factor">The auth factor needed to send code</param>
/// <param name="hint">The part of the contact method for verification</param>
public async Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null)
public async Task SendFactorCode(Shared.Models.Account account, AccountAuthFactor factor, string? hint = null)
{
var code = new Random().Next(100000, 999999).ToString("000000");
@ -355,14 +353,14 @@ public class AccountService(
if (await _GetFactorCode(factor) is not null)
throw new InvalidOperationException("A factor code has been sent and in active duration.");
await nty.SendNotification(
account,
"auth.verification",
localizer["AuthCodeTitle"],
null,
localizer["AuthCodeBody", code],
save: true
);
// await nty.SendNotification(
// account,
// "auth.verification",
// localizer["AuthCodeTitle"],
// null,
// localizer["AuthCodeBody", code],
// save: true
// );
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5));
break;
case AccountAuthFactorType.EmailCode:
@ -397,16 +395,16 @@ public class AccountService(
return;
}
await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
account.Nick,
contact.Content,
localizer["VerificationEmail"],
new VerificationEmailModel
{
Name = account.Name,
Code = code
}
);
// await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>(
// account.Nick,
// contact.Content,
// localizer["VerificationEmail"],
// new VerificationEmailModel
// {
// Name = account.Name,
// Code = code
// }
// );
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
break;
@ -454,7 +452,7 @@ public class AccountService(
);
}
public async Task<Session> UpdateSessionLabel(Account account, Guid sessionId, string label)
public async Task<Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label)
{
var session = await db.AuthSessions
.Include(s => s.Challenge)
@ -477,7 +475,7 @@ public class AccountService(
return session;
}
public async Task DeleteSession(Account account, Guid sessionId)
public async Task DeleteSession(Shared.Models.Account account, Guid sessionId)
{
var session = await db.AuthSessions
.Include(s => s.Challenge)
@ -491,7 +489,7 @@ public class AccountService(
.ToListAsync();
if (session.Challenge.DeviceId is not null)
await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
// await nty.UnsubscribePushNotifications(session.Challenge.DeviceId);
// The current session should be included in the sessions' list
await db.AuthSessions
@ -503,7 +501,7 @@ public class AccountService(
await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
}
public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content)
public async Task<AccountContact> CreateContactMethod(Shared.Models.Account account, AccountContactType type, string content)
{
var contact = new AccountContact
{
@ -518,19 +516,19 @@ public class AccountService(
return contact;
}
public async Task VerifyContactMethod(Account account, AccountContact contact)
public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact)
{
var spell = await spells.CreateMagicSpell(
account,
MagicSpellType.ContactVerification,
new Dictionary<string, object> { { "contact_method", contact.Content } },
expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
preventRepeat: true
);
await spells.NotifyMagicSpell(spell);
// var spell = await spells.CreateMagicSpell(
// account,
// MagicSpellType.ContactVerification,
// new Dictionary<string, object> { { "contact_method", contact.Content } },
// expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
// preventRepeat: true
// );
// await spells.NotifyMagicSpell(spell);
}
public async Task<AccountContact> SetContactMethodPrimary(Account account, AccountContact contact)
public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact)
{
if (contact.AccountId != account.Id)
throw new InvalidOperationException("Contact method does not belong to this account.");
@ -559,7 +557,7 @@ public class AccountService(
}
}
public async Task DeleteContactMethod(Account account, AccountContact contact)
public async Task DeleteContactMethod(Shared.Models.Account account, AccountContact contact)
{
if (contact.AccountId != account.Id)
throw new InvalidOperationException("Contact method does not belong to this account.");
@ -574,10 +572,10 @@ public class AccountService(
/// This method will grant a badge to the account.
/// Shouldn't be exposed to normal user and the user itself.
/// </summary>
public async Task<Badge> GrantBadge(Account account, Badge badge)
public async Task<Badge> GrantBadge(Shared.Models.Account account, Badge badge)
{
badge.AccountId = account.Id;
db.Badges.Add(badge);
db.AccountBadges.Add(badge);
await db.SaveChangesAsync();
return badge;
}
@ -586,9 +584,9 @@ public class AccountService(
/// This method will revoke a badge from the account.
/// Shouldn't be exposed to normal user and the user itself.
/// </summary>
public async Task RevokeBadge(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)
.OrderByDescending(b => b.CreatedAt)
.FirstOrDefaultAsync();
@ -604,19 +602,19 @@ public class AccountService(
await db.SaveChangesAsync();
}
public async Task ActiveBadge(Account account, Guid badgeId)
public async Task ActiveBadge(Shared.Models.Account account, Guid badgeId)
{
await using var transaction = await db.Database.BeginTransactionAsync();
try
{
var badge = await db.Badges
var badge = await db.AccountBadges
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
.OrderByDescending(b => b.CreatedAt)
.FirstOrDefaultAsync();
if (badge is null) throw new InvalidOperationException("Badge was not found.");
await db.Badges
await db.AccountBadges
.Where(b => b.AccountId == account.Id && b.Id != badgeId)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null));

View File

@ -1,7 +1,7 @@
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
/// <summary>
/// Service for handling username generation and validation

View File

@ -1,11 +1,12 @@
using Quartz;
using DysonNetwork.Sphere.Connection;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Sphere.Storage.Handlers;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Http;
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)
{
@ -16,11 +17,11 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
Meta = meta,
};
fbs.Enqueue(log);
// fbs.Enqueue(log);
}
public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request,
Account? account = null)
Shared.Models.Account? account = null)
{
var log = new ActionLog
{
@ -28,19 +29,19 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs)
Meta = meta,
UserAgent = request.Headers.UserAgent,
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 Account currentUser)
if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
log.AccountId = currentUser.Id;
else if (account != null)
log.AccountId = account.Id;
else
throw new ArgumentException("No user context was found");
if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession)
if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
log.SessionId = currentSession.Id;
fbs.Enqueue(log);
// fbs.Enqueue(log);
}
}

View File

@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
public enum MagicSpellType
{
@ -26,5 +26,5 @@ public class MagicSpell : ModelBase
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
public Guid? AccountId { get; set; }
public Account? Account { get; set; }
public Shared.Models.Account? Account { get; set; }
}

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
[ApiController]
[Route("/spells")]

View File

@ -1,27 +1,25 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
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 DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
public class MagicSpellService(
AppDatabase db,
EmailService email,
// EmailService email,
IConfiguration configuration,
ILogger<MagicSpellService> logger,
IStringLocalizer<Localization.EmailResource> localizer
ILogger<MagicSpellService> logger
// IStringLocalizer<Localization.EmailResource> localizer
)
{
public async Task<MagicSpell> CreateMagicSpell(
Account account,
Shared.Models.Account account,
MagicSpellType type,
Dictionary<string, object> meta,
Instant? expiredAt = null,
@ -87,54 +85,54 @@ public class MagicSpellService(
switch (spell.Type)
{
case MagicSpellType.AccountActivation:
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
contact.Account.Nick,
contact.Content,
localizer["EmailLandingTitle"],
new LandingEmailModel
{
Name = contact.Account.Name,
Link = link
}
);
// await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
// contact.Account.Nick,
// contact.Content,
// localizer["EmailLandingTitle"],
// new LandingEmailModel
// {
// Name = contact.Account.Name,
// Link = link
// }
// );
break;
case MagicSpellType.AccountRemoval:
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
contact.Account.Nick,
contact.Content,
localizer["EmailAccountDeletionTitle"],
new AccountDeletionEmailModel
{
Name = contact.Account.Name,
Link = link
}
);
// await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
// contact.Account.Nick,
// contact.Content,
// localizer["EmailAccountDeletionTitle"],
// new AccountDeletionEmailModel
// {
// Name = contact.Account.Name,
// Link = link
// }
// );
break;
case MagicSpellType.AuthPasswordReset:
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
contact.Account.Nick,
contact.Content,
localizer["EmailAccountDeletionTitle"],
new PasswordResetEmailModel
{
Name = contact.Account.Name,
Link = link
}
);
// await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
// contact.Account.Nick,
// contact.Content,
// localizer["EmailAccountDeletionTitle"],
// new PasswordResetEmailModel
// {
// Name = contact.Account.Name,
// Link = link
// }
// );
break;
case MagicSpellType.ContactVerification:
if (spell.Meta["contact_method"] is not string contactMethod)
throw new InvalidOperationException("Contact method is not found.");
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
contact.Account.Nick,
contactMethod!,
localizer["EmailContactVerificationTitle"],
new ContactVerificationEmailModel
{
Name = contact.Account.Name,
Link = link
}
);
// await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
// contact.Account.Nick,
// contactMethod!,
// localizer["EmailContactVerificationTitle"],
// new ContactVerificationEmailModel
// {
// Name = contact.Account.Name,
// Link = link
// }
// );
break;
default:
throw new ArgumentOutOfRangeException();

View File

@ -1,12 +1,14 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Shared.Models;
using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Microsoft.AspNetCore.Http;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
[ApiController]
[Route("/notifications")]
@ -17,7 +19,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
public async Task<ActionResult<int>> CountUnreadNotifications()
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized();
if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized();
var count = await db.Notifications
.Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null)
@ -35,7 +37,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
)
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
if (currentUserValue is not Account currentUser) return Unauthorized();
if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized();
var totalCount = await db.Notifications
.Where(s => s.AccountId == currentUser.Id)
@ -67,16 +69,15 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
{
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account;
var currentUser = currentUserValue as Shared.Models.Account;
if (currentUser == null) return Unauthorized();
var currentSession = currentSessionValue as Session;
if (currentSession == null) return Unauthorized();
var result =
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
request.DeviceToken);
await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!,
request.DeviceToken);
return Ok(result);
return Ok();
}
[HttpDelete("subscription")]
@ -85,7 +86,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
{
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
var currentUser = currentUserValue as Account;
var currentUser = currentUserValue as Shared.Models.Account;
if (currentUser == null) return Unauthorized();
var currentSession = currentSessionValue as Session;
if (currentSession == null) return Unauthorized();

View 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();
// }
}

View File

@ -1,10 +1,12 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Microsoft.AspNetCore.Http;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Pass.Account;
[ApiController]
[Route("/relationships")]
@ -15,7 +17,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0,
[FromQuery] int take = 20)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var query = db.AccountRelationships.AsQueryable()
@ -46,7 +48,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<List<Relationship>>> ListSentRequests()
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relationships = await db.AccountRelationships
.Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending)
@ -69,7 +71,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId,
[FromBody] RelationshipRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found.");
@ -92,7 +94,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId,
[FromBody] RelationshipRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -113,7 +115,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> GetRelationship(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
var queries = db.AccountRelationships.AsQueryable()
@ -133,7 +135,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found.");
@ -158,7 +160,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult> DeleteFriendRequest(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
try
{
@ -175,7 +177,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
if (relationship is null) return NotFound("Friend request was not found.");
@ -195,7 +197,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending);
if (relationship is null) return NotFound("Friend request was not found.");
@ -215,7 +217,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> BlockUser(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found.");
@ -235,7 +237,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) :
[Authorize]
public async Task<ActionResult<Relationship>> UnblockUser(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(userId);
if (relatedUser is null) return NotFound("Account was not found.");

View File

@ -1,10 +1,13 @@
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
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 UserBlockedCacheKeyPrefix = "accounts:blocked:";
@ -34,7 +37,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
return relationship;
}
public async Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status)
public async Task<Relationship> CreateRelationship(Shared.Models.Account sender, Shared.Models.Account target, RelationshipStatus status)
{
if (status == RelationshipStatus.Pending)
throw new InvalidOperationException(
@ -52,31 +55,31 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
db.AccountRelationships.Add(relationship);
await db.SaveChangesAsync();
await PurgeRelationshipCache(sender.Id, target.Id);
// await PurgeRelationshipCache(sender.Id, target.Id);
return relationship;
}
public async Task<Relationship> BlockAccount(Account sender, Account target)
public async Task<Relationship> BlockAccount(Shared.Models.Account sender, Shared.Models.Account target)
{
if (await HasExistingRelationship(sender.Id, target.Id))
return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
return await CreateRelationship(sender, target, RelationshipStatus.Blocked);
}
public async Task<Relationship> UnblockAccount(Account sender, Account target)
public async Task<Relationship> UnblockAccount(Shared.Models.Account sender, Shared.Models.Account target)
{
var relationship = await GetRelationship(sender.Id, target.Id, RelationshipStatus.Blocked);
if (relationship is null) throw new ArgumentException("There is no relationship between you and the user.");
db.Remove(relationship);
await db.SaveChangesAsync();
await PurgeRelationshipCache(sender.Id, target.Id);
// await PurgeRelationshipCache(sender.Id, target.Id);
return relationship;
}
public async Task<Relationship> SendFriendRequest(Account sender, Account target)
public async Task<Relationship> SendFriendRequest(Shared.Models.Account sender, Shared.Models.Account target)
{
if (await HasExistingRelationship(sender.Id, target.Id))
throw new InvalidOperationException("Found existing relationship between you and target user.");
@ -104,7 +107,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
.ExecuteDeleteAsync();
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
}
public async Task<Relationship> AcceptFriendRelationship(
@ -133,7 +136,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
await db.SaveChangesAsync();
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
// await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId);
return relationshipBackward;
}
@ -147,15 +150,16 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
db.Update(relationship);
await db.SaveChangesAsync();
await PurgeRelationshipCache(accountId, relatedId);
// await PurgeRelationshipCache(accountId, relatedId);
return relationship;
}
public async Task<List<Guid>> ListAccountFriends(Account account)
public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account)
{
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)
{
@ -165,16 +169,17 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
.Select(r => r.AccountId)
.ToListAsync();
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
// await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
}
return friends ?? [];
}
public async Task<List<Guid>> ListAccountBlocked(Account account)
public async Task<List<Guid>> ListAccountBlocked(Shared.Models.Account account)
{
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)
{
@ -184,7 +189,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
.Select(r => r.AccountId)
.ToListAsync();
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
// await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
}
return blocked ?? [];
@ -199,9 +204,9 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId)
{
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}");
// await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}");
// await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}");
// await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
// await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}");
}
}

View File

@ -0,0 +1,269 @@
using System.Linq.Expressions;
using System.Reflection;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NodaTime;
using Quartz;
namespace DysonNetwork.Pass;
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<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<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<Challenge> AuthChallenges { get; set; }
public DbSet<MagicSpell> MagicSpells { get; set; }
public DbSet<AbuseReport> AbuseReports { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(
configuration.GetConnectionString("App"),
opt => opt
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
.UseNetTopologySuite()
.UseNodaTime()
).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);
}
protected override void OnModelCreating(ModelBuilder 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);
// 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)
{
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 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 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);
}
}
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;
}
}

View File

@ -1,22 +1,16 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
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.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
using System.Text;
using DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
using SystemClock = NodaTime.SystemClock;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SystemClock = Microsoft.Extensions.Internal.SystemClock;
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Pass.Auth;
public static class AuthConstants
{
@ -46,12 +40,11 @@ public class DysonTokenAuthHandler(
IConfiguration configuration,
ILoggerFactory logger,
UrlEncoder encoder,
AppDatabase database,
OidcProviderService oidc,
ICacheService cache,
FlushBufferService fbs
)
: AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
AppDatabase database
// OidcProviderService oidc,
// ICacheService cache,
// FlushBufferService fbs
) : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder)
{
public const string AuthCachePrefix = "auth:";
@ -64,36 +57,42 @@ public class DysonTokenAuthHandler(
try
{
var now = SystemClock.Instance.GetCurrentInstant();
var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
// Validate token and extract session ID
if (!ValidateToken(tokenInfo.Token, out var sessionId))
return AuthenticateResult.Fail("Invalid token.");
// Try to get session from cache first
var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
// If not in cache, load from database
if (session is null)
{
session = await database.AuthSessions
// var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}");
var session = await database.AuthSessions
.Where(e => e.Id == sessionId)
.Include(e => e.Challenge)
.Include(e => e.Account)
.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)
);
}
}
// If not in cache, load from database
// if (session is null)
// {
// session = await database.AuthSessions
// .Where(e => e.Id == sessionId)
// .Include(e => e.Challenge)
// .Include(e => e.Account)
// .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
if (session == null)
@ -129,13 +128,13 @@ public class DysonTokenAuthHandler(
var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName);
var lastInfo = new LastActiveInfo
{
Account = session.Account,
Session = session,
SeenAt = SystemClock.Instance.GetCurrentInstant(),
};
fbs.Enqueue(lastInfo);
// var lastInfo = new LastActiveInfo
// {
// Account = session.Account,
// Session = session,
// SeenAt = SystemClock.Instance.GetCurrentInstant(),
// };
// fbs.Enqueue(lastInfo);
return AuthenticateResult.Success(ticket);
}
@ -158,12 +157,13 @@ public class DysonTokenAuthHandler(
// Handle JWT tokens (3 parts)
case 3:
{
var (isValid, jwtResult) = oidc.ValidateToken(token);
if (!isValid) return false;
var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
if (jti is null) return false;
// var (isValid, jwtResult) = oidc.ValidateToken(token);
// if (!isValid) return false;
// var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
// 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)
case 2:

View File

@ -1,23 +1,23 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Shared.Models;
using DysonNetwork.Pass.Account;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Microsoft.EntityFrameworkCore;
using System.IdentityModel.Tokens.Jwt;
using NodaTime;
using Microsoft.AspNetCore.Http;
using System.Text.Json;
using DysonNetwork.Sphere.Connection;
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Pass.Auth;
[ApiController]
[Route("/auth")]
public class AuthController(
AppDatabase db,
AccountService accounts,
AuthService auth,
GeoIpService geo,
ActionLogService als
AuthService auth
// GeoIpService geo,
// ActionLogService als
) : ControllerBase
{
public class ChallengeRequest
@ -59,7 +59,7 @@ public class AuthController(
Scopes = request.Scopes,
IpAddress = ipAddress,
UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress),
// Location = geo.GetPointFromIp(ipAddress),
DeviceId = request.DeviceId,
AccountId = account.Id
}.Normalize();
@ -67,9 +67,9 @@ public class AuthController(
await db.AuthChallenges.AddAsync(challenge);
await db.SaveChangesAsync();
als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
);
// als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt,
// new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account
// );
return challenge;
}
@ -160,13 +160,13 @@ public class AuthController(
challenge.StepRemain = Math.Max(0, challenge.StepRemain);
challenge.BlacklistFactors.Add(factor.Id);
db.Update(challenge);
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "factor_id", factor.Id }
}, Request, challenge.Account
);
// als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,
// new Dictionary<string, object>
// {
// { "challenge_id", challenge.Id },
// { "factor_id", factor.Id }
// }, Request, challenge.Account
// );
}
else
{
@ -177,26 +177,26 @@ public class AuthController(
{
challenge.FailedAttempts++;
db.Update(challenge);
als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "factor_id", factor.Id }
}, Request, challenge.Account
);
// als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure,
// new Dictionary<string, object>
// {
// { "challenge_id", challenge.Id },
// { "factor_id", factor.Id }
// }, Request, challenge.Account
// );
await db.SaveChangesAsync();
return BadRequest("Invalid password.");
}
if (challenge.StepRemain == 0)
{
als.CreateActionLogFromRequest(ActionLogType.NewLogin,
new Dictionary<string, object>
{
{ "challenge_id", challenge.Id },
{ "account_id", challenge.AccountId }
}, Request, challenge.Account
);
// als.CreateActionLogFromRequest(ActionLogType.NewLogin,
// new Dictionary<string, object>
// {
// { "challenge_id", challenge.Id },
// { "account_id", challenge.AccountId }
// }, Request, challenge.Account
// );
}
await db.SaveChangesAsync();

View File

@ -0,0 +1,96 @@
using DysonNetwork.Shared.Protos.Auth;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Text.Json;
using DysonNetwork.Shared.Models;
using DysonNetwork.Pass.Account;
using Challenge = DysonNetwork.Shared.Models.Challenge;
using Session = DysonNetwork.Shared.Models.Session;
namespace DysonNetwork.Pass.Auth;
public class AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth)
: DysonNetwork.Shared.Protos.Auth.AuthService.AuthServiceBase
{
public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
{
var account = await accounts.LookupAccount(request.Username);
if (account == null)
{
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found."));
}
var factor = await db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password);
if (factor == null || !factor.VerifyPassword(request.Password))
{
throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid credentials."));
}
var session = new Session
{
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
Account = account,
Challenge = new Challenge()
};
db.AuthSessions.Add(session);
await db.SaveChangesAsync();
var token = auth.CreateToken(session);
return new LoginResponse
{
AccessToken = token,
ExpiresIn = (long)(session.ExpiredAt.Value - session.LastGrantedAt.Value).TotalSeconds
};
}
public override async Task<IntrospectionResponse> IntrospectToken(IntrospectTokenRequest request, ServerCallContext context)
{
if (auth.ValidateToken(request.Token, out var sessionId))
{
var session = await db.AuthSessions
.Include(s => s.Account)
.Include(s => s.Challenge)
.FirstOrDefaultAsync(s => s.Id == sessionId);
if (session != null)
{
return new IntrospectionResponse
{
Active = true,
Claims = JsonSerializer.Serialize(new { sub = session.AccountId }),
ClientId = session.AppId?.ToString() ?? "",
Username = session.Account.Name,
Scope = string.Join(" ", session.Challenge.Scopes),
Iat = Timestamp.FromDateTime(session.CreatedAt.ToDateTimeUtc()),
Exp = Timestamp.FromDateTime(session.ExpiredAt?.ToDateTimeUtc() ?? DateTime.MaxValue)
};
}
}
return new IntrospectionResponse { Active = false };
}
public override async Task<Empty> Logout(Empty request, ServerCallContext context)
{
var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization");
if (authorizationHeader != null)
{
var token = authorizationHeader.Value.Replace("Bearer ", "");
if (auth.ValidateToken(token, out var sessionId))
{
var session = await db.AuthSessions.FindAsync(sessionId);
if (session != null)
{
db.AuthSessions.Remove(session);
await db.SaveChangesAsync();
}
}
}
return new Empty();
}
}

View File

@ -1,21 +1,23 @@
using System.Security.Cryptography;
using System.Text.Json;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Http;
using System.IO;
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Pass.Auth;
public class AuthService(
AppDatabase db,
IConfiguration config,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
ICacheService cache
IConfiguration config
// IHttpClientFactory httpClientFactory,
// IHttpContextAccessor httpContextAccessor,
// ICacheService cache
)
{
private HttpContext HttpContext => httpContextAccessor.HttpContext!;
// private HttpContext HttpContext => httpContextAccessor.HttpContext!;
/// <summary>
/// Detect the risk of the current request to login
@ -24,7 +26,7 @@ public class AuthService(
/// <param name="request">The request context</param>
/// <param name="account">The account to login</param>
/// <returns>The required steps to login</returns>
public async Task<int> DetectChallengeRisk(HttpRequest request, Account.Account account)
public async Task<int> DetectChallengeRisk(HttpRequest request, Shared.Models.Account account)
{
// 1) Find out how many authentication factors the account has enabled.
var maxSteps = await db.AccountAuthFactors
@ -73,13 +75,13 @@ public class AuthService(
return totalRequiredSteps;
}
public async Task<Session> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null)
public async Task<Session> CreateSessionForOidcAsync(Shared.Models.Account account, Instant time, Guid? customAppId = null)
{
var challenge = new Challenge
{
AccountId = account.Id,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = HttpContext.Request.Headers.UserAgent,
IpAddress = "127.0.0.1", // HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = "TestAgent", // HttpContext.Request.Headers.UserAgent,
StepRemain = 1,
StepTotal = 1,
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
@ -105,53 +107,54 @@ public class AuthService(
{
if (string.IsNullOrWhiteSpace(token)) return false;
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
var apiSecret = config.GetSection("Captcha")["ApiSecret"];
// var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
// var apiSecret = config.GetSection("Captcha")["ApiSecret"];
var client = httpClientFactory.CreateClient();
// var client = httpClientFactory.CreateClient();
var jsonOpts = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
};
// var jsonOpts = new JsonSerializerOptions
// {
// PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
// DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
// };
switch (provider)
{
case "cloudflare":
var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
"application/x-www-form-urlencoded");
var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
content);
response.EnsureSuccessStatusCode();
// switch (provider)
// {
// case "cloudflare":
// var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
// "application/x-www-form-urlencoded");
// var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify",
// content);
// response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
// var json = await response.Content.ReadAsStringAsync();
// var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
return result?.Success == true;
case "google":
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
"application/x-www-form-urlencoded");
response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
response.EnsureSuccessStatusCode();
// return result?.Success == true;
// case "google":
// content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
// "application/x-www-form-urlencoded");
// response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content);
// response.EnsureSuccessStatusCode();
json = await response.Content.ReadAsStringAsync();
result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
// json = await response.Content.ReadAsStringAsync();
// result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
return result?.Success == true;
case "hcaptcha":
content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
"application/x-www-form-urlencoded");
response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
response.EnsureSuccessStatusCode();
// return result?.Success == true;
// case "hcaptcha":
// content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8,
// "application/x-www-form-urlencoded");
// response = await client.PostAsync("https://hcaptcha.com/siteverify", content);
// response.EnsureSuccessStatusCode();
json = await response.Content.ReadAsStringAsync();
result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
// json = await response.Content.ReadAsStringAsync();
// result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts);
return result?.Success == true;
default:
throw new ArgumentException("The server misconfigured for the captcha.");
}
// return result?.Success == true;
// default:
// throw new ArgumentException("The server misconfigured for the captcha.");
// }
return true; // Placeholder for captcha validation
}
public string CreateToken(Session session)
@ -183,56 +186,56 @@ public class AuthService(
return $"{payloadBase64}.{signatureBase64}";
}
public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
{
// Check if the session is already in sudo mode (cached)
var sudoModeKey = $"accounts:{session.Id}:sudo";
var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
// public async Task<bool> ValidateSudoMode(Session session, string? pinCode)
// {
// // Check if the session is already in sudo mode (cached)
// var sudoModeKey = $"accounts:{session.Id}:sudo";
// var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
if (found)
{
// Session is already in sudo mode
return true;
}
// if (found)
// {
// // Session is already in sudo mode
// return true;
// }
// Check if the user has a pin code
var hasPinCode = await db.AccountAuthFactors
.Where(f => f.AccountId == session.AccountId)
.Where(f => f.EnabledAt != null)
.Where(f => f.Type == AccountAuthFactorType.PinCode)
.AnyAsync();
// // Check if the user has a pin code
// var hasPinCode = await db.AccountAuthFactors
// .Where(f => f.AccountId == session.AccountId)
// .Where(f => f.EnabledAt != null)
// .Where(f => f.Type == AccountAuthFactorType.PinCode)
// .AnyAsync();
if (!hasPinCode)
{
// User doesn't have a pin code, no validation needed
return true;
}
// if (!hasPinCode)
// {
// // User doesn't have a pin code, no validation needed
// return true;
// }
// If pin code is not provided, we can't validate
if (string.IsNullOrEmpty(pinCode))
{
return false;
}
// // If pin code is not provided, we can't validate
// if (string.IsNullOrEmpty(pinCode))
// {
// return false;
// }
try
{
// Validate the pin code
var isValid = await ValidatePinCode(session.AccountId, pinCode);
// try
// {
// // Validate the pin code
// var isValid = await ValidatePinCode(session.AccountId, pinCode);
if (isValid)
{
// Set session in sudo mode for 5 minutes
await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
}
// if (isValid)
// {
// // Set session in sudo mode for 5 minutes
// await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
// }
return isValid;
}
catch (InvalidOperationException)
{
// No pin code enabled for this account, so validation is successful
return true;
}
}
// return isValid;
// }
// catch (InvalidOperationException)
// {
// // No pin code enabled for this account, so validation is successful
// return true;
// }
// }
public async Task<bool> ValidatePinCode(Guid accountId, string pinCode)
{

View File

@ -1,4 +1,4 @@
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Pass.Auth;
public class CaptchaVerificationResponse
{

View File

@ -1,6 +1,8 @@
using System.Security.Cryptography;
using DysonNetwork.Shared.Models;
using Microsoft.Extensions.Configuration;
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Pass.Auth;
public class CompactTokenService(IConfiguration config)
{

View File

@ -1,18 +1,19 @@
using System.Security.Cryptography;
using System.Text;
using DysonNetwork.Sphere.Auth.OidcProvider.Options;
using DysonNetwork.Sphere.Auth.OidcProvider.Responses;
using DysonNetwork.Sphere.Auth.OidcProvider.Services;
using System.Text.Json.Serialization;
using DysonNetwork.Pass.Auth.OidcProvider.Options;
using DysonNetwork.Pass.Auth.OidcProvider.Responses;
using DysonNetwork.Pass.Auth.OidcProvider.Services;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers;
namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers;
[Route("/auth/open")]
[ApiController]
@ -114,7 +115,7 @@ public class OidcProviderController(
[Authorize]
public async Task<IActionResult> GetUserInfo()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser ||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser ||
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
// Get requested scopes from the token

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Models;
namespace DysonNetwork.Pass.Auth.OidcProvider.Models;
public class AuthorizationCodeInfo
{

View File

@ -1,6 +1,6 @@
using System.Security.Cryptography;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Options;
namespace DysonNetwork.Pass.Auth.OidcProvider.Options;
public class OidcProviderOptions
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
public class AuthorizationResponse
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
public class ErrorResponse
{

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
public class TokenResponse
{

View File

@ -2,17 +2,18 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
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 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 Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OidcProvider.Services;
namespace DysonNetwork.Pass.Auth.OidcProvider.Services;
public class OidcProviderService(
AppDatabase db,
@ -26,16 +27,18 @@ public class OidcProviderService(
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
{
return await db.CustomApps
.Include(c => c.Secrets)
.FirstOrDefaultAsync(c => c.Id == clientId);
return null;
// return await db.CustomApps
// .Include(c => c.Secrets)
// .FirstOrDefaultAsync(c => c.Id == clientId);
}
public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId)
{
return await db.CustomApps
.Include(c => c.Secrets)
.FirstOrDefaultAsync(c => c.Id == appId);
return null;
// return await db.CustomApps
// .Include(c => c.Secrets)
// .FirstOrDefaultAsync(c => c.Id == appId);
}
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)

View File

@ -1,8 +1,10 @@
using System.Net.Http.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(
IConfiguration configuration,

View File

@ -3,10 +3,12 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
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;
namespace DysonNetwork.Sphere.Auth.OpenId;
namespace DysonNetwork.Pass.Auth.OpenId;
/// <summary>
/// Implementation of OpenID Connect service for Apple Sign In

View File

@ -1,11 +1,14 @@
using DysonNetwork.Sphere.Account;
using DysonNetwork.Pass.Account;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Auth.OpenId;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DysonNetwork.Sphere.Storage;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId;
namespace DysonNetwork.Pass.Auth.OpenId;
[ApiController]
[Route("/accounts/me/connections")]
@ -25,7 +28,7 @@ public class ConnectionController(
[HttpGet]
public async Task<ActionResult<List<AccountConnection>>> GetConnections()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var connections = await db.AccountConnections
@ -48,7 +51,7 @@ public class ConnectionController(
[HttpDelete("{id:guid}")]
public async Task<ActionResult> RemoveConnection(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var connection = await db.AccountConnections
@ -66,7 +69,7 @@ public class ConnectionController(
[HttpPost("/auth/connect/apple/mobile")]
public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
if (GetOidcService("apple") is not AppleOidcService appleService)
@ -132,7 +135,7 @@ public class ConnectionController(
[HttpPost("connect")]
public async Task<ActionResult<object>> InitiateConnection([FromBody] ConnectProviderRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var oidcService = GetOidcService(request.Provider);

View File

@ -1,8 +1,10 @@
using System.Net.Http.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(
IConfiguration configuration,

View File

@ -1,8 +1,10 @@
using System.Net.Http.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(
IConfiguration configuration,

View File

@ -1,11 +1,11 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Sphere.Auth.OpenId;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
namespace DysonNetwork.Sphere.Auth.OpenId;
namespace DysonNetwork.Pass.Auth.OpenId;
public class GoogleOidcService(
IConfiguration configuration,

View File

@ -1,8 +1,10 @@
using System.Net.Http.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(
IConfiguration configuration,

View File

@ -1,11 +1,14 @@
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Pass.Account;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Auth.OpenId;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId;
namespace DysonNetwork.Pass.Auth.OpenId;
[ApiController]
[Route("/auth/login")]
@ -32,7 +35,7 @@ public class OidcController(
var oidcService = GetOidcService(provider);
// If the user is already authenticated, treat as an account connection request
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
{
var state = Guid.NewGuid().ToString();
var nonce = Guid.NewGuid().ToString();
@ -125,7 +128,7 @@ public class OidcController(
};
}
private async Task<Account.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
private async Task<Shared.Models.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
{
if (string.IsNullOrEmpty(userInfo.Email))
throw new ArgumentException("Email is required for account creation");

View File

@ -1,13 +1,16 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Auth.OpenId;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using NodaTime;
namespace DysonNetwork.Sphere.Auth.OpenId;
namespace DysonNetwork.Pass.Auth.OpenId;
/// <summary>
/// Base service for OpenID Connect authentication providers
@ -190,7 +193,7 @@ public abstract class OidcService(
/// </summary>
public async Task<Challenge> CreateChallengeForUserAsync(
OidcUserInfo userInfo,
Account.Account account,
Shared.Models.Account account,
HttpContext request,
string deviceId
)

View File

@ -0,0 +1,74 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<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.EntityFrameworkCore.Design" Version="9.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<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="StackExchange.Redis" Version="2.8.41" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
<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>
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore">
<HintPath>..\..\..\..\..\..\opt\homebrew\Cellar\dotnet\9.0.6\libexec\shared\Microsoft.AspNetCore.App\9.0.6\Microsoft.AspNetCore.dll</HintPath>
</Reference>
</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>

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class AccountEventResource
{
}

View File

@ -0,0 +1,5 @@
namespace DysonNetwork.Pass.Localization;
public class EmailResource
{
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class NotificationResource
{
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Pass.Localization;
public class SharedResource
{
}

View 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!;
}

View 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["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!;
}

View 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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
}

View 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!;
}

View 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!;
}

View 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!;
}

View File

@ -1,6 +1,6 @@
namespace DysonNetwork.Sphere.Permission;
using Microsoft.AspNetCore.Http;
using System;
namespace DysonNetwork.Pass.Permission;
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class RequiredPermissionAttribute(string area, string key) : Attribute
@ -21,7 +21,7 @@ public class PermissionMiddleware(RequestDelegate next)
if (attr != null)
{
if (httpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (httpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
{
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
await httpContext.Response.WriteAsync("Unauthorized");

View File

@ -1,9 +1,10 @@
using System.Text.Json;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Text.Json;
using DysonNetwork.Sphere.Storage;
namespace DysonNetwork.Sphere.Permission;
namespace DysonNetwork.Pass.Permission;
public class PermissionService(
AppDatabase db,

View File

@ -0,0 +1,36 @@
using DysonNetwork.Pass;
using DysonNetwork.Pass.Account;
using DysonNetwork.Pass.Auth;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddGrpc();
builder.Services.AddDbContext<AppDatabase>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("App")));
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<AuthService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app.MapGrpcService<AccountGrpcService>();
app.MapGrpcService<AuthGrpcService>();
// Run database migrations
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
await db.Database.MigrateAsync();
}
app.Run();

View File

@ -0,0 +1,11 @@
{
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_network_pass;Username=postgres;Password=password"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -4,7 +4,7 @@ using NodaTime;
using NodaTime.Serialization.JsonNet;
using StackExchange.Redis;
namespace DysonNetwork.Sphere.Storage;
namespace DysonNetwork.Shared.Cache;
/// <summary>
/// Represents a distributed lock that can be used to synchronize access across multiple processes

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
<PackageReference Include="Grpc.Tools" Version="2.65.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<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.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
</ItemGroup>
</Project>

View File

@ -1,14 +1,11 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Sphere.Wallet;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using OtpNet;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Name), IsUnique = true)]
public class Account : ModelBase
@ -26,8 +23,8 @@ public class Account : ModelBase
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
[JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>();
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>();
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>();
[JsonIgnore] public ICollection<Session> Sessions { get; set; } = new List<Session>();
[JsonIgnore] public ICollection<Challenge> Challenges { get; set; } = new List<Challenge>();
[JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>();
[JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>();

View File

@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
public enum StatusAttitude
{
@ -23,7 +23,7 @@ public class Status : ModelBase
public Instant? ClearedAt { get; set; }
public Guid AccountId { get; set; }
public Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
}
public enum CheckInResultLevel
@ -44,7 +44,7 @@ public class CheckInResult : ModelBase
[Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>();
public Guid AccountId { get; set; }
public Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
}
public class FortuneTip

View File

@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Point = NetTopologySuite.Geometries.Point;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
public abstract class ActionLogType
{
@ -53,6 +53,6 @@ public class ActionLog : ModelBase
public Point? Location { get; set; }
public Guid AccountId { get; set; }
public Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
public Guid? SessionId { get; set; }
}

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
public class Badge : ModelBase
{
@ -16,7 +16,7 @@ public class Badge : ModelBase
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
public BadgeReferenceObject ToReference()
{

View File

@ -1,10 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
namespace DysonNetwork.Shared.Models;
public enum ChatRoomType
{
@ -31,7 +30,7 @@ public class ChatRoom : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>();
public Guid? RealmId { get; set; }
public Realm.Realm? Realm { get; set; }
public Shared.Models.Realm? Realm { get; set; }
[NotMapped]
[JsonPropertyName("members")]
@ -73,7 +72,7 @@ public class ChatMember : ModelBase
public Guid ChatRoomId { get; set; }
public ChatRoom ChatRoom { get; set; } = null!;
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; }
@ -105,7 +104,7 @@ public class ChatMemberTransmissionObject : ModelBase
public Guid Id { get; set; }
public Guid ChatRoomId { get; set; }
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
[MaxLength(1024)] public string? Nick { get; set; }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using NodaTime;
namespace DysonNetwork.Sphere.Storage;
namespace DysonNetwork.Shared.Models;
public class RemoteStorageConfig
{
@ -20,6 +20,59 @@ public class RemoteStorageConfig
public string? AccessProxy { get; set; }
}
/// <summary>
/// Common interface for cloud file entities that can be used in file operations.
/// This interface exposes the essential properties needed for file operations
/// and is implemented by both CloudFile and CloudFileReferenceObject.
/// </summary>
public interface ICloudFile
{
public Instant CreatedAt { get; }
public Instant UpdatedAt { get; }
public Instant? DeletedAt { get; }
/// <summary>
/// Gets the unique identifier of the cloud file.
/// </summary>
string Id { get; }
/// <summary>
/// Gets the name of the cloud file.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the file metadata dictionary.
/// </summary>
Dictionary<string, object>? FileMeta { get; }
/// <summary>
/// Gets the user metadata dictionary.
/// </summary>
Dictionary<string, object>? UserMeta { get; }
/// <summary>
/// Gets the MIME type of the file.
/// </summary>
string? MimeType { get; }
/// <summary>
/// Gets the hash of the file content.
/// </summary>
string? Hash { get; }
/// <summary>
/// Gets the size of the file in bytes.
/// </summary>
long Size { get; }
/// <summary>
/// Gets whether the file has a compressed version available.
/// </summary>
bool HasCompression { get; }
}
/// <summary>
/// The class that used in jsonb columns which referenced the cloud file.
/// The aim of this class is to store some properties that won't change to a file to reduce the database load.
@ -74,7 +127,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
[MaxLength(4096)]
public string? StorageUrl { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
public Guid AccountId { get; set; }
public CloudFileReferenceObject ToReferenceObject()

View File

@ -1,11 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Storage;
using NodaTime;
namespace DysonNetwork.Sphere.Developer;
namespace DysonNetwork.Shared.Models;
public enum CustomAppStatus
{
@ -33,7 +31,7 @@ public class CustomApp : ModelBase, IIdentifiedResource
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
public Guid PublisherId { get; set; }
public Publisher.Publisher Developer { get; set; } = null!;
public Publisher Developer { get; set; } = null!;
[NotMapped] public string ResourceIdentifier => "custom-app/" + Id;
}

View File

@ -1,11 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
namespace DysonNetwork.Shared.Models;
public class Message : ModelBase, IIdentifiedResource
{

View File

@ -0,0 +1,15 @@
using NodaTime;
namespace DysonNetwork.Shared.Models;
public interface IIdentifiedResource
{
public string ResourceIdentifier { get; }
}
public abstract class ModelBase
{
public Instant CreatedAt { get; set; }
public Instant UpdatedAt { get; set; }
public Instant? DeletedAt { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
public class Notification : ModelBase
{
@ -18,7 +18,7 @@ public class Notification : ModelBase
public Instant? ViewedAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
}
public enum NotificationPushProvider
@ -37,5 +37,5 @@ public class NotificationPushSubscription : ModelBase
public Instant? LastUsedAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
}

View File

@ -1,9 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Sphere.Developer;
using NodaTime;
namespace DysonNetwork.Sphere.Wallet;
namespace DysonNetwork.Shared.Models;
public class WalletCurrency
{
@ -32,7 +31,7 @@ public class Order : ModelBase
public Instant ExpiredAt { get; set; }
public Guid? PayeeWalletId { get; set; }
public Wallet? PayeeWallet { get; set; } = null!;
public Shared.Models.Wallet? PayeeWallet { get; set; } = null!;
public Guid? TransactionId { get; set; }
public Transaction? Transaction { get; set; }
public Guid? IssuerAppId { get; set; }
@ -56,8 +55,8 @@ public class Transaction : ModelBase
// When the payer is null, it's pay from the system
public Guid? PayerWalletId { get; set; }
public Wallet? PayerWallet { get; set; }
public Shared.Models.Wallet? PayerWallet { get; set; }
// When the payee is null, it's pay for the system
public Guid? PayeeWalletId { get; set; }
public Wallet? PayeeWallet { get; set; }
public Shared.Models.Wallet? PayeeWallet { get; set; }
}

View File

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Permission;
namespace DysonNetwork.Shared.Models;
/// The permission node model provides the infrastructure of permission control in Dyson Network.
/// It based on the ABAC permission model.

View File

@ -1,12 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Publisher;
namespace DysonNetwork.Shared.Models;
public enum PublisherType
{
@ -30,10 +28,8 @@ public class Publisher : ModelBase, IIdentifiedResource
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<Post.Post> Posts { get; set; } = new List<Post.Post>();
[JsonIgnore] public ICollection<PostCollection> Collections { get; set; } = new List<PostCollection>();
[JsonIgnore] public ICollection<PublisherMember> Members { get; set; } = new List<PublisherMember>();
[JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>();
@ -41,9 +37,9 @@ public class Publisher : ModelBase, IIdentifiedResource
public ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>();
public Guid? AccountId { get; set; }
public Account.Account? Account { get; set; }
public Shared.Models.Account? Account { get; set; }
public Guid? RealmId { get; set; }
[JsonIgnore] public Realm.Realm? Realm { get; set; }
[JsonIgnore] public Realm? Realm { get; set; }
public string ResourceIdentifier => $"publisher/{Id}";
}
@ -61,7 +57,7 @@ public class PublisherMember : ModelBase
public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
public PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer;
public Instant? JoinedAt { get; set; }
@ -81,7 +77,7 @@ public class PublisherSubscription : ModelBase
public Guid PublisherId { get; set; }
[JsonIgnore] public Publisher Publisher { get; set; } = null!;
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active;
public int Tier { get; set; } = 0;

View File

@ -1,12 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Realm;
namespace DysonNetwork.Shared.Models;
[Index(nameof(Slug), IsUnique = true)]
public class Realm : ModelBase, IIdentifiedResource
@ -25,14 +23,13 @@ public class Realm : ModelBase, IIdentifiedResource
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
[Column(TypeName = "jsonb")] public Account.VerificationMark? Verification { get; set; }
[Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
[JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>();
[JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>();
[JsonIgnore] public ICollection<RealmTag> RealmTags { get; set; } = new List<RealmTag>();
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
[JsonIgnore] public Shared.Models.Account Account { get; set; } = null!;
public string ResourceIdentifier => $"realm/{Id}";
}
@ -49,7 +46,7 @@ public class RealmMember : ModelBase
public Guid RealmId { get; set; }
public Realm Realm { get; set; } = null!;
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
public int Role { get; set; } = RealmMemberRole.Normal;
public Instant? JoinedAt { get; set; }

View File

@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Chat.Realtime;
using NodaTime;
namespace DysonNetwork.Sphere.Chat;
namespace DysonNetwork.Shared.Models;
public class RealtimeCall : ModelBase
{

View File

@ -1,6 +1,6 @@
using NodaTime;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
public enum RelationshipStatus : short
{
@ -12,9 +12,9 @@ public enum RelationshipStatus : short
public class Relationship : ModelBase
{
public Guid AccountId { get; set; }
public Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
public Guid RelatedId { get; set; }
public Account Related { get; set; } = null!;
public Shared.Models.Account Related { get; set; } = null!;
public Instant? ExpiredAt { get; set; }

View File

@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Sphere.Developer;
using NodaTime;
using Point = NetTopologySuite.Geometries.Point;
namespace DysonNetwork.Sphere.Auth;
namespace DysonNetwork.Shared.Models;
public class Session : ModelBase
{
@ -15,7 +14,7 @@ public class Session : ModelBase
public Instant? ExpiredAt { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
[JsonIgnore] public Account Account { get; set; } = null!;
public Guid ChallengeId { get; set; }
public Challenge Challenge { get; set; } = null!;
public Guid? AppId { get; set; }
@ -49,9 +48,9 @@ public class Challenge : ModelBase
public int FailedAttempts { get; set; }
public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified;
public ChallengeType Type { get; set; } = ChallengeType.Login;
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new();
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new();
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new();
[Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = [];
[Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = [];
[MaxLength(128)] public string? IpAddress { get; set; }
[MaxLength(512)] public string? UserAgent { get; set; }
[MaxLength(256)] public string? DeviceId { get; set; }
@ -59,7 +58,7 @@ public class Challenge : ModelBase
public Point? Location { get; set; }
public Guid AccountId { get; set; }
[JsonIgnore] public Account.Account Account { get; set; } = null!;
[JsonIgnore] public Account Account { get; set; } = null!;
public Challenge Normalize()
{

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.Wallet;
namespace DysonNetwork.Shared.Models;
public record class SubscriptionTypeData(
string Identifier,
@ -138,7 +138,7 @@ public class Subscription : ModelBase
public Instant? RenewalAt { get; set; }
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
[NotMapped]
public bool IsAvailable

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Sphere.Account;
namespace DysonNetwork.Shared.Models;
/// <summary>
/// The verification info of a resource

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace DysonNetwork.Sphere.Wallet;
namespace DysonNetwork.Shared.Models;
public class Wallet : ModelBase
{
@ -10,7 +10,7 @@ public class Wallet : ModelBase
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Models.Account Account { get; set; } = null!;
}
public class WalletPocket : ModelBase

View File

@ -0,0 +1,51 @@
syntax = "proto3";
package dyson_network.sphere.account;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "DysonNetwork.Shared.Protos.Account";
service AccountService {
rpc GetAccount(google.protobuf.Empty) returns (AccountResponse);
rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse);
}
message AccountResponse {
string id = 1;
string name = 2;
string nick = 3;
string language = 4;
google.protobuf.Timestamp activated_at = 5;
bool is_superuser = 6;
Profile profile = 7;
}
message Profile {
string first_name = 1;
string last_name = 2;
string bio = 3;
string gender = 4;
string pronouns = 5;
string time_zone = 6;
string location = 7;
google.protobuf.Timestamp birthday = 8;
google.protobuf.Timestamp last_seen_at = 9;
int32 experience = 10;
int32 level = 11;
double leveling_progress = 12;
}
message UpdateAccountRequest {
optional string nick = 1;
optional string language = 2;
optional string first_name = 3;
optional string last_name = 4;
optional string bio = 5;
optional string gender = 6;
optional string pronouns = 7;
optional string time_zone = 8;
optional string location = 9;
optional google.protobuf.Timestamp birthday = 10;
}

View File

@ -0,0 +1,69 @@
syntax = "proto3";
package dyson_network.sphere.auth;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "DysonNetwork.Shared.Protos.Auth";
service AuthService {
rpc Login(LoginRequest) returns (LoginResponse);
rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse);
rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty);
}
message LoginRequest {
string username = 1;
string password = 2;
optional string two_factor_code = 3;
}
message LoginResponse {
string access_token = 1;
string refresh_token = 2;
int64 expires_in = 3;
}
message IntrospectTokenRequest {
string token = 1;
optional string token_type_hint = 2;
}
message IntrospectionResponse {
bool active = 1;
string claims = 2;
string client_id = 3;
string username = 4;
string scope = 5;
google.protobuf.Timestamp iat = 6;
google.protobuf.Timestamp exp = 7;
}
message Session {
string id = 1;
string label = 2;
google.protobuf.Timestamp last_granted_at = 3;
google.protobuf.Timestamp expired_at = 4;
string account_id = 5;
string challenge_id = 6;
optional string app_id = 7;
}
message Challenge {
string id = 1;
google.protobuf.Timestamp expired_at = 2;
int32 step_remain = 3;
int32 step_total = 4;
int32 failed_attempts = 5;
string platform = 6;
string type = 7;
repeated string blacklist_factors = 8;
repeated string audiences = 9;
repeated string scopes = 10;
string ip_address = 11;
string user_agent = 12;
optional string device_id = 13;
optional string nonce = 14;
string account_id = 15;
}

View File

@ -1,308 +0,0 @@
using System.Text;
using System.Text.Json;
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(
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(
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<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();
}
}

View File

@ -45,7 +45,7 @@ public class ActivityController(
var debugIncludeSet = debugInclude?.Split(',').ToHashSet() ?? new HashSet<string>();
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
return currentUserValue is not Account.Account currentUser
return currentUserValue is not Shared.Models.Account currentUser
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet))
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet));
}

View File

@ -13,7 +13,8 @@ public class ActivityService(
PublisherService pub,
RelationshipService rels,
PostService ps,
DiscoveryService ds)
DiscoveryService ds
)
{
private static double CalculateHotRank(Post.Post post, Instant now)
{
@ -32,7 +33,7 @@ public class ActivityService(
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
{
var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true);
var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true);
if (realms.Count > 0)
{
activities.Add(new DiscoveryActivity(
@ -118,7 +119,7 @@ public class ActivityService(
public async Task<List<Activity>> GetActivities(
int take,
Instant? cursor,
Account.Account currentUser,
Shared.Models.Account currentUser,
string? filter = null,
HashSet<string>? debugInclude = null
)
@ -132,7 +133,7 @@ public class ActivityService(
{
if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2))
{
var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true);
var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true);
if (realms.Count > 0)
{
activities.Add(new DiscoveryActivity(
@ -257,7 +258,7 @@ public class ActivityService(
return score + postCount;
}
private async Task<List<Publisher.Publisher>> GetPopularPublishers(int take)
private async Task<List<Shared.Models.Publisher>> GetPopularPublishers(int take)
{
var now = SystemClock.Instance.GetCurrentInstant();
var recent = now.Minus(Duration.FromDays(7));

View File

@ -1,30 +1,16 @@
using System.Linq.Expressions;
using System.Reflection;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Auth;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Developer;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Publisher;
using DysonNetwork.Sphere.Realm;
using DysonNetwork.Sphere.Sticker;
using DysonNetwork.Sphere.Storage;
using DysonNetwork.Sphere.Wallet;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Query;
using NodaTime;
using Npgsql;
using Quartz;
namespace DysonNetwork.Sphere;
public interface IIdentifiedResource
{
public string ResourceIdentifier { get; }
}
public abstract class ModelBase
{
public Instant CreatedAt { get; set; }
@ -37,32 +23,10 @@ public class AppDatabase(
IConfiguration configuration
) : 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<Account.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<CloudFileReference> FileReferences { get; set; }
public DbSet<Publisher.Publisher> Publishers { get; set; }
public DbSet<Shared.Models.Publisher> Publishers { get; set; }
public DbSet<PublisherMember> PublisherMembers { get; set; }
public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; }
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
@ -73,10 +37,8 @@ public class AppDatabase(
public DbSet<PostCategory> PostCategories { get; set; }
public DbSet<PostCollection> PostCollections { get; set; }
public DbSet<Realm.Realm> Realms { get; set; }
public DbSet<Shared.Models.Realm> Realms { get; set; }
public DbSet<RealmMember> RealmMembers { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<RealmTag> RealmTags { get; set; }
public DbSet<ChatRoom> ChatRooms { get; set; }
public DbSet<ChatMember> ChatMembers { get; set; }
@ -87,7 +49,7 @@ public class AppDatabase(
public DbSet<Sticker.Sticker> Stickers { get; set; }
public DbSet<StickerPack> StickerPacks { get; set; }
public DbSet<Wallet.Wallet> Wallets { get; set; }
public DbSet<Shared.Models.Wallet> Wallets { get; set; }
public DbSet<WalletPocket> WalletPockets { get; set; }
public DbSet<Order> PaymentOrders { get; set; }
public DbSet<Transaction> PaymentTransactions { get; set; }
@ -111,38 +73,6 @@ public class AppDatabase(
.UseNodaTime()
).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);
}
@ -150,25 +80,6 @@ public class AppDatabase(
{
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>()
.HasKey(pm => new { pm.PublisherId, pm.AccountId });
modelBuilder.Entity<PublisherMember>()
@ -243,19 +154,6 @@ public class AppDatabase(
.HasForeignKey(pm => pm.AccountId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RealmTag>()
.HasKey(rt => new { rt.RealmId, rt.TagId });
modelBuilder.Entity<RealmTag>()
.HasOne(rt => rt.Realm)
.WithMany(r => r.RealmTags)
.HasForeignKey(rt => rt.RealmId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<RealmTag>()
.HasOne(rt => rt.Tag)
.WithMany(t => t.RealmTags)
.HasForeignKey(rt => rt.TagId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ChatMember>()
.HasKey(pm => new { pm.Id });
modelBuilder.Entity<ChatMember>()
@ -352,23 +250,9 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
{
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 now = SystemClock.Instance.GetCurrentInstant();
var threshold = now - Duration.FromDays(7);
var entityTypes = db.Model.GetEntityTypes()

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Storage;
using Microsoft.AspNetCore.Authorization;
@ -32,7 +33,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[Authorize]
public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id);
var lastMessages = await cs.ListLastMessageForUser(currentUser.Id);
@ -65,7 +66,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
[FromQuery] int take = 20)
{
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
if (room is null) return NotFound();
@ -102,7 +103,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[HttpGet("{roomId:guid}/messages/{messageId:guid}")]
public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId)
{
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
if (room is null) return NotFound();
@ -139,7 +140,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[RequiredPermission("global", "chat.messages.create")]
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
request.Content = TextSanitizer.Sanitize(request.Content);
if (string.IsNullOrWhiteSpace(request.Content) &&
@ -216,7 +217,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[Authorize]
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
request.Content = TextSanitizer.Sanitize(request.Content);
@ -269,7 +270,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[Authorize]
public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var message = await db.ChatMessages
.Include(m => m.Sender)
@ -295,7 +296,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
[HttpPost("{roomId:guid}/sync")]
public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var isMember = await db.ChatMembers

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Localization;
using DysonNetwork.Sphere.Permission;
@ -36,7 +37,7 @@ public class ChatRoomController(
if (chatRoom is null) return NotFound();
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser)
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id);
return Ok(chatRoom);
@ -46,7 +47,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var userId = currentUser.Id;
@ -72,7 +73,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
@ -134,7 +135,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var room = await db.ChatRooms
@ -164,7 +165,7 @@ public class ChatRoomController(
[RequiredPermission("global", "chat.create")]
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
var chatRoom = new ChatRoom
@ -236,7 +237,7 @@ public class ChatRoomController(
[HttpPatch("{id:guid}")]
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(e => e.Id == id)
@ -321,7 +322,7 @@ public class ChatRoomController(
[HttpDelete("{id:guid}")]
public async Task<ActionResult> DeleteChatRoom(Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(e => e.Id == id)
@ -356,7 +357,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser)
return Unauthorized();
var member = await db.ChatMembers
@ -375,7 +376,7 @@ public class ChatRoomController(
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
{
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account;
var room = await db.ChatRooms
.FirstOrDefaultAsync(r => r.Id == roomId);
@ -448,7 +449,7 @@ public class ChatRoomController(
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
[FromBody] ChatMemberRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
@ -519,7 +520,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var members = await db.ChatMembers
@ -544,7 +545,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var member = await db.ChatMembers
@ -571,7 +572,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult> DeclineChatInvite(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var userId = currentUser.Id;
var member = await db.ChatMembers
@ -600,7 +601,7 @@ public class ChatRoomController(
[FromBody] ChatMemberNotifyRequest request
)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(r => r.Id == roomId)
@ -629,7 +630,7 @@ public class ChatRoomController(
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
{
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(r => r.Id == roomId)
@ -688,7 +689,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(r => r.Id == roomId)
@ -736,7 +737,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var chatRoom = await db.ChatRooms
.Where(r => r.Id == roomId)
@ -774,7 +775,7 @@ public class ChatRoomController(
[Authorize]
public async Task<ActionResult> LeaveChat(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var member = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id)
@ -807,7 +808,7 @@ public class ChatRoomController(
return NoContent();
}
private async Task _SendInviteNotify(ChatMember member, Account.Account sender)
private async Task _SendInviteNotify(ChatMember member, Shared.Models.Account sender)
{
string title = localizer["ChatInviteTitle"];

View File

@ -1,3 +1,5 @@
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
using NodaTime;

View File

@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Chat.Realtime;
using DysonNetwork.Sphere.Connection;
@ -241,7 +242,7 @@ public partial class ChatService(
Priority = 10,
};
List<Account.Account> accountsToNotify = [];
List<Shared.Models.Account> accountsToNotify = [];
foreach (var member in members)
{
scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket
@ -252,7 +253,7 @@ public partial class ChatService(
if (member.Account.Id == sender.AccountId) continue;
if (member.Notify == ChatMemberNotify.None) continue;
// if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.Account.Id))
{
var now = SystemClock.Instance.GetCurrentInstant();

View File

@ -36,7 +36,7 @@ public interface IRealtimeService
/// <param name="sessionId">The session identifier</param>
/// <param name="isAdmin">The user is the admin of session</param>
/// <returns>User-specific token for the session</returns>
string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false);
string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false);
/// <summary>
/// Processes incoming webhook requests from the realtime service provider

View File

@ -4,6 +4,8 @@ using Livekit.Server.Sdk.Dotnet;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Text.Json;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Sphere.Chat.Realtime;
@ -111,7 +113,7 @@ public class LivekitRealtimeService : IRealtimeService
}
/// <inheritdoc />
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
public string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false)
{
var token = _accessToken.WithIdentity(account.Name)
.WithName(account.Nick)

View File

@ -1,3 +1,4 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Chat.Realtime;
using Livekit.Server.Sdk.Dotnet;
using Microsoft.AspNetCore.Authorization;
@ -46,7 +47,7 @@ public class RealtimeCallController(
[Authorize]
public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var member = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
@ -71,7 +72,7 @@ public class RealtimeCallController(
[Authorize]
public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
// Check if the user is a member of the chat room
var member = await db.ChatMembers
@ -144,7 +145,7 @@ public class RealtimeCallController(
[Authorize]
public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var member = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
@ -163,7 +164,7 @@ public class RealtimeCallController(
[Authorize]
public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var member = await db.ChatMembers
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)

View File

@ -1,4 +1,5 @@
using System.Net.WebSockets;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
@ -19,7 +20,7 @@ public class MessageReadHandler(
public const string ChatMemberCacheKey = "ChatMember_{0}_{1}";
public async Task HandleAsync(
Account.Account currentUser,
Shared.Models.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,

View File

@ -10,7 +10,7 @@ public class MessageTypingHandler(ChatRoomService crs) : IWebSocketPacketHandler
public string PacketType => "messages.typing";
public async Task HandleAsync(
Account.Account currentUser,
Shared.Models.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,

View File

@ -9,7 +9,7 @@ public class MessagesSubscribeHandler(ChatRoomService crs) : IWebSocketPacketHan
public string PacketType => "messages.subscribe";
public async Task HandleAsync(
Account.Account currentUser,
Shared.Models.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,

View File

@ -8,7 +8,7 @@ public class MessagesUnsubscribeHandler() : IWebSocketPacketHandler
public string PacketType => "messages.unsubscribe";
public Task HandleAsync(
Account.Account currentUser,
Shared.Models.Account currentUser,
string deviceId,
WebSocketPacket packet,
WebSocket socket,

View File

@ -5,5 +5,5 @@ namespace DysonNetwork.Sphere.Connection;
public interface IWebSocketPacketHandler
{
string PacketType { get; }
Task HandleAsync(Account.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
Task HandleAsync(Shared.Models.Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
}

View File

@ -40,7 +40,7 @@ public class WebFeed : ModelBase
[Column(TypeName = "jsonb")] public WebFeedConfig Config { get; set; } = new();
public Guid PublisherId { get; set; }
public Publisher.Publisher Publisher { get; set; } = null!;
public Shared.Models.Publisher Publisher { get; set; } = null!;
[JsonIgnore] public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>();
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -43,7 +44,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
[Authorize]
public async Task<IActionResult> CreateWebFeed([FromRoute] string pubName, [FromBody] WebFeedRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
if (string.IsNullOrWhiteSpace(request.Url) || string.IsNullOrWhiteSpace(request.Title))
return BadRequest("Url and title are required");
@ -62,7 +63,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
[Authorize]
public async Task<IActionResult> UpdateFeed([FromRoute] string pubName, Guid id, [FromBody] WebFeedRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
@ -82,7 +83,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
[Authorize]
public async Task<IActionResult> DeleteFeed([FromRoute] string pubName, Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
@ -104,7 +105,7 @@ public class WebFeedController(WebFeedService webFeed, PublisherService ps) : Co
[Authorize]
public async Task<ActionResult> Scrap([FromRoute] string pubName, Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();

View File

@ -11,7 +11,7 @@ public class WebFeedService(
WebReaderService webReaderService
)
{
public async Task<WebFeed> CreateWebFeedAsync(Publisher.Publisher publisher,
public async Task<WebFeed> CreateWebFeedAsync(Shared.Models.Publisher publisher,
WebFeedController.WebFeedRequest request)
{
var feed = new WebFeed

View File

@ -1,6 +1,7 @@
using System.Globalization;
using AngleSharp;
using AngleSharp.Dom;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Sphere.Storage;
using HtmlAgilityPack;

Some files were not shown because too many files have changed in this diff Show More