@{
Layout = null;
}
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<title>密码迁移管理 - 认证中心</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="@Url.Content("~/css/global.css")" asp-append-version="true"/>
</head>
<body>
<div class="container wide-container">
<div class="header">
<h1>密码迁移管理</h1>
<p>将旧版本无盐密码迁移到新版本加盐密码,提升系统安全性</p>
</div>
@if (TempData["Message"] is string message)
{
<div class="success-message">
@message
</div>
}
@if (TempData["Error"] is string error)
{
<div class="error-message">
@error
</div>
}
<!-- 迁移状态概览 -->
<div class="section">
<h2>迁移状态概览</h2>
<div class="stats-container">
<div class="stat-item">
<div class="stat-number" id="pendingCount">@ViewBag.PendingCount</div>
<div class="stat-label">待迁移用户</div>
</div>
<div class="stat-item">
<div class="stat-number" id="migrationProgress">
@if (ViewBag.PendingCount > 0)
{
<span class="status-pending">进行中</span>
}
else
{
<span class="status-completed">已完成</span>
}
</div>
<div class="stat-label">迁移状态</div>
</div>
</div>
<button type="button" class="submit-button" onclick="refreshStats()">
刷新统计
</button>
</div>
<!-- 批量迁移区域 -->
<div class="section">
<h2>批量迁移</h2>
<p>此操作将自动迁移所有使用默认密码(123456)的旧版本用户。自定义密码的用户将在下次登录时自动迁移。</p>
<div class="info-section">
<div class="info-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="l 12,8 0,4"></path>
<path d="l 12,16 0.01,0"></path>
</svg>
</div>
<div class="info-content">
<h3>迁移说明</h3>
<ul>
<li>只有使用默认密码(123456)的用户会被自动迁移</li>
<li>使用自定义密码的用户需要在下次登录时自动迁移</li>
<li>迁移后的密码将使用加盐哈希,安全性大幅提升</li>
<li>迁移过程是安全的,不会影响用户正常使用</li>
</ul>
</div>
</div>
<form method="post" asp-action="BatchMigratePasswords" onsubmit="return confirmMigration()">
@Html.AntiForgeryToken()
<button type="submit" class="submit-button" id="migrateBtn">
执行批量迁移
</button>
</form>
</div>
<!-- 单用户检查区域 -->
<div class="section">
<h2>单用户检查</h2>
<div class="input-section">
<label class="input-label" for="userId">用户ID</label>
<div class="input-wrapper">
<input
id="userId"
type="text"
class="text-input"
placeholder="请输入用户ID"
/>
</div>
</div>
<div class="action-buttons">
<button type="button" class="submit-button" onclick="checkUserMigration()">
检查迁移状态
</button>
</div>
<div id="userResult" class="result-section" style="display: none;">
<!-- 结果将在这里显示 -->
</div>
</div>
<div class="footer">
<p>密码迁移管理工具 - 仅限系统管理员使用</p>
</div>
</div>
<script src="@Url.Content("~/js/ui-components.js")" asp-append-version="true"></script>
<script>
function confirmMigration() {
const pendingCount = document.getElementById('pendingCount').textContent;
return confirm(`您确定要执行批量密码迁移吗?\n\n此操作将:\n1. 迁移所有使用默认密码的用户\n2. 提升密码安全性\n3. 当前待迁移用户数:${pendingCount}\n\n建议在系统维护期间执行。`);
}
async function refreshStats() {
try {
const response = await fetch('/migration-stats');
const result = await response.json();
if (result.success) {
document.getElementById('pendingCount').textContent = result.pendingCount;
const progressElement = document.getElementById('migrationProgress');
if (result.pendingCount > 0) {
progressElement.innerHTML = '<span class="status-pending">进行中</span>';
} else {
progressElement.innerHTML = '<span class="status-completed">已完成</span>';
}
showToast('统计信息已刷新', 'success');
} else {
showToast(result.message || '刷新失败', 'error');
}
} catch (error) {
showToast('请求失败: ' + error.message, 'error');
}
}
async function checkUserMigration() {
const userId = document.getElementById('userId').value;
if (!userId.trim()) {
alert('请输入用户ID');
return;
}
try {
const response = await fetch('/check-user-migration', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: `userId=${encodeURIComponent(userId)}`
});
const result = await response.json();
displayResult(result, '检查结果');
} catch (error) {
displayResult({ success: false, message: '请求失败: ' + error.message }, '检查结果');
}
}
function displayResult(result, title) {
const resultDiv = document.getElementById('userResult');
const messageClass = result.success ? 'success-message' : 'error-message';
let content = `<h3>${title}</h3>`;
content += `<div class="${messageClass}">${result.message}</div>`;
if (result.success && result.hasOwnProperty('needsMigration')) {
const statusClass = result.needsMigration ? 'status-pending' : 'status-completed';
const statusText = result.needsMigration ? '需要迁移' : '已迁移';
content += `<p><strong>迁移状态:</strong> <span class="${statusClass}">${statusText}</span></p>`;
}
resultDiv.innerHTML = content;
resultDiv.style.display = 'block';
}
</script>
<style>
.section {
margin-bottom: 40px;
padding: 24px;
background: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border-color);
}
.section h2 {
color: var(--text-primary);
margin-bottom: 12px;
font-size: 20px;
font-weight: 600;
}
.section p {
color: var(--text-secondary);
margin-bottom: 20px;
line-height: 1.5;
}
.stats-container {
display: flex;
gap: 24px;
margin-bottom: 20px;
}
.stat-item {
flex: 1;
text-align: center;
padding: 20px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: var(--text-secondary);
}
.status-pending {
color: #F59E0B;
font-weight: 600;
}
.status-completed {
color: #10B981;
font-weight: 600;
}
.info-section {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 16px;
background: linear-gradient(135deg, #DBEAFE 0%, #BFDBFE 100%);
color: #1E40AF;
border: 1px solid #3B82F6;
border-radius: var(--radius-sm);
margin-bottom: 20px;
}
@@media (prefers-color-scheme: dark) {
.info-section {
background: linear-gradient(135deg, #1E3A8A 0%, #1E40AF 100%);
color: #DBEAFE;
border-color: #3B82F6;
}
}
.info-icon svg {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.info-content h3 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
.info-content ul {
margin: 0;
padding-left: 20px;
}
.info-content li {
margin-bottom: 8px;
line-height: 1.4;
}
.result-section {
margin-top: 20px;
padding: 16px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
}
.result-section h3 {
margin-bottom: 12px;
color: var(--text-primary);
font-size: 16px;
}
@@media (max-width: 768px) {
.stats-container {
flex-direction: column;
}
.info-section {
flex-direction: column;
}
}
</style>
</body>
</html>