Compare commits
	
		
			7 Commits
		
	
	
		
			master
			...
			8d2f4a4c47
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8d2f4a4c47 | |||
| 1672d46038 | |||
| 15fb93c2bb | |||
| 4b220e7ed7 | |||
| 65450e8511 | |||
| cb4acbb3fc | |||
| bb2f88cc54 | 
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| public enum AbuseReportType | public enum AbuseReportType | ||||||
| { | { | ||||||
| @@ -26,5 +26,5 @@ public class AbuseReport : ModelBase | |||||||
|     [MaxLength(8192)] public string? Resolution { get; set; } |     [MaxLength(8192)] public string? Resolution { get; set; } | ||||||
|      |      | ||||||
|     public Guid AccountId { 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 System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Pass.Auth; | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Extensions; | using Microsoft.AspNetCore.Http; | ||||||
| using System.Collections.Generic; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/accounts")] | [Route("/accounts")] | ||||||
| @@ -20,9 +18,9 @@ public class AccountController( | |||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|     [HttpGet("{name}")] |     [HttpGet("{name}")] | ||||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] |     [ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)] | ||||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] |     [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 |         var account = await db.Accounts | ||||||
|             .Include(e => e.Badges) |             .Include(e => e.Badges) | ||||||
| @@ -73,9 +71,9 @@ public class AccountController( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpPost] |     [HttpPost] | ||||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] |     [ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)] | ||||||
|     [ProducesResponseType(StatusCodes.Status400BadRequest)] |     [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."); |         if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token."); | ||||||
| 
 | 
 | ||||||
| @@ -163,7 +161,7 @@ public class AccountController( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpGet("search")] |     [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)) |         if (string.IsNullOrWhiteSpace(query)) | ||||||
|             return []; |             return []; | ||||||
| @@ -1,14 +1,15 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Pass.Permission; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Org.BouncyCastle.Utilities; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | 
 | ||||||
|  | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| [Authorize] | [Authorize] | ||||||
| [ApiController] | [ApiController] | ||||||
| @@ -16,16 +17,15 @@ namespace DysonNetwork.Sphere.Account; | |||||||
| public class AccountCurrentController( | public class AccountCurrentController( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     AccountService accounts, |     AccountService accounts, | ||||||
|     FileReferenceService fileRefService, |  | ||||||
|     AccountEventService events, |     AccountEventService events, | ||||||
|     AuthService auth |     AuthService auth | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|     [HttpGet] |     [HttpGet] | ||||||
|     [ProducesResponseType<Account>(StatusCodes.Status200OK)] |     [ProducesResponseType<Shared.Models.Account>(StatusCodes.Status200OK)] | ||||||
|     public async Task<ActionResult<Account>> GetCurrentIdentity() |     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 userId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var account = await db.Accounts |         var account = await db.Accounts | ||||||
| @@ -44,9 +44,9 @@ public class AccountCurrentController( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpPatch] |     [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); |         var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id); | ||||||
| 
 | 
 | ||||||
| @@ -77,7 +77,7 @@ public class AccountCurrentController( | |||||||
|     [HttpPatch("profile")] |     [HttpPatch("profile")] | ||||||
|     public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request) |     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 userId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var profile = await db.AccountProfiles |         var profile = await db.AccountProfiles | ||||||
| @@ -95,61 +95,61 @@ public class AccountCurrentController( | |||||||
|         if (request.Location is not null) profile.Location = request.Location; |         if (request.Location is not null) profile.Location = request.Location; | ||||||
|         if (request.TimeZone is not null) profile.TimeZone = request.TimeZone; |         if (request.TimeZone is not null) profile.TimeZone = request.TimeZone; | ||||||
| 
 | 
 | ||||||
|         if (request.PictureId is not null) |         // if (request.PictureId is not null) | ||||||
|         { |         // { | ||||||
|             var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); |         //     var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); | ||||||
|             if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); |         //     if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); | ||||||
| 
 |         // | ||||||
|             var profileResourceId = $"profile:{profile.Id}"; |         //     var profileResourceId = $"profile:{profile.Id}"; | ||||||
| 
 |         // | ||||||
|             // Remove old references for the profile picture |         //     // Remove old references for the profile picture | ||||||
|             if (profile.Picture is not null) |         //     if (profile.Picture is not null) | ||||||
|             { |         //     { | ||||||
|                 var oldPictureRefs = |         //         var oldPictureRefs = | ||||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture"); |         //             await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture"); | ||||||
|                 foreach (var oldRef in oldPictureRefs) |         //         foreach (var oldRef in oldPictureRefs) | ||||||
|                 { |         //         { | ||||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); |         //             await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||||
|                 } |         //         } | ||||||
|             } |         //     } | ||||||
| 
 |         // | ||||||
|             profile.Picture = picture.ToReferenceObject(); |         //     profile.Picture = picture.ToReferenceObject(); | ||||||
| 
 |         // | ||||||
|             // Create new reference |         //     // Create new reference | ||||||
|             await fileRefService.CreateReferenceAsync( |         //     await fileRefService.CreateReferenceAsync( | ||||||
|                 picture.Id, |         //         picture.Id, | ||||||
|                 "profile.picture", |         //         "profile.picture", | ||||||
|                 profileResourceId |         //         profileResourceId | ||||||
|             ); |         //     ); | ||||||
|         } |         // } | ||||||
| 
 |         // | ||||||
|         if (request.BackgroundId is not null) |         // if (request.BackgroundId is not null) | ||||||
|         { |         // { | ||||||
|             var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); |         //     var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); | ||||||
|             if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); |         //     if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); | ||||||
| 
 |         // | ||||||
|             var profileResourceId = $"profile:{profile.Id}"; |         //     var profileResourceId = $"profile:{profile.Id}"; | ||||||
| 
 |         // | ||||||
|             // Remove old references for the profile background |         //     // Remove old references for the profile background | ||||||
|             if (profile.Background is not null) |         //     if (profile.Background is not null) | ||||||
|             { |         //     { | ||||||
|                 var oldBackgroundRefs = |         //         var oldBackgroundRefs = | ||||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background"); |         //             await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background"); | ||||||
|                 foreach (var oldRef in oldBackgroundRefs) |         //         foreach (var oldRef in oldBackgroundRefs) | ||||||
|                 { |         //         { | ||||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); |         //             await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||||
|                 } |         //         } | ||||||
|             } |         //     } | ||||||
| 
 |         // | ||||||
|             profile.Background = background.ToReferenceObject(); |         //     profile.Background = background.ToReferenceObject(); | ||||||
| 
 |         // | ||||||
|             // Create new reference |         //     // Create new reference | ||||||
|             await fileRefService.CreateReferenceAsync( |         //     await fileRefService.CreateReferenceAsync( | ||||||
|                 background.Id, |         //         background.Id, | ||||||
|                 "profile.background", |         //         "profile.background", | ||||||
|                 profileResourceId |         //         profileResourceId | ||||||
|             ); |         //     ); | ||||||
|         } |         // } | ||||||
| 
 | 
 | ||||||
|         db.Update(profile); |         db.Update(profile); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| @@ -162,7 +162,7 @@ public class AccountCurrentController( | |||||||
|     [HttpDelete] |     [HttpDelete] | ||||||
|     public async Task<ActionResult> RequestDeleteAccount() |     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 |         try | ||||||
|         { |         { | ||||||
| @@ -179,7 +179,7 @@ public class AccountCurrentController( | |||||||
|     [HttpGet("statuses")] |     [HttpGet("statuses")] | ||||||
|     public async Task<ActionResult<Status>> GetCurrentStatus() |     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); |         var status = await events.GetStatus(currentUser.Id); | ||||||
|         return Ok(status); |         return Ok(status); | ||||||
|     } |     } | ||||||
| @@ -188,7 +188,7 @@ public class AccountCurrentController( | |||||||
|     [RequiredPermission("global", "accounts.statuses.update")] |     [RequiredPermission("global", "accounts.statuses.update")] | ||||||
|     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) |     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 now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var status = await db.AccountStatuses |         var status = await db.AccountStatuses | ||||||
| @@ -215,7 +215,7 @@ public class AccountCurrentController( | |||||||
|     [RequiredPermission("global", "accounts.statuses.create")] |     [RequiredPermission("global", "accounts.statuses.create")] | ||||||
|     public async Task<ActionResult<Status>> CreateStatus([FromBody] AccountController.StatusRequest request) |     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 |         var status = new Status | ||||||
|         { |         { | ||||||
| @@ -233,7 +233,7 @@ public class AccountCurrentController( | |||||||
|     [HttpDelete("me/statuses")] |     [HttpDelete("me/statuses")] | ||||||
|     public async Task<ActionResult> DeleteStatus() |     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 now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var status = await db.AccountStatuses |         var status = await db.AccountStatuses | ||||||
| @@ -250,7 +250,7 @@ public class AccountCurrentController( | |||||||
|     [HttpGet("check-in")] |     [HttpGet("check-in")] | ||||||
|     public async Task<ActionResult<CheckInResult>> GetCheckInResult() |     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 userId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
| @@ -270,7 +270,7 @@ public class AccountCurrentController( | |||||||
|     [HttpPost("check-in")] |     [HttpPost("check-in")] | ||||||
|     public async Task<ActionResult<CheckInResult>> DoCheckIn([FromBody] string? captchaToken) |     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); |         var isAvailable = await events.CheckInDailyIsAvailable(currentUser); | ||||||
|         if (!isAvailable) |         if (!isAvailable) | ||||||
| @@ -297,7 +297,7 @@ public class AccountCurrentController( | |||||||
|     public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month, |     public async Task<ActionResult<List<DailyEventResponse>>> GetEventCalendar([FromQuery] int? month, | ||||||
|         [FromQuery] int? year) |         [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; |         var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; | ||||||
|         month ??= currentDate.Month; |         month ??= currentDate.Month; | ||||||
| @@ -318,7 +318,7 @@ public class AccountCurrentController( | |||||||
|         [FromQuery] int offset = 0 |         [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 |         var query = db.ActionLogs | ||||||
|             .Where(log => log.AccountId == currentUser.Id) |             .Where(log => log.AccountId == currentUser.Id) | ||||||
| @@ -338,7 +338,7 @@ public class AccountCurrentController( | |||||||
|     [HttpGet("factors")] |     [HttpGet("factors")] | ||||||
|     public async Task<ActionResult<List<AccountAuthFactor>>> GetAuthFactors() |     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 |         var factors = await db.AccountAuthFactors | ||||||
|             .Include(f => f.Account) |             .Include(f => f.Account) | ||||||
| @@ -358,7 +358,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountAuthFactor>> CreateAuthFactor([FromBody] AuthFactorRequest request) |     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)) |         if (await accounts.CheckAuthFactorExists(currentUser, request.Type)) | ||||||
|             return BadRequest($"Auth factor with type {request.Type} is already exists."); |             return BadRequest($"Auth factor with type {request.Type} is already exists."); | ||||||
| 
 | 
 | ||||||
| @@ -370,7 +370,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code) |     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 |         var factor = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) |             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||||
| @@ -392,7 +392,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountAuthFactor>> DisableAuthFactor(Guid id) |     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 |         var factor = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) |             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||||
| @@ -414,7 +414,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountAuthFactor>> DeleteAuthFactor(Guid id) |     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 |         var factor = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == currentUser.Id && f.Id == id) |             .Where(f => f.AccountId == currentUser.Id && f.Id == id) | ||||||
| @@ -445,7 +445,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices() |     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(); |             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); |         Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); | ||||||
| @@ -480,7 +480,7 @@ public class AccountCurrentController( | |||||||
|         [FromQuery] int offset = 0 |         [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(); |             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var query = db.AuthSessions |         var query = db.AuthSessions | ||||||
| @@ -505,7 +505,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Session>> DeleteSession(Guid id) |     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 |         try | ||||||
|         { |         { | ||||||
| @@ -522,7 +522,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Session>> DeleteCurrentSession() |     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(); |             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         try |         try | ||||||
| @@ -539,7 +539,7 @@ public class AccountCurrentController( | |||||||
|     [HttpPatch("sessions/{id:guid}/label")] |     [HttpPatch("sessions/{id:guid}/label")] | ||||||
|     public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string 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 |         try | ||||||
|         { |         { | ||||||
| @@ -555,7 +555,7 @@ public class AccountCurrentController( | |||||||
|     [HttpPatch("sessions/current/label")] |     [HttpPatch("sessions/current/label")] | ||||||
|     public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string 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(); |             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         try |         try | ||||||
| @@ -573,7 +573,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<AccountContact>>> GetContacts() |     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 |         var contacts = await db.AccountContacts | ||||||
|             .Where(c => c.AccountId == currentUser.Id) |             .Where(c => c.AccountId == currentUser.Id) | ||||||
| @@ -592,7 +592,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountContact>> CreateContact([FromBody] AccountContactRequest request) |     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 |         try | ||||||
|         { |         { | ||||||
| @@ -609,7 +609,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountContact>> VerifyContact(Guid id) |     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 |         var contact = await db.AccountContacts | ||||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) |             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||||
| @@ -631,7 +631,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountContact>> SetPrimaryContact(Guid id) |     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 |         var contact = await db.AccountContacts | ||||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) |             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||||
| @@ -653,7 +653,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<AccountContact>> DeleteContact(Guid id) |     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 |         var contact = await db.AccountContacts | ||||||
|             .Where(c => c.AccountId == currentUser.Id && c.Id == id) |             .Where(c => c.AccountId == currentUser.Id && c.Id == id) | ||||||
| @@ -676,9 +676,9 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<Badge>>> GetBadges() |     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) |             .Where(b => b.AccountId == currentUser.Id) | ||||||
|             .ToListAsync(); |             .ToListAsync(); | ||||||
|         return Ok(badges); |         return Ok(badges); | ||||||
| @@ -688,7 +688,7 @@ public class AccountCurrentController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Badge>> ActivateBadge(Guid id) |     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 |         try | ||||||
|         { |         { | ||||||
| @@ -1,23 +1,19 @@ | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using DysonNetwork.Sphere.Activity; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Wallet; | using MagicOnion.Server; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Caching.Distributed; |  | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Org.BouncyCastle.Asn1.X509; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| public class AccountEventService( | public class AccountEventService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     WebSocketService ws, |  | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
|     PaymentService payment, |  | ||||||
|     IStringLocalizer<Localization.AccountEventResource> localizer |     IStringLocalizer<Localization.AccountEventResource> localizer | ||||||
| ) | ) : ServiceBase<IAccountEventService>, IAccountEventService | ||||||
| { | { | ||||||
|     private static readonly Random Random = new(); |     private static readonly Random Random = new(); | ||||||
|     private const string StatusCacheKey = "AccountStatus_"; |     private const string StatusCacheKey = "AccountStatus_"; | ||||||
| @@ -34,7 +30,7 @@ public class AccountEventService( | |||||||
|         var cachedStatus = await cache.GetAsync<Status>(cacheKey); |         var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||||
|         if (cachedStatus is not null) |         if (cachedStatus is not null) | ||||||
|         { |         { | ||||||
|             cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); |             cachedStatus!.IsOnline = !cachedStatus.IsInvisible /*&& ws.GetAccountIsConnected(userId)*/; | ||||||
|             return cachedStatus; |             return cachedStatus; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -44,12 +40,17 @@ public class AccountEventService( | |||||||
|             .Where(e => e.ClearedAt == null || e.ClearedAt > now) |             .Where(e => e.ClearedAt == null || e.ClearedAt > now) | ||||||
|             .OrderByDescending(e => e.CreatedAt) |             .OrderByDescending(e => e.CreatedAt) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         var isOnline = ws.GetAccountIsConnected(userId); |         // var isOnline = ws.GetAccountIsConnected(userId); | ||||||
|  |         var isOnline = false; // Placeholder | ||||||
|         if (status is not null) |         if (status is not null) | ||||||
|         { |         { | ||||||
|             status.IsOnline = !status.IsInvisible && isOnline; |             status.IsOnline = !status.IsInvisible && isOnline; | ||||||
|             await cache.SetWithGroupsAsync(cacheKey, status, [$"{AccountService.AccountCachePrefix}{status.AccountId}"], |             await cache.SetWithGroupsAsync( | ||||||
|                 TimeSpan.FromMinutes(5)); |                 cacheKey, | ||||||
|  |                 status, | ||||||
|  |                 [$"{AccountService.AccountCachePrefix}{status.AccountId}"], | ||||||
|  |                 TimeSpan.FromMinutes(5) | ||||||
|  |             ); | ||||||
|             return status; |             return status; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @@ -83,16 +84,16 @@ public class AccountEventService( | |||||||
|         foreach (var userId in userIds) |         foreach (var userId in userIds) | ||||||
|         { |         { | ||||||
|             var cacheKey = $"{StatusCacheKey}{userId}"; |             var cacheKey = $"{StatusCacheKey}{userId}"; | ||||||
|             var cachedStatus = await cache.GetAsync<Status>(cacheKey); |             // var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||||
|             if (cachedStatus != null) |             // if (cachedStatus != null) | ||||||
|             { |             // { | ||||||
|                 cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); |             //     cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||||
|                 results[userId] = cachedStatus; |             //     results[userId] = cachedStatus; | ||||||
|             } |             // } | ||||||
|             else |             // else | ||||||
|             { |             // { | ||||||
|             cacheMissUserIds.Add(userId); |             cacheMissUserIds.Add(userId); | ||||||
|             } |             // } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (cacheMissUserIds.Any()) |         if (cacheMissUserIds.Any()) | ||||||
| @@ -109,11 +110,12 @@ public class AccountEventService( | |||||||
| 
 | 
 | ||||||
|             foreach (var status in statusesFromDb) |             foreach (var status in statusesFromDb) | ||||||
|             { |             { | ||||||
|                 var isOnline = ws.GetAccountIsConnected(status.AccountId); |                 // var isOnline = ws.GetAccountIsConnected(status.AccountId); | ||||||
|  |                 var isOnline = false; // Placeholder | ||||||
|                 status.IsOnline = !status.IsInvisible && isOnline; |                 status.IsOnline = !status.IsInvisible && isOnline; | ||||||
|                 results[status.AccountId] = status; |                 results[status.AccountId] = status; | ||||||
|                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; |                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; | ||||||
|                 await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); |                 // await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); | ||||||
|                 foundUserIds.Add(status.AccountId); |                 foundUserIds.Add(status.AccountId); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @@ -122,7 +124,8 @@ public class AccountEventService( | |||||||
|             { |             { | ||||||
|                 foreach (var userId in usersWithoutStatus) |                 foreach (var userId in usersWithoutStatus) | ||||||
|                 { |                 { | ||||||
|                     var isOnline = ws.GetAccountIsConnected(userId); |                     // var isOnline = ws.GetAccountIsConnected(userId); | ||||||
|  |                     var isOnline = false; // Placeholder | ||||||
|                     var defaultStatus = new Status |                     var defaultStatus = new Status | ||||||
|                     { |                     { | ||||||
|                         Attitude = StatusAttitude.Neutral, |                         Attitude = StatusAttitude.Neutral, | ||||||
| @@ -139,7 +142,7 @@ public class AccountEventService( | |||||||
|         return results; |         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(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         await db.AccountStatuses |         await db.AccountStatuses | ||||||
| @@ -152,7 +155,7 @@ public class AccountEventService( | |||||||
|         return status; |         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(); |         status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         db.Update(status); |         db.Update(status); | ||||||
| @@ -164,19 +167,19 @@ public class AccountEventService( | |||||||
|     private const string CaptchaCacheKey = "CheckInCaptcha_"; |     private const string CaptchaCacheKey = "CheckInCaptcha_"; | ||||||
|     private const int CaptchaProbabilityPercent = 20; |     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 cacheKey = $"{CaptchaCacheKey}{user.Id}"; | ||||||
|         var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); |         // var needsCaptcha = await cache.GetAsync<bool?>(cacheKey); | ||||||
|         if (needsCaptcha is not null) |         // if (needsCaptcha is not null) | ||||||
|             return needsCaptcha!.Value; |         //     return needsCaptcha!.Value; | ||||||
| 
 | 
 | ||||||
|         var result = Random.Next(100) < CaptchaProbabilityPercent; |         var result = Random.Next(100) < CaptchaProbabilityPercent; | ||||||
|         await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); |         // await cache.SetAsync(cacheKey, result, TimeSpan.FromHours(24)); | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<bool> CheckInDailyIsAvailable(Account user) |     public async Task<bool> CheckInDailyIsAvailable(Shared.Models.Account user) | ||||||
|     { |     { | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var lastCheckIn = await db.AccountCheckInResults |         var lastCheckIn = await db.AccountCheckInResults | ||||||
| @@ -193,9 +196,9 @@ public class AccountEventService( | |||||||
|         return lastDate < currentDate; |         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}"; |         var lockKey = $"{CheckInLockKey}{user.Id}"; | ||||||
| 
 | 
 | ||||||
| @@ -212,7 +215,8 @@ public class AccountEventService( | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Now try to acquire the lock properly |         // Now try to acquire the lock properly | ||||||
|         await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); |         await using var lockObj = | ||||||
|  |             await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||||
|         if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); |         if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); | ||||||
| 
 | 
 | ||||||
|         var cultureInfo = new CultureInfo(user.Language, false); |         var cultureInfo = new CultureInfo(user.Language, false); | ||||||
| @@ -255,13 +259,14 @@ public class AccountEventService( | |||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             if (result.RewardPoints.HasValue) |             if (result.RewardPoints.HasValue) | ||||||
|                 await payment.CreateTransactionWithAccountAsync( |                 // await payment.CreateTransactionWithAccountAsync( | ||||||
|                     null, |                 //     null, | ||||||
|                     user.Id, |                 //     user.Id, | ||||||
|                     WalletCurrency.SourcePoint, |                 //     WalletCurrency.SourcePoint, | ||||||
|                     result.RewardPoints.Value, |                 //     result.RewardPoints.Value, | ||||||
|                     $"Check-in reward on {now:yyyy/MM/dd}" |                 //     $"Check-in reward on {now:yyyy/MM/dd}" | ||||||
|                 ); |                 // ); | ||||||
|  |                 Console.WriteLine($"Simulating transaction for {result.RewardPoints.Value} points"); | ||||||
|         } |         } | ||||||
|         catch |         catch | ||||||
|         { |         { | ||||||
| @@ -274,13 +279,54 @@ public class AccountEventService( | |||||||
|                 s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience) |                 s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience) | ||||||
|             ); |             ); | ||||||
|         db.AccountCheckInResults.Add(result); |         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 |         // The lock will be automatically released by the await using statement | ||||||
|         return result; |         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) |         bool replaceInvisible = false) | ||||||
|     { |     { | ||||||
|         if (year == 0) |         if (year == 0) | ||||||
| @@ -1,32 +1,31 @@ | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Pass.Auth; | ||||||
| using DysonNetwork.Sphere.Auth.OpenId; | using DysonNetwork.Pass.Auth.OpenId; | ||||||
| using DysonNetwork.Sphere.Email; | using DysonNetwork.Shared.Cache; | ||||||
| 
 | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Permission; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using EFCore.BulkExtensions; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Org.BouncyCastle.Utilities; |  | ||||||
| using OtpNet; | using OtpNet; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using EFCore.BulkExtensions; | ||||||
|  | using MagicOnion.Server; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| public class AccountService( | public class AccountService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     MagicSpellService spells, |     // MagicSpellService spells, | ||||||
|     AccountUsernameService uname, |     // AccountUsernameService uname, | ||||||
|     NotificationService nty, |     // NotificationService nty, | ||||||
|     EmailService mailer, |     // EmailService mailer, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     // IStringLocalizer<NotificationResource> localizer, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
|     ILogger<AccountService> logger |     ILogger<AccountService> logger | ||||||
| ) | ) : ServiceBase<IAccountService>, IAccountService | ||||||
| { | { | ||||||
|     public static void SetCultureInfo(Account account) |     public static void SetCultureInfo(Shared.Models.Account account) | ||||||
|     { |     { | ||||||
|         SetCultureInfo(account.Language); |         SetCultureInfo(account.Language); | ||||||
|     } |     } | ||||||
| @@ -40,12 +39,12 @@ public class AccountService( | |||||||
| 
 | 
 | ||||||
|     public const string AccountCachePrefix = "account:"; |     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}"); |         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(); |         var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync(); | ||||||
|         if (account is not null) return account; |         if (account is not null) return account; | ||||||
| @@ -57,7 +56,7 @@ public class AccountService( | |||||||
|         return contact?.Account; |         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 |         var connection = await db.AccountConnections | ||||||
|             .Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider) |             .Where(c => c.ProvidedIdentifier == identifier && c.Provider == provider) | ||||||
| @@ -74,7 +73,7 @@ public class AccountService( | |||||||
|         return profile?.Level; |         return profile?.Level; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<Account> CreateAccount( |     public async Task<Shared.Models.Account> CreateAccount( | ||||||
|         string name, |         string name, | ||||||
|         string nick, |         string nick, | ||||||
|         string email, |         string email, | ||||||
| @@ -91,7 +90,7 @@ public class AccountService( | |||||||
|             if (dupeNameCount > 0) |             if (dupeNameCount > 0) | ||||||
|                 throw new InvalidOperationException("Account name has already been taken."); |                 throw new InvalidOperationException("Account name has already been taken."); | ||||||
| 
 | 
 | ||||||
|             var account = new Account |             var account = new Shared.Models.Account | ||||||
|             { |             { | ||||||
|                 Name = name, |                 Name = name, | ||||||
|                 Nick = nick, |                 Nick = nick, | ||||||
| @@ -135,15 +134,15 @@ public class AccountService( | |||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                 var spell = await spells.CreateMagicSpell( |                 // var spell = await spells.CreateMagicSpell( | ||||||
|                     account, |                 //     account, | ||||||
|                     MagicSpellType.AccountActivation, |                 //     MagicSpellType.AccountActivation, | ||||||
|                     new Dictionary<string, object> |                 //     new Dictionary<string, object> | ||||||
|                     { |                 //     { | ||||||
|                         { "contact_method", account.Contacts.First().Content } |                 //         { "contact_method", account.Contacts.First().Content } | ||||||
|                     } |                 //     } | ||||||
|                 ); |                 // ); | ||||||
|                 await spells.NotifyMagicSpell(spell, true); |                 // await spells.NotifyMagicSpell(spell, true); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             db.Accounts.Add(account); |             db.Accounts.Add(account); | ||||||
| @@ -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)) |         if (string.IsNullOrEmpty(userInfo.Email)) | ||||||
|             throw new ArgumentException("Email is required for account creation"); |             throw new ArgumentException("Email is required for account creation"); | ||||||
| @@ -169,7 +168,8 @@ public class AccountService( | |||||||
|             : $"{userInfo.FirstName} {userInfo.LastName}".Trim(); |             : $"{userInfo.FirstName} {userInfo.LastName}".Trim(); | ||||||
| 
 | 
 | ||||||
|         // Generate username from email |         // Generate username from email | ||||||
|         var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); |         // var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email); | ||||||
|  |         var username = userInfo.Email.Split('@')[0]; // Placeholder | ||||||
| 
 | 
 | ||||||
|         return await CreateAccount( |         return await CreateAccount( | ||||||
|             username, |             username, | ||||||
| @@ -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( |         // var spell = await spells.CreateMagicSpell( | ||||||
|             account, |         //     account, | ||||||
|             MagicSpellType.AccountRemoval, |         //     MagicSpellType.AccountRemoval, | ||||||
|             new Dictionary<string, object>(), |         //     new Dictionary<string, object>(), | ||||||
|             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|             preventRepeat: true |         //     preventRepeat: true | ||||||
|         ); |         // ); | ||||||
|         await spells.NotifyMagicSpell(spell); |         // await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task RequestPasswordReset(Account account) |     public async Task RequestPasswordReset(Shared.Models.Account account) | ||||||
|     { |     { | ||||||
|         var spell = await spells.CreateMagicSpell( |         // var spell = await spells.CreateMagicSpell( | ||||||
|             account, |         //     account, | ||||||
|             MagicSpellType.AuthPasswordReset, |         //     MagicSpellType.AuthPasswordReset, | ||||||
|             new Dictionary<string, object>(), |         //     new Dictionary<string, object>(), | ||||||
|             SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |         //     SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|             preventRepeat: true |         //     preventRepeat: true | ||||||
|         ); |         // ); | ||||||
|         await spells.NotifyMagicSpell(spell); |         // await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<bool> CheckAuthFactorExists(Account account, AccountAuthFactorType type) |     public async Task<bool> CheckAuthFactorExists(Shared.Models.Account account, AccountAuthFactorType type) | ||||||
|     { |     { | ||||||
|         var isExists = await db.AccountAuthFactors |         var isExists = await db.AccountAuthFactors | ||||||
|             .Where(x => x.AccountId == account.Id && x.Type == type) |             .Where(x => x.AccountId == account.Id && x.Type == type) | ||||||
| @@ -214,7 +214,7 @@ public class AccountService( | |||||||
|         return isExists; |         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; |         AccountAuthFactor? factor = null; | ||||||
|         switch (type) |         switch (type) | ||||||
| @@ -329,7 +329,7 @@ public class AccountService( | |||||||
|     { |     { | ||||||
|         var count = await db.AccountAuthFactors |         var count = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == factor.AccountId) |             .Where(f => f.AccountId == factor.AccountId) | ||||||
|             .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null)) |             // .If(factor.EnabledAt is not null, q => q.Where(f => f.EnabledAt != null)) | ||||||
|             .CountAsync(); |             .CountAsync(); | ||||||
|         if (count <= 1) |         if (count <= 1) | ||||||
|             throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor."); |             throw new InvalidOperationException("Deleting this auth factor will cause you have no auth factor."); | ||||||
| @@ -345,7 +345,7 @@ public class AccountService( | |||||||
|     /// <param name="account">The owner of the auth factor</param> |     /// <param name="account">The owner of the auth factor</param> | ||||||
|     /// <param name="factor">The auth factor needed to send code</param> |     /// <param name="factor">The auth factor needed to send code</param> | ||||||
|     /// <param name="hint">The part of the contact method for verification</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"); |         var code = new Random().Next(100000, 999999).ToString("000000"); | ||||||
| 
 | 
 | ||||||
| @@ -355,14 +355,14 @@ public class AccountService( | |||||||
|                 if (await _GetFactorCode(factor) is not null) |                 if (await _GetFactorCode(factor) is not null) | ||||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); |                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||||
| 
 | 
 | ||||||
|                 await nty.SendNotification( |                 // await nty.SendNotification( | ||||||
|                     account, |                 //     account, | ||||||
|                     "auth.verification", |                 //     "auth.verification", | ||||||
|                     localizer["AuthCodeTitle"], |                 //     localizer["AuthCodeTitle"], | ||||||
|                     null, |                 //     null, | ||||||
|                     localizer["AuthCodeBody", code], |                 //     localizer["AuthCodeBody", code], | ||||||
|                     save: true |                 //     save: true | ||||||
|                 ); |                 // ); | ||||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); |                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); | ||||||
|                 break; |                 break; | ||||||
|             case AccountAuthFactorType.EmailCode: |             case AccountAuthFactorType.EmailCode: | ||||||
| @@ -397,16 +397,16 @@ public class AccountService( | |||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                                 await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( |                 // await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( | ||||||
|                     account.Nick, |                 //     account.Nick, | ||||||
|                     contact.Content, |                 //     contact.Content, | ||||||
|                     localizer["VerificationEmail"], |                 //     localizer["VerificationEmail"], | ||||||
|                     new VerificationEmailModel |                 //     new VerificationEmailModel | ||||||
|                     { |                 //     { | ||||||
|                         Name = account.Name, |                 //         Name = account.Name, | ||||||
|                         Code = code |                 //         Code = code | ||||||
|                     } |                 //     } | ||||||
|                 ); |                 // ); | ||||||
| 
 | 
 | ||||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); |                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); | ||||||
|                 break; |                 break; | ||||||
| @@ -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 |         var session = await db.AuthSessions | ||||||
|             .Include(s => s.Challenge) |             .Include(s => s.Challenge) | ||||||
| @@ -477,7 +477,7 @@ public class AccountService( | |||||||
|         return session; |         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 |         var session = await db.AuthSessions | ||||||
|             .Include(s => s.Challenge) |             .Include(s => s.Challenge) | ||||||
| @@ -491,7 +491,7 @@ public class AccountService( | |||||||
|             .ToListAsync(); |             .ToListAsync(); | ||||||
| 
 | 
 | ||||||
|         if (session.Challenge.DeviceId is not null) |         if (session.Challenge.DeviceId is not null) | ||||||
|             await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); |             // await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); | ||||||
| 
 | 
 | ||||||
|         // The current session should be included in the sessions' list |         // The current session should be included in the sessions' list | ||||||
|         await db.AuthSessions |         await db.AuthSessions | ||||||
| @@ -503,7 +503,7 @@ public class AccountService( | |||||||
|             await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}"); |             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 |         var contact = new AccountContact | ||||||
|         { |         { | ||||||
| @@ -518,19 +518,19 @@ public class AccountService( | |||||||
|         return contact; |         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( |         // var spell = await spells.CreateMagicSpell( | ||||||
|             account, |         //     account, | ||||||
|             MagicSpellType.ContactVerification, |         //     MagicSpellType.ContactVerification, | ||||||
|             new Dictionary<string, object> { { "contact_method", contact.Content } }, |         //     new Dictionary<string, object> { { "contact_method", contact.Content } }, | ||||||
|             expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), |         //     expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)), | ||||||
|             preventRepeat: true |         //     preventRepeat: true | ||||||
|         ); |         // ); | ||||||
|         await spells.NotifyMagicSpell(spell); |         // await spells.NotifyMagicSpell(spell); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<AccountContact> SetContactMethodPrimary(Account account, AccountContact contact) |     public async Task<AccountContact> SetContactMethodPrimary(Shared.Models.Account account, AccountContact contact) | ||||||
|     { |     { | ||||||
|         if (contact.AccountId != account.Id) |         if (contact.AccountId != account.Id) | ||||||
|             throw new InvalidOperationException("Contact method does not belong to this account."); |             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) |         if (contact.AccountId != account.Id) | ||||||
|             throw new InvalidOperationException("Contact method does not belong to this account."); |             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. |     /// This method will grant a badge to the account. | ||||||
|     /// Shouldn't be exposed to normal user and the user itself. |     /// Shouldn't be exposed to normal user and the user itself. | ||||||
|     /// </summary> |     /// </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; |         badge.AccountId = account.Id; | ||||||
|         db.Badges.Add(badge); |         db.AccountBadges.Add(badge); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|         return badge; |         return badge; | ||||||
|     } |     } | ||||||
| @@ -586,9 +586,9 @@ public class AccountService( | |||||||
|     /// This method will revoke a badge from the account. |     /// This method will revoke a badge from the account. | ||||||
|     /// Shouldn't be exposed to normal user and the user itself. |     /// Shouldn't be exposed to normal user and the user itself. | ||||||
|     /// </summary> |     /// </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) |             .Where(b => b.AccountId == account.Id && b.Id == badgeId) | ||||||
|             .OrderByDescending(b => b.CreatedAt) |             .OrderByDescending(b => b.CreatedAt) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
| @@ -604,19 +604,19 @@ public class AccountService( | |||||||
|         await db.SaveChangesAsync(); |         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(); |         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||||
| 
 | 
 | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var badge = await db.Badges |             var badge = await db.AccountBadges | ||||||
|                 .Where(b => b.AccountId == account.Id && b.Id == badgeId) |                 .Where(b => b.AccountId == account.Id && b.Id == badgeId) | ||||||
|                 .OrderByDescending(b => b.CreatedAt) |                 .OrderByDescending(b => b.CreatedAt) | ||||||
|                 .FirstOrDefaultAsync(); |                 .FirstOrDefaultAsync(); | ||||||
|             if (badge is null) throw new InvalidOperationException("Badge was not found."); |             if (badge is null) throw new InvalidOperationException("Badge was not found."); | ||||||
| 
 | 
 | ||||||
|             await db.Badges |             await db.AccountBadges | ||||||
|                 .Where(b => b.AccountId == account.Id && b.Id != badgeId) |                 .Where(b => b.AccountId == account.Id && b.Id != badgeId) | ||||||
|                 .ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null)); |                 .ExecuteUpdateAsync(s => s.SetProperty(p => p.ActivatedAt, p => null)); | ||||||
| 
 | 
 | ||||||
| @@ -1,12 +1,14 @@ | |||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
|  | using DysonNetwork.Shared.Services; | ||||||
|  | using MagicOnion.Server; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// Service for handling username generation and validation | /// Service for handling username generation and validation | ||||||
| /// </summary> | /// </summary> | ||||||
| public class AccountUsernameService(AppDatabase db) | public class AccountUsernameService(AppDatabase db) : ServiceBase<IAccountUsernameService>, IAccountUsernameService | ||||||
| { | { | ||||||
|     private readonly Random _random = new(); |     private readonly Random _random = new(); | ||||||
| 
 | 
 | ||||||
| @@ -1,12 +1,24 @@ | |||||||
| using Quartz; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Storage; | using MagicOnion.Server; | ||||||
| using DysonNetwork.Sphere.Storage.Handlers; | 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) |     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) | ||||||
|     { |     { | ||||||
|         var log = new ActionLog |         var log = new ActionLog | ||||||
| @@ -16,11 +28,11 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | |||||||
|             Meta = meta, |             Meta = meta, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         fbs.Enqueue(log); |         // fbs.Enqueue(log); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, |     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, | ||||||
|         Account? account = null) |         Shared.Models.Account? account = null) | ||||||
|     { |     { | ||||||
|         var log = new ActionLog |         var log = new ActionLog | ||||||
|         { |         { | ||||||
| @@ -28,19 +40,19 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | |||||||
|             Meta = meta, |             Meta = meta, | ||||||
|             UserAgent = request.Headers.UserAgent, |             UserAgent = request.Headers.UserAgent, | ||||||
|             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), |             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|             Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) |             // Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||||
|         }; |         }; | ||||||
|          |          | ||||||
|         if (request.HttpContext.Items["CurrentUser"] is Account currentUser) |         if (request.HttpContext.Items["CurrentUser"] is Shared.Models.Account currentUser) | ||||||
|             log.AccountId = currentUser.Id; |             log.AccountId = currentUser.Id; | ||||||
|         else if (account != null) |         else if (account != null) | ||||||
|             log.AccountId = account.Id; |             log.AccountId = account.Id; | ||||||
|         else |         else | ||||||
|             throw new ArgumentException("No user context was found"); |             throw new ArgumentException("No user context was found"); | ||||||
|          |          | ||||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession) |         if (request.HttpContext.Items["CurrentSession"] is Session currentSession) | ||||||
|             log.SessionId = currentSession.Id; |             log.SessionId = currentSession.Id; | ||||||
| 
 | 
 | ||||||
|         fbs.Enqueue(log); |         // fbs.Enqueue(log); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/spells")] | [Route("/spells")] | ||||||
| @@ -1,27 +1,25 @@ | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Email; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Pages.Emails; | using DysonNetwork.Shared.Services; | ||||||
| using DysonNetwork.Sphere.Permission; | using MagicOnion.Server; | ||||||
| using DysonNetwork.Sphere.Resources.Localization; |  | ||||||
| using DysonNetwork.Sphere.Resources.Pages.Emails; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| public class MagicSpellService( | public class MagicSpellService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     EmailService email, |  | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     ILogger<MagicSpellService> logger, |     ILogger<MagicSpellService> logger | ||||||
|     IStringLocalizer<Localization.EmailResource> localizer | ) : ServiceBase<IMagicSpellService>, IMagicSpellService | ||||||
| ) |  | ||||||
| { | { | ||||||
|     public async Task<MagicSpell> CreateMagicSpell( |     public async Task<MagicSpell> CreateMagicSpell( | ||||||
|         Account account, |         Shared.Models.Account account, | ||||||
|         MagicSpellType type, |         MagicSpellType type, | ||||||
|         Dictionary<string, object> meta, |         Dictionary<string, object> meta, | ||||||
|         Instant? expiredAt = null, |         Instant? expiredAt = null, | ||||||
| @@ -61,6 +59,17 @@ public class MagicSpellService( | |||||||
|         return spell; |         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) |     public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false) | ||||||
|     { |     { | ||||||
|         var contact = await db.AccountContacts |         var contact = await db.AccountContacts | ||||||
| @@ -87,54 +96,54 @@ public class MagicSpellService( | |||||||
|             switch (spell.Type) |             switch (spell.Type) | ||||||
|             { |             { | ||||||
|                 case MagicSpellType.AccountActivation: |                 case MagicSpellType.AccountActivation: | ||||||
|                     await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>( |                     // await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>( | ||||||
|                         contact.Account.Nick, |                     //     contact.Account.Nick, | ||||||
|                         contact.Content, |                     //     contact.Content, | ||||||
|                         localizer["EmailLandingTitle"], |                     //     localizer["EmailLandingTitle"], | ||||||
|                         new LandingEmailModel |                     //     new LandingEmailModel | ||||||
|                         { |                     //     { | ||||||
|                             Name = contact.Account.Name, |                     //         Name = contact.Account.Name, | ||||||
|                             Link = link |                     //         Link = link | ||||||
|                         } |                     //     } | ||||||
|                     ); |                     // ); | ||||||
|                     break; |                     break; | ||||||
|                 case MagicSpellType.AccountRemoval: |                 case MagicSpellType.AccountRemoval: | ||||||
|                     await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>( |                     // await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>( | ||||||
|                         contact.Account.Nick, |                     //     contact.Account.Nick, | ||||||
|                         contact.Content, |                     //     contact.Content, | ||||||
|                         localizer["EmailAccountDeletionTitle"], |                     //     localizer["EmailAccountDeletionTitle"], | ||||||
|                         new AccountDeletionEmailModel |                     //     new AccountDeletionEmailModel | ||||||
|                         { |                     //     { | ||||||
|                             Name = contact.Account.Name, |                     //         Name = contact.Account.Name, | ||||||
|                             Link = link |                     //         Link = link | ||||||
|                         } |                     //     } | ||||||
|                     ); |                     // ); | ||||||
|                     break; |                     break; | ||||||
|                 case MagicSpellType.AuthPasswordReset: |                 case MagicSpellType.AuthPasswordReset: | ||||||
|                     await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>( |                     // await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>( | ||||||
|                         contact.Account.Nick, |                     //     contact.Account.Nick, | ||||||
|                         contact.Content, |                     //     contact.Content, | ||||||
|                         localizer["EmailAccountDeletionTitle"], |                     //     localizer["EmailAccountDeletionTitle"], | ||||||
|                         new PasswordResetEmailModel |                     //     new PasswordResetEmailModel | ||||||
|                         { |                     //     { | ||||||
|                             Name = contact.Account.Name, |                     //         Name = contact.Account.Name, | ||||||
|                             Link = link |                     //         Link = link | ||||||
|                         } |                     //     } | ||||||
|                     ); |                     // ); | ||||||
|                     break; |                     break; | ||||||
|                 case MagicSpellType.ContactVerification: |                 case MagicSpellType.ContactVerification: | ||||||
|                     if (spell.Meta["contact_method"] is not string contactMethod) |                     if (spell.Meta["contact_method"] is not string contactMethod) | ||||||
|                         throw new InvalidOperationException("Contact method is not found."); |                         throw new InvalidOperationException("Contact method is not found."); | ||||||
|                     await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>( |                     // await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>( | ||||||
|                         contact.Account.Nick, |                     //     contact.Account.Nick, | ||||||
|                         contactMethod!, |                     //     contactMethod!, | ||||||
|                         localizer["EmailContactVerificationTitle"], |                     //     localizer["EmailContactVerificationTitle"], | ||||||
|                         new ContactVerificationEmailModel |                     //     new ContactVerificationEmailModel | ||||||
|                         { |                     //     { | ||||||
|                             Name = contact.Account.Name, |                     //         Name = contact.Account.Name, | ||||||
|                             Link = link |                     //         Link = link | ||||||
|                         } |                     //     } | ||||||
|                     ); |                     // ); | ||||||
|                     break; |                     break; | ||||||
|                 default: |                 default: | ||||||
|                     throw new ArgumentOutOfRangeException(); |                     throw new ArgumentOutOfRangeException(); | ||||||
| @@ -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) |         switch (spell.Type) | ||||||
|         { |         { | ||||||
|             case MagicSpellType.AuthPasswordReset: |             case MagicSpellType.AuthPasswordReset: | ||||||
| @@ -1,12 +1,14 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Sphere.Auth; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Pass.Auth; | ||||||
|  | using DysonNetwork.Pass.Permission; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/notifications")] | [Route("/notifications")] | ||||||
| @@ -17,7 +19,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | |||||||
|     public async Task<ActionResult<int>> CountUnreadNotifications() |     public async Task<ActionResult<int>> CountUnreadNotifications() | ||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         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 |         var count = await db.Notifications | ||||||
|             .Where(s => s.AccountId == currentUser.Id && s.ViewedAt == null) |             .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); |         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 |         var totalCount = await db.Notifications | ||||||
|             .Where(s => s.AccountId == currentUser.Id) |             .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("CurrentSession", out var currentSessionValue); | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Account; |         var currentUser = currentUserValue as Shared.Models.Account; | ||||||
|         if (currentUser == null) return Unauthorized(); |         if (currentUser == null) return Unauthorized(); | ||||||
|         var currentSession = currentSessionValue as Session; |         var currentSession = currentSessionValue as Session; | ||||||
|         if (currentSession == null) return Unauthorized(); |         if (currentSession == null) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var result = |  | ||||||
|         await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!, |         await nty.SubscribePushNotification(currentUser, request.Provider, currentSession.Challenge.DeviceId!, | ||||||
|             request.DeviceToken); |             request.DeviceToken); | ||||||
| 
 | 
 | ||||||
|         return Ok(result); |         return Ok(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpDelete("subscription")] |     [HttpDelete("subscription")] | ||||||
| @@ -85,7 +86,7 @@ public class NotificationController(AppDatabase db, NotificationService nty) : C | |||||||
|     { |     { | ||||||
|         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); |         HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue); | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); | ||||||
|         var currentUser = currentUserValue as Account; |         var currentUser = currentUserValue as Shared.Models.Account; | ||||||
|         if (currentUser == null) return Unauthorized(); |         if (currentUser == null) return Unauthorized(); | ||||||
|         var currentSession = currentSessionValue as Session; |         var currentSession = currentSessionValue as Session; | ||||||
|         if (currentSession == null) return Unauthorized(); |         if (currentSession == null) return Unauthorized(); | ||||||
| @@ -1,17 +1,20 @@ | |||||||
| using System.Text; | using System.Text; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Shared.Models; | ||||||
| using EFCore.BulkExtensions; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; | 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( | public class NotificationService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     WebSocketService ws, |     IConfiguration config, | ||||||
|     IHttpClientFactory httpFactory, |     IHttpClientFactory httpFactory | ||||||
|     IConfiguration config) | ) : ServiceBase<INotificationService>, INotificationService | ||||||
| { | { | ||||||
|     private readonly string _notifyTopic = config["Notifications:Topic"]!; |     private readonly string _notifyTopic = config["Notifications:Topic"]!; | ||||||
|     private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); |     private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); | ||||||
| @@ -24,7 +27,7 @@ public class NotificationService( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<NotificationPushSubscription> SubscribePushNotification( |     public async Task<NotificationPushSubscription> SubscribePushNotification( | ||||||
|         Account account, |         Shared.Models.Account account, | ||||||
|         NotificationPushProvider provider, |         NotificationPushProvider provider, | ||||||
|         string deviceId, |         string deviceId, | ||||||
|         string deviceToken |         string deviceToken | ||||||
| @@ -63,14 +66,14 @@ public class NotificationService( | |||||||
|             AccountId = account.Id, |             AccountId = account.Id, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         db.NotificationPushSubscriptions.Add(subscription); |         // db.NotificationPushSubscriptions.Add(subscription); | ||||||
|         await db.SaveChangesAsync(); |         // await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         return subscription; |         return subscription; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<Notification> SendNotification( |     public async Task<Notification> SendNotification( | ||||||
|         Account account, |         Shared.Models.Account account, | ||||||
|         string topic, |         string topic, | ||||||
|         string? title = null, |         string? title = null, | ||||||
|         string? subtitle = null, |         string? subtitle = null, | ||||||
| @@ -103,18 +106,19 @@ public class NotificationService( | |||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!isSilent) _ = DeliveryNotification(notification); |         if (!isSilent) | ||||||
|  |             Console.WriteLine("Simulating notification delivery."); // _ = DeliveryNotification(notification); | ||||||
| 
 | 
 | ||||||
|         return notification; |         return notification; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task DeliveryNotification(Notification notification) |     public async Task DeliveryNotification(Notification notification) | ||||||
|     { |     { | ||||||
|         ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket |         // ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket | ||||||
|         { |         // { | ||||||
|             Type = "notifications.new", |         //     Type = "notifications.new", | ||||||
|             Data = notification |         //     Data = notification | ||||||
|         }); |         // }); | ||||||
| 
 | 
 | ||||||
|         // Pushing the notification |         // Pushing the notification | ||||||
|         var subscribers = await db.NotificationPushSubscriptions |         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(); |         var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); | ||||||
|         if (id.Count == 0) return; |         if (id.Count == 0) return; | ||||||
| 
 | 
 | ||||||
|         await db.Notifications |         // await db.Notifications | ||||||
|             .Where(n => id.Contains(n.Id)) |         //     .Where(n => id.Contains(n.Id)) | ||||||
|             .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) |         //     .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) | ||||||
|             ); |         //     ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task BroadcastNotification(Notification notification, bool save = false) |     public async Task BroadcastNotification(Notification notification, bool save = false) | ||||||
|     { |     { | ||||||
|         var accounts = await db.Accounts.ToListAsync(); |         var accounts = new List<Shared.Models.Account>(); // await db.Accounts.ToListAsync(); | ||||||
| 
 | 
 | ||||||
|         if (save) |         if (save) | ||||||
|         { |         { | ||||||
| @@ -157,18 +161,18 @@ public class NotificationService( | |||||||
|                 }; |                 }; | ||||||
|                 return newNotification; |                 return newNotification; | ||||||
|             }).ToList(); |             }).ToList(); | ||||||
|             await db.BulkInsertAsync(notifications); |             // await db.BulkInsertAsync(notifications); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         foreach (var account in accounts) |         foreach (var account in accounts) | ||||||
|         { |         { | ||||||
|             notification.Account = account; |             notification.Account = account; | ||||||
|             notification.AccountId = account.Id; |             notification.AccountId = account.Id; | ||||||
|             ws.SendPacketToAccount(account.Id, new WebSocketPacket |             // ws.SendPacketToAccount(account.Id, new WebSocketPacket | ||||||
|             { |             // { | ||||||
|                 Type = "notifications.new", |             //     Type = "notifications.new", | ||||||
|                 Data = notification |             //     Data = notification | ||||||
|             }); |             // }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var subscribers = await db.NotificationPushSubscriptions |         var subscribers = await db.NotificationPushSubscriptions | ||||||
| @@ -176,7 +180,8 @@ public class NotificationService( | |||||||
|         await _PushNotification(notification, subscribers); |         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) |         if (save) | ||||||
|         { |         { | ||||||
| @@ -202,18 +207,18 @@ public class NotificationService( | |||||||
|         { |         { | ||||||
|             notification.Account = account; |             notification.Account = account; | ||||||
|             notification.AccountId = account.Id; |             notification.AccountId = account.Id; | ||||||
|             ws.SendPacketToAccount(account.Id, new WebSocketPacket |             // ws.SendPacketToAccount(account.Id, new WebSocketPacket | ||||||
|             { |             // { | ||||||
|                 Type = "notifications.new", |             //     Type = "notifications.new", | ||||||
|                 Data = notification |             //     Data = notification | ||||||
|             }); |             // }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         var accountsId = accounts.Select(x => x.Id).ToList(); |         // var accountsId = accounts.Select(x => x.Id).ToList(); | ||||||
|         var subscribers = await db.NotificationPushSubscriptions |         // var subscribers = await db.NotificationPushSubscriptions | ||||||
|             .Where(s => accountsId.Contains(s.AccountId)) |         //     .Where(s => accountsId.Contains(s.AccountId)) | ||||||
|             .ToListAsync(); |         //     .ToListAsync(); | ||||||
|         await _PushNotification(notification, subscribers); |         // await _PushNotification(notification, subscribers); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification, |     private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification, | ||||||
| @@ -1,10 +1,12 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/relationships")] | [Route("/relationships")] | ||||||
| @@ -15,7 +17,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0, |     public async Task<ActionResult<List<Relationship>>> ListRelationships([FromQuery] int offset = 0, | ||||||
|         [FromQuery] int take = 20) |         [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 userId = currentUser.Id; | ||||||
| 
 | 
 | ||||||
|         var query = db.AccountRelationships.AsQueryable() |         var query = db.AccountRelationships.AsQueryable() | ||||||
| @@ -46,7 +48,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<Relationship>>> ListSentRequests() |     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 |         var relationships = await db.AccountRelationships | ||||||
|             .Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending) |             .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, |     public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId, | ||||||
|         [FromBody] RelationshipRequest request) |         [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); |         var relatedUser = await db.Accounts.FindAsync(userId); | ||||||
|         if (relatedUser is null) return NotFound("Account was not found."); |         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, |     public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId, | ||||||
|         [FromBody] RelationshipRequest request) |         [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 |         try | ||||||
|         { |         { | ||||||
| @@ -113,7 +115,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> GetRelationship(Guid userId) |     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 now = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||||
|         var queries = db.AccountRelationships.AsQueryable() |         var queries = db.AccountRelationships.AsQueryable() | ||||||
| @@ -133,7 +135,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId) |     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); |         var relatedUser = await db.Accounts.FindAsync(userId); | ||||||
|         if (relatedUser is null) return NotFound("Account was not found."); |         if (relatedUser is null) return NotFound("Account was not found."); | ||||||
| @@ -158,7 +160,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> DeleteFriendRequest(Guid userId) |     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 |         try | ||||||
|         { |         { | ||||||
| @@ -175,7 +177,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId) |     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); |         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||||
|         if (relationship is null) return NotFound("Friend request was not found."); |         if (relationship is null) return NotFound("Friend request was not found."); | ||||||
| @@ -195,7 +197,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId) |     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); |         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||||
|         if (relationship is null) return NotFound("Friend request was not found."); |         if (relationship is null) return NotFound("Friend request was not found."); | ||||||
| @@ -215,7 +217,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> BlockUser(Guid userId) |     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); |         var relatedUser = await db.Accounts.FindAsync(userId); | ||||||
|         if (relatedUser is null) return NotFound("Account was not found."); |         if (relatedUser is null) return NotFound("Account was not found."); | ||||||
| @@ -235,7 +237,7 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Relationship>> UnblockUser(Guid userId) |     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); |         var relatedUser = await db.Accounts.FindAsync(userId); | ||||||
|         if (relatedUser is null) return NotFound("Account was not found."); |         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 Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Pass.Account; | ||||||
| 
 | 
 | ||||||
| public class RelationshipService(AppDatabase db, ICacheService cache) | public class RelationshipService(AppDatabase db, ICacheService cache) : ServiceBase<IRelationshipService>, IRelationshipService | ||||||
| { | { | ||||||
|     private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; |     private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; | ||||||
|     private const string UserBlockedCacheKeyPrefix = "accounts:blocked:"; |     private const string UserBlockedCacheKeyPrefix = "accounts:blocked:"; | ||||||
| @@ -34,7 +37,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | |||||||
|         return relationship; |         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) |         if (status == RelationshipStatus.Pending) | ||||||
|             throw new InvalidOperationException( |             throw new InvalidOperationException( | ||||||
| @@ -52,31 +55,31 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | |||||||
|         db.AccountRelationships.Add(relationship); |         db.AccountRelationships.Add(relationship); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|          |          | ||||||
|         await PurgeRelationshipCache(sender.Id, target.Id); |         // await PurgeRelationshipCache(sender.Id, target.Id); | ||||||
| 
 | 
 | ||||||
|         return relationship; |         return relationship; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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)) |         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||||
|             return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); |             return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); | ||||||
|         return await CreateRelationship(sender, target, 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); |         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."); |         if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); | ||||||
|         db.Remove(relationship); |         db.Remove(relationship); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|          |          | ||||||
|         await PurgeRelationshipCache(sender.Id, target.Id); |         // await PurgeRelationshipCache(sender.Id, target.Id); | ||||||
|          |          | ||||||
|         return relationship; |         return relationship; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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)) |         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||||
|             throw new InvalidOperationException("Found existing relationship between you and target user."); |             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) |             .Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending) | ||||||
|             .ExecuteDeleteAsync(); |             .ExecuteDeleteAsync(); | ||||||
|          |          | ||||||
|         await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); |         // await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public async Task<Relationship> AcceptFriendRelationship( |     public async Task<Relationship> AcceptFriendRelationship( | ||||||
| @@ -133,7 +136,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | |||||||
| 
 | 
 | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); |         // await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); | ||||||
| 
 | 
 | ||||||
|         return relationshipBackward; |         return relationshipBackward; | ||||||
|     } |     } | ||||||
| @@ -152,7 +155,7 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | |||||||
|         return relationship; |         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 cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; | ||||||
|         var friends = await cache.GetAsync<List<Guid>>(cacheKey); |         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)); |             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 cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}"; | ||||||
|         var blocked = await cache.GetAsync<List<Guid>>(cacheKey); |         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)); |             await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return blocked ?? []; |         return blocked; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, |     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.Claims; | ||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text.Encodings.Web; | using System.Text.Encodings.Web; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using DysonNetwork.Sphere.Storage.Handlers; |  | ||||||
| using Microsoft.AspNetCore.Authentication; | using Microsoft.AspNetCore.Authentication; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Options; | using Microsoft.Extensions.Options; | ||||||
| using Microsoft.IdentityModel.Tokens; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using System.Text; | using Microsoft.AspNetCore.Http; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Controllers; | using Microsoft.Extensions.Configuration; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Services; | using Microsoft.Extensions.Logging; | ||||||
| using SystemClock = NodaTime.SystemClock; | using SystemClock = Microsoft.Extensions.Internal.SystemClock; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Pass.Auth; | ||||||
| 
 | 
 | ||||||
| public static class AuthConstants | public static class AuthConstants | ||||||
| { | { | ||||||
| @@ -46,12 +40,11 @@ public class DysonTokenAuthHandler( | |||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     ILoggerFactory logger, |     ILoggerFactory logger, | ||||||
|     UrlEncoder encoder, |     UrlEncoder encoder, | ||||||
|     AppDatabase database, |     AppDatabase database | ||||||
|     OidcProviderService oidc, |     // OidcProviderService oidc, | ||||||
|     ICacheService cache, |     // ICacheService cache, | ||||||
|     FlushBufferService fbs |     // FlushBufferService fbs | ||||||
| ) | ) : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder) | ||||||
|     : AuthenticationHandler<DysonTokenAuthOptions>(options, logger, encoder) |  | ||||||
| { | { | ||||||
|     public const string AuthCachePrefix = "auth:"; |     public const string AuthCachePrefix = "auth:"; | ||||||
| 
 | 
 | ||||||
| @@ -64,36 +57,42 @@ public class DysonTokenAuthHandler( | |||||||
| 
 | 
 | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             var now = SystemClock.Instance.GetCurrentInstant(); |             var now = NodaTime.SystemClock.Instance.GetCurrentInstant(); | ||||||
| 
 | 
 | ||||||
|             // Validate token and extract session ID |             // Validate token and extract session ID | ||||||
|             if (!ValidateToken(tokenInfo.Token, out var sessionId)) |             if (!ValidateToken(tokenInfo.Token, out var sessionId)) | ||||||
|                 return AuthenticateResult.Fail("Invalid token."); |                 return AuthenticateResult.Fail("Invalid token."); | ||||||
| 
 | 
 | ||||||
|             // Try to get session from cache first |             // Try to get session from cache first | ||||||
|             var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}"); |             // var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}"); | ||||||
| 
 |             var session = await database.AuthSessions | ||||||
|             // If not in cache, load from database |  | ||||||
|             if (session is null) |  | ||||||
|             { |  | ||||||
|                 session = await database.AuthSessions |  | ||||||
|                     .Where(e => e.Id == sessionId) |                     .Where(e => e.Id == sessionId) | ||||||
|                     .Include(e => e.Challenge) |                     .Include(e => e.Challenge) | ||||||
|                     .Include(e => e.Account) |                     .Include(e => e.Account) | ||||||
|                     .ThenInclude(e => e.Profile) |                     .ThenInclude(e => e.Profile) | ||||||
|                     .FirstOrDefaultAsync(); |                     .FirstOrDefaultAsync(); | ||||||
| 
 | 
 | ||||||
|                 if (session is not null) |             // If not in cache, load from database | ||||||
|                 { |             // if (session is null) | ||||||
|                     // Store in cache for future requests |             // { | ||||||
|                     await cache.SetWithGroupsAsync( |             //     session = await database.AuthSessions | ||||||
|                         $"auth:{sessionId}", |             //         .Where(e => e.Id == sessionId) | ||||||
|                         session, |             //         .Include(e => e.Challenge) | ||||||
|                         [$"{AccountService.AccountCachePrefix}{session.Account.Id}"], |             //         .Include(e => e.Account) | ||||||
|                         TimeSpan.FromHours(1) |             //         .ThenInclude(e => e.Profile) | ||||||
|                     ); |             //         .FirstOrDefaultAsync(); | ||||||
|                 } | 
 | ||||||
|             } |             //     if (session is not null) | ||||||
|  |             //     { | ||||||
|  |             //         // Store in cache for future requests | ||||||
|  |             //         await cache.SetWithGroupsAsync( | ||||||
|  |             //             $"auth:{sessionId}", | ||||||
|  |             //             session, | ||||||
|  |             //             // [$"{AccountService.AccountCachePrefix}{session.Account.Id}"], | ||||||
|  |             //             TimeSpan.FromHours(1) | ||||||
|  |             //         ); | ||||||
|  |             //     } | ||||||
|  |             // } | ||||||
| 
 | 
 | ||||||
|             // Check if the session exists |             // Check if the session exists | ||||||
|             if (session == null) |             if (session == null) | ||||||
| @@ -129,13 +128,13 @@ public class DysonTokenAuthHandler( | |||||||
| 
 | 
 | ||||||
|             var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName); |             var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName); | ||||||
| 
 | 
 | ||||||
|             var lastInfo = new LastActiveInfo |             // var lastInfo = new LastActiveInfo | ||||||
|             { |             // { | ||||||
|                 Account = session.Account, |             //     Account = session.Account, | ||||||
|                 Session = session, |             //     Session = session, | ||||||
|                 SeenAt = SystemClock.Instance.GetCurrentInstant(), |             //     SeenAt = SystemClock.Instance.GetCurrentInstant(), | ||||||
|             }; |             // }; | ||||||
|             fbs.Enqueue(lastInfo); |             // fbs.Enqueue(lastInfo); | ||||||
| 
 | 
 | ||||||
|             return AuthenticateResult.Success(ticket); |             return AuthenticateResult.Success(ticket); | ||||||
|         } |         } | ||||||
| @@ -158,12 +157,13 @@ public class DysonTokenAuthHandler( | |||||||
|                 // Handle JWT tokens (3 parts) |                 // Handle JWT tokens (3 parts) | ||||||
|                 case 3: |                 case 3: | ||||||
|                 { |                 { | ||||||
|                     var (isValid, jwtResult) = oidc.ValidateToken(token); |                     // var (isValid, jwtResult) = oidc.ValidateToken(token); | ||||||
|                     if (!isValid) return false; |                     // if (!isValid) return false; | ||||||
|                     var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value; |                     // var jti = jwtResult?.Claims.FirstOrDefault(c => c.Type == "jti")?.Value; | ||||||
|                     if (jti is null) return false; |                     // if (jti is null) return false; | ||||||
| 
 | 
 | ||||||
|                     return Guid.TryParse(jti, out sessionId); |                     // return Guid.TryParse(jti, out sessionId); | ||||||
|  |                     return false; // Placeholder | ||||||
|                 } |                 } | ||||||
|                 // Handle compact tokens (2 parts) |                 // Handle compact tokens (2 parts) | ||||||
|                 case 2: |                 case 2: | ||||||
| @@ -1,23 +1,23 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
|  | using DysonNetwork.Pass.Account; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using NodaTime; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using System.IdentityModel.Tokens.Jwt; | using NodaTime; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Connection; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Pass.Auth; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/auth")] | [Route("/auth")] | ||||||
| public class AuthController( | public class AuthController( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     AccountService accounts, |     AccountService accounts, | ||||||
|     AuthService auth, |     AuthService auth | ||||||
|     GeoIpService geo, |     // GeoIpService geo, | ||||||
|     ActionLogService als |     // ActionLogService als | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
| { | { | ||||||
|     public class ChallengeRequest |     public class ChallengeRequest | ||||||
| @@ -59,7 +59,7 @@ public class AuthController( | |||||||
|             Scopes = request.Scopes, |             Scopes = request.Scopes, | ||||||
|             IpAddress = ipAddress, |             IpAddress = ipAddress, | ||||||
|             UserAgent = userAgent, |             UserAgent = userAgent, | ||||||
|             Location = geo.GetPointFromIp(ipAddress), |             // Location = geo.GetPointFromIp(ipAddress), | ||||||
|             DeviceId = request.DeviceId, |             DeviceId = request.DeviceId, | ||||||
|             AccountId = account.Id |             AccountId = account.Id | ||||||
|         }.Normalize(); |         }.Normalize(); | ||||||
| @@ -67,9 +67,9 @@ public class AuthController( | |||||||
|         await db.AuthChallenges.AddAsync(challenge); |         await db.AuthChallenges.AddAsync(challenge); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| 
 | 
 | ||||||
|         als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt, |         // als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt, | ||||||
|             new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account |         //     new Dictionary<string, object> { { "challenge_id", challenge.Id } }, Request, account | ||||||
|         ); |         // ); | ||||||
| 
 | 
 | ||||||
|         return challenge; |         return challenge; | ||||||
|     } |     } | ||||||
| @@ -160,13 +160,13 @@ public class AuthController( | |||||||
|                 challenge.StepRemain = Math.Max(0, challenge.StepRemain); |                 challenge.StepRemain = Math.Max(0, challenge.StepRemain); | ||||||
|                 challenge.BlacklistFactors.Add(factor.Id); |                 challenge.BlacklistFactors.Add(factor.Id); | ||||||
|                 db.Update(challenge); |                 db.Update(challenge); | ||||||
|                 als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, |                 // als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, | ||||||
|                     new Dictionary<string, object> |                 //     new Dictionary<string, object> | ||||||
|                     { |                 //     { | ||||||
|                         { "challenge_id", challenge.Id }, |                 //         { "challenge_id", challenge.Id }, | ||||||
|                         { "factor_id", factor.Id } |                 //         { "factor_id", factor.Id } | ||||||
|                     }, Request, challenge.Account |                 //     }, Request, challenge.Account | ||||||
|                 ); |                 // ); | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
| @@ -177,26 +177,26 @@ public class AuthController( | |||||||
|         { |         { | ||||||
|             challenge.FailedAttempts++; |             challenge.FailedAttempts++; | ||||||
|             db.Update(challenge); |             db.Update(challenge); | ||||||
|             als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, |             // als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, | ||||||
|                 new Dictionary<string, object> |             //     new Dictionary<string, object> | ||||||
|                 { |             //     { | ||||||
|                     { "challenge_id", challenge.Id }, |             //         { "challenge_id", challenge.Id }, | ||||||
|                     { "factor_id", factor.Id } |             //         { "factor_id", factor.Id } | ||||||
|                 }, Request, challenge.Account |             //     }, Request, challenge.Account | ||||||
|             ); |             // ); | ||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
|             return BadRequest("Invalid password."); |             return BadRequest("Invalid password."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (challenge.StepRemain == 0) |         if (challenge.StepRemain == 0) | ||||||
|         { |         { | ||||||
|             als.CreateActionLogFromRequest(ActionLogType.NewLogin, |             // als.CreateActionLogFromRequest(ActionLogType.NewLogin, | ||||||
|                 new Dictionary<string, object> |             //     new Dictionary<string, object> | ||||||
|                 { |             //     { | ||||||
|                     { "challenge_id", challenge.Id }, |             //         { "challenge_id", challenge.Id }, | ||||||
|                     { "account_id", challenge.AccountId } |             //         { "account_id", challenge.AccountId } | ||||||
|                 }, Request, challenge.Account |             //     }, Request, challenge.Account | ||||||
|             ); |             // ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
| @@ -1,21 +1,23 @@ | |||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
|  | using System.IO; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Pass.Auth; | ||||||
| 
 | 
 | ||||||
| public class AuthService( | public class AuthService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     IConfiguration config, |     IConfiguration config | ||||||
|     IHttpClientFactory httpClientFactory, |     // IHttpClientFactory httpClientFactory, | ||||||
|     IHttpContextAccessor httpContextAccessor, |     // IHttpContextAccessor httpContextAccessor, | ||||||
|     ICacheService cache |     // ICacheService cache | ||||||
| ) | ) | ||||||
| { | { | ||||||
|     private HttpContext HttpContext => httpContextAccessor.HttpContext!; |     // private HttpContext HttpContext => httpContextAccessor.HttpContext!; | ||||||
| 
 | 
 | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Detect the risk of the current request to login |     /// Detect the risk of the current request to login | ||||||
| @@ -24,7 +26,7 @@ public class AuthService( | |||||||
|     /// <param name="request">The request context</param> |     /// <param name="request">The request context</param> | ||||||
|     /// <param name="account">The account to login</param> |     /// <param name="account">The account to login</param> | ||||||
|     /// <returns>The required steps to login</returns> |     /// <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. |         // 1) Find out how many authentication factors the account has enabled. | ||||||
|         var maxSteps = await db.AccountAuthFactors |         var maxSteps = await db.AccountAuthFactors | ||||||
| @@ -73,13 +75,13 @@ public class AuthService( | |||||||
|         return totalRequiredSteps; |         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 |         var challenge = new Challenge | ||||||
|         { |         { | ||||||
|             AccountId = account.Id, |             AccountId = account.Id, | ||||||
|             IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), |             IpAddress = "127.0.0.1", // HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||||
|             UserAgent = HttpContext.Request.Headers.UserAgent, |             UserAgent = "TestAgent", // HttpContext.Request.Headers.UserAgent, | ||||||
|             StepRemain = 1, |             StepRemain = 1, | ||||||
|             StepTotal = 1, |             StepTotal = 1, | ||||||
|             Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc |             Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc | ||||||
| @@ -105,53 +107,54 @@ public class AuthService( | |||||||
|     { |     { | ||||||
|         if (string.IsNullOrWhiteSpace(token)) return false; |         if (string.IsNullOrWhiteSpace(token)) return false; | ||||||
| 
 | 
 | ||||||
|         var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); |         // var provider = config.GetSection("Captcha")["Provider"]?.ToLower(); | ||||||
|         var apiSecret = config.GetSection("Captcha")["ApiSecret"]; |         // var apiSecret = config.GetSection("Captcha")["ApiSecret"]; | ||||||
| 
 | 
 | ||||||
|         var client = httpClientFactory.CreateClient(); |         // var client = httpClientFactory.CreateClient(); | ||||||
| 
 | 
 | ||||||
|         var jsonOpts = new JsonSerializerOptions |         // var jsonOpts = new JsonSerializerOptions | ||||||
|         { |         // { | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |         //     PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower |         //     DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||||
|         }; |         // }; | ||||||
| 
 | 
 | ||||||
|         switch (provider) |         // switch (provider) | ||||||
|         { |         // { | ||||||
|             case "cloudflare": |         //     case "cloudflare": | ||||||
|                 var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |         //         var content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|                     "application/x-www-form-urlencoded"); |         //             "application/x-www-form-urlencoded"); | ||||||
|                 var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", |         //         var response = await client.PostAsync("https://challenges.cloudflare.com/turnstile/v0/siteverify", | ||||||
|                     content); |         //             content); | ||||||
|                 response.EnsureSuccessStatusCode(); |         //         response.EnsureSuccessStatusCode(); | ||||||
| 
 | 
 | ||||||
|                 var json = await response.Content.ReadAsStringAsync(); |         //         var json = await response.Content.ReadAsStringAsync(); | ||||||
|                 var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |         //         var result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
| 
 | 
 | ||||||
|                 return result?.Success == true; |         //         return result?.Success == true; | ||||||
|             case "google": |         //     case "google": | ||||||
|                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|                     "application/x-www-form-urlencoded"); |         //             "application/x-www-form-urlencoded"); | ||||||
|                 response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); |         //         response = await client.PostAsync("https://www.google.com/recaptcha/siteverify", content); | ||||||
|                 response.EnsureSuccessStatusCode(); |         //         response.EnsureSuccessStatusCode(); | ||||||
| 
 | 
 | ||||||
|                 json = await response.Content.ReadAsStringAsync(); |         //         json = await response.Content.ReadAsStringAsync(); | ||||||
|                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
| 
 | 
 | ||||||
|                 return result?.Success == true; |         //         return result?.Success == true; | ||||||
|             case "hcaptcha": |         //     case "hcaptcha": | ||||||
|                 content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, |         //         content = new StringContent($"secret={apiSecret}&response={token}", System.Text.Encoding.UTF8, | ||||||
|                     "application/x-www-form-urlencoded"); |         //             "application/x-www-form-urlencoded"); | ||||||
|                 response = await client.PostAsync("https://hcaptcha.com/siteverify", content); |         //         response = await client.PostAsync("https://hcaptcha.com/siteverify", content); | ||||||
|                 response.EnsureSuccessStatusCode(); |         //         response.EnsureSuccessStatusCode(); | ||||||
| 
 | 
 | ||||||
|                 json = await response.Content.ReadAsStringAsync(); |         //         json = await response.Content.ReadAsStringAsync(); | ||||||
|                 result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); |         //         result = JsonSerializer.Deserialize<CaptchaVerificationResponse>(json, options: jsonOpts); | ||||||
| 
 | 
 | ||||||
|                 return result?.Success == true; |         //         return result?.Success == true; | ||||||
|             default: |         //     default: | ||||||
|                 throw new ArgumentException("The server misconfigured for the captcha."); |         //         throw new ArgumentException("The server misconfigured for the captcha."); | ||||||
|         } |         // } | ||||||
|  |         return true; // Placeholder for captcha validation | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public string CreateToken(Session session) |     public string CreateToken(Session session) | ||||||
| @@ -183,56 +186,56 @@ public class AuthService( | |||||||
|         return $"{payloadBase64}.{signatureBase64}"; |         return $"{payloadBase64}.{signatureBase64}"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<bool> ValidateSudoMode(Session session, string? pinCode) |     // public async Task<bool> ValidateSudoMode(Session session, string? pinCode) | ||||||
|     { |     // { | ||||||
|         // Check if the session is already in sudo mode (cached) |     //     // Check if the session is already in sudo mode (cached) | ||||||
|         var sudoModeKey = $"accounts:{session.Id}:sudo"; |     //     var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||||
|         var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey); |     //     var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey); | ||||||
|          |          | ||||||
|         if (found) |     //     if (found) | ||||||
|         { |     //     { | ||||||
|             // Session is already in sudo mode |     //         // Session is already in sudo mode | ||||||
|             return true; |     //         return true; | ||||||
|         } |     //     } | ||||||
|          |          | ||||||
|         // Check if the user has a pin code |     //     // Check if the user has a pin code | ||||||
|         var hasPinCode = await db.AccountAuthFactors |     //     var hasPinCode = await db.AccountAuthFactors | ||||||
|             .Where(f => f.AccountId == session.AccountId) |     //         .Where(f => f.AccountId == session.AccountId) | ||||||
|             .Where(f => f.EnabledAt != null) |     //         .Where(f => f.EnabledAt != null) | ||||||
|             .Where(f => f.Type == AccountAuthFactorType.PinCode) |     //         .Where(f => f.Type == AccountAuthFactorType.PinCode) | ||||||
|             .AnyAsync(); |     //         .AnyAsync(); | ||||||
|              |              | ||||||
|         if (!hasPinCode) |     //     if (!hasPinCode) | ||||||
|         { |     //     { | ||||||
|             // User doesn't have a pin code, no validation needed |     //         // User doesn't have a pin code, no validation needed | ||||||
|             return true; |     //         return true; | ||||||
|         } |     //     } | ||||||
|          |          | ||||||
|         // If pin code is not provided, we can't validate |     //     // If pin code is not provided, we can't validate | ||||||
|         if (string.IsNullOrEmpty(pinCode)) |     //     if (string.IsNullOrEmpty(pinCode)) | ||||||
|         { |     //     { | ||||||
|             return false; |     //         return false; | ||||||
|         } |     //     } | ||||||
|          |          | ||||||
|         try |     //     try | ||||||
|         { |     //     { | ||||||
|             // Validate the pin code |     //         // Validate the pin code | ||||||
|             var isValid = await ValidatePinCode(session.AccountId, pinCode); |     //         var isValid = await ValidatePinCode(session.AccountId, pinCode); | ||||||
|              |              | ||||||
|             if (isValid) |     //         if (isValid) | ||||||
|             { |     //         { | ||||||
|                 // Set session in sudo mode for 5 minutes |     //             // Set session in sudo mode for 5 minutes | ||||||
|                 await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5)); |     //             await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5)); | ||||||
|             } |     //         } | ||||||
|              |              | ||||||
|             return isValid; |     //         return isValid; | ||||||
|         } |     //     } | ||||||
|         catch (InvalidOperationException) |     //     catch (InvalidOperationException) | ||||||
|         { |     //     { | ||||||
|             // No pin code enabled for this account, so validation is successful |     //         // No pin code enabled for this account, so validation is successful | ||||||
|             return true; |     //         return true; | ||||||
|         } |     //     } | ||||||
|     } |     // } | ||||||
| 
 | 
 | ||||||
|     public async Task<bool> ValidatePinCode(Guid accountId, string pinCode) |     public async Task<bool> ValidatePinCode(Guid accountId, string pinCode) | ||||||
|     { |     { | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Pass.Auth; | ||||||
| 
 | 
 | ||||||
| public class CaptchaVerificationResponse | public class CaptchaVerificationResponse | ||||||
| { | { | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| using System.Security.Cryptography; | 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) | public class CompactTokenService(IConfiguration config) | ||||||
| { | { | ||||||
| @@ -1,18 +1,19 @@ | |||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; | using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Responses; | using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Services; | using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.Extensions.Options; |  | ||||||
| using System.Text.Json.Serialization; |  | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
|  | using Microsoft.Extensions.Options; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; | namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers; | ||||||
| 
 | 
 | ||||||
| [Route("/auth/open")] | [Route("/auth/open")] | ||||||
| [ApiController] | [ApiController] | ||||||
| @@ -114,7 +115,7 @@ public class OidcProviderController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<IActionResult> GetUserInfo() |     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(); |             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         // Get requested scopes from the token |         // Get requested scopes from the token | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Models; | namespace DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||||
| 
 | 
 | ||||||
| public class AuthorizationCodeInfo | public class AuthorizationCodeInfo | ||||||
| { | { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Options; | namespace DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||||
| 
 | 
 | ||||||
| public class OidcProviderOptions | public class OidcProviderOptions | ||||||
| { | { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
| 
 | 
 | ||||||
| public class AuthorizationResponse | public class AuthorizationResponse | ||||||
| { | { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
| 
 | 
 | ||||||
| public class ErrorResponse | public class ErrorResponse | ||||||
| { | { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
| 
 | 
 | ||||||
| public class TokenResponse | public class TokenResponse | ||||||
| { | { | ||||||
| @@ -2,17 +2,18 @@ using System.IdentityModel.Tokens.Jwt; | |||||||
| using System.Security.Claims; | using System.Security.Claims; | ||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text; | using System.Text; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Models; | using DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; | using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Responses; | using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||||
| using DysonNetwork.Sphere.Developer; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
| using Microsoft.Extensions.Options; | using Microsoft.Extensions.Options; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Services; | namespace DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||||
| 
 | 
 | ||||||
| public class OidcProviderService( | public class OidcProviderService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
| @@ -26,16 +27,18 @@ public class OidcProviderService( | |||||||
| 
 | 
 | ||||||
|     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) |     public async Task<CustomApp?> FindClientByIdAsync(Guid clientId) | ||||||
|     { |     { | ||||||
|         return await db.CustomApps |         return null; | ||||||
|             .Include(c => c.Secrets) |         // return await db.CustomApps | ||||||
|             .FirstOrDefaultAsync(c => c.Id == clientId); |         //     .Include(c => c.Secrets) | ||||||
|  |         //     .FirstOrDefaultAsync(c => c.Id == clientId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId) |     public async Task<CustomApp?> FindClientByAppIdAsync(Guid appId) | ||||||
|     { |     { | ||||||
|         return await db.CustomApps |         return null; | ||||||
|             .Include(c => c.Secrets) |         // return await db.CustomApps | ||||||
|             .FirstOrDefaultAsync(c => c.Id == appId); |         //     .Include(c => c.Secrets) | ||||||
|  |         //     .FirstOrDefaultAsync(c => c.Id == appId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId) |     public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId) | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System.Net.Http.Json; |  | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
|  | using Microsoft.Extensions.Logging; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class AfdianOidcService( | public class AfdianOidcService( | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| 
 |  | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.Text.Json.Serialization; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class AppleMobileConnectRequest | public class AppleMobileConnectRequest | ||||||
| { | { | ||||||
| @@ -3,10 +3,12 @@ using System.Security.Cryptography; | |||||||
| using System.Text; | using System.Text; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// Implementation of OpenID Connect service for Apple Sign In | /// Implementation of OpenID Connect service for Apple Sign In | ||||||
| @@ -1,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.Authorization; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/accounts/me/connections")] | [Route("/accounts/me/connections")] | ||||||
| @@ -25,7 +27,7 @@ public class ConnectionController( | |||||||
|     [HttpGet] |     [HttpGet] | ||||||
|     public async Task<ActionResult<List<AccountConnection>>> GetConnections() |     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(); |             return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var connections = await db.AccountConnections |         var connections = await db.AccountConnections | ||||||
| @@ -48,7 +50,7 @@ public class ConnectionController( | |||||||
|     [HttpDelete("{id:guid}")] |     [HttpDelete("{id:guid}")] | ||||||
|     public async Task<ActionResult> RemoveConnection(Guid id) |     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(); |             return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var connection = await db.AccountConnections |         var connection = await db.AccountConnections | ||||||
| @@ -66,7 +68,7 @@ public class ConnectionController( | |||||||
|     [HttpPost("/auth/connect/apple/mobile")] |     [HttpPost("/auth/connect/apple/mobile")] | ||||||
|     public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request) |     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(); |             return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         if (GetOidcService("apple") is not AppleOidcService appleService) |         if (GetOidcService("apple") is not AppleOidcService appleService) | ||||||
| @@ -132,7 +134,7 @@ public class ConnectionController( | |||||||
|     [HttpPost("connect")] |     [HttpPost("connect")] | ||||||
|     public async Task<ActionResult<object>> InitiateConnection([FromBody] ConnectProviderRequest request) |     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(); |             return Unauthorized(); | ||||||
| 
 | 
 | ||||||
|         var oidcService = GetOidcService(request.Provider); |         var oidcService = GetOidcService(request.Provider); | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System.Net.Http.Json; | using System.Net.Http.Json; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class DiscordOidcService( | public class DiscordOidcService( | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System.Net.Http.Json; | using System.Net.Http.Json; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class GitHubOidcService( | public class GitHubOidcService( | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
| @@ -1,11 +1,11 @@ | |||||||
| using System.IdentityModel.Tokens.Jwt; | using System.IdentityModel.Tokens.Jwt; | ||||||
| using System.Net.Http.Json; | using System.Net.Http.Json; | ||||||
| using System.Security.Cryptography; | using DysonNetwork.Shared.Cache; | ||||||
| using System.Text; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Storage; | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class GoogleOidcService( | public class GoogleOidcService( | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| using System.Net.Http.Json; | using System.Net.Http.Json; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| public class MicrosoftOidcService( | public class MicrosoftOidcService( | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
| @@ -1,11 +1,13 @@ | |||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Pass.Account; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/auth/login")] | [Route("/auth/login")] | ||||||
| @@ -32,7 +34,7 @@ public class OidcController( | |||||||
|             var oidcService = GetOidcService(provider); |             var oidcService = GetOidcService(provider); | ||||||
| 
 | 
 | ||||||
|             // If the user is already authenticated, treat as an account connection request |             // 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 state = Guid.NewGuid().ToString(); | ||||||
|                 var nonce = 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)) |         if (string.IsNullOrEmpty(userInfo.Email)) | ||||||
|             throw new ArgumentException("Email is required for account creation"); |             throw new ArgumentException("Email is required for account creation"); | ||||||
| @@ -1,13 +1,15 @@ | |||||||
| using System.IdentityModel.Tokens.Jwt; | using System.IdentityModel.Tokens.Jwt; | ||||||
| using System.Net.Http.Json; | using System.Net.Http.Json; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Cache; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Shared.Models; | ||||||
|  | using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.IdentityModel.Tokens; | using Microsoft.IdentityModel.Tokens; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// Base service for OpenID Connect authentication providers | /// Base service for OpenID Connect authentication providers | ||||||
| @@ -190,7 +192,7 @@ public abstract class OidcService( | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     public async Task<Challenge> CreateChallengeForUserAsync( |     public async Task<Challenge> CreateChallengeForUserAsync( | ||||||
|         OidcUserInfo userInfo, |         OidcUserInfo userInfo, | ||||||
|         Account.Account account, |         Shared.Models.Account account, | ||||||
|         HttpContext request, |         HttpContext request, | ||||||
|         string deviceId |         string deviceId | ||||||
|     ) |     ) | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | namespace DysonNetwork.Pass.Auth.OpenId; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// Represents the state parameter used in OpenID Connect flows. | /// 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)] | [AttributeUsage(AttributeTargets.Method, Inherited = true)] | ||||||
| public class RequiredPermissionAttribute(string area, string key) : Attribute | public class RequiredPermissionAttribute(string area, string key) : Attribute | ||||||
| @@ -21,7 +21,7 @@ public class PermissionMiddleware(RequestDelegate next) | |||||||
| 
 | 
 | ||||||
|         if (attr != null) |         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; |                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; | ||||||
|                 await httpContext.Response.WriteAsync("Unauthorized"); |                 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 Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using System.Text.Json; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Permission; | namespace DysonNetwork.Pass.Permission; | ||||||
| 
 | 
 | ||||||
| public class PermissionService( | public class PermissionService( | ||||||
|     AppDatabase db, |     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 NodaTime.Serialization.JsonNet; | ||||||
| using StackExchange.Redis; | using StackExchange.Redis; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Storage; | namespace DysonNetwork.Shared.Cache; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// Represents a distributed lock that can be used to synchronize access across multiple processes | /// Represents a distributed lock that can be used to synchronize access across multiple processes | ||||||
							
								
								
									
										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; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Permission; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using DysonNetwork.Sphere.Wallet; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using OtpNet; | using OtpNet; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| [Index(nameof(Name), IsUnique = true)] | [Index(nameof(Name), IsUnique = true)] | ||||||
| public class Account : ModelBase | 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<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>(); | ||||||
|     [JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>(); |     [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<Session> Sessions { get; set; } = new List<Session>(); | ||||||
|     [JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>(); |     [JsonIgnore] public ICollection<Challenge> Challenges { get; set; } = new List<Challenge>(); | ||||||
| 
 | 
 | ||||||
|     [JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>(); |     [JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>(); | ||||||
|     [JsonIgnore] public ICollection<Relationship> IncomingRelationships { 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 System.ComponentModel.DataAnnotations.Schema; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum StatusAttitude | public enum StatusAttitude | ||||||
| { | { | ||||||
| @@ -23,7 +23,7 @@ public class Status : ModelBase | |||||||
|     public Instant? ClearedAt { get; set; } |     public Instant? ClearedAt { get; set; } | ||||||
|      |      | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|     public Account Account { get; set; } = null!; |     public Shared.Models.Account Account { get; set; } = null!; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| public enum CheckInResultLevel | public enum CheckInResultLevel | ||||||
| @@ -44,7 +44,7 @@ public class CheckInResult : ModelBase | |||||||
|     [Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>(); |     [Column(TypeName = "jsonb")] public ICollection<FortuneTip> Tips { get; set; } = new List<FortuneTip>(); | ||||||
|      |      | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|     public Account Account { get; set; } = null!; |     public Shared.Models.Account Account { get; set; } = null!; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| public class FortuneTip | public class FortuneTip | ||||||
| @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; | |||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using Point = NetTopologySuite.Geometries.Point; | using Point = NetTopologySuite.Geometries.Point; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public abstract class ActionLogType | public abstract class ActionLogType | ||||||
| { | { | ||||||
| @@ -53,6 +53,6 @@ public class ActionLog : ModelBase | |||||||
|     public Point? Location { get; set; } |     public Point? Location { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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; } |     public Guid? SessionId { get; set; } | ||||||
| } | } | ||||||
| @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class Badge : ModelBase | public class Badge : ModelBase | ||||||
| { | { | ||||||
| @@ -16,7 +16,7 @@ public class Badge : ModelBase | |||||||
|     public Instant? ExpiredAt { get; set; } |     public Instant? ExpiredAt { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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() |     public BadgeReferenceObject ToReference() | ||||||
|     { |     { | ||||||
| @@ -1,10 +1,9 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Chat; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum ChatRoomType | public enum ChatRoomType | ||||||
| { | { | ||||||
| @@ -31,7 +30,7 @@ public class ChatRoom : ModelBase, IIdentifiedResource | |||||||
|     [JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>(); |     [JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>(); | ||||||
| 
 | 
 | ||||||
|     public Guid? RealmId { get; set; } |     public Guid? RealmId { get; set; } | ||||||
|     public Realm.Realm? Realm { get; set; } |     public Shared.Models.Realm? Realm { get; set; } | ||||||
| 
 | 
 | ||||||
|     [NotMapped] |     [NotMapped] | ||||||
|     [JsonPropertyName("members")] |     [JsonPropertyName("members")] | ||||||
| @@ -73,7 +72,7 @@ public class ChatMember : ModelBase | |||||||
|     public Guid ChatRoomId { get; set; } |     public Guid ChatRoomId { get; set; } | ||||||
|     public ChatRoom ChatRoom { get; set; } = null!; |     public ChatRoom ChatRoom { get; set; } = null!; | ||||||
|     public Guid AccountId { 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; } |     [MaxLength(1024)] public string? Nick { get; set; } | ||||||
| 
 | 
 | ||||||
| @@ -105,7 +104,7 @@ public class ChatMemberTransmissionObject : ModelBase | |||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     public Guid ChatRoomId { get; set; } |     public Guid ChatRoomId { get; set; } | ||||||
|     public Guid AccountId { 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; } |     [MaxLength(1024)] public string? Nick { get; set; } | ||||||
| 
 | 
 | ||||||
| @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; | |||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Storage; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class RemoteStorageConfig | public class RemoteStorageConfig | ||||||
| { | { | ||||||
| @@ -20,6 +20,59 @@ public class RemoteStorageConfig | |||||||
|     public string? AccessProxy { get; set; } |     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> | /// <summary> | ||||||
| /// The class that used in jsonb columns which referenced the cloud file. | /// 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. | /// 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)] |     [MaxLength(4096)] | ||||||
|     public string? StorageUrl { get; set; } |     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 Guid AccountId { get; set; } | ||||||
| 
 | 
 | ||||||
|     public CloudFileReferenceObject ToReferenceObject() |     public CloudFileReferenceObject ToReferenceObject() | ||||||
| @@ -1,11 +1,9 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Account; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Developer; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum CustomAppStatus | public enum CustomAppStatus | ||||||
| { | { | ||||||
| @@ -33,7 +31,7 @@ public class CustomApp : ModelBase, IIdentifiedResource | |||||||
|     [JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>(); |     [JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>(); | ||||||
| 
 | 
 | ||||||
|     public Guid PublisherId { get; set; } |     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; |     [NotMapped] public string ResourceIdentifier => "custom-app/" + Id; | ||||||
| } | } | ||||||
| @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum MagicSpellType | public enum MagicSpellType | ||||||
| { | { | ||||||
| @@ -26,5 +26,5 @@ public class MagicSpell : ModelBase | |||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); |     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||||
| 
 | 
 | ||||||
|     public Guid? AccountId { get; set; } |     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; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Chat; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class Message : ModelBase, IIdentifiedResource | 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 Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class Notification : ModelBase | public class Notification : ModelBase | ||||||
| { | { | ||||||
| @@ -18,7 +18,7 @@ public class Notification : ModelBase | |||||||
|     public Instant? ViewedAt { get; set; } |     public Instant? ViewedAt { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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 | public enum NotificationPushProvider | ||||||
| @@ -37,5 +37,5 @@ public class NotificationPushSubscription : ModelBase | |||||||
|     public Instant? LastUsedAt { get; set; } |     public Instant? LastUsedAt { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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> | /// <summary> | ||||||
| /// Represents the user information from an OIDC provider | /// Represents the user information from an OIDC provider | ||||||
| @@ -1,9 +1,8 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using DysonNetwork.Sphere.Developer; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Wallet; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class WalletCurrency | public class WalletCurrency | ||||||
| { | { | ||||||
| @@ -32,7 +31,7 @@ public class Order : ModelBase | |||||||
|     public Instant ExpiredAt { get; set; } |     public Instant ExpiredAt { get; set; } | ||||||
|      |      | ||||||
|     public Guid? PayeeWalletId { 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 Guid? TransactionId { get; set; } | ||||||
|     public Transaction? Transaction { get; set; } |     public Transaction? Transaction { get; set; } | ||||||
|     public Guid? IssuerAppId { 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 |     // When the payer is null, it's pay from the system | ||||||
|     public Guid? PayerWalletId { get; set; } |     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 |     // When the payee is null, it's pay for the system | ||||||
|     public Guid? PayeeWalletId { get; set; } |     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 Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Permission; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| /// The permission node model provides the infrastructure of permission control in Dyson Network. | /// The permission node model provides the infrastructure of permission control in Dyson Network. | ||||||
| /// It based on the ABAC permission model. | /// It based on the ABAC permission model. | ||||||
| @@ -1,12 +1,10 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Post; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Publisher; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum PublisherType | 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? Picture { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { 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<PublisherMember> Members { get; set; } = new List<PublisherMember>(); | ||||||
|     [JsonIgnore] public ICollection<PublisherFeature> Features { get; set; } = new List<PublisherFeature>(); |     [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 ICollection<PublisherSubscription> Subscriptions { get; set; } = new List<PublisherSubscription>(); | ||||||
| 
 | 
 | ||||||
|     public Guid? AccountId { get; set; } |     public Guid? AccountId { get; set; } | ||||||
|     public Account.Account? Account { get; set; } |     public Shared.Models.Account? Account { get; set; } | ||||||
|     public Guid? RealmId { 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}"; |     public string ResourceIdentifier => $"publisher/{Id}"; | ||||||
| } | } | ||||||
| @@ -61,7 +57,7 @@ public class PublisherMember : ModelBase | |||||||
|     public Guid PublisherId { get; set; } |     public Guid PublisherId { get; set; } | ||||||
|     [JsonIgnore] public Publisher Publisher { get; set; } = null!; |     [JsonIgnore] public Publisher Publisher { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     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 PublisherMemberRole Role { get; set; } = PublisherMemberRole.Viewer; | ||||||
|     public Instant? JoinedAt { get; set; } |     public Instant? JoinedAt { get; set; } | ||||||
| @@ -81,7 +77,7 @@ public class PublisherSubscription : ModelBase | |||||||
|     public Guid PublisherId { get; set; } |     public Guid PublisherId { get; set; } | ||||||
|     [JsonIgnore] public Publisher Publisher { get; set; } = null!; |     [JsonIgnore] public Publisher Publisher { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     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 PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active; | ||||||
|     public int Tier { get; set; } = 0; |     public int Tier { get; set; } = 0; | ||||||
| @@ -1,12 +1,10 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Chat; |  | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Realm; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| [Index(nameof(Slug), IsUnique = true)] | [Index(nameof(Slug), IsUnique = true)] | ||||||
| public class Realm : ModelBase, IIdentifiedResource | 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? Picture { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { 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<RealmMember> Members { get; set; } = new List<RealmMember>(); | ||||||
|     [JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>(); |     [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; } |     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}"; |     public string ResourceIdentifier => $"realm/{Id}"; | ||||||
| } | } | ||||||
| @@ -49,7 +46,7 @@ public class RealmMember : ModelBase | |||||||
|     public Guid RealmId { get; set; } |     public Guid RealmId { get; set; } | ||||||
|     public Realm Realm { get; set; } = null!; |     public Realm Realm { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     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 int Role { get; set; } = RealmMemberRole.Normal; | ||||||
|     public Instant? JoinedAt { get; set; } |     public Instant? JoinedAt { get; set; } | ||||||
| @@ -1,12 +1,8 @@ | |||||||
| using System; |  | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Text.Json.Serialization; |  | ||||||
| using DysonNetwork.Sphere.Chat.Realtime; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Chat; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class RealtimeCall : ModelBase | public class RealtimeCall : ModelBase | ||||||
| { | { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public enum RelationshipStatus : short | public enum RelationshipStatus : short | ||||||
| { | { | ||||||
| @@ -12,9 +12,9 @@ public enum RelationshipStatus : short | |||||||
| public class Relationship : ModelBase | public class Relationship : ModelBase | ||||||
| { | { | ||||||
|     public Guid AccountId { get; set; } |     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 Guid RelatedId { get; set; } | ||||||
|     public Account Related { get; set; } = null!; |     public Shared.Models.Account Related { get; set; } = null!; | ||||||
| 
 | 
 | ||||||
|     public Instant? ExpiredAt { get; set; } |     public Instant? ExpiredAt { get; set; } | ||||||
| 
 | 
 | ||||||
| @@ -1,11 +1,10 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Sphere.Developer; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Point = NetTopologySuite.Geometries.Point; | using Point = NetTopologySuite.Geometries.Point; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class Session : ModelBase | public class Session : ModelBase | ||||||
| { | { | ||||||
| @@ -15,7 +14,7 @@ public class Session : ModelBase | |||||||
|     public Instant? ExpiredAt { get; set; } |     public Instant? ExpiredAt { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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 Guid ChallengeId { get; set; } | ||||||
|     public Challenge Challenge { get; set; } = null!; |     public Challenge Challenge { get; set; } = null!; | ||||||
|     public Guid? AppId { get; set; } |     public Guid? AppId { get; set; } | ||||||
| @@ -49,9 +48,9 @@ public class Challenge : ModelBase | |||||||
|     public int FailedAttempts { get; set; } |     public int FailedAttempts { get; set; } | ||||||
|     public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified; |     public ChallengePlatform Platform { get; set; } = ChallengePlatform.Unidentified; | ||||||
|     public ChallengeType Type { get; set; } = ChallengeType.Login; |     public ChallengeType Type { get; set; } = ChallengeType.Login; | ||||||
|     [Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = new(); |     [Column(TypeName = "jsonb")] public List<Guid> BlacklistFactors { get; set; } = []; | ||||||
|     [Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = new(); |     [Column(TypeName = "jsonb")] public List<string> Audiences { get; set; } = []; | ||||||
|     [Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = new(); |     [Column(TypeName = "jsonb")] public List<string> Scopes { get; set; } = []; | ||||||
|     [MaxLength(128)] public string? IpAddress { get; set; } |     [MaxLength(128)] public string? IpAddress { get; set; } | ||||||
|     [MaxLength(512)] public string? UserAgent { get; set; } |     [MaxLength(512)] public string? UserAgent { get; set; } | ||||||
|     [MaxLength(256)] public string? DeviceId { get; set; } |     [MaxLength(256)] public string? DeviceId { get; set; } | ||||||
| @@ -59,7 +58,7 @@ public class Challenge : ModelBase | |||||||
|     public Point? Location { get; set; } |     public Point? Location { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { 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() |     public Challenge Normalize() | ||||||
|     { |     { | ||||||
| @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Wallet; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public record class SubscriptionTypeData( | public record class SubscriptionTypeData( | ||||||
|     string Identifier, |     string Identifier, | ||||||
| @@ -138,7 +138,7 @@ public class Subscription : ModelBase | |||||||
|     public Instant? RenewalAt { get; set; } |     public Instant? RenewalAt { get; set; } | ||||||
| 
 | 
 | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|     public Account.Account Account { get; set; } = null!; |     public Shared.Models.Account Account { get; set; } = null!; | ||||||
| 
 | 
 | ||||||
|     [NotMapped] |     [NotMapped] | ||||||
|     public bool IsAvailable |     public bool IsAvailable | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Account; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| /// <summary> | /// <summary> | ||||||
| /// The verification info of a resource | /// The verification info of a resource | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Sphere.Wallet; | namespace DysonNetwork.Shared.Models; | ||||||
| 
 | 
 | ||||||
| public class Wallet : ModelBase | public class Wallet : ModelBase | ||||||
| { | { | ||||||
| @@ -10,7 +10,7 @@ public class Wallet : ModelBase | |||||||
|     public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>(); |     public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>(); | ||||||
|      |      | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|     public Account.Account Account { get; set; } = null!; |     public Shared.Models.Account Account { get; set; } = null!; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| public class WalletPocket : ModelBase | 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>(); |         var debugIncludeSet = debugInclude?.Split(',').ToHashSet() ?? new HashSet<string>(); | ||||||
|  |  | ||||||
|         HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); |         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.GetActivitiesForAnyone(take, cursorTimestamp, debugIncludeSet)) | ||||||
|             : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); |             : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter, debugIncludeSet)); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -13,7 +13,8 @@ public class ActivityService( | |||||||
|     PublisherService pub, |     PublisherService pub, | ||||||
|     RelationshipService rels, |     RelationshipService rels, | ||||||
|     PostService ps, |     PostService ps, | ||||||
|     DiscoveryService ds) |     DiscoveryService ds | ||||||
|  | ) | ||||||
| { | { | ||||||
|     private static double CalculateHotRank(Post.Post post, Instant now) |     private static double CalculateHotRank(Post.Post post, Instant now) | ||||||
|     { |     { | ||||||
| @@ -32,7 +33,7 @@ public class ActivityService( | |||||||
|  |  | ||||||
|         if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) |         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) |             if (realms.Count > 0) | ||||||
|             { |             { | ||||||
|                 activities.Add(new DiscoveryActivity( |                 activities.Add(new DiscoveryActivity( | ||||||
| @@ -118,7 +119,7 @@ public class ActivityService( | |||||||
|     public async Task<List<Activity>> GetActivities( |     public async Task<List<Activity>> GetActivities( | ||||||
|         int take, |         int take, | ||||||
|         Instant? cursor, |         Instant? cursor, | ||||||
|         Account.Account currentUser, |         Shared.Models.Account currentUser, | ||||||
|         string? filter = null, |         string? filter = null, | ||||||
|         HashSet<string>? debugInclude = null |         HashSet<string>? debugInclude = null | ||||||
|     ) |     ) | ||||||
| @@ -132,7 +133,7 @@ public class ActivityService( | |||||||
|         { |         { | ||||||
|             if (cursor == null && (debugInclude.Contains("realms") || Random.Shared.NextDouble() < 0.2)) |             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) |                 if (realms.Count > 0) | ||||||
|                 { |                 { | ||||||
|                     activities.Add(new DiscoveryActivity( |                     activities.Add(new DiscoveryActivity( | ||||||
| @@ -257,7 +258,7 @@ public class ActivityService( | |||||||
|         return score + postCount; |         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 now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var recent = now.Minus(Duration.FromDays(7)); |         var recent = now.Minus(Duration.FromDays(7)); | ||||||
|   | |||||||
| @@ -1,30 +1,16 @@ | |||||||
| using System.Linq.Expressions; | using System.Linq.Expressions; | ||||||
| using System.Reflection; | using System.Reflection; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Auth; |  | ||||||
| using DysonNetwork.Sphere.Chat; |  | ||||||
| using DysonNetwork.Sphere.Developer; |  | ||||||
| using DysonNetwork.Sphere.Permission; |  | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| using DysonNetwork.Sphere.Publisher; |  | ||||||
| using DysonNetwork.Sphere.Realm; |  | ||||||
| using DysonNetwork.Sphere.Sticker; | using DysonNetwork.Sphere.Sticker; | ||||||
| using DysonNetwork.Sphere.Storage; |  | ||||||
| using DysonNetwork.Sphere.Wallet; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Design; | using Microsoft.EntityFrameworkCore.Design; | ||||||
| using Microsoft.EntityFrameworkCore.Query; | using Microsoft.EntityFrameworkCore.Query; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Npgsql; |  | ||||||
| using Quartz; | using Quartz; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere; | namespace DysonNetwork.Sphere; | ||||||
|  |  | ||||||
| public interface IIdentifiedResource |  | ||||||
| { |  | ||||||
|     public string ResourceIdentifier { get; } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| public abstract class ModelBase | public abstract class ModelBase | ||||||
| { | { | ||||||
|     public Instant CreatedAt { get; set; } |     public Instant CreatedAt { get; set; } | ||||||
| @@ -37,32 +23,10 @@ public class AppDatabase( | |||||||
|     IConfiguration configuration |     IConfiguration configuration | ||||||
| ) : DbContext(options) | ) : DbContext(options) | ||||||
| { | { | ||||||
|     public DbSet<PermissionNode> PermissionNodes { get; set; } |  | ||||||
|     public DbSet<PermissionGroup> PermissionGroups { get; set; } |  | ||||||
|     public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } |  | ||||||
|  |  | ||||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } |  | ||||||
|     public DbSet<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<CloudFile> Files { get; set; } | ||||||
|     public DbSet<CloudFileReference> FileReferences { 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<PublisherMember> PublisherMembers { get; set; } | ||||||
|     public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; } |     public DbSet<PublisherSubscription> PublisherSubscriptions { get; set; } | ||||||
|     public DbSet<PublisherFeature> PublisherFeatures { get; set; } |     public DbSet<PublisherFeature> PublisherFeatures { get; set; } | ||||||
| @@ -73,10 +37,8 @@ public class AppDatabase( | |||||||
|     public DbSet<PostCategory> PostCategories { get; set; } |     public DbSet<PostCategory> PostCategories { get; set; } | ||||||
|     public DbSet<PostCollection> PostCollections { 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<RealmMember> RealmMembers { get; set; } | ||||||
|     public DbSet<Tag> Tags { get; set; } |  | ||||||
|     public DbSet<RealmTag> RealmTags { get; set; } |  | ||||||
|  |  | ||||||
|     public DbSet<ChatRoom> ChatRooms { get; set; } |     public DbSet<ChatRoom> ChatRooms { get; set; } | ||||||
|     public DbSet<ChatMember> ChatMembers { get; set; } |     public DbSet<ChatMember> ChatMembers { get; set; } | ||||||
| @@ -87,7 +49,7 @@ public class AppDatabase( | |||||||
|     public DbSet<Sticker.Sticker> Stickers { get; set; } |     public DbSet<Sticker.Sticker> Stickers { get; set; } | ||||||
|     public DbSet<StickerPack> StickerPacks { 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<WalletPocket> WalletPockets { get; set; } | ||||||
|     public DbSet<Order> PaymentOrders { get; set; } |     public DbSet<Order> PaymentOrders { get; set; } | ||||||
|     public DbSet<Transaction> PaymentTransactions { get; set; } |     public DbSet<Transaction> PaymentTransactions { get; set; } | ||||||
| @@ -111,38 +73,6 @@ public class AppDatabase( | |||||||
|                 .UseNodaTime() |                 .UseNodaTime() | ||||||
|         ).UseSnakeCaseNamingConvention(); |         ).UseSnakeCaseNamingConvention(); | ||||||
|          |          | ||||||
|         optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) => |  | ||||||
|         { |  | ||||||
|             var defaultPermissionGroup = await context.Set<PermissionGroup>() |  | ||||||
|                 .FirstOrDefaultAsync(g => g.Key == "default", cancellationToken); |  | ||||||
|             if (defaultPermissionGroup is null) |  | ||||||
|             { |  | ||||||
|                 context.Set<PermissionGroup>().Add(new PermissionGroup |  | ||||||
|                 { |  | ||||||
|                     Key = "default", |  | ||||||
|                     Nodes = new List<string> |  | ||||||
|                         { |  | ||||||
|                             "posts.create", |  | ||||||
|                             "posts.react", |  | ||||||
|                             "publishers.create", |  | ||||||
|                             "files.create", |  | ||||||
|                             "chat.create", |  | ||||||
|                             "chat.messages.create", |  | ||||||
|                             "chat.realtime.create", |  | ||||||
|                             "accounts.statuses.create", |  | ||||||
|                             "accounts.statuses.update", |  | ||||||
|                             "stickers.packs.create", |  | ||||||
|                             "stickers.create" |  | ||||||
|                         }.Select(permission => |  | ||||||
|                             PermissionService.NewPermissionNode("group:default", "global", permission, true)) |  | ||||||
|                         .ToList() |  | ||||||
|                 }); |  | ||||||
|                 await context.SaveChangesAsync(cancellationToken); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         optionsBuilder.UseSeeding((context, _) => {}); |  | ||||||
|  |  | ||||||
|         base.OnConfiguring(optionsBuilder); |         base.OnConfiguring(optionsBuilder); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -150,25 +80,6 @@ public class AppDatabase( | |||||||
|     { |     { | ||||||
|         base.OnModelCreating(modelBuilder); |         base.OnModelCreating(modelBuilder); | ||||||
|  |  | ||||||
|         modelBuilder.Entity<PermissionGroupMember>() |  | ||||||
|             .HasKey(pg => new { pg.GroupId, pg.Actor }); |  | ||||||
|         modelBuilder.Entity<PermissionGroupMember>() |  | ||||||
|             .HasOne(pg => pg.Group) |  | ||||||
|             .WithMany(g => g.Members) |  | ||||||
|             .HasForeignKey(pg => pg.GroupId) |  | ||||||
|             .OnDelete(DeleteBehavior.Cascade); |  | ||||||
|  |  | ||||||
|         modelBuilder.Entity<Relationship>() |  | ||||||
|             .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); |  | ||||||
|         modelBuilder.Entity<Relationship>() |  | ||||||
|             .HasOne(r => r.Account) |  | ||||||
|             .WithMany(a => a.OutgoingRelationships) |  | ||||||
|             .HasForeignKey(r => r.AccountId); |  | ||||||
|         modelBuilder.Entity<Relationship>() |  | ||||||
|             .HasOne(r => r.Related) |  | ||||||
|             .WithMany(a => a.IncomingRelationships) |  | ||||||
|             .HasForeignKey(r => r.RelatedId); |  | ||||||
|  |  | ||||||
|         modelBuilder.Entity<PublisherMember>() |         modelBuilder.Entity<PublisherMember>() | ||||||
|             .HasKey(pm => new { pm.PublisherId, pm.AccountId }); |             .HasKey(pm => new { pm.PublisherId, pm.AccountId }); | ||||||
|         modelBuilder.Entity<PublisherMember>() |         modelBuilder.Entity<PublisherMember>() | ||||||
| @@ -243,19 +154,6 @@ public class AppDatabase( | |||||||
|             .HasForeignKey(pm => pm.AccountId) |             .HasForeignKey(pm => pm.AccountId) | ||||||
|             .OnDelete(DeleteBehavior.Cascade); |             .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>() |         modelBuilder.Entity<ChatMember>() | ||||||
|             .HasKey(pm => new { pm.Id }); |             .HasKey(pm => new { pm.Id }); | ||||||
|         modelBuilder.Entity<ChatMember>() |         modelBuilder.Entity<ChatMember>() | ||||||
| @@ -352,23 +250,9 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin | |||||||
| { | { | ||||||
|     public async Task Execute(IJobExecutionContext context) |     public async Task Execute(IJobExecutionContext context) | ||||||
|     { |     { | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Cleaning up expired records..."); |  | ||||||
|  |  | ||||||
|         // Expired relationships |  | ||||||
|         var affectedRows = await db.AccountRelationships |  | ||||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) |  | ||||||
|             .ExecuteDeleteAsync(); |  | ||||||
|         logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows); |  | ||||||
|         // Expired permission group members |  | ||||||
|         affectedRows = await db.PermissionGroupMembers |  | ||||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) |  | ||||||
|             .ExecuteDeleteAsync(); |  | ||||||
|         logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows); |  | ||||||
|  |  | ||||||
|         logger.LogInformation("Deleting soft-deleted records..."); |         logger.LogInformation("Deleting soft-deleted records..."); | ||||||
|  |  | ||||||
|  |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var threshold = now - Duration.FromDays(7); |         var threshold = now - Duration.FromDays(7); | ||||||
|  |  | ||||||
|         var entityTypes = db.Model.GetEntityTypes() |         var entityTypes = db.Model.GetEntityTypes() | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Sphere.Permission; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -32,7 +33,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary() |     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 unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id); | ||||||
|         var lastMessages = await cs.ListLastMessageForUser(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, |     public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset, | ||||||
|         [FromQuery] int take = 20) |         [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); |         var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId); | ||||||
|         if (room is null) return NotFound(); |         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}")] |     [HttpGet("{roomId:guid}/messages/{messageId:guid}")] | ||||||
|     public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId) |     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); |         var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId); | ||||||
|         if (room is null) return NotFound(); |         if (room is null) return NotFound(); | ||||||
| @@ -139,7 +140,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|     [RequiredPermission("global", "chat.messages.create")] |     [RequiredPermission("global", "chat.messages.create")] | ||||||
|     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId) |     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|         request.Content = TextSanitizer.Sanitize(request.Content); |         request.Content = TextSanitizer.Sanitize(request.Content); | ||||||
|         if (string.IsNullOrWhiteSpace(request.Content) && |         if (string.IsNullOrWhiteSpace(request.Content) && | ||||||
| @@ -216,7 +217,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId) |     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); |         request.Content = TextSanitizer.Sanitize(request.Content); | ||||||
|  |  | ||||||
| @@ -269,7 +270,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId) |     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 |         var message = await db.ChatMessages | ||||||
|             .Include(m => m.Sender) |             .Include(m => m.Sender) | ||||||
| @@ -295,7 +296,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ | |||||||
|     [HttpPost("{roomId:guid}/sync")] |     [HttpPost("{roomId:guid}/sync")] | ||||||
|     public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId) |     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(); |             return Unauthorized(); | ||||||
|  |  | ||||||
|         var isMember = await db.ChatMembers |         var isMember = await db.ChatMembers | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Sphere.Account; | ||||||
| using DysonNetwork.Sphere.Localization; | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Permission; | using DysonNetwork.Sphere.Permission; | ||||||
| @@ -36,7 +37,7 @@ public class ChatRoomController( | |||||||
|         if (chatRoom is null) return NotFound(); |         if (chatRoom is null) return NotFound(); | ||||||
|         if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom); |         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); |             chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id); | ||||||
|  |  | ||||||
|         return Ok(chatRoom); |         return Ok(chatRoom); | ||||||
| @@ -46,7 +47,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms() |     public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms() | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||||
|             return Unauthorized(); |             return Unauthorized(); | ||||||
|         var userId = currentUser.Id; |         var userId = currentUser.Id; | ||||||
|  |  | ||||||
| @@ -72,7 +73,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request) |     public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) |         if (HttpContext.Items["CurrentUser"] is not Shared.Models.Account currentUser) | ||||||
|             return Unauthorized(); |             return Unauthorized(); | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); | ||||||
| @@ -134,7 +135,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId) |     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(); |             return Unauthorized(); | ||||||
|  |  | ||||||
|         var room = await db.ChatRooms |         var room = await db.ChatRooms | ||||||
| @@ -164,7 +165,7 @@ public class ChatRoomController( | |||||||
|     [RequiredPermission("global", "chat.create")] |     [RequiredPermission("global", "chat.create")] | ||||||
|     public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request) |     public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not 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."); |         if (request.Name is null) return BadRequest("You cannot create a chat room without a name."); | ||||||
|  |  | ||||||
|         var chatRoom = new ChatRoom |         var chatRoom = new ChatRoom | ||||||
| @@ -236,7 +237,7 @@ public class ChatRoomController( | |||||||
|     [HttpPatch("{id:guid}")] |     [HttpPatch("{id:guid}")] | ||||||
|     public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request) |     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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(e => e.Id == id) |             .Where(e => e.Id == id) | ||||||
| @@ -321,7 +322,7 @@ public class ChatRoomController( | |||||||
|     [HttpDelete("{id:guid}")] |     [HttpDelete("{id:guid}")] | ||||||
|     public async Task<ActionResult> DeleteChatRoom(Guid id) |     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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(e => e.Id == id) |             .Where(e => e.Id == id) | ||||||
| @@ -356,7 +357,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId) |     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(); |             return Unauthorized(); | ||||||
|  |  | ||||||
|         var member = await db.ChatMembers |         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, |     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) |         [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 |         var room = await db.ChatRooms | ||||||
|             .FirstOrDefaultAsync(r => r.Id == roomId); |             .FirstOrDefaultAsync(r => r.Id == roomId); | ||||||
| @@ -448,7 +449,7 @@ public class ChatRoomController( | |||||||
|     public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId, |     public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId, | ||||||
|         [FromBody] ChatMemberRequest request) |         [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 userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); |         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); | ||||||
| @@ -519,7 +520,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<List<ChatMember>>> ListChatInvites() |     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 userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var members = await db.ChatMembers |         var members = await db.ChatMembers | ||||||
| @@ -544,7 +545,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId) |     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 userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var member = await db.ChatMembers |         var member = await db.ChatMembers | ||||||
| @@ -571,7 +572,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> DeclineChatInvite(Guid roomId) |     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 userId = currentUser.Id; | ||||||
|  |  | ||||||
|         var member = await db.ChatMembers |         var member = await db.ChatMembers | ||||||
| @@ -600,7 +601,7 @@ public class ChatRoomController( | |||||||
|         [FromBody] ChatMemberNotifyRequest request |         [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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(r => r.Id == roomId) |             .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) |     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 (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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(r => r.Id == roomId) |             .Where(r => r.Id == roomId) | ||||||
| @@ -688,7 +689,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId) |     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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(r => r.Id == roomId) |             .Where(r => r.Id == roomId) | ||||||
| @@ -736,7 +737,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId) |     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 |         var chatRoom = await db.ChatRooms | ||||||
|             .Where(r => r.Id == roomId) |             .Where(r => r.Id == roomId) | ||||||
| @@ -774,7 +775,7 @@ public class ChatRoomController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult> LeaveChat(Guid roomId) |     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 |         var member = await db.ChatMembers | ||||||
|             .Where(m => m.AccountId == currentUser.Id) |             .Where(m => m.AccountId == currentUser.Id) | ||||||
| @@ -807,7 +808,7 @@ public class ChatRoomController( | |||||||
|         return NoContent(); |         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"]; |         string title = localizer["ChatInviteTitle"]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using System.Text.RegularExpressions; | using System.Text.RegularExpressions; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Sphere.Account; | ||||||
| using DysonNetwork.Sphere.Chat.Realtime; | using DysonNetwork.Sphere.Chat.Realtime; | ||||||
| using DysonNetwork.Sphere.Connection; | using DysonNetwork.Sphere.Connection; | ||||||
| @@ -241,7 +242,7 @@ public partial class ChatService( | |||||||
|             Priority = 10, |             Priority = 10, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         List<Account.Account> accountsToNotify = []; |         List<Shared.Models.Account> accountsToNotify = []; | ||||||
|         foreach (var member in members) |         foreach (var member in members) | ||||||
|         { |         { | ||||||
|             scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket |             scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ public interface IRealtimeService | |||||||
|     /// <param name="sessionId">The session identifier</param> |     /// <param name="sessionId">The session identifier</param> | ||||||
|     /// <param name="isAdmin">The user is the admin of session</param> |     /// <param name="isAdmin">The user is the admin of session</param> | ||||||
|     /// <returns>User-specific token for the session</returns> |     /// <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> |     /// <summary> | ||||||
|     /// Processes incoming webhook requests from the realtime service provider |     /// Processes incoming webhook requests from the realtime service provider | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ using Livekit.Server.Sdk.Dotnet; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using DysonNetwork.Shared.Cache; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Chat.Realtime; | namespace DysonNetwork.Sphere.Chat.Realtime; | ||||||
|  |  | ||||||
| @@ -111,7 +113,7 @@ public class LivekitRealtimeService : IRealtimeService | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <inheritdoc /> |     /// <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) |         var token = _accessToken.WithIdentity(account.Name) | ||||||
|             .WithName(account.Nick) |             .WithName(account.Nick) | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Chat.Realtime; | using DysonNetwork.Sphere.Chat.Realtime; | ||||||
| using Livekit.Server.Sdk.Dotnet; | using Livekit.Server.Sdk.Dotnet; | ||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| @@ -46,7 +47,7 @@ public class RealtimeCallController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId) |     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 |         var member = await db.ChatMembers | ||||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) |             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||||
| @@ -71,7 +72,7 @@ public class RealtimeCallController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId) |     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 |         // Check if the user is a member of the chat room | ||||||
|         var member = await db.ChatMembers |         var member = await db.ChatMembers | ||||||
| @@ -144,7 +145,7 @@ public class RealtimeCallController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId) |     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 |         var member = await db.ChatMembers | ||||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) |             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||||
| @@ -163,7 +164,7 @@ public class RealtimeCallController( | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId) |     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 |         var member = await db.ChatMembers | ||||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) |             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using System.Net.WebSockets; | using System.Net.WebSockets; | ||||||
|  | using DysonNetwork.Shared.Models; | ||||||
| using DysonNetwork.Sphere.Chat; | using DysonNetwork.Sphere.Chat; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -19,7 +20,7 @@ public class MessageReadHandler( | |||||||
|     public const string ChatMemberCacheKey = "ChatMember_{0}_{1}"; |     public const string ChatMemberCacheKey = "ChatMember_{0}_{1}"; | ||||||
|  |  | ||||||
|     public async Task HandleAsync( |     public async Task HandleAsync( | ||||||
|         Account.Account currentUser, |         Shared.Models.Account currentUser, | ||||||
|         string deviceId, |         string deviceId, | ||||||
|         WebSocketPacket packet, |         WebSocketPacket packet, | ||||||
|         WebSocket socket, |         WebSocket socket, | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ public class MessageTypingHandler(ChatRoomService crs) : IWebSocketPacketHandler | |||||||
|     public string PacketType => "messages.typing"; |     public string PacketType => "messages.typing"; | ||||||
|  |  | ||||||
|     public async Task HandleAsync( |     public async Task HandleAsync( | ||||||
|         Account.Account currentUser, |         Shared.Models.Account currentUser, | ||||||
|         string deviceId, |         string deviceId, | ||||||
|         WebSocketPacket packet, |         WebSocketPacket packet, | ||||||
|         WebSocket socket, |         WebSocket socket, | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ public class MessagesSubscribeHandler(ChatRoomService crs) : IWebSocketPacketHan | |||||||
|     public string PacketType => "messages.subscribe"; |     public string PacketType => "messages.subscribe"; | ||||||
|  |  | ||||||
|     public async Task HandleAsync( |     public async Task HandleAsync( | ||||||
|         Account.Account currentUser, |         Shared.Models.Account currentUser, | ||||||
|         string deviceId, |         string deviceId, | ||||||
|         WebSocketPacket packet, |         WebSocketPacket packet, | ||||||
|         WebSocket socket, |         WebSocket socket, | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ public class MessagesUnsubscribeHandler() : IWebSocketPacketHandler | |||||||
|     public string PacketType => "messages.unsubscribe"; |     public string PacketType => "messages.unsubscribe"; | ||||||
|  |  | ||||||
|     public Task HandleAsync( |     public Task HandleAsync( | ||||||
|         Account.Account currentUser, |         Shared.Models.Account currentUser, | ||||||
|         string deviceId, |         string deviceId, | ||||||
|         WebSocketPacket packet, |         WebSocketPacket packet, | ||||||
|         WebSocket socket, |         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