using System.ComponentModel.DataAnnotations; using DysonNetwork.Sphere.Auth; using DysonNetwork.Sphere.Storage; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using NodaTime; namespace DysonNetwork.Sphere.Account; [ApiController] [Route("/accounts")] public class AccountController( AppDatabase db, FileService fs, AuthService auth, MagicSpellService spells, IMemoryCache memCache ) : ControllerBase { [HttpGet("{name}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetByName(string name) { var account = await db.Accounts .Include(e => e.Profile) .Include(e => e.Profile.Picture) .Include(e => e.Profile.Background) .Where(a => a.Name == name) .FirstOrDefaultAsync(); return account is null ? new NotFoundResult() : account; } public class AccountCreateRequest { [Required] [MaxLength(256)] public string Name { get; set; } = string.Empty; [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty; [EmailAddress] [Required] [MaxLength(1024)] public string Email { get; set; } = string.Empty; [Required] [MinLength(4)] [MaxLength(128)] public string Password { get; set; } = string.Empty; [MaxLength(128)] public string Language { get; set; } = "en"; [Required] public string CaptchaToken { get; set; } = string.Empty; } [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateAccount([FromBody] AccountCreateRequest request) { if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token."); var dupeNameCount = await db.Accounts.Where(a => a.Name == request.Name).CountAsync(); if (dupeNameCount > 0) return BadRequest("The name is already taken."); var account = new Account { Name = request.Name, Nick = request.Nick, Language = request.Language, Contacts = new List { new() { Type = AccountContactType.Email, Content = request.Email } }, AuthFactors = new List { new AccountAuthFactor { Type = AccountAuthFactorType.Password, Secret = request.Password }.HashSecret() }, Profile = new Profile() }; await db.Accounts.AddAsync(account); await db.SaveChangesAsync(); var spell = await spells.CreateMagicSpell( account, MagicSpellType.AccountActivation, new Dictionary { { "contact_method", account.Contacts.First().Content } }, expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(7)) ); spells.NotifyMagicSpell(spell); return account; } [Authorize] [HttpGet("me")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetMe() { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var userId = currentUser.Id; var account = await db.Accounts .Include(e => e.Profile) .Include(e => e.Profile.Picture) .Include(e => e.Profile.Background) .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 account) return Unauthorized(); if (request.Nick is not null) account.Nick = request.Nick; if (request.Language is not null) account.Language = request.Language; memCache.Remove($"user_${account.Id}"); await db.SaveChangesAsync(); return account; } 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; } public string? PictureId { get; set; } 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(); memCache.Remove($"user_${userId}"); return profile; } }