Compare commits
10 Commits
refactor/a
...
63b2b989ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 63b2b989ba | |||
| 2c67472894 | |||
| 0d47716713 | |||
| 8d2f4a4c47 | |||
| 1672d46038 | |||
| 15fb93c2bb | |||
| 4b220e7ed7 | |||
| 65450e8511 | |||
| cb4acbb3fc | |||
| bb2f88cc54 |
@@ -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 [];
|
||||
@@ -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.Shared.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
|
||||
{
|
||||
@@ -1,23 +1,19 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Sphere.Activity;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Sphere.Wallet;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using MagicOnion.Server;
|
||||
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,
|
||||
IStringLocalizer<Localization.AccountEventResource> localizer
|
||||
)
|
||||
) : ServiceBase<IAccountEventService>, IAccountEventService
|
||||
{
|
||||
private static readonly Random Random = new();
|
||||
private const string StatusCacheKey = "AccountStatus_";
|
||||
@@ -34,7 +30,7 @@ public class AccountEventService(
|
||||
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||
if (cachedStatus is not null)
|
||||
{
|
||||
cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||
cachedStatus!.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/;
|
||||
return cachedStatus;
|
||||
}
|
||||
|
||||
@@ -44,12 +40,17 @@ 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;
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ public class AccountEventService(
|
||||
};
|
||||
}
|
||||
|
||||
return new Status
|
||||
return new Status
|
||||
{
|
||||
Attitude = StatusAttitude.Neutral,
|
||||
IsOnline = false,
|
||||
@@ -86,7 +87,7 @@ public class AccountEventService(
|
||||
var cachedStatus = await cache.GetAsync<Status>(cacheKey);
|
||||
if (cachedStatus != null)
|
||||
{
|
||||
cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId);
|
||||
cachedStatus.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/;
|
||||
results[userId] = cachedStatus;
|
||||
}
|
||||
else
|
||||
@@ -109,7 +110,8 @@ 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}";
|
||||
@@ -122,7 +124,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 +142,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 +155,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,7 +167,7 @@ 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);
|
||||
@@ -176,7 +179,7 @@ public class AccountEventService(
|
||||
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
|
||||
@@ -193,16 +196,16 @@ public class AccountEventService(
|
||||
return lastDate < currentDate;
|
||||
}
|
||||
|
||||
public const string CheckInLockKey = "CheckInLock_";
|
||||
private const string CheckInLockKey = "checkin-lock:";
|
||||
|
||||
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));
|
||||
|
||||
|
||||
if (lk != null)
|
||||
await lk.ReleaseAsync();
|
||||
}
|
||||
@@ -210,9 +213,10 @@ public class AccountEventService(
|
||||
{
|
||||
// Ignore errors from this pre-check
|
||||
}
|
||||
|
||||
|
||||
// Now try to acquire the lock properly
|
||||
await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
||||
await using var lockObj =
|
||||
await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
|
||||
if (lockObj is null) throw new InvalidOperationException("Check-in was in progress.");
|
||||
|
||||
var cultureInfo = new CultureInfo(user.Language, false);
|
||||
@@ -255,13 +259,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
|
||||
{
|
||||
@@ -274,13 +279,54 @@ public class AccountEventService(
|
||||
s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience)
|
||||
);
|
||||
db.AccountCheckInResults.Add(result);
|
||||
await db.SaveChangesAsync(); // Don't forget to save changes to the database
|
||||
await db.SaveChangesAsync(); // Remember to save changes to the database
|
||||
|
||||
// The lock will be automatically released by the await using statement
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<DailyEventResponse>> GetEventCalendar(Account user, int month, int year = 0,
|
||||
public async Task<int> GetCheckInStreak(Shared.Models.Account user)
|
||||
{
|
||||
var today = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
|
||||
var yesterdayEnd = today.PlusDays(-1).AtMidnight().InUtc().ToInstant();
|
||||
var yesterdayStart = today.PlusDays(-1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||
var tomorrowEnd = today.PlusDays(1).AtMidnight().InUtc().ToInstant();
|
||||
var tomorrowStart = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||
|
||||
var yesterdayResult = await db.AccountCheckInResults
|
||||
.Where(x => x.AccountId == user.Id)
|
||||
.Where(x => x.CreatedAt >= yesterdayStart)
|
||||
.Where(x => x.CreatedAt < yesterdayEnd)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var tomorrowResult = await db.AccountCheckInResults
|
||||
.Where(x => x.AccountId == user.Id)
|
||||
.Where(x => x.CreatedAt >= tomorrowStart)
|
||||
.Where(x => x.CreatedAt < tomorrowEnd)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (yesterdayResult is null && tomorrowResult is null)
|
||||
return 1;
|
||||
|
||||
var results = await db.AccountCheckInResults
|
||||
.Where(x => x.AccountId == user.Id)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var streak = 0;
|
||||
var day = today;
|
||||
while (results.Any(x =>
|
||||
x.CreatedAt >= day.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant() &&
|
||||
x.CreatedAt < day.AtMidnight().InUtc().ToInstant()))
|
||||
{
|
||||
streak++;
|
||||
day = day.PlusDays(-1);
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
public async Task<List<DailyEventResponse>> GetEventCalendar(Shared.Models.Account user, int month, int year = 0,
|
||||
bool replaceInvisible = false)
|
||||
{
|
||||
if (year == 0)
|
||||
72
DysonNetwork.Pass/Account/AccountProfileService.cs
Normal file
72
DysonNetwork.Pass/Account/AccountProfileService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MagicOnion.Server;
|
||||
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
public class AccountProfileService(AppDatabase db) : ServiceBase<IAccountProfileService>, IAccountProfileService
|
||||
{
|
||||
public async Task<Profile?> GetAccountProfileByIdAsync(Guid accountId)
|
||||
{
|
||||
return await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId);
|
||||
}
|
||||
|
||||
public async Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription)
|
||||
{
|
||||
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = new Profile { AccountId = accountId };
|
||||
db.AccountProfiles.Add(profile);
|
||||
}
|
||||
|
||||
profile.StellarMembership = subscription;
|
||||
await db.SaveChangesAsync();
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task<List<Profile>> GetAccountsWithStellarMembershipAsync()
|
||||
{
|
||||
return await db.AccountProfiles
|
||||
.Where(a => a.StellarMembership != null)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds)
|
||||
{
|
||||
return await db.AccountProfiles
|
||||
.Where(a => accountIds.Contains(a.Id))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(a => a.StellarMembership, p => null)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture)
|
||||
{
|
||||
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = new Profile { AccountId = accountId };
|
||||
db.AccountProfiles.Add(profile);
|
||||
}
|
||||
|
||||
profile.Picture = picture;
|
||||
await db.SaveChangesAsync();
|
||||
return profile;
|
||||
}
|
||||
|
||||
public async Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background)
|
||||
{
|
||||
var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = new Profile { AccountId = accountId };
|
||||
db.AccountProfiles.Add(profile);
|
||||
}
|
||||
|
||||
profile.Background = background;
|
||||
await db.SaveChangesAsync();
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,40 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
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 DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Auth.OpenId;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NodaTime;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
using OtpNet;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EFCore.BulkExtensions;
|
||||
using MagicOnion.Server;
|
||||
using Grpc.Core;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using DysonNetwork.Shared.Services;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
public class AccountService(
|
||||
AppDatabase db,
|
||||
MagicSpellService spells,
|
||||
AccountUsernameService uname,
|
||||
NotificationService nty,
|
||||
EmailService mailer,
|
||||
// EmailService mailer, // Commented out for now
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ICacheService cache,
|
||||
ILogger<AccountService> logger
|
||||
)
|
||||
ILogger<AccountService> logger,
|
||||
AuthService authService,
|
||||
ActionLogService actionLogService,
|
||||
RelationshipService relationshipService
|
||||
) : ServiceBase<IAccountService>, IAccountService
|
||||
{
|
||||
public static void SetCultureInfo(Account account)
|
||||
public static void SetCultureInfo(Shared.Models.Account account)
|
||||
{
|
||||
SetCultureInfo(account.Language);
|
||||
}
|
||||
@@ -40,12 +48,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 +65,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 +82,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 +99,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,
|
||||
@@ -159,7 +167,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");
|
||||
@@ -168,7 +176,6 @@ public class AccountService(
|
||||
? userInfo.DisplayName
|
||||
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
||||
|
||||
// Generate username from email
|
||||
var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
|
||||
|
||||
return await CreateAccount(
|
||||
@@ -182,7 +189,7 @@ public class AccountService(
|
||||
);
|
||||
}
|
||||
|
||||
public async Task RequestAccountDeletion(Account account)
|
||||
public async Task RequestAccountDeletion(Shared.Models.Account account)
|
||||
{
|
||||
var spell = await spells.CreateMagicSpell(
|
||||
account,
|
||||
@@ -194,7 +201,7 @@ public class AccountService(
|
||||
await spells.NotifyMagicSpell(spell);
|
||||
}
|
||||
|
||||
public async Task RequestPasswordReset(Account account)
|
||||
public async Task RequestPasswordReset(Shared.Models.Account account)
|
||||
{
|
||||
var spell = await spells.CreateMagicSpell(
|
||||
account,
|
||||
@@ -206,7 +213,7 @@ public class AccountService(
|
||||
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 +221,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 +336,6 @@ public class AccountService(
|
||||
{
|
||||
var count = await db.AccountAuthFactors
|
||||
.Where(f => f.AccountId == factor.AccountId)
|
||||
.If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null))
|
||||
.CountAsync();
|
||||
if (count <= 1)
|
||||
throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor.");
|
||||
@@ -345,7 +351,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");
|
||||
|
||||
@@ -397,16 +403,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.Pass.Pages.Emails.VerificationEmail, DysonNetwork.Pass.Pages.Emails.VerificationEmailModel>(
|
||||
// account.Nick,
|
||||
// contact.Content,
|
||||
// localizer["VerificationEmail"],
|
||||
// new DysonNetwork.Pass.Pages.Emails.VerificationEmailModel
|
||||
// {
|
||||
// Name = account.Name,
|
||||
// Code = code
|
||||
// }
|
||||
// );
|
||||
|
||||
await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30));
|
||||
break;
|
||||
@@ -454,7 +460,7 @@ public class AccountService(
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Session> UpdateSessionLabel(Account account, Guid sessionId, string label)
|
||||
public async Task<Shared.Models.Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label)
|
||||
{
|
||||
var session = await db.AuthSessions
|
||||
.Include(s => s.Challenge)
|
||||
@@ -477,7 +483,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)
|
||||
@@ -503,7 +509,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,7 +524,7 @@ 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,
|
||||
@@ -530,7 +536,7 @@ public class AccountService(
|
||||
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 +565,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 +580,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 +592,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 +610,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
|
||||
.Where(b => b.AccountId == account.Id && b.Id == badgeId)
|
||||
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));
|
||||
|
||||
@@ -654,4 +660,246 @@ public class AccountService(
|
||||
await db.BulkInsertAsync(newProfiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Shared.Models.Account?> GetAccountById(Guid accountId, bool withProfile = false)
|
||||
{
|
||||
return await db.Accounts
|
||||
.Where(a => a.Id == accountId)
|
||||
.If(withProfile, q => q.Include(a => a.Profile))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Profile?> GetAccountProfile(Guid accountId)
|
||||
{
|
||||
return await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == accountId);
|
||||
}
|
||||
|
||||
public async Task<Challenge?> GetAuthChallenge(Guid challengeId)
|
||||
{
|
||||
return await db.AuthChallenges.FindAsync(challengeId);
|
||||
}
|
||||
|
||||
public async Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, Instant now)
|
||||
{
|
||||
return await db.AuthChallenges
|
||||
.Where(e => e.AccountId == accountId)
|
||||
.Where(e => e.IpAddress == ipAddress)
|
||||
.Where(e => e.UserAgent == userAgent)
|
||||
.Where(e => e.StepRemain > 0)
|
||||
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Challenge> CreateAuthChallenge(Challenge challenge)
|
||||
{
|
||||
db.AuthChallenges.Add(challenge);
|
||||
await db.SaveChangesAsync();
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public async Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId)
|
||||
{
|
||||
return await db.AccountAuthFactors.FirstOrDefaultAsync(f => f.Id == factorId && f.AccountId == accountId);
|
||||
}
|
||||
|
||||
public async Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId)
|
||||
{
|
||||
return await db.AccountAuthFactors
|
||||
.Where(e => e.AccountId == accountId)
|
||||
.Where(e => e.EnabledAt != null && e.Trustworthy >= 1)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Session?> GetAuthSession(Guid sessionId)
|
||||
{
|
||||
return await db.AuthSessions.FindAsync(sessionId);
|
||||
}
|
||||
|
||||
public async Task<MagicSpell?> GetMagicSpell(Guid spellId)
|
||||
{
|
||||
return await db.MagicSpells.FindAsync(spellId);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport?> GetAbuseReport(Guid reportId)
|
||||
{
|
||||
return await db.AbuseReports.FindAsync(reportId);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId)
|
||||
{
|
||||
var existingReport = await db.AbuseReports
|
||||
.Where(r => r.ResourceIdentifier == resourceIdentifier &&
|
||||
r.AccountId == accountId &&
|
||||
r.DeletedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
throw new InvalidOperationException("You have already reported this content.");
|
||||
}
|
||||
|
||||
var report = new AbuseReport
|
||||
{
|
||||
ResourceIdentifier = resourceIdentifier,
|
||||
Type = type,
|
||||
Reason = reason,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.AbuseReports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogInformation("New abuse report created: {ReportId} for resource {ResourceId}",
|
||||
report.Id, resourceIdentifier);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> CountAbuseReports(bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.Include(r => r.Account)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.AccountId == accountId)
|
||||
.Where(r => includeResolved || r.ResolvedAt == null)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution)
|
||||
{
|
||||
var report = await db.AbuseReports.FindAsync(id);
|
||||
if (report == null)
|
||||
{
|
||||
throw new KeyNotFoundException("Report not found");
|
||||
}
|
||||
|
||||
report.ResolvedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
report.Resolution = resolution;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingAbuseReportsCount()
|
||||
{
|
||||
return await db.AbuseReports
|
||||
.Where(r => r.ResolvedAt == null)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, Shared.Models.RelationshipStatus status)
|
||||
{
|
||||
return await db.AccountRelationships.AnyAsync(r =>
|
||||
(r.AccountId == accountId1 && r.RelatedId == accountId2 && r.Status == status) ||
|
||||
(r.AccountId == accountId2 && r.RelatedId == accountId1 && r.Status == status)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, Shared.Models.Status>> GetStatuses(List<Guid> accountIds)
|
||||
{
|
||||
return await db.AccountStatuses
|
||||
.Where(s => accountIds.Contains(s.AccountId))
|
||||
.GroupBy(s => s.AccountId)
|
||||
.ToDictionaryAsync(g => g.Key, g => g.OrderByDescending(s => s.CreatedAt).First());
|
||||
}
|
||||
|
||||
public async Task SendNotification(Shared.Models.Account account, string topic, string title, string? subtitle, string body, string? actionUri = null)
|
||||
{
|
||||
await nty.SendNotification(account, topic, title, subtitle, body, actionUri: actionUri);
|
||||
}
|
||||
|
||||
public async Task<List<Shared.Models.Account>> ListAccountFriends(Shared.Models.Account account)
|
||||
{
|
||||
return await relationshipService.ListAccountFriends(account);
|
||||
}
|
||||
|
||||
public string CreateToken(Shared.Models.Session session)
|
||||
{
|
||||
return authService.CreateToken(session);
|
||||
}
|
||||
|
||||
public string GetAuthCookieTokenName()
|
||||
{
|
||||
return AuthConstants.CookieTokenName;
|
||||
}
|
||||
|
||||
public async Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Shared.Models.Account? account = null)
|
||||
{
|
||||
return await actionLogService.CreateActionLogFromRequest(type, meta, ipAddress, userAgent, account);
|
||||
}
|
||||
|
||||
public async Task<Challenge> UpdateAuthChallenge(Challenge challenge)
|
||||
{
|
||||
db.AuthChallenges.Update(challenge);
|
||||
await db.SaveChangesAsync();
|
||||
return challenge;
|
||||
}
|
||||
|
||||
public async Task<Session> CreateSession(Instant lastGrantedAt, Instant expiredAt, Shared.Models.Account account, Challenge challenge)
|
||||
{
|
||||
var session = new Session
|
||||
{
|
||||
LastGrantedAt = lastGrantedAt,
|
||||
ExpiredAt = expiredAt,
|
||||
Account = account,
|
||||
Challenge = challenge,
|
||||
};
|
||||
db.AuthSessions.Add(session);
|
||||
await db.SaveChangesAsync();
|
||||
return session;
|
||||
}
|
||||
|
||||
public async Task UpdateSessionLastGrantedAt(Guid sessionId, Instant lastGrantedAt)
|
||||
{
|
||||
await db.AuthSessions
|
||||
.Where(s => s.Id == sessionId)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, lastGrantedAt));
|
||||
}
|
||||
|
||||
public async Task UpdateAccountProfileLastSeenAt(Guid accountId, Instant lastSeenAt)
|
||||
{
|
||||
await db.AccountProfiles
|
||||
.Where(a => a.AccountId == accountId)
|
||||
.ExecuteUpdateAsync(a => a.SetProperty(x => x.LastSeenAt, lastSeenAt));
|
||||
}
|
||||
|
||||
public async Task<List<Shared.Models.Account>> SearchAccountsAsync(string searchTerm)
|
||||
{
|
||||
return await db.Accounts
|
||||
.Where(a => EF.Functions.ILike(a.Name, $"%{searchTerm}%"))
|
||||
.OrderBy(a => a.Name)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using MagicOnion.Server;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
/// <summary>
|
||||
/// Service for handling username generation and validation
|
||||
/// </summary>
|
||||
public class AccountUsernameService(AppDatabase db)
|
||||
public class AccountUsernameService(AppDatabase db) : ServiceBase<IAccountUsernameService>, IAccountUsernameService
|
||||
{
|
||||
private readonly Random _random = new();
|
||||
|
||||
58
DysonNetwork.Pass/Account/ActionLogService.cs
Normal file
58
DysonNetwork.Pass/Account/ActionLogService.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using MagicOnion.Server;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
public class ActionLogService : ServiceBase<IActionLogService>, IActionLogService
|
||||
{
|
||||
// private readonly GeoIpService _geo;
|
||||
// private readonly FlushBufferService _fbs;
|
||||
|
||||
public ActionLogService(
|
||||
// GeoIpService geo,
|
||||
// FlushBufferService fbs
|
||||
)
|
||||
{
|
||||
// _geo = geo;
|
||||
// _fbs = fbs;
|
||||
}
|
||||
|
||||
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
|
||||
{
|
||||
var log = new ActionLog
|
||||
{
|
||||
Action = action,
|
||||
AccountId = accountId,
|
||||
Meta = meta,
|
||||
};
|
||||
|
||||
// fbs.Enqueue(log);
|
||||
}
|
||||
|
||||
public async Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Shared.Models.Account? account = null)
|
||||
{
|
||||
var log = new ActionLog
|
||||
{
|
||||
Action = type,
|
||||
Meta = meta,
|
||||
UserAgent = userAgent,
|
||||
IpAddress = ipAddress,
|
||||
// Location = geo.GetPointFromIp(ipAddress)
|
||||
};
|
||||
|
||||
if (account != null)
|
||||
log.AccountId = account.Id;
|
||||
else
|
||||
throw new ArgumentException("No user context was found");
|
||||
|
||||
// For MagicOnion, HttpContext.Items["CurrentSession"] is not directly available.
|
||||
// You might need to pass session ID explicitly if needed.
|
||||
// if (request.HttpContext.Items["CurrentSession"] is Session currentSession)
|
||||
// log.SessionId = currentSession.Id;
|
||||
|
||||
// fbs.Enqueue(log);
|
||||
return log;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/spells")]
|
||||
@@ -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 DysonNetwork.Shared.Services;
|
||||
using MagicOnion.Server;
|
||||
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,
|
||||
IConfiguration configuration,
|
||||
ILogger<MagicSpellService> logger,
|
||||
IStringLocalizer<Localization.EmailResource> localizer
|
||||
)
|
||||
ILogger<MagicSpellService> logger
|
||||
) : ServiceBase<IMagicSpellService>, IMagicSpellService
|
||||
{
|
||||
public async Task<MagicSpell> CreateMagicSpell(
|
||||
Account account,
|
||||
Shared.Models.Account account,
|
||||
MagicSpellType type,
|
||||
Dictionary<string, object> meta,
|
||||
Instant? expiredAt = null,
|
||||
@@ -61,6 +59,22 @@ public class MagicSpellService(
|
||||
return spell;
|
||||
}
|
||||
|
||||
public async Task<MagicSpell?> GetMagicSpellAsync(string token)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var spell = await db.MagicSpells
|
||||
.Where(s => s.Spell == token)
|
||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return spell;
|
||||
}
|
||||
|
||||
public async Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId)
|
||||
{
|
||||
return await db.MagicSpells.FirstOrDefaultAsync(s => s.Id == spellId);
|
||||
}
|
||||
|
||||
public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
|
||||
{
|
||||
var contact = await db.AccountContacts
|
||||
@@ -87,54 +101,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();
|
||||
@@ -146,8 +160,15 @@ public class MagicSpellService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ApplyMagicSpell(MagicSpell spell)
|
||||
public async Task ApplyMagicSpell(string token)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var spell = await db.MagicSpells
|
||||
.Where(s => s.Spell == token)
|
||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
||||
.FirstOrDefaultAsync();
|
||||
if (spell is null) throw new ArgumentException("Magic spell not found.");
|
||||
|
||||
switch (spell.Type)
|
||||
{
|
||||
case MagicSpellType.AuthPasswordReset:
|
||||
@@ -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.Shared.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();
|
||||
@@ -140,7 +141,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C
|
||||
|
||||
[HttpPost("send")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "notifications.send")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("global", "notifications.send")]
|
||||
public async Task<ActionResult> SendNotification(
|
||||
[FromBody] NotificationWithAimRequest request,
|
||||
[FromQuery] bool save = false
|
||||
@@ -1,17 +1,20 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using NodaTime;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using EFCore.BulkExtensions;
|
||||
using MagicOnion.Server;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
|
||||
public class NotificationService(
|
||||
AppDatabase db,
|
||||
WebSocketService ws,
|
||||
IHttpClientFactory httpFactory,
|
||||
IConfiguration config)
|
||||
IConfiguration config,
|
||||
IHttpClientFactory httpFactory
|
||||
) : ServiceBase<INotificationService>, INotificationService
|
||||
{
|
||||
private readonly string _notifyTopic = config["Notifications:Topic"]!;
|
||||
private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!);
|
||||
@@ -24,14 +27,14 @@ public class NotificationService(
|
||||
}
|
||||
|
||||
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
||||
Account account,
|
||||
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)
|
||||
@@ -70,7 +73,7 @@ public class NotificationService(
|
||||
}
|
||||
|
||||
public async Task<Notification> SendNotification(
|
||||
Account account,
|
||||
Shared.Models.Account account,
|
||||
string topic,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
@@ -103,18 +106,19 @@ public class NotificationService(
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (!isSilent) _ = DeliveryNotification(notification);
|
||||
if (!isSilent)
|
||||
_ = DeliveryNotification(notification);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
public async Task DeliveryNotification(Notification notification)
|
||||
{
|
||||
ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
||||
{
|
||||
Type = "notifications.new",
|
||||
Data = notification
|
||||
});
|
||||
// ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
||||
// {
|
||||
// Type = "notifications.new",
|
||||
// Data = notification
|
||||
// });
|
||||
|
||||
// Pushing the notification
|
||||
var subscribers = await db.NotificationPushSubscriptions
|
||||
@@ -138,7 +142,7 @@ public class NotificationService(
|
||||
|
||||
public async Task BroadcastNotification(Notification notification, bool save = false)
|
||||
{
|
||||
var accounts = await db.Accounts.ToListAsync();
|
||||
var accounts = new List<Shared.Models.Account>(); // await db.Accounts.ToListAsync();
|
||||
|
||||
if (save)
|
||||
{
|
||||
@@ -164,11 +168,11 @@ public class NotificationService(
|
||||
{
|
||||
notification.Account = account;
|
||||
notification.AccountId = account.Id;
|
||||
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||
{
|
||||
Type = "notifications.new",
|
||||
Data = notification
|
||||
});
|
||||
// ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||
// {
|
||||
// Type = "notifications.new",
|
||||
// Data = notification
|
||||
// });
|
||||
}
|
||||
|
||||
var subscribers = await db.NotificationPushSubscriptions
|
||||
@@ -176,7 +180,8 @@ public class NotificationService(
|
||||
await _PushNotification(notification, subscribers);
|
||||
}
|
||||
|
||||
public async Task SendNotificationBatch(Notification notification, List<Account> accounts, bool save = false)
|
||||
public async Task SendNotificationBatch(Notification notification, List<Shared.Models.Account> accounts,
|
||||
bool save = false)
|
||||
{
|
||||
if (save)
|
||||
{
|
||||
@@ -202,18 +207,18 @@ public class NotificationService(
|
||||
{
|
||||
notification.Account = account;
|
||||
notification.AccountId = account.Id;
|
||||
ws.SendPacketToAccount(account.Id, new WebSocketPacket
|
||||
{
|
||||
Type = "notifications.new",
|
||||
Data = notification
|
||||
});
|
||||
// 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);
|
||||
// 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,
|
||||
@@ -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.");
|
||||
@@ -1,10 +1,13 @@
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using MagicOnion.Server;
|
||||
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) : ServiceBase<IRelationshipService>, IRelationshipService
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -152,26 +155,30 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> ListAccountFriends(Account account)
|
||||
public async Task<List<Shared.Models.Account>> ListAccountFriends(Shared.Models.Account account)
|
||||
{
|
||||
var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}";
|
||||
var friends = await cache.GetAsync<List<Guid>>(cacheKey);
|
||||
var friends = await cache.GetAsync<List<Shared.Models.Account>>(cacheKey);
|
||||
|
||||
if (friends == null)
|
||||
{
|
||||
friends = await db.AccountRelationships
|
||||
var friendIds = await db.AccountRelationships
|
||||
.Where(r => r.RelatedId == account.Id)
|
||||
.Where(r => r.Status == RelationshipStatus.Friends)
|
||||
.Select(r => r.AccountId)
|
||||
.ToListAsync();
|
||||
|
||||
friends = await db.Accounts
|
||||
.Where(a => friendIds.Contains(a.Id))
|
||||
.ToListAsync();
|
||||
|
||||
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
return friends ?? [];
|
||||
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);
|
||||
@@ -187,7 +194,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
return blocked ?? [];
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
||||
276
DysonNetwork.Pass/AppDatabase.cs
Normal file
276
DysonNetwork.Pass/AppDatabase.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
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; }
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; }
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; }
|
||||
|
||||
public DbSet<DysonNetwork.Shared.Models.Publisher> Publishers { get; set; }
|
||||
public DbSet<PublisherMember> PublisherMembers { get; set; }
|
||||
public DbSet<PublisherFeature> PublisherFeatures { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
@@ -189,8 +189,6 @@ public class DysonTokenAuthHandler(
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
|
||||
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
|
||||
@@ -103,6 +105,7 @@ public class AuthService(
|
||||
|
||||
public async Task<bool> ValidateCaptcha(string token)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||
|
||||
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
||||
@@ -152,6 +155,7 @@ public class AuthService(
|
||||
default:
|
||||
throw new ArgumentException("The server misconfigured for the captcha.");
|
||||
}
|
||||
return true; // Placeholder for captcha validation
|
||||
}
|
||||
|
||||
public string CreateToken(Session session)
|
||||
@@ -183,56 +187,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)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
namespace DysonNetwork.Pass.Auth;
|
||||
|
||||
public class CaptchaVerificationResponse
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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]
|
||||
@@ -20,8 +21,7 @@ public class OidcProviderController(
|
||||
AppDatabase db,
|
||||
OidcProviderService oidcService,
|
||||
IConfiguration configuration,
|
||||
IOptions<OidcProviderOptions> options,
|
||||
ILogger<OidcProviderController> logger
|
||||
IOptions<OidcProviderOptions> options
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
@@ -114,7 +114,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
|
||||
@@ -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
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Options;
|
||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||
|
||||
public class OidcProviderOptions
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||
|
||||
public class AuthorizationResponse
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||
|
||||
public class ErrorResponse
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses;
|
||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Responses;
|
||||
|
||||
public class TokenResponse
|
||||
{
|
||||
@@ -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,20 @@ public class OidcProviderService(
|
||||
|
||||
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
||||
{
|
||||
return await db.CustomApps
|
||||
.Include(c => c.Secrets)
|
||||
.FirstOrDefaultAsync(c => c.Id == clientId);
|
||||
await Task.CompletedTask;
|
||||
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);
|
||||
await Task.CompletedTask;
|
||||
return null;
|
||||
// return await db.CustomApps
|
||||
// .Include(c => c.Secrets)
|
||||
// .FirstOrDefaultAsync(c => c.Id == appId);
|
||||
}
|
||||
|
||||
public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class AfdianOidcService(
|
||||
IConfiguration configuration,
|
||||
@@ -1,8 +1,6 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class AppleMobileConnectRequest
|
||||
{
|
||||
@@ -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.Shared.Models;
|
||||
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
|
||||
@@ -1,11 +1,13 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
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 +27,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 +50,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 +68,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 +134,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);
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class DiscordOidcService(
|
||||
IConfiguration configuration,
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class GitHubOidcService(
|
||||
IConfiguration configuration,
|
||||
@@ -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.Shared.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class GoogleOidcService(
|
||||
IConfiguration configuration,
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
public class MicrosoftOidcService(
|
||||
IConfiguration configuration,
|
||||
@@ -1,11 +1,13 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
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 +34,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 +127,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");
|
||||
@@ -1,13 +1,15 @@
|
||||
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 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 +192,7 @@ public abstract class OidcService(
|
||||
/// </summary>
|
||||
public async Task<Challenge> CreateChallengeForUserAsync(
|
||||
OidcUserInfo userInfo,
|
||||
Account.Account account,
|
||||
Shared.Models.Account account,
|
||||
HttpContext request,
|
||||
string deviceId
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Pass.Auth.OpenId;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state parameter used in OpenID Connect flows.
|
||||
29
DysonNetwork.Pass/Developer/CustomAppService.cs
Normal file
29
DysonNetwork.Pass/Developer/CustomAppService.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MagicOnion.Server;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Pass.Developer;
|
||||
|
||||
public class CustomAppService : ServiceBase<ICustomAppService>, ICustomAppService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
|
||||
public CustomAppService(AppDatabase db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> FindClientByIdAsync(Guid clientId)
|
||||
{
|
||||
return await _db.CustomApps.FirstOrDefaultAsync(app => app.Id == clientId);
|
||||
}
|
||||
|
||||
public async Task<int> CountCustomAppsByPublisherId(Guid publisherId)
|
||||
{
|
||||
return await _db.CustomApps.CountAsync(app => app.PublisherId == publisherId);
|
||||
}
|
||||
}
|
||||
77
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal file
77
DysonNetwork.Pass/DysonNetwork.Pass.csproj
Normal file
@@ -0,0 +1,77 @@
|
||||
<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="MagicOnion.Client" Version="7.0.5" />
|
||||
<PackageReference Include="MagicOnion.Server" Version="7.0.5" />
|
||||
<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="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<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" Version="9.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
|
||||
<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>
|
||||
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
6
DysonNetwork.Pass/Localization/AccountEventResource.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Pass.Localization;
|
||||
|
||||
public class AccountEventResource
|
||||
{
|
||||
|
||||
}
|
||||
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
6
DysonNetwork.Pass/Localization/NotificationResource.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Pass.Localization;
|
||||
|
||||
public class NotificationResource
|
||||
{
|
||||
|
||||
}
|
||||
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
6
DysonNetwork.Pass/Localization/SharedResource.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DysonNetwork.Pass.Localization;
|
||||
|
||||
public class SharedResource
|
||||
{
|
||||
|
||||
}
|
||||
43
DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
Normal file
43
DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
Normal file
@@ -0,0 +1,43 @@
|
||||
@using DysonNetwork.Pass.Localization
|
||||
@using DysonNetwork.Shared.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<EmailLayout>
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<p class="font-bold">@(Localizer["AccountDeletionHeader"])</p>
|
||||
<p>@(Localizer["AccountDeletionPara1"]) @@@Name,</p>
|
||||
<p>@(Localizer["AccountDeletionPara2"])</p>
|
||||
<p>@(Localizer["AccountDeletionPara3"])</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@Link" target="_blank">
|
||||
@(Localizer["AccountDeletionButton"])
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>@(Localizer["AccountDeletionPara4"])</p>
|
||||
</td>
|
||||
</tr>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Link { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@using DysonNetwork.Shared.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<EmailLayout>
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<p class="font-bold">@(Localizer["ContactVerificationHeader"])</p>
|
||||
<p>@(Localizer["ContactVerificationPara1"]) @Name,</p>
|
||||
<p>@(Localizer["ContactVerificationPara2"])</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@Link" target="_blank">
|
||||
@(Localizer["ContactVerificationButton"])
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>@(Localizer["ContactVerificationPara3"])</p>
|
||||
<p>@(Localizer["ContactVerificationPara4"])</p>
|
||||
</td>
|
||||
</tr>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Link { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
}
|
||||
337
DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
Normal file
337
DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
Normal file
@@ -0,0 +1,337 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style media="all" type="text/css">
|
||||
body {
|
||||
font-family: Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 16px;
|
||||
line-height: 1.3;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f4f5f6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: #f4f5f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto !important;
|
||||
max-width: 600px;
|
||||
padding: 0;
|
||||
padding-top: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eaebed;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
padding-top: 24px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #9a9ea6;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0867ec;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
min-width: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 2px #0867ec;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
color: #0867ec;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #0867ec;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #0867ec;
|
||||
border-color: #0867ec;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.verification-code
|
||||
{
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
@@media all {
|
||||
.btn-primary table td:hover {
|
||||
background-color: #ec0867 !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #ec0867 !important;
|
||||
border-color: #ec0867 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: #0867ec !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@media only screen and (max-width: 640px) {
|
||||
.main p,
|
||||
.main td,
|
||||
.main span {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
padding-top: 8px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
font-size: 16px !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
@ChildContent
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<span class="apple-link">Solar Network</span>
|
||||
<br> Solsynth LLC © @(DateTime.Now.Year)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block powered-by">
|
||||
Powered by <a href="https://github.com/solsynth/dysonnetwork">Dyson Network</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER --></div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
42
DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
Normal file
42
DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@using DysonNetwork.Shared.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<EmailLayout>
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<p class="font-bold">@(Localizer["LandingHeader1"])</p>
|
||||
<p>@(Localizer["LandingPara1"]) @@@Name,</p>
|
||||
<p>@(Localizer["LandingPara2"])</p>
|
||||
<p>@(Localizer["LandingPara3"])</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@Link" target="_blank">
|
||||
@(Localizer["LandingButton1"])
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>@(Localizer["LandingPara4"])</p>
|
||||
</td>
|
||||
</tr>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Link { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
}
|
||||
44
DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
Normal file
44
DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
Normal file
@@ -0,0 +1,44 @@
|
||||
@using DysonNetwork.Pass.Localization
|
||||
@using DysonNetwork.Shared.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<EmailLayout>
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<p class="font-bold">@(Localizer["PasswordResetHeader"])</p>
|
||||
<p>@(Localizer["PasswordResetPara1"]) @@@Name,</p>
|
||||
<p>@(Localizer["PasswordResetPara2"])</p>
|
||||
<p>@(Localizer["PasswordResetPara3"])</p>
|
||||
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="@Link" target="_blank">
|
||||
@(Localizer["PasswordResetButton"])
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>@(Localizer["PasswordResetPara4"])</p>
|
||||
</td>
|
||||
</tr>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Link { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||
}
|
||||
27
DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
Normal file
27
DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
Normal file
@@ -0,0 +1,27 @@
|
||||
@using DysonNetwork.Pass.Localization
|
||||
@using DysonNetwork.Shared.Localization
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<EmailLayout>
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<p class="font-bold">@(Localizer["VerificationHeader1"])</p>
|
||||
<p>@(Localizer["VerificationPara1"]) @@@Name,</p>
|
||||
<p>@(Localizer["VerificationPara2"])</p>
|
||||
<p>@(Localizer["VerificationPara3"])</p>
|
||||
|
||||
<p class="verification-code">@Code</p>
|
||||
|
||||
<p>@(Localizer["VerificationPara4"])</p>
|
||||
<p>@(Localizer["VerificationPara5"])</p>
|
||||
</td>
|
||||
</tr>
|
||||
</EmailLayout>
|
||||
|
||||
@code {
|
||||
[Parameter] public required string Name { get; set; }
|
||||
[Parameter] public required string Code { get; set; }
|
||||
|
||||
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
using MagicOnion;
|
||||
using MagicOnion.Server;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
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,
|
||||
ICacheService cache
|
||||
)
|
||||
) : ServiceBase<IPermissionService>, IPermissionService
|
||||
{
|
||||
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
|
||||
|
||||
@@ -194,4 +198,11 @@ public class PermissionService(
|
||||
Value = _SerializePermissionValue(value),
|
||||
};
|
||||
}
|
||||
|
||||
public async UnaryResult<bool> CheckPermission(string scope, string permission)
|
||||
{
|
||||
// Assuming the actor is always "user:current" for client-side checks
|
||||
// You might need to adjust this based on how your client identifies itself
|
||||
return await HasPermissionAsync("user:current", scope, permission);
|
||||
}
|
||||
}
|
||||
47
DysonNetwork.Pass/Program.cs
Normal file
47
DysonNetwork.Pass/Program.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using DysonNetwork.Pass;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Startup;
|
||||
using DysonNetwork.Shared.Startup;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MagicOnion.Server;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.ConfigureAppKestrel();
|
||||
builder.Services.AddAppSwagger();
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddAppRateLimiting();
|
||||
builder.Services.AddAppBusinessServices(builder.Configuration);
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddMagicOnion();
|
||||
builder.Services.AddDbContext<AppDatabase>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("App")));
|
||||
|
||||
builder.Services.AddScoped<AccountService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<DysonNetwork.Pass.Publisher.PublisherService>();
|
||||
builder.Services.AddScoped<DysonNetwork.Pass.Developer.CustomAppService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseAuthorization();
|
||||
app.ConfigureAppMiddleware(builder.Configuration);
|
||||
|
||||
app.MapControllers();
|
||||
app.MapMagicOnionService();
|
||||
|
||||
// Run database migrations
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
53
DysonNetwork.Pass/Publisher/PublisherService.cs
Normal file
53
DysonNetwork.Pass/Publisher/PublisherService.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using MagicOnion.Server;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Publisher;
|
||||
|
||||
public class PublisherService : ServiceBase<IPublisherService>, IPublisherService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
|
||||
public PublisherService(AppDatabase db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<Shared.Models.Publisher?> GetPublisherByName(string name)
|
||||
{
|
||||
return await _db.Publishers.FirstOrDefaultAsync(p => p.Name == name);
|
||||
}
|
||||
|
||||
public async Task<List<Shared.Models.Publisher>> GetUserPublishers(Guid accountId)
|
||||
{
|
||||
var publisherIds = await _db.PublisherMembers
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Select(m => m.PublisherId)
|
||||
.ToListAsync();
|
||||
|
||||
return await _db.Publishers
|
||||
.Where(p => publisherIds.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role)
|
||||
{
|
||||
return await _db.PublisherMembers.AnyAsync(m =>
|
||||
m.PublisherId == publisherId &&
|
||||
m.AccountId == accountId &&
|
||||
m.Role >= role);
|
||||
}
|
||||
|
||||
public async Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId)
|
||||
{
|
||||
return await _db.PublisherFeatures
|
||||
.Where(f => f.PublisherId == publisherId)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Permission;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Sphere.Safety;
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
[ApiController]
|
||||
[Route("/safety/reports")]
|
||||
@@ -30,7 +31,7 @@ public class AbuseReportController(
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<AbuseReport>> CreateReport([FromBody] CreateReportRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -75,7 +76,7 @@ public class AbuseReportController(
|
||||
[FromQuery] bool includeResolved = false
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized();
|
||||
|
||||
var totalCount = await safety.CountUserReports(currentUser.Id, includeResolved);
|
||||
var reports = await safety.GetUserReports(currentUser.Id, offset, take, includeResolved);
|
||||
@@ -85,7 +86,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AbuseReport>> GetReportById(Guid id)
|
||||
@@ -101,7 +102,7 @@ public class AbuseReportController(
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<AbuseReport>> GetMyReportById(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 report = await safety.GetReportById(id);
|
||||
if (report == null) return NotFound();
|
||||
@@ -122,7 +123,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpPost("{id}/resolve")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.resolve")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.resolve")]
|
||||
[ProducesResponseType<AbuseReport>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
|
||||
@@ -144,7 +145,7 @@ public class AbuseReportController(
|
||||
|
||||
[HttpGet("count")]
|
||||
[Authorize]
|
||||
[RequiredPermission("safety", "reports.view")]
|
||||
[DysonNetwork.Shared.Permission.RequiredPermission("safety", "reports.view")]
|
||||
[ProducesResponseType<object>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<object>> GetReportsCount()
|
||||
{
|
||||
61
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
61
DysonNetwork.Pass/Safety/SafetyService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DysonNetwork.Pass.Safety;
|
||||
|
||||
public class SafetyService(AppDatabase db, IAccountService accountService, ILogger<SafetyService> logger)
|
||||
{
|
||||
public async Task<AbuseReport> CreateReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId)
|
||||
{
|
||||
// Check if a similar report already exists from this user
|
||||
var existingReport = await db.AbuseReports
|
||||
.Where(r => r.ResourceIdentifier == resourceIdentifier &&
|
||||
r.AccountId == accountId &&
|
||||
r.DeletedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
throw new InvalidOperationException("You have already reported this content.");
|
||||
}
|
||||
|
||||
return await accountService.CreateAbuseReport(resourceIdentifier, type, reason, accountId);
|
||||
}
|
||||
|
||||
public async Task<int> CountReports(bool includeResolved = false)
|
||||
{
|
||||
return await accountService.CountAbuseReports(includeResolved);
|
||||
}
|
||||
|
||||
public async Task<int> CountUserReports(Guid accountId, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.CountUserAbuseReports(accountId, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetReports(int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.GetAbuseReports(skip, take, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<List<AbuseReport>> GetUserReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false)
|
||||
{
|
||||
return await accountService.GetUserAbuseReports(accountId, skip, take, includeResolved);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport?> GetReportById(Guid id)
|
||||
{
|
||||
return await accountService.GetAbuseReport(id);
|
||||
}
|
||||
|
||||
public async Task<AbuseReport> ResolveReport(Guid id, string resolution)
|
||||
{
|
||||
return await accountService.ResolveAbuseReport(id, resolution);
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingReportsCount()
|
||||
{
|
||||
return await accountService.GetPendingAbuseReportsCount();
|
||||
}
|
||||
}
|
||||
187
DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
Normal file
187
DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Threading.RateLimiting;
|
||||
using DysonNetwork.Pass.Account;
|
||||
using DysonNetwork.Pass.Auth;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||
using DysonNetwork.Pass.Auth.OpenId;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Pass.Permission;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace DysonNetwork.Pass.Startup;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||
{
|
||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
||||
return ConnectionMultiplexer.Connect(connection);
|
||||
});
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
// Register MagicOnion services
|
||||
services.AddScoped<IAccountService, AccountService>();
|
||||
services.AddScoped<INotificationService, NotificationService>();
|
||||
services.AddScoped<IRelationshipService, RelationshipService>();
|
||||
services.AddScoped<IActionLogService, ActionLogService>();
|
||||
services.AddScoped<IAccountUsernameService, AccountUsernameService>();
|
||||
services.AddScoped<IMagicSpellService, MagicSpellService>();
|
||||
services.AddScoped<IAccountEventService, AccountEventService>();
|
||||
services.AddScoped<IAccountProfileService, AccountProfileService>();
|
||||
|
||||
// Register OIDC services
|
||||
services.AddScoped<OidcService, GoogleOidcService>();
|
||||
services.AddScoped<OidcService, AppleOidcService>();
|
||||
services.AddScoped<OidcService, GitHubOidcService>();
|
||||
services.AddScoped<OidcService, MicrosoftOidcService>();
|
||||
services.AddScoped<OidcService, DiscordOidcService>();
|
||||
services.AddScoped<OidcService, AfdianOidcService>();
|
||||
services.AddScoped<GoogleOidcService>();
|
||||
services.AddScoped<AppleOidcService>();
|
||||
services.AddScoped<GitHubOidcService>();
|
||||
services.AddScoped<MicrosoftOidcService>();
|
||||
services.AddScoped<DiscordOidcService>();
|
||||
services.AddScoped<AfdianOidcService>();
|
||||
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
}).AddDataAnnotationsLocalization(options =>
|
||||
{
|
||||
options.DataAnnotationLocalizerProvider = (type, factory) =>
|
||||
factory.Create(typeof(SharedResource));
|
||||
});
|
||||
services.AddRazorPages();
|
||||
|
||||
services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
var supportedCultures = new[]
|
||||
{
|
||||
new CultureInfo("en-US"),
|
||||
new CultureInfo("zh-Hans"),
|
||||
};
|
||||
|
||||
options.SupportedCultures = supportedCultures;
|
||||
options.SupportedUICultures = supportedCultures;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppRateLimiting(this IServiceCollection services)
|
||||
{
|
||||
services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts =>
|
||||
{
|
||||
opts.Window = TimeSpan.FromMinutes(1);
|
||||
opts.PermitLimit = 120;
|
||||
opts.QueueLimit = 2;
|
||||
opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
}));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthorization();
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = AuthConstants.SchemeName;
|
||||
options.DefaultChallengeScheme = AuthConstants.SchemeName;
|
||||
})
|
||||
.AddScheme<DysonTokenAuthOptions, DysonTokenAuthHandler>(AuthConstants.SchemeName, _ => { });
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "Solar Network API",
|
||||
Description = "An open-source social network",
|
||||
TermsOfService = new Uri("https://solsynth.dev/terms"),
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Name = "APGLv3",
|
||||
Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html")
|
||||
}
|
||||
});
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Please enter a valid token",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
[]
|
||||
}
|
||||
});
|
||||
});
|
||||
services.AddOpenApi();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppBusinessServices(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<CompactTokenService>();
|
||||
services.AddScoped<PermissionService>();
|
||||
services.AddScoped<ActionLogService>();
|
||||
services.AddScoped<AccountService>();
|
||||
services.AddScoped<AccountEventService>();
|
||||
services.AddScoped<ActionLogService>();
|
||||
services.AddScoped<RelationshipService>();
|
||||
services.AddScoped<MagicSpellService>();
|
||||
services.AddScoped<NotificationService>();
|
||||
services.AddScoped<AuthService>();
|
||||
services.AddScoped<AccountUsernameService>();
|
||||
|
||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||
services.AddScoped<OidcProviderService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
11
DysonNetwork.Pass/appsettings.json
Normal file
11
DysonNetwork.Pass/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
43
DysonNetwork.Shared/DysonNetwork.Shared.csproj
Normal file
43
DysonNetwork.Shared/DysonNetwork.Shared.csproj
Normal file
@@ -0,0 +1,43 @@
|
||||
<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="dotnet-etcd" Version="8.0.1" />
|
||||
<PackageReference Include="MagicOnion.Client" Version="7.0.5" />
|
||||
<PackageReference Include="MagicOnion.Server" Version="7.0.5" />
|
||||
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" 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="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.11.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
70
DysonNetwork.Shared/Etcd/EtcdService.cs
Normal file
70
DysonNetwork.Shared/Etcd/EtcdService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using dotnet_etcd;
|
||||
using Etcdserverpb;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace DysonNetwork.Shared.Etcd;
|
||||
|
||||
public class EtcdService(string connectionString) : IEtcdService
|
||||
{
|
||||
private readonly EtcdClient _etcdClient = new(connectionString);
|
||||
private long _leaseId;
|
||||
private string? _serviceKey;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public async Task RegisterServiceAsync(string serviceName, string serviceAddress, int ttl = 15)
|
||||
{
|
||||
_serviceKey = $"/services/{serviceName}/{Guid.NewGuid()}";
|
||||
var leaseGrantResponse = await _etcdClient.LeaseGrantAsync(new LeaseGrantRequest { TTL = ttl });
|
||||
_leaseId = leaseGrantResponse.ID;
|
||||
|
||||
await _etcdClient.PutAsync(new PutRequest
|
||||
{
|
||||
Key = Google.Protobuf.ByteString.CopyFromUtf8(_serviceKey),
|
||||
Value = Google.Protobuf.ByteString.CopyFromUtf8(serviceAddress),
|
||||
Lease = _leaseId
|
||||
});
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _etcdClient.LeaseKeepAlive(new LeaseKeepAliveRequest { ID = _leaseId },
|
||||
_ => { }, _cts.Token);
|
||||
await Task.Delay(TimeSpan.FromSeconds(ttl / 3), _cts.Token);
|
||||
}
|
||||
catch (RpcException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
public async Task UnregisterServiceAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_serviceKey))
|
||||
{
|
||||
await _etcdClient.DeleteRangeAsync(_serviceKey);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> DiscoverServicesAsync(string serviceName)
|
||||
{
|
||||
var prefix = $"/services/{serviceName}/";
|
||||
var rangeResponse = await _etcdClient.GetRangeAsync(prefix);
|
||||
return rangeResponse.Kvs.Select(kv => kv.Value.ToStringUtf8()).ToList();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
if (_leaseId != 0)
|
||||
{
|
||||
_etcdClient.LeaseRevoke(new LeaseRevokeRequest { ID = _leaseId });
|
||||
}
|
||||
|
||||
_etcdClient.Dispose();
|
||||
}
|
||||
}
|
||||
46
DysonNetwork.Shared/Etcd/EtcdServiceExtensions.cs
Normal file
46
DysonNetwork.Shared/Etcd/EtcdServiceExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Grpc.Net.Client;
|
||||
using MagicOnion.Client;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Etcd
|
||||
{
|
||||
public static class EtcdServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddEtcdService(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var etcdConnectionString = configuration.GetConnectionString("Etcd");
|
||||
services.AddSingleton<IEtcdService>(new EtcdService(etcdConnectionString!));
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddMagicOnionService<TService>(this IServiceCollection services)
|
||||
where TService : class, MagicOnion.IService<TService>
|
||||
{
|
||||
services.AddSingleton(serviceProvider =>
|
||||
{
|
||||
var etcdService = serviceProvider.GetRequiredService<IEtcdService>();
|
||||
var serviceName = typeof(TService).Name.TrimStart('I'); // Convention: IMyService -> MyService
|
||||
|
||||
// Synchronously wait for service discovery (or handle asynchronously if preferred)
|
||||
var endpoints = etcdService.DiscoverServicesAsync(serviceName).GetAwaiter().GetResult();
|
||||
|
||||
if (!endpoints.Any())
|
||||
{
|
||||
throw new InvalidOperationException($"No endpoints found for MagicOnion service: {serviceName}");
|
||||
}
|
||||
|
||||
// For simplicity, use the first discovered endpoint
|
||||
var endpoint = endpoints.First();
|
||||
|
||||
var channel = GrpcChannel.ForAddress(endpoint);
|
||||
return MagicOnionClient.Create<TService>(channel);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
DysonNetwork.Shared/Etcd/IEtcdService.cs
Normal file
13
DysonNetwork.Shared/Etcd/IEtcdService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Etcd
|
||||
{
|
||||
public interface IEtcdService : IDisposable
|
||||
{
|
||||
Task RegisterServiceAsync(string serviceName, string serviceAddress, int ttl = 15);
|
||||
Task UnregisterServiceAsync();
|
||||
Task<List<string>> DiscoverServicesAsync(string serviceName);
|
||||
}
|
||||
}
|
||||
18
DysonNetwork.Shared/Localization/CultureInfoService.cs
Normal file
18
DysonNetwork.Shared/Localization/CultureInfoService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public abstract class CultureInfoService
|
||||
{
|
||||
public static void SetCultureInfo(Shared.Models.Account account)
|
||||
{
|
||||
SetCultureInfo(account.Language);
|
||||
}
|
||||
|
||||
public static void SetCultureInfo(string? languageCode)
|
||||
{
|
||||
var info = new CultureInfo(languageCode ?? "en-us", false);
|
||||
CultureInfo.CurrentCulture = info;
|
||||
CultureInfo.CurrentUICulture = info;
|
||||
}
|
||||
}
|
||||
5
DysonNetwork.Shared/Localization/EmailResource.cs
Normal file
5
DysonNetwork.Shared/Localization/EmailResource.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace DysonNetwork.Shared.Localization;
|
||||
|
||||
public class EmailResource
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
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!;
|
||||
}
|
||||
@@ -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>();
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
15
DysonNetwork.Shared/Models/ModelBase.cs
Normal file
15
DysonNetwork.Shared/Models/ModelBase.cs
Normal 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; }
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the user information from an OIDC provider
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
{
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
namespace DysonNetwork.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The verification info of a resource
|
||||
@@ -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
|
||||
63
DysonNetwork.Shared/Permission/MagicOnionPermissionFilter.cs
Normal file
63
DysonNetwork.Shared/Permission/MagicOnionPermissionFilter.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using MagicOnion.Server.Filters;
|
||||
using MagicOnion.Server;
|
||||
using DysonNetwork.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Grpc.Core;
|
||||
using MagicOnion;
|
||||
using MagicOnion.Server.Hubs;
|
||||
|
||||
namespace DysonNetwork.Shared.Permission;
|
||||
|
||||
public class MagicOnionPermissionFilter : IMagicOnionServiceFilter
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
|
||||
public MagicOnionPermissionFilter(IPermissionService permissionService)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
}
|
||||
|
||||
public async ValueTask Invoke(ServiceContext context, Func<ServiceContext, ValueTask> next)
|
||||
{
|
||||
var attribute = context.MethodInfo.GetCustomAttribute<RequiredPermissionAttribute>();
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
// If no RequiredPermissionAttribute is present, just continue
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Correct way to get HttpContext from ServiceContext
|
||||
var httpContext = context.CallContext.GetHttpContext();
|
||||
|
||||
if (httpContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("HttpContext is not available in ServiceContext.");
|
||||
}
|
||||
|
||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
{
|
||||
throw new ReturnStatusException(StatusCode.PermissionDenied, "Unauthorized: Current user not found.");
|
||||
}
|
||||
|
||||
if (currentUser.IsSuperuser)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var hasPermission = await _permissionService.CheckPermission(attribute.Scope, attribute.Permission);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
throw new ReturnStatusException(StatusCode.PermissionDenied, $"Permission {attribute.Scope}/{attribute.Permission} was required.");
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace DysonNetwork.Shared.Permission;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||
public class RequiredPermissionAttribute : Attribute
|
||||
{
|
||||
public string Scope { get; }
|
||||
public string Permission { get; }
|
||||
|
||||
public RequiredPermissionAttribute(string scope, string permission)
|
||||
{
|
||||
Scope = scope;
|
||||
Permission = permission;
|
||||
}
|
||||
}
|
||||
78
DysonNetwork.Shared/Services/EmailService.cs
Normal file
78
DysonNetwork.Shared/Services/EmailService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(
|
||||
string toName,
|
||||
string toEmail,
|
||||
string subject,
|
||||
string body,
|
||||
Dictionary<string, string>? headers = null
|
||||
)
|
||||
{
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(
|
||||
_configuration["Email:SenderName"],
|
||||
_configuration["Email:SenderEmail"]
|
||||
));
|
||||
message.To.Add(new MailboxAddress(toName, toEmail));
|
||||
message.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder { HtmlBody = body };
|
||||
message.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
message.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
using var client = new SmtpClient();
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(
|
||||
_configuration["Email:SmtpHost"],
|
||||
int.Parse(_configuration["Email:SmtpPort"]),
|
||||
MailKit.Security.SecureSocketOptions.StartTls
|
||||
);
|
||||
await client.AuthenticateAsync(
|
||||
_configuration["Email:SmtpUser"],
|
||||
_configuration["Email:SmtpPass"]
|
||||
);
|
||||
await client.SendAsync(message);
|
||||
_logger.LogInformation("Email sent to {ToEmail}", toEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send email to {ToEmail}", toEmail);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendTemplatedEmailAsync<T>(string toName, string toEmail, string subject, string htmlBody)
|
||||
{
|
||||
await SendEmailAsync(toName, toEmail, subject, htmlBody);
|
||||
}
|
||||
}
|
||||
34
DysonNetwork.Shared/Services/IAccountEventService.cs
Normal file
34
DysonNetwork.Shared/Services/IAccountEventService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountEventService : IService<IAccountEventService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Purges the status cache for a user
|
||||
/// </summary>
|
||||
void PurgeStatusCache(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of a user
|
||||
/// </summary>
|
||||
Task<Status> GetStatus(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statuses of a list of users
|
||||
/// </summary>
|
||||
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a daily check-in for a user
|
||||
/// </summary>
|
||||
Task<CheckInResult> CheckInDaily(Account user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the check-in streak for a user
|
||||
/// </summary>
|
||||
Task<int> GetCheckInStreak(Account user);
|
||||
}
|
||||
54
DysonNetwork.Shared/Services/IAccountProfileService.cs
Normal file
54
DysonNetwork.Shared/Services/IAccountProfileService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services
|
||||
{
|
||||
public interface IAccountProfileService : IService<IAccountProfileService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an account profile by account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account profile if found, otherwise null.</returns>
|
||||
Task<Profile?> GetAccountProfileByIdAsync(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the StellarMembership of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="subscription">The subscription to set as the StellarMembership.</param>
|
||||
/// <returns>The updated account profile.</returns>
|
||||
Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all account profiles that have a non-null StellarMembership.
|
||||
/// </summary>
|
||||
/// <returns>A list of account profiles with StellarMembership.</returns>
|
||||
Task<List<Profile>> GetAccountsWithStellarMembershipAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the StellarMembership for a list of account IDs.
|
||||
/// </summary>
|
||||
/// <param name="accountIds">The list of account IDs for which to clear the StellarMembership.</param>
|
||||
/// <returns>The number of accounts updated.</returns>
|
||||
Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the profile picture of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="picture">The new profile picture reference object.</param>
|
||||
/// <returns>The updated profile.</returns>
|
||||
Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the profile background of an account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="background">The new profile background reference object.</param>
|
||||
/// <returns>The updated profile.</returns>
|
||||
Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background);
|
||||
}
|
||||
}
|
||||
308
DysonNetwork.Shared/Services/IAccountService.cs
Normal file
308
DysonNetwork.Shared/Services/IAccountService.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountService : IService<IAccountService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes all cached data for the specified account
|
||||
/// </summary>
|
||||
Task PurgeAccountCache(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an account by username or contact information
|
||||
/// </summary>
|
||||
/// <param name="probe">Username or contact information to search for</param>
|
||||
/// <returns>The matching account if found, otherwise null</returns>
|
||||
Task<Account?> LookupAccount(string probe);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up an account by external authentication provider connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">The provider's unique identifier for the user</param>
|
||||
/// <param name="provider">The name of the authentication provider</param>
|
||||
/// <returns>The matching account if found, otherwise null</returns>
|
||||
Task<Account?> LookupAccountByConnection(string identifier, string provider);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the account level for the specified account ID
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account</param>
|
||||
/// <returns>The account level if found, otherwise null</returns>
|
||||
Task<int?> GetAccountLevel(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new account with the specified details
|
||||
/// </summary>
|
||||
/// <param name="name">The account username</param>
|
||||
/// <param name="nick">The display name/nickname</param>
|
||||
/// <param name="email">The primary email address</param>
|
||||
/// <param name="password">The account password (optional, can be set later)</param>
|
||||
/// <param name="language">The preferred language (defaults to en-US)</param>
|
||||
/// <param name="isEmailVerified">Whether the email is verified (defaults to false)</param>
|
||||
/// <param name="isActivated">Whether the account is activated (defaults to false)</param>
|
||||
/// <returns>The newly created account</returns>
|
||||
Task<Account> CreateAccount(
|
||||
string name,
|
||||
string nick,
|
||||
string email,
|
||||
string? password,
|
||||
string language = "en-US",
|
||||
bool isEmailVerified = false,
|
||||
bool isActivated = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new account using OpenID Connect user information
|
||||
/// </summary>
|
||||
/// <param name="userInfo">The OpenID Connect user information</param>
|
||||
/// <returns>The newly created account</returns>
|
||||
Task<Account> CreateAccount(OidcUserInfo userInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account by its ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="withProfile">Join the profile table or not.</param>
|
||||
/// <returns>The account if found, otherwise null.</returns>
|
||||
Task<Account?> GetAccountById(Guid accountId, bool withProfile = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account profile by account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account profile if found, otherwise null.</returns>
|
||||
Task<Profile?> GetAccountProfile(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication challenge by its ID.
|
||||
/// </summary>
|
||||
/// <param name="challengeId">The ID of the challenge.</param>
|
||||
/// <returns>The authentication challenge if found, otherwise null.</returns>
|
||||
Task<Challenge?> GetAuthChallenge(Guid challengeId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication challenge by account ID, IP address, and user agent.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="ipAddress">The IP address.</param>
|
||||
/// <param name="userAgent">The user agent.</param>
|
||||
/// <param name="now">The current instant.</param>
|
||||
/// <returns>The authentication challenge if found, otherwise null.</returns>
|
||||
Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, NodaTime.Instant now);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new authentication challenge.
|
||||
/// </summary>
|
||||
/// <param name="challenge">The challenge to create.</param>
|
||||
/// <returns>The created challenge.</returns>
|
||||
Task<Challenge> CreateAuthChallenge(Challenge challenge);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets an account authentication factor by its ID and account ID.
|
||||
/// </summary>
|
||||
/// <param name="factorId">The ID of the factor.</param>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>The account authentication factor if found, otherwise null.</returns>
|
||||
Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of account authentication factors for a given account ID.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <returns>A list of account authentication factors.</returns>
|
||||
Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authentication session by its ID.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The ID of the session.</param>
|
||||
/// <returns>The authentication session if found, otherwise null.</returns>
|
||||
Task<Session?> GetAuthSession(Guid sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its ID.
|
||||
/// </summary>
|
||||
/// <param name="spellId">The ID of the magic spell.</param>
|
||||
/// <returns>The magic spell if found, otherwise null.</returns>
|
||||
Task<MagicSpell?> GetMagicSpell(Guid spellId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an abuse report by its ID.
|
||||
/// </summary>
|
||||
/// <param name="reportId">The ID of the abuse report.</param>
|
||||
/// <returns>The abuse report if found, otherwise null.</returns>
|
||||
Task<AbuseReport?> GetAbuseReport(Guid reportId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new abuse report.
|
||||
/// </summary>
|
||||
/// <param name="resourceIdentifier">The identifier of the resource being reported.</param>
|
||||
/// <param name="type">The type of abuse report.</param>
|
||||
/// <param name="reason">The reason for the report.</param>
|
||||
/// <param name="accountId">The ID of the account making the report.</param>
|
||||
/// <returns>The created abuse report.</returns>
|
||||
Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Counts abuse reports.
|
||||
/// </summary>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>The count of abuse reports.</returns>
|
||||
Task<int> CountAbuseReports(bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Counts abuse reports by a specific user.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>The count of abuse reports by the user.</returns>
|
||||
Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of abuse reports.
|
||||
/// </summary>
|
||||
/// <param name="skip">Number of reports to skip.</param>
|
||||
/// <param name="take">Number of reports to take.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>A list of abuse reports.</returns>
|
||||
Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of abuse reports by a specific user.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="skip">Number of reports to skip.</param>
|
||||
/// <param name="take">Number of reports to take.</param>
|
||||
/// <param name="includeResolved">Whether to include resolved reports.</param>
|
||||
/// <returns>A list of abuse reports by the user.</returns>
|
||||
Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an abuse report.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the report to resolve.</param>
|
||||
/// <param name="resolution">The resolution message.</param>
|
||||
/// <returns>The resolved abuse report.</returns>
|
||||
Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of pending abuse reports.
|
||||
/// </summary>
|
||||
/// <returns>The count of pending abuse reports.</returns>
|
||||
Task<int> GetPendingAbuseReportsCount();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a relationship with a specific status exists between two accounts.
|
||||
/// </summary>
|
||||
/// <param name="accountId1">The ID of the first account.</param>
|
||||
/// <param name="accountId2">The ID of the second account.</param>
|
||||
/// <param name="status">The relationship status to check for.</param>
|
||||
/// <returns>True if the relationship exists, otherwise false.</returns>
|
||||
Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the statuses for a list of account IDs.
|
||||
/// </summary>
|
||||
/// <param name="accountIds">A list of account IDs.</param>
|
||||
/// <returns>A dictionary where the key is the account ID and the value is the status.</returns>
|
||||
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> accountIds);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to an account.
|
||||
/// </summary>
|
||||
/// <param name="account">The target account.</param>
|
||||
/// <param name="topic">The notification topic.</param>
|
||||
/// <param name="title">The notification title.</param>
|
||||
/// <param name="subtitle">The notification subtitle.</param>
|
||||
/// <param name="body">The notification body.</param>
|
||||
/// <param name="actionUri">The action URI for the notification.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
Task SendNotification(Account account, string topic, string title, string? subtitle, string body, string? actionUri = null);
|
||||
|
||||
/// <summary>
|
||||
/// Lists the friends of an account.
|
||||
/// </summary>
|
||||
/// <param name="account">The account.</param>
|
||||
/// <returns>A list of friend accounts.</returns>
|
||||
Task<List<Account>> ListAccountFriends(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies an authentication factor code.
|
||||
/// </summary>
|
||||
/// <param name="factor">The authentication factor.</param>
|
||||
/// <param name="code">The code to verify.</param>
|
||||
/// <returns>True if the code is valid, otherwise false.</returns>
|
||||
Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code);
|
||||
|
||||
/// <summary>
|
||||
/// Send the auth factor verification code to users, for factors like in-app code and email.
|
||||
/// Sometimes it requires a hint, like a part of the user's email address to ensure the user is who own the account.
|
||||
/// </summary>
|
||||
/// <param name="account">The owner of the auth factor</param>
|
||||
/// <param name="factor">The auth factor needed to send code</param>
|
||||
/// <param name="hint">The part of the contact method for verification</param>
|
||||
Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action log entry.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of action log.</param>
|
||||
/// <param name="meta">Additional metadata for the action log.</param>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <param name="account">The account associated with the action.</param>
|
||||
/// <returns>The created action log.</returns>
|
||||
Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Account? account = null);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new session.
|
||||
/// </summary>
|
||||
/// <param name="lastGrantedAt">The last granted instant.</param>
|
||||
/// <param name="expiredAt">The expiration instant.</param>
|
||||
/// <param name="account">The associated account.</param>
|
||||
/// <param name="challenge">The associated challenge.</param>
|
||||
/// <returns>The created session.</returns>
|
||||
Task<Session> CreateSession(NodaTime.Instant lastGrantedAt, NodaTime.Instant expiredAt, Account account, Challenge challenge);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the LastGrantedAt for a session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The ID of the session.</param>
|
||||
/// <param name="lastGrantedAt">The new LastGrantedAt instant.</param>
|
||||
Task UpdateSessionLastGrantedAt(Guid sessionId, NodaTime.Instant lastGrantedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the LastSeenAt for an account profile.
|
||||
/// </summary>
|
||||
/// <param name="accountId">The ID of the account.</param>
|
||||
/// <param name="lastSeenAt">The new LastSeenAt instant.</param>
|
||||
Task UpdateAccountProfileLastSeenAt(Guid accountId, NodaTime.Instant lastSeenAt);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a token for a session.
|
||||
/// </summary>
|
||||
/// <param name="session">The session.</param>
|
||||
/// <returns>The token string.</returns>
|
||||
string CreateToken(Session session);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AuthConstants.CookieTokenName.
|
||||
/// </summary>
|
||||
/// <returns>The cookie token name.</returns>
|
||||
string GetAuthCookieTokenName();
|
||||
|
||||
/// <summary>
|
||||
/// Searches for accounts by a search term.
|
||||
/// </summary>
|
||||
/// <param name="searchTerm">The term to search for.</param>
|
||||
/// <returns>A list of matching accounts.</returns>
|
||||
Task<List<Account>> SearchAccountsAsync(string searchTerm);
|
||||
}
|
||||
23
DysonNetwork.Shared/Services/IAccountUsernameService.cs
Normal file
23
DysonNetwork.Shared/Services/IAccountUsernameService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IAccountUsernameService : IService<IAccountUsernameService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a unique username based on the provided base name
|
||||
/// </summary>
|
||||
/// <param name="baseName">The preferred username</param>
|
||||
/// <returns>A unique username</returns>
|
||||
Task<string> GenerateUniqueUsernameAsync(string baseName);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a username already exists
|
||||
/// </summary>
|
||||
Task<bool> IsUsernameExistsAsync(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a username to remove invalid characters
|
||||
/// </summary>
|
||||
string SanitizeUsername(string username);
|
||||
}
|
||||
24
DysonNetwork.Shared/Services/IActionLogService.cs
Normal file
24
DysonNetwork.Shared/Services/IActionLogService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IActionLogService : IService<IActionLogService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an action log entry
|
||||
/// </summary>
|
||||
void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action log entry from an HTTP request
|
||||
/// </summary>
|
||||
Task<ActionLog> CreateActionLogFromRequest(
|
||||
string type,
|
||||
Dictionary<string, object> meta,
|
||||
string? ipAddress,
|
||||
string? userAgent,
|
||||
Account? account = null
|
||||
);
|
||||
}
|
||||
13
DysonNetwork.Shared/Services/ICustomAppService.cs
Normal file
13
DysonNetwork.Shared/Services/ICustomAppService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using MagicOnion;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface ICustomAppService : IService<ICustomAppService>
|
||||
{
|
||||
Task<CustomApp?> FindClientByIdAsync(Guid clientId);
|
||||
Task<int> CountCustomAppsByPublisherId(Guid publisherId);
|
||||
}
|
||||
44
DysonNetwork.Shared/Services/IMagicSpellService.cs
Normal file
44
DysonNetwork.Shared/Services/IMagicSpellService.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IMagicSpellService : IService<IMagicSpellService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new magic spell
|
||||
/// </summary>
|
||||
Task<MagicSpell> CreateMagicSpell(
|
||||
Account account,
|
||||
MagicSpellType type,
|
||||
Dictionary<string, object> meta,
|
||||
Instant? expiredAt = null,
|
||||
Instant? affectedAt = null,
|
||||
bool preventRepeat = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its token
|
||||
/// </summary>
|
||||
Task<MagicSpell?> GetMagicSpellAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a magic spell by its ID.
|
||||
/// </summary>
|
||||
/// <param name="spellId">The ID of the magic spell.</param>
|
||||
/// <returns>The magic spell if found, otherwise null.</returns>
|
||||
Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a password reset magic spell.
|
||||
/// </summary>
|
||||
/// <param name="spell">The magic spell object.</param>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
Task ApplyPasswordReset(MagicSpell spell, string newPassword);
|
||||
|
||||
/// <summary>
|
||||
/// Consumes a magic spell
|
||||
/// </summary>
|
||||
Task ApplyMagicSpell(string token);
|
||||
}
|
||||
46
DysonNetwork.Shared/Services/INotificationService.cs
Normal file
46
DysonNetwork.Shared/Services/INotificationService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface INotificationService : IService<INotificationService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsubscribes a device from push notifications
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device ID to unsubscribe</param>
|
||||
Task UnsubscribePushNotifications(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes a device to push notifications
|
||||
/// </summary>
|
||||
Task<NotificationPushSubscription> SubscribePushNotification(
|
||||
Account account,
|
||||
NotificationPushProvider provider,
|
||||
string deviceId,
|
||||
string deviceToken
|
||||
);
|
||||
|
||||
Task<Notification> SendNotification(
|
||||
Account account,
|
||||
string topic,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
string? content = null,
|
||||
Dictionary<string, object>? meta = null,
|
||||
string? actionUri = null,
|
||||
bool isSilent = false,
|
||||
bool save = true
|
||||
);
|
||||
|
||||
Task DeliveryNotification(Notification notification);
|
||||
|
||||
Task MarkNotificationsViewed(ICollection<Notification> notifications);
|
||||
|
||||
Task BroadcastNotification(Notification notification, bool save = false);
|
||||
|
||||
Task SendNotificationBatch(Notification notification, List<Account> accounts,
|
||||
bool save = false);
|
||||
}
|
||||
8
DysonNetwork.Shared/Services/IPermissionService.cs
Normal file
8
DysonNetwork.Shared/Services/IPermissionService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IPermissionService : IService<IPermissionService>
|
||||
{
|
||||
UnaryResult<bool> CheckPermission(string scope, string permission);
|
||||
}
|
||||
15
DysonNetwork.Shared/Services/IPublisherService.cs
Normal file
15
DysonNetwork.Shared/Services/IPublisherService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using MagicOnion;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IPublisherService : IService<IPublisherService>
|
||||
{
|
||||
Task<Publisher?> GetPublisherByName(string name);
|
||||
Task<List<Publisher>> GetUserPublishers(Guid accountId);
|
||||
Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role);
|
||||
Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId);
|
||||
}
|
||||
79
DysonNetwork.Shared/Services/IRelationshipService.cs
Normal file
79
DysonNetwork.Shared/Services/IRelationshipService.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using MagicOnion;
|
||||
|
||||
namespace DysonNetwork.Shared.Services;
|
||||
|
||||
public interface IRelationshipService : IService<IRelationshipService>
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a relationship exists between two accounts
|
||||
/// </summary>
|
||||
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a relationship between two accounts
|
||||
/// </summary>
|
||||
Task<Relationship?> GetRelationship(
|
||||
Guid accountId,
|
||||
Guid relatedId,
|
||||
RelationshipStatus? status = null,
|
||||
bool ignoreExpired = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new relationship between two accounts
|
||||
/// </summary>
|
||||
Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Blocks a user
|
||||
/// </summary>
|
||||
Task<Relationship> BlockAccount(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Unblocks a user
|
||||
/// </summary>
|
||||
Task<Relationship> UnblockAccount(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a friend request to a user
|
||||
/// </summary>
|
||||
Task<Relationship> SendFriendRequest(Account sender, Account target);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a friend request
|
||||
/// </summary>
|
||||
Task DeleteFriendRequest(Guid accountId, Guid relatedId);
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a friend request
|
||||
/// </summary>
|
||||
Task<Relationship> AcceptFriendRelationship(
|
||||
Relationship relationship,
|
||||
RelationshipStatus status = RelationshipStatus.Friends
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a relationship between two users
|
||||
/// </summary>
|
||||
Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all friends of an account
|
||||
/// </summary>
|
||||
Task<List<Account>> ListAccountFriends(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all blocked users of an account
|
||||
/// </summary>
|
||||
Task<List<Guid>> ListAccountBlocked(Account account);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a relationship with a specific status exists between two accounts
|
||||
/// </summary>
|
||||
Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
|
||||
RelationshipStatus status = RelationshipStatus.Friends);
|
||||
}
|
||||
60
DysonNetwork.Shared/Startup/ApplicationConfiguration.cs
Normal file
60
DysonNetwork.Shared/Startup/ApplicationConfiguration.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DysonNetwork.Shared.Startup;
|
||||
|
||||
public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
|
||||
ConfigureForwardedHeaders(app, configuration);
|
||||
|
||||
app.UseCors(opts =>
|
||||
opts.SetIsOriginAllowed(_ => true)
|
||||
.WithExposedHeaders("*")
|
||||
.WithHeaders()
|
||||
.AllowCredentials()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
);
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseRateLimiter();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers().RequireRateLimiting("fixed");
|
||||
app.MapStaticAssets().RequireRateLimiting("fixed");
|
||||
app.MapRazorPages().RequireRateLimiting("fixed");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||
|
||||
if (knownProxiesSection.Exists())
|
||||
{
|
||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
||||
if (proxyAddresses != null)
|
||||
foreach (var proxy in proxyAddresses)
|
||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
||||
}
|
||||
|
||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user