✨ Added magic spell page
This commit is contained in:
		@@ -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<ActionResult> 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<ActionResult> 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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<MagicSpellService> logger,
 | 
			
		||||
    IStringLocalizer<EmailResource> localizer
 | 
			
		||||
    IStringLocalizer<EmailResource> localizer,
 | 
			
		||||
    EmailService email
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<MagicSpell> CreateMagicSpell(
 | 
			
		||||
@@ -79,61 +82,62 @@ public class MagicSpellService(
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // switch (spell.Type)
 | 
			
		||||
            // {
 | 
			
		||||
            //     case MagicSpellType.AccountActivation:
 | 
			
		||||
            //         await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
 | 
			
		||||
            //             contact.Account.Nick,
 | 
			
		||||
            //             contact.Content,
 | 
			
		||||
            //             localizer["EmailLandingTitle"],
 | 
			
		||||
            //             new LandingEmailModel
 | 
			
		||||
            //             {
 | 
			
		||||
            //                 Name = contact.Account.Name,
 | 
			
		||||
            //                 Link = link
 | 
			
		||||
            //             }
 | 
			
		||||
            //         );
 | 
			
		||||
            //         break;
 | 
			
		||||
            //     case MagicSpellType.AccountRemoval:
 | 
			
		||||
            //         await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
 | 
			
		||||
            //             contact.Account.Nick,
 | 
			
		||||
            //             contact.Content,
 | 
			
		||||
            //             localizer["EmailAccountDeletionTitle"],
 | 
			
		||||
            //             new AccountDeletionEmailModel
 | 
			
		||||
            //             {
 | 
			
		||||
            //                 Name = contact.Account.Name,
 | 
			
		||||
            //                 Link = link
 | 
			
		||||
            //             }
 | 
			
		||||
            //         );
 | 
			
		||||
            //         break;
 | 
			
		||||
            //     case MagicSpellType.AuthPasswordReset:
 | 
			
		||||
            //         await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
 | 
			
		||||
            //             contact.Account.Nick,
 | 
			
		||||
            //             contact.Content,
 | 
			
		||||
            //             localizer["EmailAccountDeletionTitle"],
 | 
			
		||||
            //             new PasswordResetEmailModel
 | 
			
		||||
            //             {
 | 
			
		||||
            //                 Name = contact.Account.Name,
 | 
			
		||||
            //                 Link = link
 | 
			
		||||
            //             }
 | 
			
		||||
            //         );
 | 
			
		||||
            //         break;
 | 
			
		||||
            //     case MagicSpellType.ContactVerification:
 | 
			
		||||
            //         if (spell.Meta["contact_method"] is not string contactMethod)
 | 
			
		||||
            //             throw new InvalidOperationException("Contact method is not found.");
 | 
			
		||||
            //         await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
 | 
			
		||||
            //             contact.Account.Nick,
 | 
			
		||||
            //             contactMethod!,
 | 
			
		||||
            //             localizer["EmailContactVerificationTitle"],
 | 
			
		||||
            //             new ContactVerificationEmailModel
 | 
			
		||||
            //             {
 | 
			
		||||
            //                 Name = contact.Account.Name,
 | 
			
		||||
            //                 Link = link
 | 
			
		||||
            //             }
 | 
			
		||||
            //         );
 | 
			
		||||
            //         break;
 | 
			
		||||
            //     default:
 | 
			
		||||
            //         throw new ArgumentOutOfRangeException();
 | 
			
		||||
            // }
 | 
			
		||||
            switch (spell.Type)
 | 
			
		||||
            {
 | 
			
		||||
                case MagicSpellType.AccountActivation:
 | 
			
		||||
                    await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
 | 
			
		||||
                        contact.Account.Nick,
 | 
			
		||||
                        contact.Content,
 | 
			
		||||
                        localizer["EmailLandingTitle"],
 | 
			
		||||
                        new LandingEmailModel
 | 
			
		||||
                        {
 | 
			
		||||
                            Name = contact.Account.Name,
 | 
			
		||||
                            Link = link
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                    break;
 | 
			
		||||
                case MagicSpellType.AccountRemoval:
 | 
			
		||||
                    await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
 | 
			
		||||
                        contact.Account.Nick,
 | 
			
		||||
                        contact.Content,
 | 
			
		||||
                        localizer["EmailAccountDeletionTitle"],
 | 
			
		||||
                        new AccountDeletionEmailModel
 | 
			
		||||
                        {
 | 
			
		||||
                            Name = contact.Account.Name,
 | 
			
		||||
                            Link = link
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                    break;
 | 
			
		||||
                case MagicSpellType.AuthPasswordReset:
 | 
			
		||||
                    await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
 | 
			
		||||
                        contact.Account.Nick,
 | 
			
		||||
                        contact.Content,
 | 
			
		||||
                        localizer["EmailAccountDeletionTitle"],
 | 
			
		||||
                        new PasswordResetEmailModel
 | 
			
		||||
                        {
 | 
			
		||||
                            Name = contact.Account.Name,
 | 
			
		||||
                            Link = link
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                    break;
 | 
			
		||||
                case MagicSpellType.ContactVerification:
 | 
			
		||||
                    if (spell.Meta["contact_method"] is not string contactMethod)
 | 
			
		||||
                        throw new InvalidOperationException("Contact method is not found.");
 | 
			
		||||
                    await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
 | 
			
		||||
                        contact.Account.Nick,
 | 
			
		||||
                        contactMethod!,
 | 
			
		||||
                        localizer["EmailContactVerificationTitle"],
 | 
			
		||||
                        new ContactVerificationEmailModel
 | 
			
		||||
                        {
 | 
			
		||||
                            Name = contact.Account.Name,
 | 
			
		||||
                            Link = link
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                    break;
 | 
			
		||||
                case MagicSpellType.AccountDeactivation:
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new ArgumentOutOfRangeException();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception err)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Solarpass</title>
 | 
			
		||||
    %%APP_DATA%%
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div id="app"></div>
 | 
			
		||||
<script type="module" src="/src/main.ts"></script>
 | 
			
		||||
</body>
 | 
			
		||||
    <app-data />
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -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'),
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								DysonNetwork.Pass/Client/src/views/spells.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								DysonNetwork.Pass/Client/src/views/spells.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-full flex items-center justify-center">
 | 
			
		||||
    <n-card class="max-w-lg" title="Spell">
 | 
			
		||||
      <n-alert type="success" v-if="done">
 | 
			
		||||
        The magic spell has been applied successfully. Now you can close this tab and back to the
 | 
			
		||||
        Solar Network!
 | 
			
		||||
      </n-alert>
 | 
			
		||||
      <n-alert type="error" v-else-if="!!error" title="Something went wrong">{{ error }}</n-alert>
 | 
			
		||||
      <div v-else-if="!!spell">
 | 
			
		||||
        <p class="mb-2">Magic spell for {{ spellTypes[spell.type] ?? 'unknown' }}</p>
 | 
			
		||||
        <div class="flex items-center gap-1">
 | 
			
		||||
          <n-icon size="18"><account-circle-outlined /></n-icon>
 | 
			
		||||
          <b>@{{ spell.account.name }}</b>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex items-center gap-1">
 | 
			
		||||
          <n-icon size="18"><play-arrow-filled /></n-icon>
 | 
			
		||||
          <span>Available at</span>
 | 
			
		||||
          <b>{{ new Date(spell.created_at ?? spell.affected_at).toLocaleString() }}</b>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex items-center gap-1" v-if="spell.expired_at">
 | 
			
		||||
          <n-icon size="18"><date-range-filled /></n-icon>
 | 
			
		||||
          <span>Until</span>
 | 
			
		||||
          <b>{{ spell.expired_at.toString() }}</b>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="mt-4">
 | 
			
		||||
          <n-input v-if="spell.type == 3" v-model:value="newPassword" />
 | 
			
		||||
          <n-button type="primary" :loading="submitting" @click="applySpell">
 | 
			
		||||
            <template #icon><check-filled /></template>
 | 
			
		||||
            Apply
 | 
			
		||||
          </n-button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <n-spin v-else size="small" />
 | 
			
		||||
    </n-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NCard, NAlert, NSpin, NIcon, NButton, NInput } from 'naive-ui'
 | 
			
		||||
import {
 | 
			
		||||
  AccountCircleOutlined,
 | 
			
		||||
  PlayArrowFilled,
 | 
			
		||||
  DateRangeFilled,
 | 
			
		||||
  CheckFilled,
 | 
			
		||||
} from '@vicons/material'
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
 | 
			
		||||
const spellWord: string = route.params.word.toString()
 | 
			
		||||
const spell = ref<any>(null)
 | 
			
		||||
const error = ref<string | null>(null)
 | 
			
		||||
 | 
			
		||||
const newPassword = ref<string>()
 | 
			
		||||
 | 
			
		||||
const submitting = ref(false)
 | 
			
		||||
const done = ref(false)
 | 
			
		||||
 | 
			
		||||
const spellTypes = [
 | 
			
		||||
  'Account Acivation',
 | 
			
		||||
  'Account Deactivation',
 | 
			
		||||
  'Account Deletion',
 | 
			
		||||
  'Reset Password',
 | 
			
		||||
  'Contact Method Verification',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
async function fetchSpell() {
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  if (window.__APP_DATA__ != null) {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    spell.value = window.__APP_DATA__['Spell']
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const resp = await fetch(`/api/spells/${encodeURIComponent(spellWord)}`)
 | 
			
		||||
  if (resp.status === 200) {
 | 
			
		||||
    const data = await resp.json()
 | 
			
		||||
    spell.value = data
 | 
			
		||||
  } else {
 | 
			
		||||
    error.value = await resp.text()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function applySpell() {
 | 
			
		||||
  submitting.value = true
 | 
			
		||||
  const resp = await fetch(`/api/spells/${encodeURIComponent(spellWord)}/apply`, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
    body: newPassword.value ? JSON.stringify({ new_password: newPassword.value }) : null,
 | 
			
		||||
  })
 | 
			
		||||
  if (resp.status === 200) {
 | 
			
		||||
    done.value = true
 | 
			
		||||
  } else {
 | 
			
		||||
    error.value = await resp.text()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => fetchSpell())
 | 
			
		||||
</script>
 | 
			
		||||
@@ -18,8 +18,6 @@ public class EmailService(
 | 
			
		||||
        string htmlBody
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        subject = $"[Solarpass] {subject}";
 | 
			
		||||
 | 
			
		||||
        await pusher.SendEmailAsync(
 | 
			
		||||
            new SendEmailRequest()
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								DysonNetwork.Pass/Pages/Data/SpellPageData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Pass/Pages/Data/SpellPageData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -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<IDictionary<string, object?>> 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<string, object?>
 | 
			
		||||
        {
 | 
			
		||||
            ["Spell"] = spell
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -34,6 +34,7 @@ builder.Services.AddAppScheduledJobs();
 | 
			
		||||
 | 
			
		||||
builder.Services.AddTransient<IPageDataProvider, VersionPageData>();
 | 
			
		||||
builder.Services.AddTransient<IPageDataProvider, CaptchaPageData>();
 | 
			
		||||
builder.Services.AddTransient<IPageDataProvider, SpellPageData>();
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public static class PageStartup
 | 
			
		||||
                appData[key] = value;
 | 
			
		||||
 | 
			
		||||
            var json = JsonSerializer.Serialize(appData);
 | 
			
		||||
            html = html.Replace("%%APP_DATA%%", $"<script>window.__APP_DATA__ = {json};</script>");
 | 
			
		||||
            html = html.Replace("<app-data />", $"<script>window.__APP_DATA__ = {json};</script>");
 | 
			
		||||
 | 
			
		||||
            context.Response.ContentType = "text/html";
 | 
			
		||||
            await context.Response.WriteAsync(html);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user