✨ Added magic spell page
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
@@ -16,4 +17,48 @@ public class MagicSpellController(AppDatabase db, MagicSpellService sp) : Contro
|
|||||||
await sp.NotifyMagicSpell(spell, true);
|
await sp.NotifyMagicSpell(spell, true);
|
||||||
return Ok();
|
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.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Pass.Email;
|
||||||
|
using DysonNetwork.Pass.Pages.Emails;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
@@ -12,7 +14,8 @@ public class MagicSpellService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger,
|
ILogger<MagicSpellService> logger,
|
||||||
IStringLocalizer<EmailResource> localizer
|
IStringLocalizer<EmailResource> localizer,
|
||||||
|
EmailService email
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<MagicSpell> CreateMagicSpell(
|
public async Task<MagicSpell> CreateMagicSpell(
|
||||||
@@ -79,61 +82,62 @@ public class MagicSpellService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 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:
|
case MagicSpellType.AccountDeactivation:
|
||||||
// throw new ArgumentOutOfRangeException();
|
default:
|
||||||
// }
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception err)
|
catch (Exception err)
|
||||||
{
|
{
|
||||||
|
@@ -23,6 +23,8 @@ export default defineConfigWithVueTs(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skipFormatting,
|
skipFormatting,
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Solarpass</title>
|
<title>Solarpass</title>
|
||||||
%%APP_DATA%%
|
<app-data />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -12,6 +12,11 @@ const router = createRouter({
|
|||||||
path: '/captcha',
|
path: '/captcha',
|
||||||
name: 'captcha',
|
name: 'captcha',
|
||||||
component: () => import('../views/captcha.vue'),
|
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
|
string htmlBody
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
subject = $"[Solarpass] {subject}";
|
|
||||||
|
|
||||||
await pusher.SendEmailAsync(
|
await pusher.SendEmailAsync(
|
||||||
new SendEmailRequest()
|
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, VersionPageData>();
|
||||||
builder.Services.AddTransient<IPageDataProvider, CaptchaPageData>();
|
builder.Services.AddTransient<IPageDataProvider, CaptchaPageData>();
|
||||||
|
builder.Services.AddTransient<IPageDataProvider, SpellPageData>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
@@ -27,13 +27,7 @@ public class EmailService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string htmlBody)
|
||||||
{
|
|
||||||
await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody,
|
|
||||||
string? htmlBody)
|
|
||||||
{
|
{
|
||||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||||
|
|
||||||
@@ -42,13 +36,7 @@ public class EmailService
|
|||||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||||
emailMessage.Subject = subject;
|
emailMessage.Subject = subject;
|
||||||
|
|
||||||
var bodyBuilder = new BodyBuilder
|
var bodyBuilder = new BodyBuilder { HtmlBody = htmlBody };
|
||||||
{
|
|
||||||
TextBody = textBody
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(htmlBody))
|
|
||||||
bodyBuilder.HtmlBody = htmlBody;
|
|
||||||
|
|
||||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||||
|
|
||||||
|
@@ -30,7 +30,7 @@ public static class PageStartup
|
|||||||
appData[key] = value;
|
appData[key] = value;
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(appData);
|
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";
|
context.Response.ContentType = "text/html";
|
||||||
await context.Response.WriteAsync(html);
|
await context.Response.WriteAsync(html);
|
||||||
|
Reference in New Issue
Block a user