From 7f4c7563654cad5fd10c2cb4a957927a4f606e40 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 29 May 2025 01:12:51 +0800 Subject: [PATCH] :art: Split the account current related endpoints --- .../Account/AccountController.cs | 282 ----------------- .../Account/AccountCurrentController.cs | 294 ++++++++++++++++++ DysonNetwork.Sphere/Storage/CloudFile.cs | 2 + 3 files changed, 296 insertions(+), 282 deletions(-) create mode 100644 DysonNetwork.Sphere/Account/AccountCurrentController.cs diff --git a/DysonNetwork.Sphere/Account/AccountController.cs b/DysonNetwork.Sphere/Account/AccountController.cs index dd9910e..c3d68f0 100644 --- a/DysonNetwork.Sphere/Account/AccountController.cs +++ b/DysonNetwork.Sphere/Account/AccountController.cs @@ -120,123 +120,6 @@ public class AccountController( return account; } - [Authorize] - [HttpGet("me")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetCurrentIdentity() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; - - var account = await db.Accounts - .Include(e => e.Badges) - .Include(e => e.Profile) - .Where(e => e.Id == userId) - .FirstOrDefaultAsync(); - - return Ok(account); - } - - public class BasicInfoRequest - { - [MaxLength(256)] public string? Nick { get; set; } - [MaxLength(32)] public string? Language { get; set; } - } - - [Authorize] - [HttpPatch("me")] - public async Task> UpdateBasicInfo([FromBody] BasicInfoRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id); - - if (request.Nick is not null) account.Nick = request.Nick; - if (request.Language is not null) account.Language = request.Language; - - await db.SaveChangesAsync(); - await accounts.PurgeAccountCache(currentUser); - return currentUser; - } - - public class ProfileRequest - { - [MaxLength(256)] public string? FirstName { get; set; } - [MaxLength(256)] public string? MiddleName { get; set; } - [MaxLength(256)] public string? LastName { get; set; } - [MaxLength(4096)] public string? Bio { get; set; } - - [MaxLength(32)] public string? PictureId { get; set; } - [MaxLength(32)] public string? BackgroundId { get; set; } - } - - [Authorize] - [HttpPatch("me/profile")] - public async Task> UpdateProfile([FromBody] ProfileRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; - - var profile = await db.AccountProfiles - .Where(p => p.Account.Id == userId) - .Include(profile => profile.Background) - .Include(profile => profile.Picture) - .FirstOrDefaultAsync(); - if (profile is null) return BadRequest("Unable to get your account."); - - if (request.FirstName is not null) profile.FirstName = request.FirstName; - if (request.MiddleName is not null) profile.MiddleName = request.MiddleName; - if (request.LastName is not null) profile.LastName = request.LastName; - if (request.Bio is not null) profile.Bio = request.Bio; - - if (request.PictureId is not null) - { - var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); - if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); - if (profile.Picture is not null) - await fs.MarkUsageAsync(profile.Picture, -1); - - profile.Picture = picture; - await fs.MarkUsageAsync(picture, 1); - } - - if (request.BackgroundId is not null) - { - var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); - if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); - if (profile.Background is not null) - await fs.MarkUsageAsync(profile.Background, -1); - - profile.Background = background; - await fs.MarkUsageAsync(background, 1); - } - - db.Update(profile); - await db.SaveChangesAsync(); - - await accounts.PurgeAccountCache(currentUser); - - return profile; - } - - [HttpDelete("me")] - [Authorize] - public async Task RequestDeleteAccount() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - try - { - await accounts.RequestAccountDeletion(currentUser); - } - catch (InvalidOperationException) - { - return BadRequest("You already requested account deletion within 24 hours."); - } - - return Ok(); - } - public class RecoveryPasswordRequest { [Required] public string Account { get; set; } = null!; @@ -272,15 +155,6 @@ public class AccountController( public Instant? ClearedAt { get; set; } } - [HttpGet("me/statuses")] - [Authorize] - public async Task> GetCurrentStatus() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var status = await events.GetStatus(currentUser.Id); - return Ok(status); - } - [HttpGet("{name}/statuses")] public async Task> GetOtherStatus(string name) { @@ -291,138 +165,6 @@ public class AccountController( return Ok(status); } - [HttpPatch("me/statuses")] - [Authorize] - [RequiredPermission("global", "accounts.statuses.update")] - public async Task> UpdateStatus([FromBody] StatusRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var now = SystemClock.Instance.GetCurrentInstant(); - var status = await db.AccountStatuses - .Where(e => e.AccountId == currentUser.Id) - .Where(e => e.ClearedAt == null || e.ClearedAt > now) - .OrderByDescending(e => e.CreatedAt) - .FirstOrDefaultAsync(); - if (status is null) return NotFound(); - - status.Attitude = request.Attitude; - status.IsInvisible = request.IsInvisible; - status.IsNotDisturb = request.IsNotDisturb; - status.Label = request.Label; - status.ClearedAt = request.ClearedAt; - - db.Update(status); - await db.SaveChangesAsync(); - events.PurgeStatusCache(currentUser.Id); - - return status; - } - - [HttpPost("me/statuses")] - [Authorize] - [RequiredPermission("global", "accounts.statuses.create")] - public async Task> CreateStatus([FromBody] StatusRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var status = new Status - { - AccountId = currentUser.Id, - Attitude = request.Attitude, - IsInvisible = request.IsInvisible, - IsNotDisturb = request.IsNotDisturb, - Label = request.Label, - ClearedAt = request.ClearedAt - }; - - return await events.CreateStatus(currentUser, status); - } - - [HttpDelete("me/statuses")] - [Authorize] - public async Task DeleteStatus() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var now = SystemClock.Instance.GetCurrentInstant(); - var status = await db.AccountStatuses - .Where(s => s.AccountId == currentUser.Id) - .Where(s => s.ClearedAt == null || s.ClearedAt > now) - .OrderByDescending(s => s.CreatedAt) - .FirstOrDefaultAsync(); - if (status is null) return NotFound(); - - await events.ClearStatus(currentUser, status); - return NoContent(); - } - - [HttpGet("me/check-in")] - [Authorize] - public async Task> GetCheckInResult() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - var userId = currentUser.Id; - - var now = SystemClock.Instance.GetCurrentInstant(); - var today = now.InUtc().Date; - var startOfDay = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); - var endOfDay = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); - - var result = await db.AccountCheckInResults - .Where(x => x.AccountId == userId) - .Where(x => x.CreatedAt >= startOfDay && x.CreatedAt < endOfDay) - .OrderByDescending(x => x.CreatedAt) - .FirstOrDefaultAsync(); - - return result is null ? NotFound() : Ok(result); - } - - [HttpPost("me/check-in")] - [Authorize] - public async Task> DoCheckIn([FromBody] string? captchaToken) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var isAvailable = await events.CheckInDailyIsAvailable(currentUser); - if (!isAvailable) - return BadRequest("Check-in is not available for today."); - - try - { - var needsCaptcha = await events.CheckInDailyDoAskCaptcha(currentUser); - return needsCaptcha switch - { - true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423, - "Captcha is required for this check-in."), - true when !await auth.ValidateCaptcha(captchaToken!) => BadRequest("Invalid captcha token."), - _ => await events.CheckInDaily(currentUser) - }; - } - catch (InvalidOperationException ex) - { - return BadRequest(ex.Message); - } - } - - [HttpGet("me/calendar")] - [Authorize] - public async Task>> GetEventCalendar([FromQuery] int? month, - [FromQuery] int? year) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; - month ??= currentDate.Month; - year ??= currentDate.Year; - - if (month is < 1 or > 12) return BadRequest("Invalid month."); - if (year < 1) return BadRequest("Invalid year."); - - var calendar = await events.GetEventCalendar(currentUser, month.Value, year.Value); - return Ok(calendar); - } - [HttpGet("{name}/calendar")] public async Task>> GetOtherEventCalendar( string name, @@ -444,30 +186,6 @@ public class AccountController( return Ok(calendar); } - [Authorize] - [HttpGet("me/actions")] - [ProducesResponseType>(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task>> GetActionLogs([FromQuery] int take = 20, - [FromQuery] int offset = 0) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); - - var query = db.ActionLogs - .Where(log => log.AccountId == currentUser.Id) - .OrderByDescending(log => log.CreatedAt); - - var total = await query.CountAsync(); - Response.Headers.Append("X-Total", total.ToString()); - - var logs = await query - .Skip(offset) - .Take(take) - .ToListAsync(); - - return Ok(logs); - } - [HttpGet("search")] public async Task> Search([FromQuery] string query, [FromQuery] int take = 20) { diff --git a/DysonNetwork.Sphere/Account/AccountCurrentController.cs b/DysonNetwork.Sphere/Account/AccountCurrentController.cs new file mode 100644 index 0000000..112a6fe --- /dev/null +++ b/DysonNetwork.Sphere/Account/AccountCurrentController.cs @@ -0,0 +1,294 @@ +using System.ComponentModel.DataAnnotations; +using DysonNetwork.Sphere.Auth; +using DysonNetwork.Sphere.Permission; +using DysonNetwork.Sphere.Storage; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Sphere.Account; + +[Authorize] +[ApiController] +[Route("/accounts/me")] +public class AccountCurrentController( + AppDatabase db, + AccountService accounts, + FileService fs, + AccountEventService events, + AuthService auth +) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetCurrentIdentity() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var userId = currentUser.Id; + + var account = await db.Accounts + .Include(e => e.Badges) + .Include(e => e.Profile) + .Where(e => e.Id == userId) + .FirstOrDefaultAsync(); + + return Ok(account); + } + + public class BasicInfoRequest + { + [MaxLength(256)] public string? Nick { get; set; } + [MaxLength(32)] public string? Language { get; set; } + } + + [HttpPatch] + public async Task> UpdateBasicInfo([FromBody] BasicInfoRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id); + + if (request.Nick is not null) account.Nick = request.Nick; + if (request.Language is not null) account.Language = request.Language; + + await db.SaveChangesAsync(); + await accounts.PurgeAccountCache(currentUser); + return currentUser; + } + + public class ProfileRequest + { + [MaxLength(256)] public string? FirstName { get; set; } + [MaxLength(256)] public string? MiddleName { get; set; } + [MaxLength(256)] public string? LastName { get; set; } + [MaxLength(4096)] public string? Bio { get; set; } + + [MaxLength(32)] public string? PictureId { get; set; } + [MaxLength(32)] public string? BackgroundId { get; set; } + } + + [HttpPatch("profile")] + public async Task> UpdateProfile([FromBody] ProfileRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var userId = currentUser.Id; + + var profile = await db.AccountProfiles + .Where(p => p.Account.Id == userId) + .Include(profile => profile.Background) + .Include(profile => profile.Picture) + .FirstOrDefaultAsync(); + if (profile is null) return BadRequest("Unable to get your account."); + + if (request.FirstName is not null) profile.FirstName = request.FirstName; + if (request.MiddleName is not null) profile.MiddleName = request.MiddleName; + if (request.LastName is not null) profile.LastName = request.LastName; + if (request.Bio is not null) profile.Bio = request.Bio; + + if (request.PictureId is not null) + { + var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); + if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); + if (profile.Picture is not null) + await fs.MarkUsageAsync(profile.Picture, -1); + + profile.Picture = picture; + await fs.MarkUsageAsync(picture, 1); + } + + if (request.BackgroundId is not null) + { + var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); + if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); + if (profile.Background is not null) + await fs.MarkUsageAsync(profile.Background, -1); + + profile.Background = background; + await fs.MarkUsageAsync(background, 1); + } + + db.Update(profile); + await db.SaveChangesAsync(); + + await accounts.PurgeAccountCache(currentUser); + + return profile; + } + + [HttpDelete] + public async Task RequestDeleteAccount() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + try + { + await accounts.RequestAccountDeletion(currentUser); + } + catch (InvalidOperationException) + { + return BadRequest("You already requested account deletion within 24 hours."); + } + + return Ok(); + } + + [HttpGet("statuses")] + public async Task> GetCurrentStatus() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var status = await events.GetStatus(currentUser.Id); + return Ok(status); + } + + [HttpPatch("statuses")] + [RequiredPermission("global", "accounts.statuses.update")] + public async Task> UpdateStatus([FromBody] AccountController.StatusRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var now = SystemClock.Instance.GetCurrentInstant(); + var status = await db.AccountStatuses + .Where(e => e.AccountId == currentUser.Id) + .Where(e => e.ClearedAt == null || e.ClearedAt > now) + .OrderByDescending(e => e.CreatedAt) + .FirstOrDefaultAsync(); + if (status is null) return NotFound(); + + status.Attitude = request.Attitude; + status.IsInvisible = request.IsInvisible; + status.IsNotDisturb = request.IsNotDisturb; + status.Label = request.Label; + status.ClearedAt = request.ClearedAt; + + db.Update(status); + await db.SaveChangesAsync(); + events.PurgeStatusCache(currentUser.Id); + + return status; + } + + [HttpPost("statuses")] + [RequiredPermission("global", "accounts.statuses.create")] + public async Task> CreateStatus([FromBody] AccountController.StatusRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var status = new Status + { + AccountId = currentUser.Id, + Attitude = request.Attitude, + IsInvisible = request.IsInvisible, + IsNotDisturb = request.IsNotDisturb, + Label = request.Label, + ClearedAt = request.ClearedAt + }; + + return await events.CreateStatus(currentUser, status); + } + + [HttpDelete("me/statuses")] + public async Task DeleteStatus() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var now = SystemClock.Instance.GetCurrentInstant(); + var status = await db.AccountStatuses + .Where(s => s.AccountId == currentUser.Id) + .Where(s => s.ClearedAt == null || s.ClearedAt > now) + .OrderByDescending(s => s.CreatedAt) + .FirstOrDefaultAsync(); + if (status is null) return NotFound(); + + await events.ClearStatus(currentUser, status); + return NoContent(); + } + + [HttpGet("check-in")] + public async Task> GetCheckInResult() + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var userId = currentUser.Id; + + var now = SystemClock.Instance.GetCurrentInstant(); + var today = now.InUtc().Date; + var startOfDay = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); + var endOfDay = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); + + var result = await db.AccountCheckInResults + .Where(x => x.AccountId == userId) + .Where(x => x.CreatedAt >= startOfDay && x.CreatedAt < endOfDay) + .OrderByDescending(x => x.CreatedAt) + .FirstOrDefaultAsync(); + + return result is null ? NotFound() : Ok(result); + } + + [HttpPost("check-in")] + public async Task> DoCheckIn([FromBody] string? captchaToken) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var isAvailable = await events.CheckInDailyIsAvailable(currentUser); + if (!isAvailable) + return BadRequest("Check-in is not available for today."); + + try + { + var needsCaptcha = await events.CheckInDailyDoAskCaptcha(currentUser); + return needsCaptcha switch + { + true when string.IsNullOrWhiteSpace(captchaToken) => StatusCode(423, + "Captcha is required for this check-in."), + true when !await auth.ValidateCaptcha(captchaToken!) => BadRequest("Invalid captcha token."), + _ => await events.CheckInDaily(currentUser) + }; + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + } + + [HttpGet("calendar")] + public async Task>> GetEventCalendar([FromQuery] int? month, + [FromQuery] int? year) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; + month ??= currentDate.Month; + year ??= currentDate.Year; + + if (month is < 1 or > 12) return BadRequest("Invalid month."); + if (year < 1) return BadRequest("Invalid year."); + + var calendar = await events.GetEventCalendar(currentUser, month.Value, year.Value); + return Ok(calendar); + } + + [HttpGet("actions")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetActionLogs( + [FromQuery] int take = 20, + [FromQuery] int offset = 0 + ) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var query = db.ActionLogs + .Where(log => log.AccountId == currentUser.Id) + .OrderByDescending(log => log.CreatedAt); + + var total = await query.CountAsync(); + Response.Headers.Append("X-Total", total.ToString()); + + var logs = await query + .Skip(offset) + .Take(take) + .ToListAsync(); + + return Ok(logs); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Storage/CloudFile.cs b/DysonNetwork.Sphere/Storage/CloudFile.cs index 5d6af40..5dbc5a7 100644 --- a/DysonNetwork.Sphere/Storage/CloudFile.cs +++ b/DysonNetwork.Sphere/Storage/CloudFile.cs @@ -51,6 +51,8 @@ public class CloudFile : ModelBase /// Metrics /// When this used count keep zero, it means it's not used by anybody, so it can be recycled public int UsedCount { get; set; } = 0; + /// An optional package identifier that indicates the cloud file's usage + [MaxLength(1024)] public string? Usage { get; set; } [JsonIgnore] public Account.Account Account { get; set; } = null!; public Guid AccountId { get; set; }