feat: 支持绑定/换绑2FA找回邮箱
This commit is contained in:
1 parent
1f9c6aed64
commit
8a164c3397
3 files changed
+529
No files matched your search
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Mail;
|
||||
using TelegramPanel.Core.Interfaces;
|
||||
using TelegramPanel.Core.Models;
|
||||
using TelegramPanel.Data.Entities;
|
||||
@@ -326,6 +327,190 @@ public class AccountTelegramToolsService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取两步验证找回邮箱状态(是否已绑定、是否存在待确认的邮箱)。
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error, bool HasTwoFactorPassword, bool HasRecoveryEmail, string? UnconfirmedEmailPattern)>
|
||||
GetTwoFactorRecoveryEmailStatusAsync(int accountId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var pwd = await client.Account_GetPassword();
|
||||
var hasPassword = pwd.current_algo != null;
|
||||
var hasRecoveryEmail = pwd.flags.HasFlag(TL.Account_Password.Flags.has_recovery);
|
||||
var unconfirmed = pwd.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
|
||||
? (pwd.email_unconfirmed_pattern ?? "").Trim()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(unconfirmed))
|
||||
unconfirmed = null;
|
||||
|
||||
return (true, null, hasPassword, hasRecoveryEmail, unconfirmed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg, false, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定/换绑两步验证找回邮箱(会发送验证码到邮箱,需调用 ConfirmTwoFactorRecoveryEmailAsync 确认)。
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error, string? EmailPattern)> SetTwoFactorRecoveryEmailAsync(
|
||||
int accountId,
|
||||
string? currentPassword,
|
||||
string email,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
email = (email ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
return (false, "邮箱不能为空", null);
|
||||
|
||||
try
|
||||
{
|
||||
_ = new MailAddress(email);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, "邮箱格式不正确", null);
|
||||
}
|
||||
|
||||
currentPassword = (currentPassword ?? string.Empty).Trim();
|
||||
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var pwd = await client.Account_GetPassword();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (pwd.current_algo == null)
|
||||
return (false, "该账号未开启两步验证,无法绑定找回邮箱,请先设置二级密码", null);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currentPassword))
|
||||
return (false, "请填写原二级密码", null);
|
||||
|
||||
var oldCheck = await WTelegram.Client.InputCheckPassword(pwd, currentPassword);
|
||||
|
||||
var settings = new TL.Account_PasswordInputSettings
|
||||
{
|
||||
flags = TL.Account_PasswordInputSettings.Flags.has_email,
|
||||
email = email
|
||||
};
|
||||
|
||||
await client.Account_UpdatePasswordSettings(oldCheck, settings);
|
||||
|
||||
// 更新后可通过 getPassword 获取“待确认邮箱”掩码信息
|
||||
var after = await client.Account_GetPassword();
|
||||
var pattern = after.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
|
||||
? (after.email_unconfirmed_pattern ?? "").Trim()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
pattern = null;
|
||||
|
||||
return (true, null, pattern);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认两步验证找回邮箱验证码。
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> ConfirmTwoFactorRecoveryEmailAsync(
|
||||
int accountId,
|
||||
string code,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
code = (code ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return (false, "验证码不能为空");
|
||||
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_ = await client.Account_ConfirmPasswordEmail(code);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重发两步验证找回邮箱验证码(需要先设置邮箱)。
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error, string? EmailPattern, int? CodeLength)> ResendTwoFactorRecoveryEmailAsync(
|
||||
int accountId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ok = await client.Account_ResendPasswordEmail();
|
||||
if (!ok)
|
||||
return (false, "重发失败", null, null);
|
||||
|
||||
var pwd = await client.Account_GetPassword();
|
||||
var pattern = pwd.flags.HasFlag(TL.Account_Password.Flags.has_email_unconfirmed_pattern)
|
||||
? (pwd.email_unconfirmed_pattern ?? "").Trim()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
pattern = null;
|
||||
|
||||
// 该 API 不返回验证码长度,仅返回邮箱掩码信息
|
||||
return (true, null, pattern, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消待确认的找回邮箱验证码。
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> CancelTwoFactorRecoveryEmailAsync(
|
||||
int accountId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_ = await client.Account_CancelPasswordEmail();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> KickAuthorizationAsync(int accountId, long authorizationHash)
|
||||
{
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId);
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
@namespace TelegramPanel.Web.Components.Dialogs
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject TelegramPanel.Core.Services.Telegram.AccountTelegramToolsService AccountTelegramTools
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@using TelegramPanel.Data.Entities
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
|
||||
这里绑定的是 Telegram 两步验证(2FA)的“找回邮箱”。修改后会向新邮箱发送验证码,需要输入验证码确认。
|
||||
</MudAlert>
|
||||
|
||||
@if (!loaded)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
}
|
||||
else if (account == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true">账号不存在</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2">账号:@account.Phone</MudText>
|
||||
|
||||
<MudStack direction="Row" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudChip T="string" Size="Size.Small" Color="@(hasTwoFactorPassword ? Color.Success : Color.Warning)">
|
||||
@(hasTwoFactorPassword ? "已开启2FA" : "未开启2FA")
|
||||
</MudChip>
|
||||
<MudChip T="string" Size="Size.Small" Color="@(hasRecoveryEmail ? Color.Success : Color.Default)">
|
||||
@(hasRecoveryEmail ? "已绑定找回邮箱" : "未绑定找回邮箱")
|
||||
</MudChip>
|
||||
@if (!string.IsNullOrWhiteSpace(unconfirmedEmailPattern))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Warning">待确认:@unconfirmedEmailPattern</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@if (!hasTwoFactorPassword)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
该账号未开启两步验证,无法绑定找回邮箱。请先设置二级密码。
|
||||
</MudAlert>
|
||||
}
|
||||
else if (step == Step.SetEmail)
|
||||
{
|
||||
<MudTextField @bind-Value="currentPassword" Label="原二级密码" Variant="Variant.Outlined"
|
||||
InputType="InputType.Password" Required="true" />
|
||||
|
||||
<MudTextField @bind-Value="email" Label="新找回邮箱" Variant="Variant.Outlined"
|
||||
HelperText="将会向该邮箱发送验证码" Required="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField @bind-Value="code" Label="邮箱验证码" Variant="Variant.Outlined"
|
||||
HelperText="请到邮箱中查看验证码(注意垃圾箱/延迟)" Required="true" />
|
||||
|
||||
<MudStack direction="Row" Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" Disabled="@running" OnClick="Resend">
|
||||
重发验证码
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" Disabled="@running" OnClick="CancelVerification">
|
||||
取消验证
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" Disabled="@running" OnClick="Close">关闭</MudButton>
|
||||
|
||||
@if (loaded && account != null && !hasTwoFactorPassword)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Disabled="@running" OnClick="OpenPasswordDialog">
|
||||
去设置二级密码
|
||||
</MudButton>
|
||||
}
|
||||
else if (loaded && account != null && hasTwoFactorPassword)
|
||||
{
|
||||
@if (step == Step.SetEmail)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@running" OnClick="SendCode">
|
||||
@if (running)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
<span>发送中...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>发送验证码</span>
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@running" OnClick="Confirm">
|
||||
@if (running)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
<span>确认中...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>确认绑定</span>
|
||||
}
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Parameter] public int AccountId { get; set; }
|
||||
|
||||
private Account? account;
|
||||
private bool loaded;
|
||||
private bool running;
|
||||
|
||||
private bool hasTwoFactorPassword;
|
||||
private bool hasRecoveryEmail;
|
||||
private string? unconfirmedEmailPattern;
|
||||
|
||||
private Step step = Step.SetEmail;
|
||||
|
||||
private string currentPassword = "";
|
||||
private string email = "";
|
||||
private string code = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await ReloadStatus();
|
||||
}
|
||||
|
||||
private async Task ReloadStatus()
|
||||
{
|
||||
loaded = false;
|
||||
try
|
||||
{
|
||||
var all = await AccountManagement.GetAllAccountsAsync();
|
||||
account = all.FirstOrDefault(a => a.Id == AccountId);
|
||||
|
||||
if (account != null)
|
||||
{
|
||||
var (ok, err, hasPwd, hasEmail, unconfirmed) = await AccountTelegramTools.GetTwoFactorRecoveryEmailStatusAsync(AccountId);
|
||||
if (!ok)
|
||||
{
|
||||
Snackbar.Add($"获取2FA邮箱状态失败:{err}", Severity.Warning);
|
||||
}
|
||||
hasTwoFactorPassword = hasPwd;
|
||||
hasRecoveryEmail = hasEmail;
|
||||
unconfirmedEmailPattern = unconfirmed;
|
||||
|
||||
step = string.IsNullOrWhiteSpace(unconfirmedEmailPattern) ? Step.SetEmail : Step.Confirm;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendCode()
|
||||
{
|
||||
if (running)
|
||||
return;
|
||||
|
||||
if (account == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currentPassword))
|
||||
{
|
||||
Snackbar.Add("请填写原二级密码", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Snackbar.Add("请填写新找回邮箱", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"确认发送验证码",
|
||||
"将向新邮箱发送验证码,用于绑定/换绑找回邮箱。是否继续?",
|
||||
yesText: "继续", cancelText: "取消");
|
||||
|
||||
if (confirm != true)
|
||||
return;
|
||||
|
||||
running = true;
|
||||
try
|
||||
{
|
||||
var (ok, err, pattern) = await AccountTelegramTools.SetTwoFactorRecoveryEmailAsync(
|
||||
accountId: AccountId,
|
||||
currentPassword: currentPassword,
|
||||
email: email,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
Snackbar.Add($"发送失败:{err}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
unconfirmedEmailPattern = pattern;
|
||||
step = Step.Confirm;
|
||||
code = "";
|
||||
Snackbar.Add("验证码已发送,请到邮箱中查看并输入验证码确认。", Severity.Success);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Confirm()
|
||||
{
|
||||
if (running)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
Snackbar.Add("请填写邮箱验证码", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
running = true;
|
||||
try
|
||||
{
|
||||
var (ok, err) = await AccountTelegramTools.ConfirmTwoFactorRecoveryEmailAsync(AccountId, code, CancellationToken.None);
|
||||
if (!ok)
|
||||
{
|
||||
Snackbar.Add($"确认失败:{err}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("找回邮箱已确认绑定。", Severity.Success);
|
||||
await ReloadStatus();
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Resend()
|
||||
{
|
||||
if (running)
|
||||
return;
|
||||
|
||||
running = true;
|
||||
try
|
||||
{
|
||||
var (ok, err, pattern, _) = await AccountTelegramTools.ResendTwoFactorRecoveryEmailAsync(AccountId, CancellationToken.None);
|
||||
if (!ok)
|
||||
{
|
||||
Snackbar.Add($"重发失败:{err}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
unconfirmedEmailPattern = pattern ?? unconfirmedEmailPattern;
|
||||
Snackbar.Add("验证码已重发。", Severity.Success);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelVerification()
|
||||
{
|
||||
if (running)
|
||||
return;
|
||||
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"取消验证",
|
||||
"将取消当前待确认的邮箱验证码。是否继续?",
|
||||
yesText: "继续", cancelText: "取消");
|
||||
|
||||
if (confirm != true)
|
||||
return;
|
||||
|
||||
running = true;
|
||||
try
|
||||
{
|
||||
var (ok, err) = await AccountTelegramTools.CancelTwoFactorRecoveryEmailAsync(AccountId, CancellationToken.None);
|
||||
if (!ok)
|
||||
{
|
||||
Snackbar.Add($"取消失败:{err}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add("已取消验证。", Severity.Success);
|
||||
await ReloadStatus();
|
||||
}
|
||||
finally
|
||||
{
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
|
||||
private async Task OpenPasswordDialog()
|
||||
{
|
||||
if (account == null)
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["AccountIds"] = new List<int> { account.Id }
|
||||
};
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
DialogService.Show<ChangeTwoFactorPasswordDialog>("设置二级密码", parameters, options);
|
||||
MudDialog.Close();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private enum Step
|
||||
{
|
||||
SetEmail,
|
||||
Confirm
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,8 @@
|
||||
Disabled="@loading" OnClick="@(() => OpenDevices(context))" Title="在线设备" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Password" Size="Size.Small" Color="Color.Warning"
|
||||
Disabled="@loading" OnClick="@(() => OpenChangeTwoFactorPassword(context))" Title="修改二级密码" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AlternateEmail" Size="Size.Small" Color="Color.Warning"
|
||||
Disabled="@loading" OnClick="@(() => OpenChangeTwoFactorRecoveryEmail(context))" Title="绑定/换绑找回邮箱" />
|
||||
<MudIconButton Icon="@(context.IsActive ? Icons.Material.Filled.Stop : Icons.Material.Filled.PlayArrow)"
|
||||
Size="Size.Small" Color="@(context.IsActive ? Color.Warning : Color.Success)"
|
||||
OnClick="@(() => ToggleActive(context))"
|
||||
@@ -248,6 +250,20 @@
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OpenChangeTwoFactorRecoveryEmail(Account account)
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["AccountId"] = account.Id
|
||||
};
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
DialogService.Show<TelegramPanel.Web.Components.Dialogs.ChangeTwoFactorRecoveryEmailDialog>("绑定/换绑找回邮箱", parameters, options);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OpenBatchChangeTwoFactorPassword()
|
||||
{
|
||||
if (loading)
|
||||
|
||||
Reference in new issue
Block a user