✨ Reset password
This commit is contained in:
parent
25c721a42b
commit
fcaeb9afbe
@ -34,7 +34,7 @@ public class AccountController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
return account is null ? new NotFoundResult() : account;
|
return account is null ? new NotFoundResult() : account;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{name}/badges")]
|
[HttpGet("{name}/badges")]
|
||||||
[ProducesResponseType<List<Badge>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<Badge>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
@ -46,7 +46,7 @@ public class AccountController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
return account is null ? NotFound() : account.Badges.ToList();
|
return account is null ? NotFound() : account.Badges.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class AccountCreateRequest
|
public class AccountCreateRequest
|
||||||
{
|
{
|
||||||
@ -150,7 +150,7 @@ public class AccountController(
|
|||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
var account = await db.Accounts.FirstAsync(a => a.Id == currentUser.Id);
|
||||||
|
|
||||||
if (request.Nick is not null) account.Nick = request.Nick;
|
if (request.Nick is not null) account.Nick = request.Nick;
|
||||||
if (request.Language is not null) account.Language = request.Language;
|
if (request.Language is not null) account.Language = request.Language;
|
||||||
|
|
||||||
@ -237,6 +237,32 @@ public class AccountController(
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RecoveryPasswordRequest
|
||||||
|
{
|
||||||
|
[Required] public string Account { get; set; } = null!;
|
||||||
|
[Required] public string CaptchaToken { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("recovery/password")]
|
||||||
|
public async Task<ActionResult> RequestResetPassword([FromBody] RecoveryPasswordRequest request)
|
||||||
|
{
|
||||||
|
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
||||||
|
|
||||||
|
var account = await accounts.LookupAccount(request.Account);
|
||||||
|
if (account is null) return BadRequest("Unable to find the account.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await accounts.RequestPasswordReset(account);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return BadRequest("You already requested password reset within 24 hours.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
public class StatusRequest
|
public class StatusRequest
|
||||||
{
|
{
|
||||||
public StatusAttitude Attitude { get; set; }
|
public StatusAttitude Attitude { get; set; }
|
||||||
@ -279,17 +305,17 @@ public class AccountController(
|
|||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (status is null) return NotFound();
|
if (status is null) return NotFound();
|
||||||
|
|
||||||
status.Attitude = request.Attitude;
|
status.Attitude = request.Attitude;
|
||||||
status.IsInvisible = request.IsInvisible;
|
status.IsInvisible = request.IsInvisible;
|
||||||
status.IsNotDisturb = request.IsNotDisturb;
|
status.IsNotDisturb = request.IsNotDisturb;
|
||||||
status.Label = request.Label;
|
status.Label = request.Label;
|
||||||
status.ClearedAt = request.ClearedAt;
|
status.ClearedAt = request.ClearedAt;
|
||||||
|
|
||||||
db.Update(status);
|
db.Update(status);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
events.PurgeStatusCache(currentUser.Id);
|
events.PurgeStatusCache(currentUser.Id);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,13 +368,13 @@ public class AccountController(
|
|||||||
var today = now.InUtc().Date;
|
var today = now.InUtc().Date;
|
||||||
var startOfDay = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
var startOfDay = today.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||||
var endOfDay = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
var endOfDay = today.PlusDays(1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||||
|
|
||||||
var result = await db.AccountCheckInResults
|
var result = await db.AccountCheckInResults
|
||||||
.Where(x => x.AccountId == userId)
|
.Where(x => x.AccountId == userId)
|
||||||
.Where(x => x.CreatedAt >= startOfDay && x.CreatedAt < endOfDay)
|
.Where(x => x.CreatedAt >= startOfDay && x.CreatedAt < endOfDay)
|
||||||
.OrderByDescending(x => x.CreatedAt)
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
return result is null ? NotFound() : Ok(result);
|
return result is null ? NotFound() : Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,30 +443,31 @@ public class AccountController(
|
|||||||
var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true);
|
var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true);
|
||||||
return Ok(calendar);
|
return Ok(calendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("me/actions")]
|
[HttpGet("me/actions")]
|
||||||
[ProducesResponseType<List<ActionLog>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<List<ActionLog>>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
public async Task<ActionResult<List<ActionLog>>> GetActionLogs([FromQuery] int take = 20, [FromQuery] int offset = 0)
|
public async Task<ActionResult<List<ActionLog>>> GetActionLogs([FromQuery] int take = 20,
|
||||||
|
[FromQuery] int offset = 0)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var query = db.ActionLogs
|
var query = db.ActionLogs
|
||||||
.Where(log => log.AccountId == currentUser.Id)
|
.Where(log => log.AccountId == currentUser.Id)
|
||||||
.OrderByDescending(log => log.CreatedAt);
|
.OrderByDescending(log => log.CreatedAt);
|
||||||
|
|
||||||
var total = await query.CountAsync();
|
var total = await query.CountAsync();
|
||||||
Response.Headers.Append("X-Total", total.ToString());
|
Response.Headers.Append("X-Total", total.ToString());
|
||||||
|
|
||||||
var logs = await query
|
var logs = await query
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(logs);
|
return Ok(logs);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")]
|
[HttpGet("search")]
|
||||||
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20)
|
||||||
{
|
{
|
||||||
@ -453,7 +480,7 @@ public class AccountController(
|
|||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/maintenance/ensureProfileCreated")]
|
[HttpPost("/maintenance/ensureProfileCreated")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("maintenance", "accounts.profiles")]
|
[RequiredPermission("maintenance", "accounts.profiles")]
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
|
||||||
using DysonNetwork.Sphere.Localization;
|
|
||||||
using DysonNetwork.Sphere.Permission;
|
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
@ -59,6 +52,18 @@ public class AccountService(
|
|||||||
await spells.NotifyMagicSpell(spell);
|
await spells.NotifyMagicSpell(spell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RequestPasswordReset(Account account)
|
||||||
|
{
|
||||||
|
var spell = await spells.CreateMagicSpell(
|
||||||
|
account,
|
||||||
|
MagicSpellType.AuthPasswordReset,
|
||||||
|
new Dictionary<string, object>(),
|
||||||
|
SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromHours(24)),
|
||||||
|
preventRepeat: true
|
||||||
|
);
|
||||||
|
await spells.NotifyMagicSpell(spell);
|
||||||
|
}
|
||||||
|
|
||||||
/// Maintenance methods for server administrator
|
/// Maintenance methods for server administrator
|
||||||
public async Task EnsureAccountProfileCreated()
|
public async Task EnsureAccountProfileCreated()
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ public enum MagicSpellType
|
|||||||
AccountActivation,
|
AccountActivation,
|
||||||
AccountDeactivation,
|
AccountDeactivation,
|
||||||
AccountRemoval,
|
AccountRemoval,
|
||||||
AuthFactorReset,
|
AuthPasswordReset,
|
||||||
ContactVerification,
|
ContactVerification,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +36,13 @@ public class MagicSpellService(
|
|||||||
.Where(s => s.Type == type)
|
.Where(s => s.Type == type)
|
||||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (existingSpell != null)
|
if (existingSpell != null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Account already has an active magic spell of type {type}");
|
throw new InvalidOperationException($"Account already has an active magic spell of type {type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var spellWord = _GenerateRandomString(128);
|
var spellWord = _GenerateRandomString(128);
|
||||||
var spell = new MagicSpell
|
var spell = new MagicSpell
|
||||||
{
|
{
|
||||||
@ -111,6 +111,18 @@ public class MagicSpellService(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case MagicSpellType.AuthPasswordReset:
|
||||||
|
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||||
|
contact.Account.Name,
|
||||||
|
contact.Content,
|
||||||
|
localizer["EmailAccountDeletionTitle"],
|
||||||
|
new PasswordResetEmailModel
|
||||||
|
{
|
||||||
|
Name = contact.Account.Name,
|
||||||
|
Link = link
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
@ -125,6 +137,16 @@ public class MagicSpellService(
|
|||||||
{
|
{
|
||||||
switch (spell.Type)
|
switch (spell.Type)
|
||||||
{
|
{
|
||||||
|
case MagicSpellType.AuthPasswordReset:
|
||||||
|
throw new ArgumentException(
|
||||||
|
"For password reset spell, please use the ApplyPasswordReset method instead."
|
||||||
|
);
|
||||||
|
case MagicSpellType.AccountRemoval:
|
||||||
|
var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
|
||||||
|
if (account is null) break;
|
||||||
|
db.Accounts.Remove(account);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
break;
|
||||||
case MagicSpellType.AccountActivation:
|
case MagicSpellType.AccountActivation:
|
||||||
var contactMethod = spell.Meta["contact_method"] as string;
|
var contactMethod = spell.Meta["contact_method"] as string;
|
||||||
var contact = await
|
var contact = await
|
||||||
@ -137,7 +159,7 @@ public class MagicSpellService(
|
|||||||
db.Update(contact);
|
db.Update(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
|
account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
|
||||||
if (account is not null)
|
if (account is not null)
|
||||||
{
|
{
|
||||||
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
|
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
@ -162,6 +184,37 @@ public class MagicSpellService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ApplyPasswordReset(MagicSpell spell, string newPassword)
|
||||||
|
{
|
||||||
|
if (spell.Type != MagicSpellType.AuthPasswordReset)
|
||||||
|
throw new ArgumentException("This spell is not a password reset spell.");
|
||||||
|
|
||||||
|
var passwordFactor = await db.AccountAuthFactors
|
||||||
|
.Include(f => f.Account)
|
||||||
|
.Where(f => f.Type == AccountAuthFactorType.Password && f.Account.Id == spell.AccountId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (passwordFactor is null)
|
||||||
|
{
|
||||||
|
var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
|
||||||
|
if (account is null) throw new InvalidOperationException("Both account and auth factor was not found.");
|
||||||
|
passwordFactor = new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.Password,
|
||||||
|
Account = account,
|
||||||
|
Secret = newPassword
|
||||||
|
}.HashSecret();
|
||||||
|
db.AccountAuthFactors.Add(passwordFactor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
passwordFactor.Secret = newPassword;
|
||||||
|
passwordFactor.HashSecret();
|
||||||
|
db.Update(passwordFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private static string _GenerateRandomString(int length)
|
private static string _GenerateRandomString(int length)
|
||||||
{
|
{
|
||||||
using var rng = RandomNumberGenerator.Create();
|
using var rng = RandomNumberGenerator.Create();
|
||||||
|
@ -18,6 +18,8 @@ public class AuthService(IConfiguration config, IHttpClientFactory httpClientFac
|
|||||||
{
|
{
|
||||||
public async Task<bool> ValidateCaptcha(string token)
|
public async Task<bool> ValidateCaptcha(string token)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||||
|
|
||||||
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
var provider = config.GetSection("Captcha")["Provider"]?.ToLower();
|
||||||
var apiSecret = config.GetSection("Captcha")["ApiSecret"];
|
var apiSecret = config.GetSection("Captcha")["ApiSecret"];
|
||||||
|
|
||||||
|
@ -10,4 +10,10 @@ public class AccountDeletionEmailModel
|
|||||||
{
|
{
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public required string Link { get; set; }
|
public required string Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordResetEmailModel
|
||||||
|
{
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required string Link { get; set; }
|
||||||
}
|
}
|
65
DysonNetwork.Sphere/Pages/Emails/PasswordResetEmail.razor
Normal file
65
DysonNetwork.Sphere/Pages/Emails/PasswordResetEmail.razor
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
@using DysonNetwork.Sphere.Localization
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using EmailResource = DysonNetwork.Sphere.Localization.EmailResource
|
||||||
|
|
||||||
|
<EmailLayout>
|
||||||
|
<table class="container">
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0; text-align: center;">
|
||||||
|
@(Localizer["PasswordResetHeader"])
|
||||||
|
</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["PasswordResetPara1"]) @@@Name,
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["PasswordResetPara2"])
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["PasswordResetPara3"])
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="@Link" target="_blank"
|
||||||
|
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
|
||||||
|
@(Localizer["PasswordResetButton"])
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="columns">
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(LocalizerShared["EmailLinkHint"])
|
||||||
|
<br>
|
||||||
|
<a href="@Link" style="color: #2563eb; word-break: break-all;">@Link</a>
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
@(Localizer["PasswordResetPara4"])
|
||||||
|
</p>
|
||||||
|
<p style="color: #374151; margin: 2rem 0 0 0;">
|
||||||
|
@(LocalizerShared["EmailFooter1"]) <br />
|
||||||
|
@(LocalizerShared["EmailFooter2"])
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string Link { get; set; }
|
||||||
|
|
||||||
|
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
|
||||||
|
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
@page "/spells/{spellWord}"
|
@page "/spells/{spellWord}"
|
||||||
|
@using DysonNetwork.Sphere.Account
|
||||||
@model DysonNetwork.Sphere.Pages.Spell.MagicSpellPage
|
@model DysonNetwork.Sphere.Pages.Spell.MagicSpellPage
|
||||||
|
|
||||||
@{
|
@{
|
||||||
@ -25,11 +26,13 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="px-4 py-12 bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-lg rounded-lg mb-6">
|
<div
|
||||||
|
class="px-4 py-12 bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-lg rounded-lg mb-6">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<p>
|
<p>
|
||||||
<span class="font-medium">The spell is for </span>
|
<span class="font-medium">The spell is for </span>
|
||||||
<span class="font-bold">@System.Text.RegularExpressions.Regex.Replace(Model.CurrentSpell!.Type.ToString(), "([a-z])([A-Z])", "$1 $2")</span>
|
<span
|
||||||
|
class="font-bold">@System.Text.RegularExpressions.Regex.Replace(Model.CurrentSpell!.Type.ToString(), "([a-z])([A-Z])", "$1 $2")</span>
|
||||||
</p>
|
</p>
|
||||||
<p><span class="font-medium">for @@</span>@Model.CurrentSpell.Account?.Name</p>
|
<p><span class="font-medium">for @@</span>@Model.CurrentSpell.Account?.Name</p>
|
||||||
</div>
|
</div>
|
||||||
@ -48,6 +51,28 @@
|
|||||||
|
|
||||||
<form method="post" class="mt-4">
|
<form method="post" class="mt-4">
|
||||||
<input type="hidden" asp-for="CurrentSpell!.Id"/>
|
<input type="hidden" asp-for="CurrentSpell!.Id"/>
|
||||||
|
|
||||||
|
@if (Model.CurrentSpell?.Type == MagicSpellType.AuthPasswordReset)
|
||||||
|
{
|
||||||
|
<div class="mb-4">
|
||||||
|
<label
|
||||||
|
asp-for="NewPassword"
|
||||||
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input type="password"
|
||||||
|
asp-for="NewPassword"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
style="padding: 0.5rem 1rem"
|
||||||
|
placeholder="Your new password"
|
||||||
|
class="w-full border-2 border-gray-300 dark:border-gray-600 rounded-lg
|
||||||
|
focus:ring-2 focus:ring-blue-400
|
||||||
|
dark:text-white bg-gray-100 dark:bg-gray-800"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors
|
class="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors
|
||||||
transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400">
|
transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
@ -8,8 +8,8 @@ namespace DysonNetwork.Sphere.Pages.Spell;
|
|||||||
|
|
||||||
public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageModel
|
public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty]
|
[BindProperty] public MagicSpell? CurrentSpell { get; set; }
|
||||||
public MagicSpell? CurrentSpell { get; set; }
|
[BindProperty] public string? NewPassword { get; set; }
|
||||||
|
|
||||||
public bool IsSuccess { get; set; }
|
public bool IsSuccess { get; set; }
|
||||||
|
|
||||||
@ -39,10 +39,13 @@ public class MagicSpellPage(AppDatabase db, MagicSpellService spells) : PageMode
|
|||||||
.Where(e => e.AffectedAt == null || now >= e.AffectedAt)
|
.Where(e => e.AffectedAt == null || now >= e.AffectedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (spell == null)
|
if (spell == null || spell.Type == MagicSpellType.AuthPasswordReset && string.IsNullOrWhiteSpace(NewPassword))
|
||||||
return Page();
|
return Page();
|
||||||
|
|
||||||
await spells.ApplyMagicSpell(spell);
|
if (spell.Type == MagicSpellType.AuthPasswordReset)
|
||||||
|
await spells.ApplyPasswordReset(spell, NewPassword!);
|
||||||
|
else
|
||||||
|
await spells.ApplyMagicSpell(spell);
|
||||||
IsSuccess = true;
|
IsSuccess = true;
|
||||||
return Page();
|
return Page();
|
||||||
}
|
}
|
||||||
|
@ -60,4 +60,25 @@
|
|||||||
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
||||||
<value>Confirm your account deletion</value>
|
<value>Confirm your account deletion</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="EmailPasswordResetTitle" xml:space="preserve">
|
||||||
|
<value>Reset your password</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetButton" xml:space="preserve">
|
||||||
|
<value>Reset Password</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetHeader" xml:space="preserve">
|
||||||
|
<value>Password Reset Request</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara1" xml:space="preserve">
|
||||||
|
<value>Dear,</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara2" xml:space="preserve">
|
||||||
|
<value>We recieved a request to reset your Solar Network account password.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara3" xml:space="preserve">
|
||||||
|
<value>You can click the button below to continue reset your password.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara4" xml:space="preserve">
|
||||||
|
<value>If you didn't request this, you can ignore this email safety.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -53,4 +53,25 @@
|
|||||||
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
<data name="EmailAccountDeletionTitle" xml:space="preserve">
|
||||||
<value>确认删除您的账户</value>
|
<value>确认删除您的账户</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PasswordResetHeader" xml:space="preserve">
|
||||||
|
<value>密码重置请求</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara1" xml:space="preserve">
|
||||||
|
<value>尊敬的 </value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara2" xml:space="preserve">
|
||||||
|
<value>我们收到了重置您 Solar Network 账户密码的请求。</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara3" xml:space="preserve">
|
||||||
|
<value>请点击下方按钮重置您的密码。此链接将在24小时后失效。</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetButton" xml:space="preserve">
|
||||||
|
<value>重置密码</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordResetPara4" xml:space="preserve">
|
||||||
|
<value>如果您并未请求重置密码,你可以安全地忽略此邮件。</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailPasswordResetTitle" xml:space="preserve">
|
||||||
|
<value>重置您的密码</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
Loading…
x
Reference in New Issue
Block a user