Compare commits
	
		
			7 Commits
		
	
	
		
			refactor/w
			...
			8d2f4a4c47
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8d2f4a4c47 | |||
| 1672d46038 | |||
| 15fb93c2bb | |||
| 4b220e7ed7 | |||
| 65450e8511 | |||
| cb4acbb3fc | |||
| bb2f88cc54 | 
| @@ -1,7 +1,7 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public enum AbuseReportType | ||||
| { | ||||
| @@ -26,5 +26,5 @@ public class AbuseReport : ModelBase | ||||
|     [MaxLength(8192)] public string? Resolution { get; set; } | ||||
|      | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|     public Shared.Models.Account Account { get; set; } = null!; | ||||
| } | ||||
| @@ -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.Pass.Permission; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using Org.BouncyCastle.Utilities; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| 
 | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [Authorize] | ||||
| [ApiController] | ||||
| @@ -16,16 +17,15 @@ namespace DysonNetwork.Sphere.Account; | ||||
| public class AccountCurrentController( | ||||
|     AppDatabase db, | ||||
|     AccountService accounts, | ||||
|     FileReferenceService fileRefService, | ||||
|     AccountEventService events, | ||||
|     AuthService auth | ||||
| ) : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<Account>> GetCurrentIdentity() | ||||
|     [ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<Shared.Models.Account>> GetCurrentIdentity() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
| 
 | ||||
|         var account = await db.Accounts | ||||
| @@ -44,9 +44,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch] | ||||
|     public async Task<ActionResult<Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request) | ||||
|     public async Task<ActionResult<Shared.Models.Account>> UpdateBasicInfo([FromBody] BasicInfoRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id); | ||||
| 
 | ||||
| @@ -77,7 +77,7 @@ public class AccountCurrentController( | ||||
|     [HttpPatch("profile")] | ||||
|     public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
| 
 | ||||
|         var profile = await db.AccountProfiles | ||||
| @@ -95,61 +95,61 @@ public class AccountCurrentController( | ||||
|         if (request.Location is not null) profile.Location = request.Location; | ||||
|         if (request.TimeZone is not null) profile.TimeZone = request.TimeZone; | ||||
| 
 | ||||
|         if (request.PictureId is not null) | ||||
|         { | ||||
|             var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); | ||||
|             if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); | ||||
| 
 | ||||
|             var profileResourceId = $"profile:{profile.Id}"; | ||||
| 
 | ||||
|             // Remove old references for the profile picture | ||||
|             if (profile.Picture is not null) | ||||
|             { | ||||
|                 var oldPictureRefs = | ||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture"); | ||||
|                 foreach (var oldRef in oldPictureRefs) | ||||
|                 { | ||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             profile.Picture = picture.ToReferenceObject(); | ||||
| 
 | ||||
|             // Create new reference | ||||
|             await fileRefService.CreateReferenceAsync( | ||||
|                 picture.Id, | ||||
|                 "profile.picture", | ||||
|                 profileResourceId | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (request.BackgroundId is not null) | ||||
|         { | ||||
|             var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); | ||||
|             if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); | ||||
| 
 | ||||
|             var profileResourceId = $"profile:{profile.Id}"; | ||||
| 
 | ||||
|             // Remove old references for the profile background | ||||
|             if (profile.Background is not null) | ||||
|             { | ||||
|                 var oldBackgroundRefs = | ||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background"); | ||||
|                 foreach (var oldRef in oldBackgroundRefs) | ||||
|                 { | ||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             profile.Background = background.ToReferenceObject(); | ||||
| 
 | ||||
|             // Create new reference | ||||
|             await fileRefService.CreateReferenceAsync( | ||||
|                 background.Id, | ||||
|                 "profile.background", | ||||
|                 profileResourceId | ||||
|             ); | ||||
|         } | ||||
|         // if (request.PictureId is not null) | ||||
|         // { | ||||
|         //     var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); | ||||
|         //     if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); | ||||
|         // | ||||
|         //     var profileResourceId = $"profile:{profile.Id}"; | ||||
|         // | ||||
|         //     // Remove old references for the profile picture | ||||
|         //     if (profile.Picture is not null) | ||||
|         //     { | ||||
|         //         var oldPictureRefs = | ||||
|         //             await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture"); | ||||
|         //         foreach (var oldRef in oldPictureRefs) | ||||
|         //         { | ||||
|         //             await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|         //         } | ||||
|         //     } | ||||
|         // | ||||
|         //     profile.Picture = picture.ToReferenceObject(); | ||||
|         // | ||||
|         //     // Create new reference | ||||
|         //     await fileRefService.CreateReferenceAsync( | ||||
|         //         picture.Id, | ||||
|         //         "profile.picture", | ||||
|         //         profileResourceId | ||||
|         //     ); | ||||
|         // } | ||||
|         // | ||||
|         // if (request.BackgroundId is not null) | ||||
|         // { | ||||
|         //     var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); | ||||
|         //     if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); | ||||
|         // | ||||
|         //     var profileResourceId = $"profile:{profile.Id}"; | ||||
|         // | ||||
|         //     // Remove old references for the profile background | ||||
|         //     if (profile.Background is not null) | ||||
|         //     { | ||||
|         //         var oldBackgroundRefs = | ||||
|         //             await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background"); | ||||
|         //         foreach (var oldRef in oldBackgroundRefs) | ||||
|         //         { | ||||
|         //             await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|         //         } | ||||
|         //     } | ||||
|         // | ||||
|         //     profile.Background = background.ToReferenceObject(); | ||||
|         // | ||||
|         //     // Create new reference | ||||
|         //     await fileRefService.CreateReferenceAsync( | ||||
|         //         background.Id, | ||||
|         //         "profile.background", | ||||
|         //         profileResourceId | ||||
|         //     ); | ||||
|         // } | ||||
| 
 | ||||
|         db.Update(profile); | ||||
|         await db.SaveChangesAsync(); | ||||
| @@ -162,7 +162,7 @@ public class AccountCurrentController( | ||||
|     [HttpDelete] | ||||
|     public async Task<ActionResult> RequestDeleteAccount() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -179,7 +179,7 @@ public class AccountCurrentController( | ||||
|     [HttpGet("statuses")] | ||||
|     public async Task<ActionResult<Status>> GetCurrentStatus() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var status = await events.GetStatus(currentUser.Id); | ||||
|         return Ok(status); | ||||
|     } | ||||
| @@ -188,7 +188,7 @@ public class AccountCurrentController( | ||||
|     [RequiredPermission("global", "accounts.statuses.update")] | ||||
|     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
| @@ -215,7 +215,7 @@ public class AccountCurrentController( | ||||
|     [RequiredPermission("global", "accounts.statuses.create")] | ||||
|     public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var status = new Status | ||||
|         { | ||||
| @@ -233,7 +233,7 @@ public class AccountCurrentController( | ||||
|     [HttpDelete("me/statuses")] | ||||
|     public async Task<ActionResult> DeleteStatus() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
| @@ -250,7 +250,7 @@ public class AccountCurrentController( | ||||
|     [HttpGet("check-in")] | ||||
|     public async Task<ActionResult<CheckInResult>> GetCheckInResult() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
| 
 | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
| @@ -270,7 +270,7 @@ public class AccountCurrentController( | ||||
|     [HttpPost("check-in")] | ||||
|     public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var isAvailable = await events.CheckInDailyIsAvailable(currentUser); | ||||
|         if (!isAvailable) | ||||
| @@ -297,7 +297,7 @@ public class AccountCurrentController( | ||||
|     public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month, | ||||
|         [FromQuery] int? year) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||
|         month ??= currentDate.Month; | ||||
| @@ -318,7 +318,7 @@ public class AccountCurrentController( | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var query = db.ActionLogs | ||||
|             .Where(log => log.AccountId == currentUser.Id) | ||||
| @@ -338,7 +338,7 @@ public class AccountCurrentController( | ||||
|     [HttpGet("factors")] | ||||
|     public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var factors = await db.AccountAuthFactors | ||||
|             .Include(f => f.Account) | ||||
| @@ -358,7 +358,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         if (await accounts.CheckAuthFactorExists(currentUser, request.Type)) | ||||
|             return BadRequest($"Auth factor with type {request.Type} is already exists."); | ||||
| 
 | ||||
| @@ -370,7 +370,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -392,7 +392,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -414,7 +414,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var factor = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||
| @@ -445,7 +445,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); | ||||
| @@ -480,7 +480,7 @@ public class AccountCurrentController( | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         var query = db.AuthSessions | ||||
| @@ -505,7 +505,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Session>> DeleteSession(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -522,7 +522,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Session>> DeleteCurrentSession() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
| @@ -539,7 +539,7 @@ public class AccountCurrentController( | ||||
|     [HttpPatch("sessions/{id:guid}/label")] | ||||
|     public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -555,7 +555,7 @@ public class AccountCurrentController( | ||||
|     [HttpPatch("sessions/current/label")] | ||||
|     public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
| @@ -573,7 +573,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<AccountContact>>> GetContacts() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var contacts = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id) | ||||
| @@ -592,7 +592,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -609,7 +609,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> VerifyContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -631,7 +631,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -653,7 +653,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<AccountContact>> DeleteContact(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var contact = await db.AccountContacts | ||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||
| @@ -676,9 +676,9 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Badge>>> GetBadges() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var badges = await db.Badges | ||||
|         var badges = await db.AccountBadges | ||||
|             .Where(b => b.AccountId == currentUser.Id) | ||||
|             .ToListAsync(); | ||||
|         return Ok(badges); | ||||
| @@ -688,7 +688,7 @@ public class AccountCurrentController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Badge>> ActivateBadge(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -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, | ||||
| @@ -83,16 +84,16 @@ public class AccountEventService( | ||||
|         foreach (var userId in userIds) | ||||
|         { | ||||
|             var cacheKey = $"{StatusCacheKey}{userId}"; | ||||
|             var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|             if (cachedStatus != null) | ||||
|             { | ||||
|                 cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||
|                 results[userId] = cachedStatus; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 cacheMissUserIds.Add(userId); | ||||
|             } | ||||
|             // var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|             // if (cachedStatus != null) | ||||
|             // { | ||||
|             //     cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||
|             //     results[userId] = cachedStatus; | ||||
|             // } | ||||
|             // else | ||||
|             // { | ||||
|             cacheMissUserIds.Add(userId); | ||||
|             // } | ||||
|         } | ||||
| 
 | ||||
|         if (cacheMissUserIds.Any()) | ||||
| @@ -109,11 +110,12 @@ public class AccountEventService( | ||||
| 
 | ||||
|             foreach (var status in statusesFromDb) | ||||
|             { | ||||
|                 var isOnline = ws.GetAccountIsConnected(status.AccountId); | ||||
|                 // var isOnline = ws.GetAccountIsConnected(status.AccountId); | ||||
|                 var isOnline = false; // Placeholder | ||||
|                 status.IsOnline = !status.IsInvisible && isOnline; | ||||
|                 results[status.AccountId] = status; | ||||
|                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; | ||||
|                 await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); | ||||
|                 // await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); | ||||
|                 foundUserIds.Add(status.AccountId); | ||||
|             } | ||||
| 
 | ||||
| @@ -122,7 +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,19 +167,19 @@ public class AccountEventService( | ||||
|     private const string CaptchaCacheKey = "CheckInCaptcha_"; | ||||
|     private const int CaptchaProbabilityPercent = 20; | ||||
| 
 | ||||
|     public async Task<bool> CheckInDailyDoAskCaptcha(Account user) | ||||
|     public async Task<bool> CheckInDailyDoAskCaptcha(Shared.Models.Account user) | ||||
|     { | ||||
|         var cacheKey = $"{CaptchaCacheKey}{user.Id}"; | ||||
|         var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); | ||||
|         if (needsCaptcha is not null) | ||||
|             return needsCaptcha!.Value; | ||||
|         // var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); | ||||
|         // if (needsCaptcha is not null) | ||||
|         //     return needsCaptcha!.Value; | ||||
| 
 | ||||
|         var result = Random.Next(100) < CaptchaProbabilityPercent; | ||||
|         await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); | ||||
|         // await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> CheckInDailyIsAvailable(Account user) | ||||
|     public async Task<bool> CheckInDailyIsAvailable(Shared.Models.Account user) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var lastCheckIn = await db.AccountCheckInResults | ||||
| @@ -193,9 +196,9 @@ 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}"; | ||||
| 
 | ||||
| @@ -212,7 +215,8 @@ public class AccountEventService( | ||||
|         } | ||||
| 
 | ||||
|         // Now try to acquire the lock properly | ||||
|         await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         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) | ||||
| @@ -1,32 +1,31 @@ | ||||
| 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; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class AccountService( | ||||
|     AppDatabase db, | ||||
|     MagicSpellService spells, | ||||
|     AccountUsernameService uname, | ||||
|     NotificationService nty, | ||||
|     EmailService mailer, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     // MagicSpellService spells, | ||||
|     // AccountUsernameService uname, | ||||
|     // NotificationService nty, | ||||
|     // EmailService mailer, | ||||
|     // IStringLocalizer<NotificationResource> localizer, | ||||
|     ICacheService cache, | ||||
|     ILogger<AccountService> logger | ||||
| ) | ||||
| ) : ServiceBase<IAccountService>, IAccountService | ||||
| { | ||||
|     public static void SetCultureInfo(Account account) | ||||
|     public static void SetCultureInfo(Shared.Models.Account account) | ||||
|     { | ||||
|         SetCultureInfo(account.Language); | ||||
|     } | ||||
| @@ -40,12 +39,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 +56,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 +73,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 +90,7 @@ public class AccountService( | ||||
|             if (dupeNameCount > 0) | ||||
|                 throw new InvalidOperationException("Account name has already been taken."); | ||||
| 
 | ||||
|             var account = new Account | ||||
|             var account = new Shared.Models.Account | ||||
|             { | ||||
|                 Name = name, | ||||
|                 Nick = nick, | ||||
| @@ -135,15 +134,15 @@ public class AccountService( | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var spell = await spells.CreateMagicSpell( | ||||
|                     account, | ||||
|                     MagicSpellType.AccountActivation, | ||||
|                     new Dictionary<string, object> | ||||
|                     { | ||||
|                         { "contact_method", account.Contacts.First().Content } | ||||
|                     } | ||||
|                 ); | ||||
|                 await spells.NotifyMagicSpell(spell, true); | ||||
|                 // var spell = await spells.CreateMagicSpell( | ||||
|                 //     account, | ||||
|                 //     MagicSpellType.AccountActivation, | ||||
|                 //     new Dictionary<string, object> | ||||
|                 //     { | ||||
|                 //         { "contact_method", account.Contacts.First().Content } | ||||
|                 //     } | ||||
|                 // ); | ||||
|                 // await spells.NotifyMagicSpell(spell, true); | ||||
|             } | ||||
| 
 | ||||
|             db.Accounts.Add(account); | ||||
| @@ -159,7 +158,7 @@ public class AccountService( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Account> CreateAccount(OidcUserInfo userInfo) | ||||
|     public async Task<Shared.Models.Account> CreateAccount(OidcUserInfo userInfo) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(userInfo.Email)) | ||||
|             throw new ArgumentException("Email is required for account creation"); | ||||
| @@ -169,7 +168,8 @@ public class AccountService( | ||||
|             : $"{userInfo.FirstName} {userInfo.LastName}".Trim(); | ||||
| 
 | ||||
|         // Generate username from email | ||||
|         var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); | ||||
|         // var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); | ||||
|         var username = userInfo.Email.Split('@')[0]; // Placeholder | ||||
| 
 | ||||
|         return await CreateAccount( | ||||
|             username, | ||||
| @@ -182,31 +182,31 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public async Task RequestAccountDeletion(Account account) | ||||
|     public async Task RequestAccountDeletion(Shared.Models.Account account) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
|             MagicSpellType.AccountRemoval, | ||||
|             new Dictionary<string, object>(), | ||||
|             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|             preventRepeat: true | ||||
|         ); | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|         // var spell = await spells.CreateMagicSpell( | ||||
|         //     account, | ||||
|         //     MagicSpellType.AccountRemoval, | ||||
|         //     new Dictionary<string, object>(), | ||||
|         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|         //     preventRepeat: true | ||||
|         // ); | ||||
|         // await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
| 
 | ||||
|     public async Task RequestPasswordReset(Account account) | ||||
|     public async Task RequestPasswordReset(Shared.Models.Account account) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
|             MagicSpellType.AuthPasswordReset, | ||||
|             new Dictionary<string, object>(), | ||||
|             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|             preventRepeat: true | ||||
|         ); | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|         // var spell = await spells.CreateMagicSpell( | ||||
|         //     account, | ||||
|         //     MagicSpellType.AuthPasswordReset, | ||||
|         //     new Dictionary<string, object>(), | ||||
|         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|         //     preventRepeat: true | ||||
|         // ); | ||||
|         // await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type) | ||||
|     public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type) | ||||
|     { | ||||
|         var isExists = await db.AccountAuthFactors | ||||
|             .Where(x => x.AccountId == account.Id && x.Type == type) | ||||
| @@ -214,7 +214,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 +329,7 @@ public class AccountService( | ||||
|     { | ||||
|         var count = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == factor.AccountId) | ||||
|             .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null)) | ||||
|             // .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null)) | ||||
|             .CountAsync(); | ||||
|         if (count <= 1) | ||||
|             throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor."); | ||||
| @@ -345,7 +345,7 @@ public class AccountService( | ||||
|     /// <param name="account">The owner of the auth factor</param> | ||||
|     /// <param name="factor">The auth factor needed to send code</param> | ||||
|     /// <param name="hint">The part of the contact method for verification</param> | ||||
|     public async Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null) | ||||
|     public async Task SendFactorCode(Shared.Models.Account account, AccountAuthFactor factor, string? hint = null) | ||||
|     { | ||||
|         var code = new Random().Next(100000, 999999).ToString("000000"); | ||||
| 
 | ||||
| @@ -355,14 +355,14 @@ public class AccountService( | ||||
|                 if (await _GetFactorCode(factor) is not null) | ||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||
| 
 | ||||
|                 await nty.SendNotification( | ||||
|                     account, | ||||
|                     "auth.verification", | ||||
|                     localizer["AuthCodeTitle"], | ||||
|                     null, | ||||
|                     localizer["AuthCodeBody", code], | ||||
|                     save: true | ||||
|                 ); | ||||
|                 // await nty.SendNotification( | ||||
|                 //     account, | ||||
|                 //     "auth.verification", | ||||
|                 //     localizer["AuthCodeTitle"], | ||||
|                 //     null, | ||||
|                 //     localizer["AuthCodeBody", code], | ||||
|                 //     save: true | ||||
|                 // ); | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); | ||||
|                 break; | ||||
|             case AccountAuthFactorType.EmailCode: | ||||
| @@ -397,16 +397,16 @@ public class AccountService( | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                                 await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( | ||||
|                     account.Nick, | ||||
|                     contact.Content, | ||||
|                     localizer["VerificationEmail"], | ||||
|                     new VerificationEmailModel | ||||
|                     { | ||||
|                         Name = account.Name, | ||||
|                         Code = code | ||||
|                     } | ||||
|                 ); | ||||
|                 // await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( | ||||
|                 //     account.Nick, | ||||
|                 //     contact.Content, | ||||
|                 //     localizer["VerificationEmail"], | ||||
|                 //     new VerificationEmailModel | ||||
|                 //     { | ||||
|                 //         Name = account.Name, | ||||
|                 //         Code = code | ||||
|                 //     } | ||||
|                 // ); | ||||
| 
 | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); | ||||
|                 break; | ||||
| @@ -454,7 +454,7 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session> UpdateSessionLabel(Account account, Guid sessionId, string label) | ||||
|     public async Task<Session> UpdateSessionLabel(Shared.Models.Account account, Guid sessionId, string label) | ||||
|     { | ||||
|         var session = await db.AuthSessions | ||||
|             .Include(s => s.Challenge) | ||||
| @@ -477,7 +477,7 @@ public class AccountService( | ||||
|         return session; | ||||
|     } | ||||
| 
 | ||||
|     public async Task DeleteSession(Account account, Guid sessionId) | ||||
|     public async Task DeleteSession(Shared.Models.Account account, Guid sessionId) | ||||
|     { | ||||
|         var session = await db.AuthSessions | ||||
|             .Include(s => s.Challenge) | ||||
| @@ -491,7 +491,7 @@ public class AccountService( | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         if (session.Challenge.DeviceId is not null) | ||||
|             await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); | ||||
|             // await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); | ||||
| 
 | ||||
|         // The current session should be included in the sessions' list | ||||
|         await db.AuthSessions | ||||
| @@ -503,7 +503,7 @@ public class AccountService( | ||||
|             await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}"); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<AccountContact> CreateContactMethod(Account account, AccountContactType type, string content) | ||||
|     public async Task<AccountContact> CreateContactMethod(Shared.Models.Account account, AccountContactType type, string content) | ||||
|     { | ||||
|         var contact = new AccountContact | ||||
|         { | ||||
| @@ -518,19 +518,19 @@ public class AccountService( | ||||
|         return contact; | ||||
|     } | ||||
| 
 | ||||
|     public async Task VerifyContactMethod(Account account, AccountContact contact) | ||||
|     public async Task VerifyContactMethod(Shared.Models.Account account, AccountContact contact) | ||||
|     { | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
|             MagicSpellType.ContactVerification, | ||||
|             new Dictionary<string, object> { { "contact_method", contact.Content } }, | ||||
|             expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|             preventRepeat: true | ||||
|         ); | ||||
|         await spells.NotifyMagicSpell(spell); | ||||
|         // var spell = await spells.CreateMagicSpell( | ||||
|         //     account, | ||||
|         //     MagicSpellType.ContactVerification, | ||||
|         //     new Dictionary<string, object> { { "contact_method", contact.Content } }, | ||||
|         //     expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||
|         //     preventRepeat: true | ||||
|         // ); | ||||
|         // await spells.NotifyMagicSpell(spell); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<AccountContact> SetContactMethodPrimary(Account account, AccountContact contact) | ||||
|     public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact) | ||||
|     { | ||||
|         if (contact.AccountId != account.Id) | ||||
|             throw new InvalidOperationException("Contact method does not belong to this account."); | ||||
| @@ -559,7 +559,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 +574,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 +586,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 +604,19 @@ public class AccountService( | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task ActiveBadge(Account account, Guid badgeId) | ||||
|     public async Task ActiveBadge(Shared.Models.Account account, Guid badgeId) | ||||
|     { | ||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var badge = await db.Badges | ||||
|             var badge = await db.AccountBadges | ||||
|                 .Where(b => b.AccountId == account.Id && b.Id == badgeId) | ||||
|                 .OrderByDescending(b => b.CreatedAt) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (badge is null) throw new InvalidOperationException("Badge was not found."); | ||||
| 
 | ||||
|             await db.Badges | ||||
|             await db.AccountBadges | ||||
|                 .Where(b => b.AccountId == account.Id && b.Id != badgeId) | ||||
|                 .ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null)); | ||||
| 
 | ||||
| @@ -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(); | ||||
| 
 | ||||
| @@ -1,12 +1,24 @@ | ||||
| using Quartz; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Storage.Handlers; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Services; | ||||
| using MagicOnion.Server; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
| 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 | ||||
| @@ -16,11 +28,11 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
|             Meta = meta, | ||||
|         }; | ||||
| 
 | ||||
|         fbs.Enqueue(log); | ||||
|         // fbs.Enqueue(log); | ||||
|     } | ||||
| 
 | ||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, | ||||
|         Account? account = null) | ||||
|         Shared.Models.Account? account = null) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|         { | ||||
| @@ -28,19 +40,19 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
|             Meta = meta, | ||||
|             UserAgent = request.Headers.UserAgent, | ||||
|             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
|             Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||
|             // Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||
|         }; | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentUser"] is Account currentUser) | ||||
|         if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser) | ||||
|             log.AccountId = currentUser.Id; | ||||
|         else if (account != null) | ||||
|             log.AccountId = account.Id; | ||||
|         else | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession) | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Session currentSession) | ||||
|             log.SessionId = currentSession.Id; | ||||
| 
 | ||||
|         fbs.Enqueue(log); | ||||
|         // fbs.Enqueue(log); | ||||
|     } | ||||
| } | ||||
| @@ -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,17 @@ 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 NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) | ||||
|     { | ||||
|         var contact = await db.AccountContacts | ||||
| @@ -87,54 +96,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 +155,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.Pass.Permission; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/notifications")] | ||||
| @@ -17,7 +19,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | ||||
|     public async Task<ActionResult<int>> CountUnreadNotifications() | ||||
|     { | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         if (currentUserValue is not Account currentUser) return Unauthorized(); | ||||
|         if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var count = await db.Notifications | ||||
|             .Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null) | ||||
| @@ -35,7 +37,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | ||||
|     ) | ||||
|     { | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         if (currentUserValue is not Account currentUser) return Unauthorized(); | ||||
|         if (currentUserValue is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
|         var totalCount = await db.Notifications | ||||
|             .Where(s => s.AccountId == currentUser.Id) | ||||
| @@ -67,16 +69,15 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | ||||
|     { | ||||
|         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         var currentUser = currentUserValue as Account; | ||||
|         var currentUser = currentUserValue as Shared.Models.Account; | ||||
|         if (currentUser == null) return Unauthorized(); | ||||
|         var currentSession = currentSessionValue as Session; | ||||
|         if (currentSession == null) return Unauthorized(); | ||||
| 
 | ||||
|         var result = | ||||
|             await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!, | ||||
|                 request.DeviceToken); | ||||
|         await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!, | ||||
|             request.DeviceToken); | ||||
| 
 | ||||
|         return Ok(result); | ||||
|         return Ok(); | ||||
|     } | ||||
| 
 | ||||
|     [HttpDelete("subscription")] | ||||
| @@ -85,7 +86,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | ||||
|     { | ||||
|         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         var currentUser = currentUserValue as Account; | ||||
|         var currentUser = currentUserValue as Shared.Models.Account; | ||||
|         if (currentUser == null) return Unauthorized(); | ||||
|         var currentSession = currentSessionValue as Session; | ||||
|         if (currentSession == null) return Unauthorized(); | ||||
| @@ -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,7 +27,7 @@ public class NotificationService( | ||||
|     } | ||||
| 
 | ||||
|     public async Task<NotificationPushSubscription> SubscribePushNotification( | ||||
|         Account account, | ||||
|         Shared.Models.Account account, | ||||
|         NotificationPushProvider provider, | ||||
|         string deviceId, | ||||
|         string deviceToken | ||||
| @@ -63,14 +66,14 @@ public class NotificationService( | ||||
|             AccountId = account.Id, | ||||
|         }; | ||||
| 
 | ||||
|         db.NotificationPushSubscriptions.Add(subscription); | ||||
|         await db.SaveChangesAsync(); | ||||
|         // db.NotificationPushSubscriptions.Add(subscription); | ||||
|         // await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         return subscription; | ||||
|     } | ||||
| 
 | ||||
|     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) | ||||
|             Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification); | ||||
| 
 | ||||
|         return notification; | ||||
|     } | ||||
| 
 | ||||
|     public async Task DeliveryNotification(Notification notification) | ||||
|     { | ||||
|         ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket | ||||
|         { | ||||
|             Type = "notifications.new", | ||||
|             Data = notification | ||||
|         }); | ||||
|         // ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket | ||||
|         // { | ||||
|         //     Type = "notifications.new", | ||||
|         //     Data = notification | ||||
|         // }); | ||||
| 
 | ||||
|         // Pushing the notification | ||||
|         var subscribers = await db.NotificationPushSubscriptions | ||||
| @@ -130,15 +134,15 @@ public class NotificationService( | ||||
|         var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); | ||||
|         if (id.Count == 0) return; | ||||
| 
 | ||||
|         await db.Notifications | ||||
|             .Where(n => id.Contains(n.Id)) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) | ||||
|             ); | ||||
|         // await db.Notifications | ||||
|         //     .Where(n => id.Contains(n.Id)) | ||||
|         //     .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) | ||||
|         //     ); | ||||
|     } | ||||
| 
 | ||||
|     public async Task BroadcastNotification(Notification notification, bool save = false) | ||||
|     { | ||||
|         var accounts = await db.Accounts.ToListAsync(); | ||||
|         var accounts = new List<Shared.Models.Account>(); // await db.Accounts.ToListAsync(); | ||||
| 
 | ||||
|         if (save) | ||||
|         { | ||||
| @@ -157,18 +161,18 @@ public class NotificationService( | ||||
|                 }; | ||||
|                 return newNotification; | ||||
|             }).ToList(); | ||||
|             await db.BulkInsertAsync(notifications); | ||||
|             // await db.BulkInsertAsync(notifications); | ||||
|         } | ||||
| 
 | ||||
|         foreach (var account in accounts) | ||||
|         { | ||||
|             notification.Account = account; | ||||
|             notification.AccountId = account.Id; | ||||
|             ws.SendPacketToAccount(account.Id, new WebSocketPacket | ||||
|             { | ||||
|                 Type = "notifications.new", | ||||
|                 Data = notification | ||||
|             }); | ||||
|             // 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,7 +155,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | ||||
|         return relationship; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<Guid>> ListAccountFriends(Account account) | ||||
|     public async Task<List<Guid>> ListAccountFriends(Shared.Models.Account account) | ||||
|     { | ||||
|         var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; | ||||
|         var friends = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
| @@ -168,10 +171,10 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | ||||
|             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 +190,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, | ||||
							
								
								
									
										269
									
								
								DysonNetwork.Pass/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								DysonNetwork.Pass/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Pass; | ||||
|  | ||||
| public abstract class ModelBase | ||||
| { | ||||
|     public Instant CreatedAt { get; set; } | ||||
|     public Instant UpdatedAt { get; set; } | ||||
|     public Instant? DeletedAt { get; set; } | ||||
| } | ||||
|  | ||||
| public class AppDatabase( | ||||
|     DbContextOptions<AppDatabase> options, | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<PermissionNode> PermissionNodes { get; set; } | ||||
|     public DbSet<PermissionGroup> PermissionGroups { get; set; } | ||||
|     public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } | ||||
|  | ||||
|     public DbSet<Shared.Models.Account> Accounts { get; set; } | ||||
|     public DbSet<AccountConnection> AccountConnections { get; set; } | ||||
|     public DbSet<Profile> AccountProfiles { get; set; } | ||||
|     public DbSet<AccountContact> AccountContacts { get; set; } | ||||
|     public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } | ||||
|     public DbSet<Relationship> AccountRelationships { get; set; } | ||||
|     public DbSet<Status> AccountStatuses { get; set; } | ||||
|     public DbSet<CheckInResult> AccountCheckInResults { get; set; } | ||||
|     public DbSet<Badge> AccountBadges { get; set; } | ||||
|      | ||||
|     public DbSet<ActionLog> ActionLogs { get; set; } | ||||
|      | ||||
|     public DbSet<Notification> Notifications { get; set; } | ||||
|     public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; } | ||||
|  | ||||
|     public DbSet<Session> AuthSessions { get; set; } | ||||
|     public DbSet<Challenge> AuthChallenges { get; set; } | ||||
|      | ||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } | ||||
|     public DbSet<AbuseReport> AbuseReports { get; set; } | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|         optionsBuilder.UseNpgsql( | ||||
|             configuration.GetConnectionString("App"), | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|          | ||||
|         optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) => | ||||
|         { | ||||
|             var defaultPermissionGroup = await context.Set<PermissionGroup>() | ||||
|                 .FirstOrDefaultAsync(g => g.Key == "default", cancellationToken); | ||||
|             if (defaultPermissionGroup is null) | ||||
|             { | ||||
|                 context.Set<PermissionGroup>().Add(new PermissionGroup | ||||
|                 { | ||||
|                     Key = "default", | ||||
|                     Nodes = new List<string> | ||||
|                         { | ||||
|                             "posts.create", | ||||
|                             "posts.react", | ||||
|                             "publishers.create", | ||||
|                             "files.create", | ||||
|                             "chat.create", | ||||
|                             "chat.messages.create", | ||||
|                             "chat.realtime.create", | ||||
|                             "accounts.statuses.create", | ||||
|                             "accounts.statuses.update", | ||||
|                             "stickers.packs.create", | ||||
|                             "stickers.create" | ||||
|                         }.Select(permission => | ||||
|                             PermissionService.NewPermissionNode("group:default", "global", permission, true)) | ||||
|                         .ToList() | ||||
|                 }); | ||||
|                 await context.SaveChangesAsync(cancellationToken); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         base.OnConfiguring(optionsBuilder); | ||||
|     } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|  | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasKey(pg => new { pg.GroupId, pg.Actor }); | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasOne(pg => pg.Group) | ||||
|             .WithMany(g => g.Members) | ||||
|             .HasForeignKey(pg => pg.GroupId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Account) | ||||
|             .WithMany(a => a.OutgoingRelationships) | ||||
|             .HasForeignKey(r => r.AccountId); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Related) | ||||
|             .WithMany(a => a.IncomingRelationships) | ||||
|             .HasForeignKey(r => r.RelatedId); | ||||
|          | ||||
|         // Automatically apply soft-delete filter to all entities inheriting BaseModel | ||||
|         foreach (var entityType in modelBuilder.Model.GetEntityTypes()) | ||||
|         { | ||||
|             if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue; | ||||
|             var method = typeof(AppDatabase) | ||||
|                 .GetMethod(nameof(SetSoftDeleteFilter), | ||||
|                     BindingFlags.NonPublic | BindingFlags.Static)! | ||||
|                 .MakeGenericMethod(entityType.ClrType); | ||||
|  | ||||
|             method.Invoke(null, [modelBuilder]); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder) | ||||
|         where TEntity : ModelBase | ||||
|     { | ||||
|         modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null); | ||||
|     } | ||||
|  | ||||
|     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|         foreach (var entry in ChangeTracker.Entries<ModelBase>()) | ||||
|         { | ||||
|             if (entry.State == EntityState.Added) | ||||
|             { | ||||
|                 entry.Entity.CreatedAt = now; | ||||
|                 entry.Entity.UpdatedAt = now; | ||||
|             } | ||||
|             else if (entry.State == EntityState.Modified) | ||||
|             { | ||||
|                 entry.Entity.UpdatedAt = now; | ||||
|             } | ||||
|             else if (entry.State == EntityState.Deleted) | ||||
|             { | ||||
|                 entry.State = EntityState.Modified; | ||||
|                 entry.Entity.DeletedAt = now; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return await base.SaveChangesAsync(cancellationToken); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> logger) : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|         logger.LogInformation("Cleaning up expired records..."); | ||||
|  | ||||
|         // Expired relationships | ||||
|         var affectedRows = await db.AccountRelationships | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows); | ||||
|         // Expired permission group members | ||||
|         affectedRows = await db.PermissionGroupMembers | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows); | ||||
|  | ||||
|         logger.LogInformation("Deleting soft-deleted records..."); | ||||
|  | ||||
|         var threshold = now - Duration.FromDays(7); | ||||
|  | ||||
|         var entityTypes = db.Model.GetEntityTypes() | ||||
|             .Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase)) | ||||
|             .Select(t => t.ClrType); | ||||
|  | ||||
|         foreach (var entityType in entityTypes) | ||||
|         { | ||||
|             var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! | ||||
|                 .MakeGenericMethod(entityType).Invoke(db, null)!; | ||||
|             var parameter = Expression.Parameter(entityType, "e"); | ||||
|             var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt)); | ||||
|             var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?))); | ||||
|             var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?))); | ||||
|             var finalCondition = Expression.AndAlso(notNull, condition); | ||||
|             var lambda = Expression.Lambda(finalCondition, parameter); | ||||
|  | ||||
|             var queryable = set.Provider.CreateQuery( | ||||
|                 Expression.Call( | ||||
|                     typeof(Queryable), | ||||
|                     "Where", | ||||
|                     [entityType], | ||||
|                     set.Expression, | ||||
|                     Expression.Quote(lambda) | ||||
|                 ) | ||||
|             ); | ||||
|  | ||||
|             var toListAsync = typeof(EntityFrameworkQueryableExtensions) | ||||
|                 .GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))! | ||||
|                 .MakeGenericMethod(entityType); | ||||
|  | ||||
|             var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!; | ||||
|             db.RemoveRange(items); | ||||
|         } | ||||
|  | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> | ||||
| { | ||||
|     public AppDatabase CreateDbContext(string[] args) | ||||
|     { | ||||
|         var configuration = new ConfigurationBuilder() | ||||
|             .SetBasePath(Directory.GetCurrentDirectory()) | ||||
|             .AddJsonFile("appsettings.json") | ||||
|             .Build(); | ||||
|  | ||||
|         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); | ||||
|         return new AppDatabase(optionsBuilder.Options, configuration); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public static class OptionalQueryExtensions | ||||
| { | ||||
|     public static IQueryable<T> If<T>( | ||||
|         this IQueryable<T> source, | ||||
|         bool condition, | ||||
|         Func<IQueryable<T>, IQueryable<T>> transform | ||||
|     ) | ||||
|     { | ||||
|         return condition ? transform(source) : source; | ||||
|     } | ||||
|  | ||||
|     public static IQueryable<T> If<T, TP>( | ||||
|         this IIncludableQueryable<T, TP> source, | ||||
|         bool condition, | ||||
|         Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform | ||||
|     ) | ||||
|         where T : class | ||||
|     { | ||||
|         return condition ? transform(source) : source; | ||||
|     } | ||||
|  | ||||
|     public static IQueryable<T> If<T, TP>( | ||||
|         this IIncludableQueryable<T, IEnumerable<TP>> source, | ||||
|         bool condition, | ||||
|         Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform | ||||
|     ) | ||||
|         where T : class | ||||
|     { | ||||
|         return condition ? transform(source) : source; | ||||
|     } | ||||
| } | ||||
| @@ -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: | ||||
| @@ -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 | ||||
|     IConfiguration config | ||||
|     // IHttpClientFactory httpClientFactory, | ||||
|     // IHttpContextAccessor httpContextAccessor, | ||||
|     // ICacheService cache | ||||
| ) | ||||
| { | ||||
|     private HttpContext HttpContext => httpContextAccessor.HttpContext!; | ||||
|     // private HttpContext HttpContext => httpContextAccessor.HttpContext!; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Detect the risk of the current request to login | ||||
| @@ -24,7 +26,7 @@ public class AuthService( | ||||
|     /// <param name="request">The request context</param> | ||||
|     /// <param name="account">The account to login</param> | ||||
|     /// <returns>The required steps to login</returns> | ||||
|     public async Task<int> DetectChallengeRisk(HttpRequest request, Account.Account account) | ||||
|     public async Task<int> DetectChallengeRisk(HttpRequest request, Shared.Models.Account account) | ||||
|     { | ||||
|         // 1) Find out how many authentication factors the account has enabled. | ||||
|         var maxSteps = await db.AccountAuthFactors | ||||
| @@ -73,13 +75,13 @@ public class AuthService( | ||||
|         return totalRequiredSteps; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null) | ||||
|     public async Task<Session> CreateSessionForOidcAsync(Shared.Models.Account account, Instant time, Guid? customAppId = null) | ||||
|     { | ||||
|         var challenge = new Challenge | ||||
|         { | ||||
|             AccountId = account.Id, | ||||
|             IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
|             UserAgent = HttpContext.Request.Headers.UserAgent, | ||||
|             IpAddress = "127.0.0.1", // HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
|             UserAgent = "TestAgent", // HttpContext.Request.Headers.UserAgent, | ||||
|             StepRemain = 1, | ||||
|             StepTotal = 1, | ||||
|             Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc | ||||
| @@ -105,53 +107,54 @@ public class AuthService( | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(token)) return false; | ||||
| 
 | ||||
|         var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); | ||||
|         var apiSecret = config.GetSection("Captcha")["ApiSecret"]; | ||||
|         // var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); | ||||
|         // var apiSecret = config.GetSection("Captcha")["ApiSecret"]; | ||||
| 
 | ||||
|         var client = httpClientFactory.CreateClient(); | ||||
|         // var client = httpClientFactory.CreateClient(); | ||||
| 
 | ||||
|         var jsonOpts = new JsonSerializerOptions | ||||
|         { | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||
|         }; | ||||
|         // var jsonOpts = new JsonSerializerOptions | ||||
|         // { | ||||
|         //     PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|         //     DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||
|         // }; | ||||
| 
 | ||||
|         switch (provider) | ||||
|         { | ||||
|             case "cloudflare": | ||||
|                 var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|                     "application/x-www-form-urlencoded"); | ||||
|                 var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", | ||||
|                     content); | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|         // switch (provider) | ||||
|         // { | ||||
|         //     case "cloudflare": | ||||
|         //         var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|         //             "application/x-www-form-urlencoded"); | ||||
|         //         var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", | ||||
|         //             content); | ||||
|         //         response.EnsureSuccessStatusCode(); | ||||
| 
 | ||||
|                 var json = await response.Content.ReadAsStringAsync(); | ||||
|                 var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
|         //         var json = await response.Content.ReadAsStringAsync(); | ||||
|         //         var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
| 
 | ||||
|                 return result?.Success == true; | ||||
|             case "google": | ||||
|                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|                     "application/x-www-form-urlencoded"); | ||||
|                 response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|         //         return result?.Success == true; | ||||
|         //     case "google": | ||||
|         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|         //             "application/x-www-form-urlencoded"); | ||||
|         //         response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); | ||||
|         //         response.EnsureSuccessStatusCode(); | ||||
| 
 | ||||
|                 json = await response.Content.ReadAsStringAsync(); | ||||
|                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
|         //         json = await response.Content.ReadAsStringAsync(); | ||||
|         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
| 
 | ||||
|                 return result?.Success == true; | ||||
|             case "hcaptcha": | ||||
|                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|                     "application/x-www-form-urlencoded"); | ||||
|                 response = await client.PostAsync("https://hcaptcha.com/siteverify", content); | ||||
|                 response.EnsureSuccessStatusCode(); | ||||
|         //         return result?.Success == true; | ||||
|         //     case "hcaptcha": | ||||
|         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||
|         //             "application/x-www-form-urlencoded"); | ||||
|         //         response = await client.PostAsync("https://hcaptcha.com/siteverify", content); | ||||
|         //         response.EnsureSuccessStatusCode(); | ||||
| 
 | ||||
|                 json = await response.Content.ReadAsStringAsync(); | ||||
|                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
|         //         json = await response.Content.ReadAsStringAsync(); | ||||
|         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||
| 
 | ||||
|                 return result?.Success == true; | ||||
|             default: | ||||
|                 throw new ArgumentException("The server misconfigured for the captcha."); | ||||
|         } | ||||
|         //         return result?.Success == true; | ||||
|         //     default: | ||||
|         //         throw new ArgumentException("The server misconfigured for the captcha."); | ||||
|         // } | ||||
|         return true; // Placeholder for captcha validation | ||||
|     } | ||||
| 
 | ||||
|     public string CreateToken(Session session) | ||||
| @@ -183,56 +186,56 @@ public class AuthService( | ||||
|         return $"{payloadBase64}.{signatureBase64}"; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> ValidateSudoMode(Session session, string? pinCode) | ||||
|     { | ||||
|         // Check if the session is already in sudo mode (cached) | ||||
|         var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||
|         var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey); | ||||
|     // public async Task<bool> ValidateSudoMode(Session session, string? pinCode) | ||||
|     // { | ||||
|     //     // Check if the session is already in sudo mode (cached) | ||||
|     //     var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||
|     //     var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey); | ||||
|          | ||||
|         if (found) | ||||
|         { | ||||
|             // Session is already in sudo mode | ||||
|             return true; | ||||
|         } | ||||
|     //     if (found) | ||||
|     //     { | ||||
|     //         // Session is already in sudo mode | ||||
|     //         return true; | ||||
|     //     } | ||||
|          | ||||
|         // Check if the user has a pin code | ||||
|         var hasPinCode = await db.AccountAuthFactors | ||||
|             .Where(f => f.AccountId == session.AccountId) | ||||
|             .Where(f => f.EnabledAt != null) | ||||
|             .Where(f => f.Type == AccountAuthFactorType.PinCode) | ||||
|             .AnyAsync(); | ||||
|     //     // Check if the user has a pin code | ||||
|     //     var hasPinCode = await db.AccountAuthFactors | ||||
|     //         .Where(f => f.AccountId == session.AccountId) | ||||
|     //         .Where(f => f.EnabledAt != null) | ||||
|     //         .Where(f => f.Type == AccountAuthFactorType.PinCode) | ||||
|     //         .AnyAsync(); | ||||
|              | ||||
|         if (!hasPinCode) | ||||
|         { | ||||
|             // User doesn't have a pin code, no validation needed | ||||
|             return true; | ||||
|         } | ||||
|     //     if (!hasPinCode) | ||||
|     //     { | ||||
|     //         // User doesn't have a pin code, no validation needed | ||||
|     //         return true; | ||||
|     //     } | ||||
|          | ||||
|         // If pin code is not provided, we can't validate | ||||
|         if (string.IsNullOrEmpty(pinCode)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|     //     // If pin code is not provided, we can't validate | ||||
|     //     if (string.IsNullOrEmpty(pinCode)) | ||||
|     //     { | ||||
|     //         return false; | ||||
|     //     } | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             // Validate the pin code | ||||
|             var isValid = await ValidatePinCode(session.AccountId, pinCode); | ||||
|     //     try | ||||
|     //     { | ||||
|     //         // Validate the pin code | ||||
|     //         var isValid = await ValidatePinCode(session.AccountId, pinCode); | ||||
|              | ||||
|             if (isValid) | ||||
|             { | ||||
|                 // Set session in sudo mode for 5 minutes | ||||
|                 await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5)); | ||||
|             } | ||||
|     //         if (isValid) | ||||
|     //         { | ||||
|     //             // Set session in sudo mode for 5 minutes | ||||
|     //             await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5)); | ||||
|     //         } | ||||
|              | ||||
|             return isValid; | ||||
|         } | ||||
|         catch (InvalidOperationException) | ||||
|         { | ||||
|             // No pin code enabled for this account, so validation is successful | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     //         return isValid; | ||||
|     //     } | ||||
|     //     catch (InvalidOperationException) | ||||
|     //     { | ||||
|     //         // No pin code enabled for this account, so validation is successful | ||||
|     //         return true; | ||||
|     //     } | ||||
|     // } | ||||
| 
 | ||||
|     public async Task<bool> ValidatePinCode(Guid accountId, string pinCode) | ||||
|     { | ||||
| @@ -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] | ||||
| @@ -114,7 +115,7 @@ public class OidcProviderController( | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetUserInfo() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         // Get requested scopes from the token | ||||
| @@ -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,18 @@ public class OidcProviderService( | ||||
| 
 | ||||
|     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) | ||||
|     { | ||||
|         return await db.CustomApps | ||||
|             .Include(c => c.Secrets) | ||||
|             .FirstOrDefaultAsync(c => c.Id == clientId); | ||||
|         return null; | ||||
|         // return await db.CustomApps | ||||
|         //     .Include(c => c.Secrets) | ||||
|         //     .FirstOrDefaultAsync(c => c.Id == clientId); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId) | ||||
|     { | ||||
|         return await db.CustomApps | ||||
|             .Include(c => c.Secrets) | ||||
|             .FirstOrDefaultAsync(c => c.Id == appId); | ||||
|         return null; | ||||
|         // return await db.CustomApps | ||||
|         //     .Include(c => c.Secrets) | ||||
|         //     .FirstOrDefaultAsync(c => c.Id == appId); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId) | ||||
| @@ -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. | ||||
							
								
								
									
										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 | ||||
| { | ||||
|      | ||||
| } | ||||
							
								
								
									
										5
									
								
								DysonNetwork.Pass/Localization/EmailResource.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								DysonNetwork.Pass/Localization/EmailResource.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| namespace DysonNetwork.Pass.Localization; | ||||
|  | ||||
| public class EmailResource | ||||
| { | ||||
| } | ||||
							
								
								
									
										6
									
								
								DysonNetwork.Pass/Localization/NotificationResource.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								DysonNetwork.Pass/Localization/NotificationResource.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| namespace DysonNetwork.Pass.Localization; | ||||
|  | ||||
| public class NotificationResource | ||||
| { | ||||
|      | ||||
| } | ||||
							
								
								
									
										6
									
								
								DysonNetwork.Pass/Localization/SharedResource.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								DysonNetwork.Pass/Localization/SharedResource.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| namespace DysonNetwork.Pass.Localization; | ||||
|  | ||||
| public class SharedResource | ||||
| { | ||||
|      | ||||
| } | ||||
							
								
								
									
										42
									
								
								DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								DysonNetwork.Pass/Pages/Emails/AccountDeletionEmail.razor
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| @using DysonNetwork.Pass.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["AccountDeletionHeader"])</p> | ||||
|             <p>@(Localizer["AccountDeletionPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["AccountDeletionPara2"])</p> | ||||
|             <p>@(Localizer["AccountDeletionPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["AccountDeletionButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["AccountDeletionPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| @using DysonNetwork.Pass.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["ContactVerificationHeader"])</p> | ||||
|             <p>@(Localizer["ContactVerificationPara1"]) @Name,</p> | ||||
|             <p>@(Localizer["ContactVerificationPara2"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["ContactVerificationButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["ContactVerificationPara3"])</p> | ||||
|             <p>@(Localizer["ContactVerificationPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										337
									
								
								DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								DysonNetwork.Pass/Pages/Emails/EmailLayout.razor
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,337 @@ | ||||
| @inherits LayoutComponentBase | ||||
|  | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | ||||
|     <style media="all" type="text/css"> | ||||
|         body { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             -webkit-font-smoothing: antialiased; | ||||
|             font-size: 16px; | ||||
|             line-height: 1.3; | ||||
|             -ms-text-size-adjust: 100%; | ||||
|             -webkit-text-size-adjust: 100%; | ||||
|         } | ||||
|  | ||||
|         table { | ||||
|             border-collapse: separate; | ||||
|             mso-table-lspace: 0pt; | ||||
|             mso-table-rspace: 0pt; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         table td { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             font-size: 16px; | ||||
|             vertical-align: top; | ||||
|         } | ||||
|  | ||||
|         body { | ||||
|             background-color: #f4f5f6; | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .body { | ||||
|             background-color: #f4f5f6; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .container { | ||||
|             margin: 0 auto !important; | ||||
|             max-width: 600px; | ||||
|             padding: 0; | ||||
|             padding-top: 24px; | ||||
|             width: 600px; | ||||
|         } | ||||
|  | ||||
|         .content { | ||||
|             box-sizing: border-box; | ||||
|             display: block; | ||||
|             margin: 0 auto; | ||||
|             max-width: 600px; | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         .main { | ||||
|             background: #ffffff; | ||||
|             border: 1px solid #eaebed; | ||||
|             border-radius: 16px; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .wrapper { | ||||
|             box-sizing: border-box; | ||||
|             padding: 24px; | ||||
|         } | ||||
|  | ||||
|         .footer { | ||||
|             clear: both; | ||||
|             padding-top: 24px; | ||||
|             text-align: center; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .footer td, | ||||
|         .footer p, | ||||
|         .footer span, | ||||
|         .footer a { | ||||
|             color: #9a9ea6; | ||||
|             font-size: 16px; | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         p { | ||||
|             font-family: Helvetica, sans-serif; | ||||
|             font-size: 16px; | ||||
|             font-weight: normal; | ||||
|             margin: 0; | ||||
|             margin-bottom: 16px; | ||||
|         } | ||||
|  | ||||
|         a { | ||||
|             color: #0867ec; | ||||
|             text-decoration: underline; | ||||
|         } | ||||
|  | ||||
|         .btn { | ||||
|             box-sizing: border-box; | ||||
|             min-width: 100% !important; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .btn > tbody > tr > td { | ||||
|             padding-bottom: 16px; | ||||
|         } | ||||
|  | ||||
|         .btn table { | ||||
|             width: auto; | ||||
|         } | ||||
|  | ||||
|         .btn table td { | ||||
|             background-color: #ffffff; | ||||
|             border-radius: 4px; | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         .btn a { | ||||
|             background-color: #ffffff; | ||||
|             border: solid 2px #0867ec; | ||||
|             border-radius: 4px; | ||||
|             box-sizing: border-box; | ||||
|             color: #0867ec; | ||||
|             cursor: pointer; | ||||
|             display: inline-block; | ||||
|             font-size: 16px; | ||||
|             font-weight: bold; | ||||
|             margin: 0; | ||||
|             padding: 12px 24px; | ||||
|             text-decoration: none; | ||||
|             text-transform: capitalize; | ||||
|         } | ||||
|  | ||||
|         .btn-primary table td { | ||||
|             background-color: #0867ec; | ||||
|         } | ||||
|  | ||||
|         .btn-primary a { | ||||
|             background-color: #0867ec; | ||||
|             border-color: #0867ec; | ||||
|             color: #ffffff; | ||||
|         } | ||||
|          | ||||
|         .font-bold { | ||||
|             font-weight: bold; | ||||
|         } | ||||
|          | ||||
|         .verification-code | ||||
|         { | ||||
|             font-family: "Courier New", Courier, monospace; | ||||
|             font-size: 24px; | ||||
|             letter-spacing: 0.5em; | ||||
|         } | ||||
|  | ||||
|         @@media all { | ||||
|             .btn-primary table td:hover { | ||||
|                 background-color: #ec0867 !important; | ||||
|             } | ||||
|  | ||||
|             .btn-primary a:hover { | ||||
|                 background-color: #ec0867 !important; | ||||
|                 border-color: #ec0867 !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .last { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|  | ||||
|         .first { | ||||
|             margin-top: 0; | ||||
|         } | ||||
|  | ||||
|         .align-center { | ||||
|             text-align: center; | ||||
|         } | ||||
|  | ||||
|         .align-right { | ||||
|             text-align: right; | ||||
|         } | ||||
|  | ||||
|         .align-left { | ||||
|             text-align: left; | ||||
|         } | ||||
|  | ||||
|         .text-link { | ||||
|             color: #0867ec !important; | ||||
|             text-decoration: underline !important; | ||||
|         } | ||||
|  | ||||
|         .clear { | ||||
|             clear: both; | ||||
|         } | ||||
|  | ||||
|         .mt0 { | ||||
|             margin-top: 0; | ||||
|         } | ||||
|  | ||||
|         .mb0 { | ||||
|             margin-bottom: 0; | ||||
|         } | ||||
|  | ||||
|         .preheader { | ||||
|             color: transparent; | ||||
|             display: none; | ||||
|             height: 0; | ||||
|             max-height: 0; | ||||
|             max-width: 0; | ||||
|             opacity: 0; | ||||
|             overflow: hidden; | ||||
|             mso-hide: all; | ||||
|             visibility: hidden; | ||||
|             width: 0; | ||||
|         } | ||||
|  | ||||
|         .powered-by a { | ||||
|             text-decoration: none; | ||||
|         } | ||||
|  | ||||
|         @@media only screen and (max-width: 640px) { | ||||
|             .main p, | ||||
|             .main td, | ||||
|             .main span { | ||||
|                 font-size: 16px !important; | ||||
|             } | ||||
|  | ||||
|             .wrapper { | ||||
|                 padding: 8px !important; | ||||
|             } | ||||
|  | ||||
|             .content { | ||||
|                 padding: 0 !important; | ||||
|             } | ||||
|  | ||||
|             .container { | ||||
|                 padding: 0 !important; | ||||
|                 padding-top: 8px !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|  | ||||
|             .main { | ||||
|                 border-left-width: 0 !important; | ||||
|                 border-radius: 0 !important; | ||||
|                 border-right-width: 0 !important; | ||||
|             } | ||||
|  | ||||
|             .btn table { | ||||
|                 max-width: 100% !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|  | ||||
|             .btn a { | ||||
|                 font-size: 16px !important; | ||||
|                 max-width: 100% !important; | ||||
|                 width: 100% !important; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @@media all { | ||||
|             .ExternalClass { | ||||
|                 width: 100%; | ||||
|             } | ||||
|  | ||||
|             .ExternalClass, | ||||
|             .ExternalClass p, | ||||
|             .ExternalClass span, | ||||
|             .ExternalClass font, | ||||
|             .ExternalClass td, | ||||
|             .ExternalClass div { | ||||
|                 line-height: 100%; | ||||
|             } | ||||
|  | ||||
|             .apple-link a { | ||||
|                 color: inherit !important; | ||||
|                 font-family: inherit !important; | ||||
|                 font-size: inherit !important; | ||||
|                 font-weight: inherit !important; | ||||
|                 line-height: inherit !important; | ||||
|                 text-decoration: none !important; | ||||
|             } | ||||
|  | ||||
|             #MessageViewBody a { | ||||
|                 color: inherit; | ||||
|                 text-decoration: none; | ||||
|                 font-size: inherit; | ||||
|                 font-family: inherit; | ||||
|                 font-weight: inherit; | ||||
|                 line-height: inherit; | ||||
|             } | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
| <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"> | ||||
|     <tr> | ||||
|         <td> </td> | ||||
|         <td class="container"> | ||||
|             <div class="content"> | ||||
|  | ||||
|                 <!-- START CENTERED WHITE CONTAINER --> | ||||
|                 <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main"> | ||||
|                     <!-- START MAIN CONTENT AREA --> | ||||
|                     @ChildContent | ||||
|                     <!-- END MAIN CONTENT AREA --> | ||||
|                 </table> | ||||
|  | ||||
|                 <!-- START FOOTER --> | ||||
|                 <div class="footer"> | ||||
|                     <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                         <tr> | ||||
|                             <td class="content-block"> | ||||
|                                 <span class="apple-link">Solar Network</span> | ||||
|                                 <br> Solsynth LLC © @(DateTime.Now.Year) | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                         <tr> | ||||
|                             <td class="content-block powered-by"> | ||||
|                                 Powered by <a href="https://github.com/solsynth/dysonnetwork">Dyson Network</a> | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                     </table> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- END FOOTER --> | ||||
|  | ||||
|                 <!-- END CENTERED WHITE CONTAINER --></div> | ||||
|         </td> | ||||
|         <td> </td> | ||||
|     </tr> | ||||
| </table> | ||||
| </body> | ||||
| </html> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public RenderFragment? ChildContent { get; set; } | ||||
| } | ||||
							
								
								
									
										43
									
								
								DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								DysonNetwork.Pass/Pages/Emails/LandingEmail.razor
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| @using DysonNetwork.Pass.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["LandingHeader1"])</p> | ||||
|             <p>@(Localizer["LandingPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["LandingPara2"])</p> | ||||
|             <p>@(Localizer["LandingPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["LandingButton1"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["LandingPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										44
									
								
								DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								DysonNetwork.Pass/Pages/Emails/PasswordResetEmail.razor
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| @using DysonNetwork.Pass.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["PasswordResetHeader"])</p> | ||||
|             <p>@(Localizer["PasswordResetPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["PasswordResetPara2"])</p> | ||||
|             <p>@(Localizer["PasswordResetPara3"])</p> | ||||
|  | ||||
|             <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary"> | ||||
|                 <tbody> | ||||
|                 <tr> | ||||
|                     <td align="left"> | ||||
|                         <table role="presentation" border="0" cellpadding="0" cellspacing="0"> | ||||
|                             <tbody> | ||||
|                             <tr> | ||||
|                                 <td> | ||||
|                                     <a href="@Link" target="_blank"> | ||||
|                                         @(Localizer["PasswordResetButton"]) | ||||
|                                     </a> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p>@(Localizer["PasswordResetPara4"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Link { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
|     [Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										27
									
								
								DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Pass/Pages/Emails/VerificationEmail.razor
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| @using DysonNetwork.Pass.Localization | ||||
| @using Microsoft.Extensions.Localization | ||||
| @using EmailResource = DysonNetwork.Pass.Localization.EmailResource | ||||
|  | ||||
| <EmailLayout> | ||||
|     <tr> | ||||
|         <td class="wrapper"> | ||||
|             <p class="font-bold">@(Localizer["VerificationHeader1"])</p> | ||||
|             <p>@(Localizer["VerificationPara1"]) @@@Name,</p> | ||||
|             <p>@(Localizer["VerificationPara2"])</p> | ||||
|             <p>@(Localizer["VerificationPara3"])</p> | ||||
|  | ||||
|             <p class="verification-code">@Code</p> | ||||
|  | ||||
|             <p>@(Localizer["VerificationPara4"])</p> | ||||
|             <p>@(Localizer["VerificationPara5"])</p> | ||||
|         </td> | ||||
|     </tr> | ||||
| </EmailLayout> | ||||
|  | ||||
| @code { | ||||
|     [Parameter] public required string Name { get; set; } | ||||
|     [Parameter] public required string Code { get; set; } | ||||
|  | ||||
|     [Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!; | ||||
|     [Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!; | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| namespace DysonNetwork.Sphere.Permission; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| using System; | ||||
| namespace DysonNetwork.Pass.Permission; | ||||
| 
 | ||||
| [AttributeUsage(AttributeTargets.Method, Inherited = true)] | ||||
| public class RequiredPermissionAttribute(string area, string key) : Attribute | ||||
| @@ -21,7 +21,7 @@ public class PermissionMiddleware(RequestDelegate next) | ||||
| 
 | ||||
|         if (attr != null) | ||||
|         { | ||||
|             if (httpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|             if (httpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             { | ||||
|                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; | ||||
|                 await httpContext.Response.WriteAsync("Unauthorized"); | ||||
| @@ -1,9 +1,10 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Permission; | ||||
| namespace DysonNetwork.Pass.Permission; | ||||
| 
 | ||||
| public class PermissionService( | ||||
|     AppDatabase db, | ||||
							
								
								
									
										44
									
								
								DysonNetwork.Pass/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								DysonNetwork.Pass/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| 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; | ||||
|  | ||||
| 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>(); | ||||
|  | ||||
| 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(); | ||||
							
								
								
									
										186
									
								
								DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| 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>(); | ||||
|  | ||||
|         // 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 | ||||
							
								
								
									
										36
									
								
								DysonNetwork.Shared/DysonNetwork.Shared.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								DysonNetwork.Shared/DysonNetwork.Shared.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| <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="Google.Protobuf" Version="3.27.2" /> | ||||
|         <PackageReference Include="MagicOnion.Client" Version="7.0.5" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" /> | ||||
|         <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="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> | ||||
| @@ -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 | ||||
							
								
								
									
										28
									
								
								DysonNetwork.Shared/Services/IAccountEventService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Shared/Services/IAccountEventService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| 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> | ||||
|     /// 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); | ||||
| } | ||||
							
								
								
									
										62
									
								
								DysonNetwork.Shared/Services/IAccountService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								DysonNetwork.Shared/Services/IAccountService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using MagicOnion; | ||||
|  | ||||
| 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); | ||||
| } | ||||
							
								
								
									
										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; | ||||
| using Microsoft.AspNetCore.Http; | ||||
|  | ||||
| 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> | ||||
|     void CreateActionLogFromRequest( | ||||
|         string action,  | ||||
|         Dictionary<string, object> meta,  | ||||
|         HttpRequest request, | ||||
|         Account? account = null | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										30
									
								
								DysonNetwork.Shared/Services/IMagicSpellService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								DysonNetwork.Shared/Services/IMagicSpellService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| 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> | ||||
|     /// Consumes a magic spell | ||||
|     /// </summary> | ||||
|     Task ApplyMagicSpell(string token); | ||||
| } | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Shared/Services/INotificationService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Shared/Services/INotificationService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using MagicOnion; | ||||
|  | ||||
| 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 | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										27
									
								
								DysonNetwork.Shared/Services/IRelationshipService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Shared/Services/IRelationshipService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| 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); | ||||
| } | ||||
							
								
								
									
										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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								DysonNetwork.Shared/Startup/KestrelConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								DysonNetwork.Shared/Startup/KestrelConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.Extensions.Hosting; | ||||
|  | ||||
| namespace DysonNetwork.Shared.Startup; | ||||
|  | ||||
| public static class KestrelConfiguration | ||||
| { | ||||
|     public static WebApplicationBuilder ConfigureAppKestrel(this WebApplicationBuilder builder) | ||||
|     { | ||||
|         builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); | ||||
|         builder.WebHost.ConfigureKestrel(options => | ||||
|         { | ||||
|             options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; | ||||
|             options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); | ||||
|             options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); | ||||
|         }); | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
| } | ||||
| @@ -45,7 +45,7 @@ public class ActivityController( | ||||
|         var debugIncludeSet = debugInclude?.Split(',').ToHashSet() ?? new HashSet<string>(); | ||||
|  | ||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||
|         return currentUserValue is not Account.Account currentUser | ||||
|         return currentUserValue is not Shared.Models.Account currentUser | ||||
|             ? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet)) | ||||
|             : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); | ||||
|     } | ||||
|   | ||||
| @@ -13,7 +13,8 @@ public class ActivityService( | ||||
|     PublisherService pub, | ||||
|     RelationshipService rels, | ||||
|     PostService ps, | ||||
|     DiscoveryService ds) | ||||
|     DiscoveryService ds | ||||
| ) | ||||
| { | ||||
|     private static double CalculateHotRank(Post.Post post, Instant now) | ||||
|     { | ||||
| @@ -32,7 +33,7 @@ public class ActivityService( | ||||
|  | ||||
|         if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) | ||||
|         { | ||||
|             var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true); | ||||
|             var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true); | ||||
|             if (realms.Count > 0) | ||||
|             { | ||||
|                 activities.Add(new DiscoveryActivity( | ||||
| @@ -118,7 +119,7 @@ public class ActivityService( | ||||
|     public async Task<List<Activity>> GetActivities( | ||||
|         int take, | ||||
|         Instant? cursor, | ||||
|         Account.Account currentUser, | ||||
|         Shared.Models.Account currentUser, | ||||
|         string? filter = null, | ||||
|         HashSet<string>? debugInclude = null | ||||
|     ) | ||||
| @@ -132,7 +133,7 @@ public class ActivityService( | ||||
|         { | ||||
|             if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) | ||||
|             { | ||||
|                 var realms = await ds.GetPublicRealmsAsync(null, null, 5, 0, true); | ||||
|                 var realms = await ds.GetPublicRealmsAsync(null, 5, 0, true); | ||||
|                 if (realms.Count > 0) | ||||
|                 { | ||||
|                     activities.Add(new DiscoveryActivity( | ||||
| @@ -257,7 +258,7 @@ public class ActivityService( | ||||
|         return score + postCount; | ||||
|     } | ||||
|  | ||||
|     private async Task<List<Publisher.Publisher>> GetPopularPublishers(int take) | ||||
|     private async Task<List<Shared.Models.Publisher>> GetPopularPublishers(int take) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var recent = now.Minus(Duration.FromDays(7)); | ||||
|   | ||||
| @@ -1,30 +1,16 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Chat; | ||||
| using DysonNetwork.Sphere.Developer; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Publisher; | ||||
| using DysonNetwork.Sphere.Realm; | ||||
| using DysonNetwork.Sphere.Sticker; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Wallet; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| using NodaTime; | ||||
| using Npgsql; | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Sphere; | ||||
|  | ||||
| public interface IIdentifiedResource | ||||
| { | ||||
|     public string ResourceIdentifier { get; } | ||||
| } | ||||
|  | ||||
| public abstract class ModelBase | ||||
| { | ||||
|     public Instant CreatedAt { get; set; } | ||||
| @@ -37,32 +23,10 @@ public class AppDatabase( | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<PermissionNode> PermissionNodes { get; set; } | ||||
|     public DbSet<PermissionGroup> PermissionGroups { get; set; } | ||||
|     public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } | ||||
|  | ||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } | ||||
|     public DbSet<Account.Account> Accounts { get; set; } | ||||
|     public DbSet<AccountConnection> AccountConnections { get; set; } | ||||
|     public DbSet<Profile> AccountProfiles { get; set; } | ||||
|     public DbSet<AccountContact> AccountContacts { get; set; } | ||||
|     public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } | ||||
|     public DbSet<Relationship> AccountRelationships { get; set; } | ||||
|     public DbSet<Status> AccountStatuses { get; set; } | ||||
|     public DbSet<CheckInResult> AccountCheckInResults { get; set; } | ||||
|     public DbSet<Notification> Notifications { get; set; } | ||||
|     public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; } | ||||
|     public DbSet<Badge> Badges { get; set; } | ||||
|     public DbSet<ActionLog> ActionLogs { get; set; } | ||||
|     public DbSet<AbuseReport> AbuseReports { get; set; } | ||||
|  | ||||
|     public DbSet<Session> AuthSessions { get; set; } | ||||
|     public DbSet<Challenge> AuthChallenges { get; set; } | ||||
|  | ||||
|     public DbSet<CloudFile> Files { get; set; } | ||||
|     public DbSet<CloudFileReference> FileReferences { get; set; } | ||||
|  | ||||
|     public DbSet<Publisher.Publisher> Publishers { get; set; } | ||||
|     public DbSet<Shared.Models.Publisher> Publishers { get; set; } | ||||
|     public DbSet<PublisherMember> PublisherMembers { get; set; } | ||||
|     public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; } | ||||
|     public DbSet<PublisherFeature> PublisherFeatures { get; set; } | ||||
| @@ -73,10 +37,8 @@ public class AppDatabase( | ||||
|     public DbSet<PostCategory> PostCategories { get; set; } | ||||
|     public DbSet<PostCollection> PostCollections { get; set; } | ||||
|  | ||||
|     public DbSet<Realm.Realm> Realms { get; set; } | ||||
|     public DbSet<Shared.Models.Realm> Realms { get; set; } | ||||
|     public DbSet<RealmMember> RealmMembers { get; set; } | ||||
|     public DbSet<Tag> Tags { get; set; } | ||||
|     public DbSet<RealmTag> RealmTags { get; set; } | ||||
|  | ||||
|     public DbSet<ChatRoom> ChatRooms { get; set; } | ||||
|     public DbSet<ChatMember> ChatMembers { get; set; } | ||||
| @@ -87,7 +49,7 @@ public class AppDatabase( | ||||
|     public DbSet<Sticker.Sticker> Stickers { get; set; } | ||||
|     public DbSet<StickerPack> StickerPacks { get; set; } | ||||
|  | ||||
|     public DbSet<Wallet.Wallet> Wallets { get; set; } | ||||
|     public DbSet<Shared.Models.Wallet> Wallets { get; set; } | ||||
|     public DbSet<WalletPocket> WalletPockets { get; set; } | ||||
|     public DbSet<Order> PaymentOrders { get; set; } | ||||
|     public DbSet<Transaction> PaymentTransactions { get; set; } | ||||
| @@ -111,38 +73,6 @@ public class AppDatabase( | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|          | ||||
|         optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) => | ||||
|         { | ||||
|             var defaultPermissionGroup = await context.Set<PermissionGroup>() | ||||
|                 .FirstOrDefaultAsync(g => g.Key == "default", cancellationToken); | ||||
|             if (defaultPermissionGroup is null) | ||||
|             { | ||||
|                 context.Set<PermissionGroup>().Add(new PermissionGroup | ||||
|                 { | ||||
|                     Key = "default", | ||||
|                     Nodes = new List<string> | ||||
|                         { | ||||
|                             "posts.create", | ||||
|                             "posts.react", | ||||
|                             "publishers.create", | ||||
|                             "files.create", | ||||
|                             "chat.create", | ||||
|                             "chat.messages.create", | ||||
|                             "chat.realtime.create", | ||||
|                             "accounts.statuses.create", | ||||
|                             "accounts.statuses.update", | ||||
|                             "stickers.packs.create", | ||||
|                             "stickers.create" | ||||
|                         }.Select(permission => | ||||
|                             PermissionService.NewPermissionNode("group:default", "global", permission, true)) | ||||
|                         .ToList() | ||||
|                 }); | ||||
|                 await context.SaveChangesAsync(cancellationToken); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         optionsBuilder.UseSeeding((context, _) => {}); | ||||
|  | ||||
|         base.OnConfiguring(optionsBuilder); | ||||
|     } | ||||
|  | ||||
| @@ -150,25 +80,6 @@ public class AppDatabase( | ||||
|     { | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|  | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasKey(pg => new { pg.GroupId, pg.Actor }); | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasOne(pg => pg.Group) | ||||
|             .WithMany(g => g.Members) | ||||
|             .HasForeignKey(pg => pg.GroupId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Account) | ||||
|             .WithMany(a => a.OutgoingRelationships) | ||||
|             .HasForeignKey(r => r.AccountId); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Related) | ||||
|             .WithMany(a => a.IncomingRelationships) | ||||
|             .HasForeignKey(r => r.RelatedId); | ||||
|  | ||||
|         modelBuilder.Entity<PublisherMember>() | ||||
|             .HasKey(pm => new { pm.PublisherId, pm.AccountId }); | ||||
|         modelBuilder.Entity<PublisherMember>() | ||||
| @@ -243,19 +154,6 @@ public class AppDatabase( | ||||
|             .HasForeignKey(pm => pm.AccountId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<RealmTag>() | ||||
|             .HasKey(rt => new { rt.RealmId, rt.TagId }); | ||||
|         modelBuilder.Entity<RealmTag>() | ||||
|             .HasOne(rt => rt.Realm) | ||||
|             .WithMany(r => r.RealmTags) | ||||
|             .HasForeignKey(rt => rt.RealmId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|         modelBuilder.Entity<RealmTag>() | ||||
|             .HasOne(rt => rt.Tag) | ||||
|             .WithMany(t => t.RealmTags) | ||||
|             .HasForeignKey(rt => rt.TagId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<ChatMember>() | ||||
|             .HasKey(pm => new { pm.Id }); | ||||
|         modelBuilder.Entity<ChatMember>() | ||||
| @@ -352,23 +250,9 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|         logger.LogInformation("Cleaning up expired records..."); | ||||
|  | ||||
|         // Expired relationships | ||||
|         var affectedRows = await db.AccountRelationships | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows); | ||||
|         // Expired permission group members | ||||
|         affectedRows = await db.PermissionGroupMembers | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows); | ||||
|  | ||||
|         logger.LogInformation("Deleting soft-deleted records..."); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var threshold = now - Duration.FromDays(7); | ||||
|  | ||||
|         var entityTypes = db.Model.GetEntityTypes() | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Text.RegularExpressions; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| @@ -32,7 +33,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id); | ||||
|         var lastMessages = await cs.ListLastMessageForUser(currentUser.Id); | ||||
| @@ -65,7 +66,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset, | ||||
|         [FromQuery] int take = 20) | ||||
|     { | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account; | ||||
|  | ||||
|         var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId); | ||||
|         if (room is null) return NotFound(); | ||||
| @@ -102,7 +103,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [HttpGet("{roomId:guid}/messages/{messageId:guid}")] | ||||
|     public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId) | ||||
|     { | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account; | ||||
|  | ||||
|         var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId); | ||||
|         if (room is null) return NotFound(); | ||||
| @@ -139,7 +140,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [RequiredPermission("global", "chat.messages.create")] | ||||
|     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         request.Content = TextSanitizer.Sanitize(request.Content); | ||||
|         if (string.IsNullOrWhiteSpace(request.Content) && | ||||
| @@ -216,7 +217,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         request.Content = TextSanitizer.Sanitize(request.Content); | ||||
|  | ||||
| @@ -269,7 +270,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var message = await db.ChatMessages | ||||
|             .Include(m => m.Sender) | ||||
| @@ -295,7 +296,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | ||||
|     [HttpPost("{roomId:guid}/sync")] | ||||
|     public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var isMember = await db.ChatMembers | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| @@ -36,7 +37,7 @@ public class ChatRoomController( | ||||
|         if (chatRoom is null) return NotFound(); | ||||
|         if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom); | ||||
|  | ||||
|         if (HttpContext.Items["CurrentUser"] is Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser) | ||||
|             chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id); | ||||
|  | ||||
|         return Ok(chatRoom); | ||||
| @@ -46,7 +47,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
| @@ -72,7 +73,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); | ||||
| @@ -134,7 +135,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var room = await db.ChatRooms | ||||
| @@ -164,7 +165,7 @@ public class ChatRoomController( | ||||
|     [RequiredPermission("global", "chat.create")] | ||||
|     public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         if (request.Name is null) return BadRequest("You cannot create a chat room without a name."); | ||||
|  | ||||
|         var chatRoom = new ChatRoom | ||||
| @@ -236,7 +237,7 @@ public class ChatRoomController( | ||||
|     [HttpPatch("{id:guid}")] | ||||
|     public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(e => e.Id == id) | ||||
| @@ -321,7 +322,7 @@ public class ChatRoomController( | ||||
|     [HttpDelete("{id:guid}")] | ||||
|     public async Task<ActionResult> DeleteChatRoom(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(e => e.Id == id) | ||||
| @@ -356,7 +357,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
| @@ -375,7 +376,7 @@ public class ChatRoomController( | ||||
|     public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20, | ||||
|         [FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null) | ||||
|     { | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Shared.Models.Account; | ||||
|  | ||||
|         var room = await db.ChatRooms | ||||
|             .FirstOrDefaultAsync(r => r.Id == roomId); | ||||
| @@ -448,7 +449,7 @@ public class ChatRoomController( | ||||
|     public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId, | ||||
|         [FromBody] ChatMemberRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); | ||||
| @@ -519,7 +520,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<ChatMember>>> ListChatInvites() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var members = await db.ChatMembers | ||||
| @@ -544,7 +545,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
| @@ -571,7 +572,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeclineChatInvite(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
| @@ -600,7 +601,7 @@ public class ChatRoomController( | ||||
|         [FromBody] ChatMemberNotifyRequest request | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(r => r.Id == roomId) | ||||
| @@ -629,7 +630,7 @@ public class ChatRoomController( | ||||
|     public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole) | ||||
|     { | ||||
|         if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role."); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(r => r.Id == roomId) | ||||
| @@ -688,7 +689,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(r => r.Id == roomId) | ||||
| @@ -736,7 +737,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(r => r.Id == roomId) | ||||
| @@ -774,7 +775,7 @@ public class ChatRoomController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> LeaveChat(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id) | ||||
| @@ -807,7 +808,7 @@ public class ChatRoomController( | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     private async Task _SendInviteNotify(ChatMember member, Account.Account sender) | ||||
|     private async Task _SendInviteNotify(ChatMember member, Shared.Models.Account sender) | ||||
|     { | ||||
|         string title = localizer["ChatInviteTitle"]; | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Chat.Realtime; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| @@ -241,7 +242,7 @@ public partial class ChatService( | ||||
|             Priority = 10, | ||||
|         }; | ||||
|  | ||||
|         List<Account.Account> accountsToNotify = []; | ||||
|         List<Shared.Models.Account> accountsToNotify = []; | ||||
|         foreach (var member in members) | ||||
|         { | ||||
|             scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket | ||||
|   | ||||
| @@ -36,7 +36,7 @@ public interface IRealtimeService | ||||
|     /// <param name="sessionId">The session identifier</param> | ||||
|     /// <param name="isAdmin">The user is the admin of session</param> | ||||
|     /// <returns>User-specific token for the session</returns> | ||||
|     string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false); | ||||
|     string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false); | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Processes incoming webhook requests from the realtime service provider | ||||
|   | ||||
| @@ -4,6 +4,8 @@ using Livekit.Server.Sdk.Dotnet; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Chat.Realtime; | ||||
|  | ||||
| @@ -111,7 +113,7 @@ public class LivekitRealtimeService : IRealtimeService | ||||
|     } | ||||
|  | ||||
|     /// <inheritdoc /> | ||||
|     public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false) | ||||
|     public string GetUserToken(Shared.Models.Account account, string sessionId, bool isAdmin = false) | ||||
|     { | ||||
|         var token = _accessToken.WithIdentity(account.Name) | ||||
|             .WithName(account.Nick) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Chat.Realtime; | ||||
| using Livekit.Server.Sdk.Dotnet; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| @@ -46,7 +47,7 @@ public class RealtimeCallController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
| @@ -71,7 +72,7 @@ public class RealtimeCallController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         // Check if the user is a member of the chat room | ||||
|         var member = await db.ChatMembers | ||||
| @@ -144,7 +145,7 @@ public class RealtimeCallController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
| @@ -163,7 +164,7 @@ public class RealtimeCallController( | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Net.WebSockets; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Sphere.Chat; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -19,7 +20,7 @@ public class MessageReadHandler( | ||||
|     public const string ChatMemberCacheKey = "ChatMember_{0}_{1}"; | ||||
|  | ||||
|     public async Task HandleAsync( | ||||
|         Account.Account currentUser, | ||||
|         Shared.Models.Account currentUser, | ||||
|         string deviceId, | ||||
|         WebSocketPacket packet, | ||||
|         WebSocket socket, | ||||
|   | ||||
| @@ -10,7 +10,7 @@ public class MessageTypingHandler(ChatRoomService crs) : IWebSocketPacketHandler | ||||
|     public string PacketType => "messages.typing"; | ||||
|  | ||||
|     public async Task HandleAsync( | ||||
|         Account.Account currentUser, | ||||
|         Shared.Models.Account currentUser, | ||||
|         string deviceId, | ||||
|         WebSocketPacket packet, | ||||
|         WebSocket socket, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ public class MessagesSubscribeHandler(ChatRoomService crs) : IWebSocketPacketHan | ||||
|     public string PacketType => "messages.subscribe"; | ||||
|  | ||||
|     public async Task HandleAsync( | ||||
|         Account.Account currentUser, | ||||
|         Shared.Models.Account currentUser, | ||||
|         string deviceId, | ||||
|         WebSocketPacket packet, | ||||
|         WebSocket socket, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ public class MessagesUnsubscribeHandler() : IWebSocketPacketHandler | ||||
|     public string PacketType => "messages.unsubscribe"; | ||||
|  | ||||
|     public Task HandleAsync( | ||||
|         Account.Account currentUser, | ||||
|         Shared.Models.Account currentUser, | ||||
|         string deviceId, | ||||
|         WebSocketPacket packet, | ||||
|         WebSocket socket, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user