diff --git a/DysonNetwork.Pass/Account/MagicSpellController.cs b/DysonNetwork.Pass/Account/MagicSpellController.cs index ec1a905..4735c2a 100644 --- a/DysonNetwork.Pass/Account/MagicSpellController.cs +++ b/DysonNetwork.Pass/Account/MagicSpellController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Pass.Account; @@ -16,4 +17,48 @@ public class MagicSpellController(AppDatabase db, MagicSpellService sp) : Contro await sp.NotifyMagicSpell(spell, true); return Ok(); } + + [HttpGet("{spellWord}")] + public async Task GetMagicSpell(string spellWord) + { + var word = Uri.UnescapeDataString(spellWord); + var spell = await db.MagicSpells + .Where(x => x.Spell == word) + .Include(x => x.Account) + .ThenInclude(x => x.Profile) + .FirstOrDefaultAsync(); + if (spell is null) + return NotFound(); + return Ok(spell); + } + + public record class MagicSpellApplyRequest + { + public string? NewPassword { get; set; } + } + + [HttpPost("{spellWord}/apply")] + public async Task ApplyMagicSpell([FromRoute] string spellWord, [FromBody] MagicSpellApplyRequest request) + { + var word = Uri.UnescapeDataString(spellWord); + var spell = await db.MagicSpells + .Where(x => x.Spell == word) + .Include(x => x.Account) + .ThenInclude(x => x.Profile) + .FirstOrDefaultAsync(); + if (spell is null) + return NotFound(); + try + { + if (spell.Type == MagicSpellType.AuthPasswordReset && request.NewPassword is not null) + await sp.ApplyPasswordReset(spell, request.NewPassword); + else + await sp.ApplyMagicSpell(spell); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + return Ok(); + } } \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/MagicSpellService.cs b/DysonNetwork.Pass/Account/MagicSpellService.cs index 955390e..f4600bf 100644 --- a/DysonNetwork.Pass/Account/MagicSpellService.cs +++ b/DysonNetwork.Pass/Account/MagicSpellService.cs @@ -1,5 +1,7 @@ using System.Security.Cryptography; using System.Text.Json; +using DysonNetwork.Pass.Email; +using DysonNetwork.Pass.Pages.Emails; using DysonNetwork.Pass.Permission; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; @@ -12,7 +14,8 @@ public class MagicSpellService( AppDatabase db, IConfiguration configuration, ILogger logger, - IStringLocalizer localizer + IStringLocalizer localizer, + EmailService email ) { public async Task CreateMagicSpell( @@ -79,61 +82,62 @@ public class MagicSpellService( try { - // switch (spell.Type) - // { - // case MagicSpellType.AccountActivation: - // await email.SendTemplatedEmailAsync( - // contact.Account.Nick, - // contact.Content, - // localizer["EmailLandingTitle"], - // new LandingEmailModel - // { - // Name = contact.Account.Name, - // Link = link - // } - // ); - // break; - // case MagicSpellType.AccountRemoval: - // await email.SendTemplatedEmailAsync( - // contact.Account.Nick, - // contact.Content, - // localizer["EmailAccountDeletionTitle"], - // new AccountDeletionEmailModel - // { - // Name = contact.Account.Name, - // Link = link - // } - // ); - // break; - // case MagicSpellType.AuthPasswordReset: - // await email.SendTemplatedEmailAsync( - // contact.Account.Nick, - // contact.Content, - // localizer["EmailAccountDeletionTitle"], - // new PasswordResetEmailModel - // { - // Name = contact.Account.Name, - // Link = link - // } - // ); - // break; - // case MagicSpellType.ContactVerification: - // if (spell.Meta["contact_method"] is not string contactMethod) - // throw new InvalidOperationException("Contact method is not found."); - // await email.SendTemplatedEmailAsync( - // contact.Account.Nick, - // contactMethod!, - // localizer["EmailContactVerificationTitle"], - // new ContactVerificationEmailModel - // { - // Name = contact.Account.Name, - // Link = link - // } - // ); - // break; - // default: - // throw new ArgumentOutOfRangeException(); - // } + switch (spell.Type) + { + case MagicSpellType.AccountActivation: + await email.SendTemplatedEmailAsync( + contact.Account.Nick, + contact.Content, + localizer["EmailLandingTitle"], + new LandingEmailModel + { + Name = contact.Account.Name, + Link = link + } + ); + break; + case MagicSpellType.AccountRemoval: + await email.SendTemplatedEmailAsync( + contact.Account.Nick, + contact.Content, + localizer["EmailAccountDeletionTitle"], + new AccountDeletionEmailModel + { + Name = contact.Account.Name, + Link = link + } + ); + break; + case MagicSpellType.AuthPasswordReset: + await email.SendTemplatedEmailAsync( + contact.Account.Nick, + contact.Content, + localizer["EmailAccountDeletionTitle"], + new PasswordResetEmailModel + { + Name = contact.Account.Name, + Link = link + } + ); + break; + case MagicSpellType.ContactVerification: + if (spell.Meta["contact_method"] is not string contactMethod) + throw new InvalidOperationException("Contact method is not found."); + await email.SendTemplatedEmailAsync( + contact.Account.Nick, + contactMethod!, + localizer["EmailContactVerificationTitle"], + new ContactVerificationEmailModel + { + Name = contact.Account.Name, + Link = link + } + ); + break; + case MagicSpellType.AccountDeactivation: + default: + throw new ArgumentOutOfRangeException(); + } } catch (Exception err) { diff --git a/DysonNetwork.Pass/Client/eslint.config.ts b/DysonNetwork.Pass/Client/eslint.config.ts index 88dcb9a..07ad90a 100644 --- a/DysonNetwork.Pass/Client/eslint.config.ts +++ b/DysonNetwork.Pass/Client/eslint.config.ts @@ -23,6 +23,8 @@ export default defineConfigWithVueTs( { rules: { 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', }, }, skipFormatting, diff --git a/DysonNetwork.Pass/Client/index.html b/DysonNetwork.Pass/Client/index.html index 08ed20b..d577c0f 100644 --- a/DysonNetwork.Pass/Client/index.html +++ b/DysonNetwork.Pass/Client/index.html @@ -1,14 +1,14 @@ - + - - - - + + + + Solarpass - %%APP_DATA%% - - -
- - + + + +
+ + diff --git a/DysonNetwork.Pass/Client/src/router/index.ts b/DysonNetwork.Pass/Client/src/router/index.ts index 413ea9b..8b755e6 100644 --- a/DysonNetwork.Pass/Client/src/router/index.ts +++ b/DysonNetwork.Pass/Client/src/router/index.ts @@ -12,6 +12,11 @@ const router = createRouter({ path: '/captcha', name: 'captcha', component: () => import('../views/captcha.vue'), + }, + { + path: '/spells/:word', + name: 'spells', + component: () => import('../views/spells.vue'), } ], }) diff --git a/DysonNetwork.Pass/Client/src/views/spells.vue b/DysonNetwork.Pass/Client/src/views/spells.vue new file mode 100644 index 0000000..b9c538f --- /dev/null +++ b/DysonNetwork.Pass/Client/src/views/spells.vue @@ -0,0 +1,99 @@ + + + diff --git a/DysonNetwork.Pass/Email/EmailService.cs b/DysonNetwork.Pass/Email/EmailService.cs index 6384a1a..1c19a60 100644 --- a/DysonNetwork.Pass/Email/EmailService.cs +++ b/DysonNetwork.Pass/Email/EmailService.cs @@ -18,8 +18,6 @@ public class EmailService( string htmlBody ) { - subject = $"[Solarpass] {subject}"; - await pusher.SendEmailAsync( new SendEmailRequest() { diff --git a/DysonNetwork.Pass/Pages/Data/SpellPageData.cs b/DysonNetwork.Pass/Pages/Data/SpellPageData.cs new file mode 100644 index 0000000..50b24b3 --- /dev/null +++ b/DysonNetwork.Pass/Pages/Data/SpellPageData.cs @@ -0,0 +1,28 @@ +using DysonNetwork.Shared.PageData; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Pass.Pages.Data; + +public class SpellPageData(AppDatabase db) : IPageDataProvider +{ + public bool CanHandlePath(PathString path) => path.StartsWithSegments("/spells"); + + public async Task> GetAppDataAsync(HttpContext context) + { + var spellWord = context.Request.Path.Value!.Split('/').Last(); + spellWord = Uri.UnescapeDataString(spellWord); + var now = SystemClock.Instance.GetCurrentInstant(); + var spell = await db.MagicSpells + .Where(e => e.Spell == spellWord) + .Where(e => e.ExpiresAt == null || now < e.ExpiresAt) + .Where(e => e.AffectedAt == null || now >= e.AffectedAt) + .Include(e => e.Account) + .ThenInclude(e => e.Profile) + .FirstOrDefaultAsync(); + return new Dictionary + { + ["Spell"] = spell + }; + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs index 6a18a1b..bb6ce4b 100644 --- a/DysonNetwork.Pass/Program.cs +++ b/DysonNetwork.Pass/Program.cs @@ -34,6 +34,7 @@ builder.Services.AddAppScheduledJobs(); builder.Services.AddTransient(); builder.Services.AddTransient(); +builder.Services.AddTransient(); var app = builder.Build(); diff --git a/DysonNetwork.Pusher/Email/EmailService.cs b/DysonNetwork.Pusher/Email/EmailService.cs index 6ebe5d3..0364c68 100644 --- a/DysonNetwork.Pusher/Email/EmailService.cs +++ b/DysonNetwork.Pusher/Email/EmailService.cs @@ -27,13 +27,7 @@ public class EmailService _logger = logger; } - public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody) - { - await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null); - } - - public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody, - string? htmlBody) + public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string htmlBody) { subject = $"[{_configuration.SubjectPrefix}] {subject}"; @@ -42,13 +36,7 @@ public class EmailService emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail)); emailMessage.Subject = subject; - var bodyBuilder = new BodyBuilder - { - TextBody = textBody - }; - - if (!string.IsNullOrEmpty(htmlBody)) - bodyBuilder.HtmlBody = htmlBody; + var bodyBuilder = new BodyBuilder { HtmlBody = htmlBody }; emailMessage.Body = bodyBuilder.ToMessageBody(); diff --git a/DysonNetwork.Shared/PageData/Startup.cs b/DysonNetwork.Shared/PageData/Startup.cs index 8769d95..c163968 100644 --- a/DysonNetwork.Shared/PageData/Startup.cs +++ b/DysonNetwork.Shared/PageData/Startup.cs @@ -30,7 +30,7 @@ public static class PageStartup appData[key] = value; var json = JsonSerializer.Serialize(appData); - html = html.Replace("%%APP_DATA%%", $""); + html = html.Replace("", $""); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html);