当前位置: 首页 > news >正文

Gemini 的超长回复

以下是对管理员后台功能的更新,包括修改用户密码、用户名、切换用户以及分页和查找功能。

cdn/js/ui-admin.js

这个文件将进行以下修改:

  1. 添加 adminUsersSearchInputadminUsersPerPageSelect 变量,用于用户搜索和每页显示数量的控制。
  2. 更新 loadAdminUsersData 函数,使其支持搜索和分页。
  3. 添加 handleUserSearchhandlePerPageChange 函数,用于处理搜索输入和每页显示数量的变化。
  4. 添加 showEditUserModal 函数,用于显示编辑用户信息的模态框。
  5. 添加 handleEditUserSubmit 函数,用于处理修改用户信息的提交。
  6. 添加 handleSwitchUser 函数,用于切换到指定用户。
  7. 添加 renderPagination 函数,用于渲染分页控件。
let adminUsersListContainer, adminOauthClientsListContainer;
let adminUsersSearchInput, adminUsersPerPageSelect, adminUsersPagination;
let currentPage = 1;
let currentSearchTerm = '';
let currentUsersPerPage = 10;
let totalUsers = 0;
let allUsersCache = [];
let editUserModal, editUserForm, editUserEmailInput, editUsernameInput, editPasswordInput, editPhoneNumberInput, btnCancelEditUser;
function formatDateFromTimestamp(timestamp) {if (!timestamp) return 'N/A';const date = new Date(timestamp * 1000);return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
async function loadAdminUsersData() {adminUsersListContainer = adminUsersListContainer || document.getElementById('admin-users-list-container');adminUsersSearchInput = adminUsersSearchInput || document.getElementById('admin-users-search-input');adminUsersPerPageSelect = adminUsersPerPageSelect || document.getElementById('admin-users-per-page');adminUsersPagination = adminUsersPagination || document.getElementById('admin-users-pagination');if (!adminUsersListContainer) return;adminUsersListContainer.innerHTML = '<p>正在加载用户列表...</p>';if (adminUsersPagination) adminUsersPagination.innerHTML = '';const { ok, data, status } = await window.apiCall('/api/admin/users');if (ok && data.success && Array.isArray(data.users)) {allUsersCache = data.users;filterAndDisplayUsers();} else {adminUsersListContainer.innerHTML = `<p style="color: var(--danger-color);">加载用户列表失败: ${data.error || '状态码 ' + status}</p>`;}
}
function filterAndDisplayUsers() {const searchTerm = currentSearchTerm.toLowerCase();const filteredUsers = allUsersCache.filter(user =>user.email.toLowerCase().includes(searchTerm) ||user.username.toLowerCase().includes(searchTerm) ||(user.phone_number && user.phone_number.toLowerCase().includes(searchTerm)));totalUsers = filteredUsers.length;const totalPages = Math.ceil(totalUsers / currentUsersPerPage);currentPage = Math.min(currentPage, totalPages > 0 ? totalPages : 1);const startIndex = (currentPage - 1) * currentUsersPerPage;const endIndex = startIndex + currentUsersPerPage;const paginatedUsers = filteredUsers.slice(startIndex, endIndex);renderUsersTable(paginatedUsers);renderPagination(totalPages);
}
function renderUsersTable(users) {if (!adminUsersListContainer) return;if (users.length === 0 && currentSearchTerm === '') {adminUsersListContainer.innerHTML = '<p>系统中没有用户。</p>';return;}if (users.length === 0 && currentSearchTerm !== '') {adminUsersListContainer.innerHTML = `<p>没有找到与 "${window.escapeHtml(currentSearchTerm)}" 相关的用户。</p>`;return;}let tableHtml = `<table class="admin-panel-table"><thead><tr><th>邮箱</th><th>用户名</th><th>手机号</th><th>2FA状态</th><th>账户状态</th><th>注册时间</th><th>操作</th></tr></thead><tbody>`;users.forEach(user => {tableHtml += `<tr><td>${window.escapeHtml(user.email)}</td><td>${window.escapeHtml(user.username)}</td><td>${user.phone_number ? window.escapeHtml(user.phone_number) : '未设置'}</td><td>${user.two_factor_enabled ? '已启用' : '未启用'}</td><td class="${user.is_active ? 'status-active' : 'status-inactive'}">${user.is_active ? '已激活' : '已禁用'}</td><td>${formatDateFromTimestamp(user.created_at)}</td><td class="admin-actions"><button class="button small secondary" onclick="window.showEditUserModal('${window.escapeHtml(user.email)}', '${window.escapeHtml(user.username || '')}', '${window.escapeHtml(user.phone_number || '')}')">编辑</button><button class="button small ${user.is_active ? 'danger' : 'success'}" onclick="window.toggleUserStatus('${window.escapeHtml(user.email)}', ${!user.is_active})">${user.is_active ? '禁用' : '激活'}</button><button class="button small primary" onclick="window.handleSwitchUser('${window.escapeHtml(user.email)}')">切换</button></td></tr>`;});tableHtml += '</tbody></table>';adminUsersListContainer.innerHTML = tableHtml;
}
function renderPagination(totalPages) {if (!adminUsersPagination) return;adminUsersPagination.innerHTML = '';if (totalPages <= 1) return;let paginationHtml = '<ul>';paginationHtml += `<li class="${currentPage === 1 ? 'disabled' : ''}"><a href="#" data-page="prev">上一页</a></li>`;let startPage = Math.max(1, currentPage - 2);let endPage = Math.min(totalPages, currentPage + 2);if (startPage > 1) {paginationHtml += `<li><a href="#" data-page="1">1</a></li>`;if (startPage > 2) paginationHtml += `<li class="ellipsis"><span>...</span></li>`;}for (let i = startPage; i <= endPage; i++) {paginationHtml += `<li class="${currentPage === i ? 'active' : ''}"><a href="#" data-page="${i}">${i}</a></li>`;}if (endPage < totalPages) {if (endPage < totalPages - 1) paginationHtml += `<li class="ellipsis"><span>...</span></li>`;paginationHtml += `<li><a href="#" data-page="${totalPages}">${totalPages}</a></li>`;}paginationHtml += `<li class="${currentPage === totalPages ? 'disabled' : ''}"><a href="#" data-page="next">下一页</a></li>`;paginationHtml += '</ul>';adminUsersPagination.innerHTML = paginationHtml;adminUsersPagination.querySelectorAll('a').forEach(link => {link.addEventListener('click', (e) => {e.preventDefault();const page = e.target.dataset.page;if (page === 'prev') {currentPage = Math.max(1, currentPage - 1);} else if (page === 'next') {currentPage = Math.min(totalPages, currentPage + 1);} else {currentPage = parseInt(page);}filterAndDisplayUsers();});});
}
function handleUserSearch() {currentSearchTerm = adminUsersSearchInput.value;currentPage = 1;filterAndDisplayUsers();
}
function handlePerPageChange() {currentUsersPerPage = parseInt(adminUsersPerPageSelect.value);currentPage = 1;filterAndDisplayUsers();
}
async function toggleUserStatus(userEmail, newStatus) {if (typeof window.clearMessages === 'function') window.clearMessages();const actionText = newStatus ? '激活' : '禁用';if (!confirm(`确定要${actionText}用户 ${userEmail} 吗?`)) return;const { ok, data, status } = await window.apiCall(`/api/admin/users/${encodeURIComponent(userEmail)}`, 'PUT', { is_active: newStatus });if (ok && data.success) {if (typeof window.showMessage === 'function') window.showMessage(data.message || `用户 ${userEmail}${actionText}`, 'success');loadAdminUsersData();} else {if (typeof window.showMessage === 'function') window.showMessage(data.error || `操作失败 (${status})`, 'error');}
}
function showEditUserModal(email, username, phoneNumber) {editUserModal.classList.add('active');editUserEmailInput.value = email;editUsernameInput.value = username;editPhoneNumberInput.value = phoneNumber;editPasswordInput.value = '';
}
function hideEditUserModal() {editUserModal.classList.remove('active');
}
async function handleEditUserSubmit(event) {event.preventDefault();if (typeof window.clearMessages === 'function') window.clearMessages();const userEmail = editUserEmailInput.value;const username = editUsernameInput.value;const newPassword = editPasswordInput.value;const phoneNumber = editPhoneNumberInput.value;if (!username || username.trim() === '') {if (typeof window.showMessage === 'function') window.showMessage('用户名不能为空。', 'error');return;}if (username.length < 3 || username.length > 30 || !/^[a-zA-Z0-9_-]+$/.test(username)) {if (typeof window.showMessage === 'function') window.showMessage('用户名必须为3-30位,可包含字母、数字、下划线和连字符', 'error');return;}if (phoneNumber && phoneNumber.trim() !== '' && !/^\+?[0-9\s-]{7,20}$/.test(phoneNumber)) {if (typeof window.showMessage === 'function') window.showMessage('手机号码格式无效', 'error');return;}if (newPassword && newPassword.length < 6) {if (typeof window.showMessage === 'function') window.showMessage('新密码至少需要6个字符。', 'error');return;}const requestBody = { username, phoneNumber: phoneNumber.trim() === '' ? null : phoneNumber.trim() };if (newPassword) {requestBody.newPassword = newPassword;}const { ok, data, status } = await window.apiCall(`/api/admin/users/${encodeURIComponent(userEmail)}`, 'PUT', requestBody);if (ok && data.success) {if (typeof window.showMessage === 'function') window.showMessage(data.message || `用户 ${userEmail} 信息更新成功。`, 'success');hideEditUserModal();loadAdminUsersData();} else {if (typeof window.showMessage === 'function') window.showMessage(data.error || `更新失败 (${status})`, 'error');}
}
async function handleSwitchUser(userEmail) {if (typeof window.clearMessages === 'function') window.clearMessages();if (!confirm(`确定要切换到用户 ${userEmail} 吗?这会登出当前管理员账户。`)) return;const { ok, data, status } = await window.apiCall('/api/admin/switch-user', 'POST', { targetUserEmail: userEmail });if (ok && data.success) {if (typeof window.showMessage === 'function') window.showMessage(`已成功切换到用户 ${userEmail}`, 'success');window.location.href = '/user/profile';} else {if (typeof window.showMessage === 'function') window.showMessage(data.error || `切换用户失败 (${status})`, 'error');}
}
async function loadAdminOauthClientsData() {adminOauthClientsListContainer = adminOauthClientsListContainer || document.getElementById('admin-oauth-clients-list-container');if (!adminOauthClientsListContainer) return;adminOauthClientsListContainer.innerHTML = '<p>正在加载 OAuth 应用列表...</p>';const { ok, data, status } = await window.apiCall('/api/admin/oauth/clients');if (ok && data.success && Array.isArray(data.clients)) {if (data.clients.length === 0) {adminOauthClientsListContainer.innerHTML = '<p>系统中没有已注册的 OAuth 应用。</p>';return;}let tableHtml = `<table class="admin-panel-table"><thead><tr><th>应用名称</th><th>客户端 ID</th><th>所有者邮箱</th><th>回调地址</th><th>状态</th><th>创建时间</th><th>操作</th></tr></thead><tbody>`;data.clients.forEach(client => {let displayRedirectUri = '解析错误';try {const uris = JSON.parse(client.redirect_uris || '[]');displayRedirectUri = uris.length > 0 ? window.escapeHtml(uris[0]) : '未设置';} catch(e) {}const clientStatus = client.status || 'active';tableHtml += `<tr><td>${window.escapeHtml(client.client_name)}</td><td><code>${window.escapeHtml(client.client_id)}</code></td><td>${window.escapeHtml(client.owner_email)}</td><td><code>${displayRedirectUri}</code></td><td class="${clientStatus === 'active' ? 'status-active' : 'status-suspended'}">${clientStatus === 'active' ? '激活' : '暂停'}</td><td>${formatDateFromTimestamp(client.created_at)}</td><td class="admin-actions"><button class="button small ${clientStatus === 'active' ? 'danger' : 'success'}" onclick="toggleOauthClientStatus('${window.escapeHtml(client.client_id)}', '${clientStatus === 'active' ? 'suspended' : 'active'}')">${clientStatus === 'active' ? '暂停' : '激活'}</button></td></tr>`;});tableHtml += '</tbody></table>';adminOauthClientsListContainer.innerHTML = tableHtml;} else {adminOauthClientsListContainer.innerHTML = `<p style="color: var(--danger-color);">加载应用列表失败: ${data.error || '状态码 ' + status}</p>`;}
}
async function toggleOauthClientStatus(clientId, newStatus) {if (typeof window.clearMessages === 'function') window.clearMessages();const actionText = newStatus === 'active' ? '激活' : '暂停';if (!confirm(`确定要${actionText}应用 ${clientId} 吗?`)) return;const { ok, data, status } = await window.apiCall(`/api/admin/oauth/clients/${encodeURIComponent(clientId)}`, 'PUT', { status: newStatus });if (ok && data.success) {if (typeof window.showMessage === 'function') window.showMessage(data.message || `应用 ${clientId}${actionText}`, 'success');loadAdminOauthClientsData();} else {if (typeof window.showMessage === 'function') window.showMessage(data.error || `操作失败 (${status})`, 'error');}
}
document.addEventListener('DOMContentLoaded', () => {editUserModal = document.getElementById('edit-user-modal');editUserForm = document.getElementById('edit-user-form');editUserEmailInput = document.getElementById('edit-user-email');editUsernameInput = document.getElementById('edit-username');editPasswordInput = document.getElementById('edit-password');editPhoneNumberInput = document.getElementById('edit-phone-number');btnCancelEditUser = document.getElementById('btn-cancel-edit-user');adminUsersSearchInput = document.getElementById('admin-users-search-input');adminUsersPerPageSelect = document.getElementById('admin-users-per-page');if (editUserForm) editUserForm.addEventListener('submit', handleEditUserSubmit);if (btnCancelEditUser) btnCancelEditUser.addEventListener('click', hideEditUserModal);if (adminUsersSearchInput) adminUsersSearchInput.addEventListener('input', handleUserSearch);if (adminUsersPerPageSelect) adminUsersPerPageSelect.addEventListener('change', handlePerPageChange);window.loadAdminUsersData = loadAdminUsersData;window.loadAdminOauthClientsData = loadAdminOauthClientsData;window.toggleUserStatus = toggleUserStatus;window.toggleOauthClientStatus = toggleOauthClientStatus;window.showEditUserModal = showEditUserModal;window.handleSwitchUser = handleSwitchUser;
});

my/src/api-handlers.js

这个文件将进行以下修改:

  1. 修改 handleAdminUpdateUser 函数,使其支持修改用户密码。
  2. 添加 handleAdminSwitchUser 函数,用于管理员切换到指定用户。
import { jsonResponse, hashPassword, constantTimeCompare, generateSessionId, generateTotpSecret, verifyTotp, EXTERNAL_PASTE_API_BASE_URL, isValidEmail, isAdminUser } from './helpers.js';
import { verifyTurnstileToken } from './turnstile-handler.js';
export async function authenticateApiRequest(sessionId, env) {
if (!sessionId || !env.DB) {
return null;
}
try {
const sessionStmt = env.DB.prepare("SELECT user_email, expires_at FROM sessions WHERE session_id = ?");
const sessionResult = await sessionStmt.bind(sessionId).first();
const nowSeconds = Math.floor(Date.now() / 1000);
if (sessionResult && sessionResult.expires_at > nowSeconds) {
return sessionResult.user_email;
} else if (sessionResult) {
} else {
}
} catch (dbError) {
}
return null;
}
async function getPasteApiBearerToken(env) {
const token = env.PASTE_API_BEARER_TOKEN;
if (!token) {
throw new Error("External API authentication token is not configured.");
}
return token;
}
export async function handleGetMe(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.DB) return jsonResponse({ error: '服务器配置错误 (DB_NOT_CONFIGURED)' }, 500);
const userDetailsStmt = env.DB.prepare("SELECT email, username, phone_number, two_factor_enabled FROM users WHERE email = ?");
const user = await userDetailsStmt.bind(userEmailFromSession).first();
if (!user) return jsonResponse({ error: '用户未找到' }, 404);
const adminCheck = isAdminUser(userEmailFromSession, env);
return jsonResponse({ ...user, is_admin: adminCheck });
}
export async function handleRegister(request, env, clientIp) {
try {
const reqBody = await request.json();
const { email, username, password, confirmPassword, phoneNumber, turnstileToken } = reqBody;
const turnstileVerification = await verifyTurnstileToken(turnstileToken, env.TURNSTILE_SECRET_KEY, clientIp);
if (!turnstileVerification.success) {
return jsonResponse({ error: turnstileVerification.error || '人机验证失败', details: turnstileVerification['error-codes'] }, 403);
}
if (!email || !isValidEmail(email)) return jsonResponse({ error: '需要有效的邮箱地址' }, 400);
if (!username || username.length < 3 || username.length > 30 || !/^[a-zA-Z0-9_-]+$/.test(username)) return jsonResponse({ error: '用户名必须为3-30位,可包含字母、数字、下划线和连字符' }, 400);
if (!password || password.length < 6) return jsonResponse({ error: '密码至少需要6个字符' }, 400);
if (password !== confirmPassword) return jsonResponse({ error: '两次输入的密码不一致' }, 400);
if (phoneNumber && phoneNumber.trim() !== '' && !/^\+?[0-9\s-]{7,20}$/.test(phoneNumber)) return jsonResponse({ error: '手机号码格式无效' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误 (DB_NOT_CONFIGURED)' }, 500);
const batchStmts = [
env.DB.prepare("SELECT email FROM users WHERE email = ?").bind(email),
env.DB.prepare("SELECT username FROM users WHERE username = ?").bind(username)
];
const [emailExists, usernameExists] = await env.DB.batch(batchStmts);
if (emailExists.results.length > 0) return jsonResponse({ error: '该邮箱已被注册' }, 409);
if (usernameExists.results.length > 0) return jsonResponse({ error: '该用户名已被占用' }, 409);
const hashedPassword = await hashPassword(password);
const stmt = env.DB.prepare('INSERT INTO users (email, username, password_hash, phone_number, is_active, created_at) VALUES (?, ?, ?, ?, 1, ?)');
await stmt.bind(email, username, hashedPassword, phoneNumber && phoneNumber.trim() !== '' ? phoneNumber.trim() : null, Math.floor(Date.now()/1000)).run();
return jsonResponse({ success: true, message: '用户注册成功' }, 201);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMsg = e.cause?.message || e.message || "未知数据库错误";
if (errorMsg.includes("UNIQUE constraint failed")) return jsonResponse({ error: '邮箱或用户名已存在 (DB)' }, 409);
return jsonResponse({ error: `注册时数据库发生错误: ${errorMsg}` }, 500);
}
}
export async function createSessionAndSetCookie(env, email, userAgent, protocol, hostname) {
const sessionId = await generateSessionId();
const nowSeconds = Math.floor(Date.now() / 1000);
const expirationSeconds = nowSeconds + (60 * 60 * 24);
try {
const sessionStmt = env.DB.prepare("INSERT INTO sessions (session_id, user_email, user_agent, created_at, expires_at) VALUES (?, ?, ?, ?, ?)");
await sessionStmt.bind(sessionId, email, userAgent, nowSeconds, expirationSeconds).run();
} catch (dbError) {
return jsonResponse({ error: `无法创建会话: ${dbError.message || '未知数据库错误'}` }, 500);
}
const cookieDomain = (hostname === 'localhost' || hostname.endsWith('.workers.dev') || hostname.endsWith('pages.dev')) ? '' : `Domain=${hostname}; `;
let cookieFlags = `${cookieDomain}Path=/; Max-Age=${60 * 60 * 24}; HttpOnly; SameSite=Strict`;
if (protocol === 'https:') cookieFlags += '; Secure';
const cookieValue = `AuthToken=${sessionId}; ${cookieFlags}`;
return new Response(JSON.stringify({ success: true, message: '登录成功' }), {
status: 200,
headers: { 'Content-Type': 'application/json;charset=UTF-8', 'Set-Cookie': cookieValue }
});
}
export async function handleLogin(request, env, protocol, hostname, clientIp) {
try {
const reqBody = await request.json();
const { identifier, password, turnstileToken } = reqBody;
const turnstileVerification = await verifyTurnstileToken(turnstileToken, env.TURNSTILE_SECRET_KEY, clientIp);
if (!turnstileVerification.success) {
return jsonResponse({ error: turnstileVerification.error || '人机验证失败', details: turnstileVerification['error-codes'] }, 403);
}
const userAgent = request.headers.get('User-Agent') || 'Unknown';
if (!identifier || !password) return jsonResponse({ error: '邮箱/用户名和密码不能为空' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误 (DB_NOT_CONFIGURED)' }, 500);
const userStmt = env.DB.prepare('SELECT email, username, password_hash, two_factor_enabled, two_factor_secret, is_active FROM users WHERE email = ? OR username = ?');
const userResult = await userStmt.bind(identifier, identifier).first();
if (!userResult) return jsonResponse({ error: '邮箱/用户名或密码无效' }, 401);
if (!userResult.is_active) return jsonResponse({ error: '您的账户已被禁用,请联系管理员。' }, 403);
const passwordsMatch = await constantTimeCompare(userResult.password_hash, await hashPassword(password));
if (!passwordsMatch) return jsonResponse({ error: '邮箱/用户名或密码无效' }, 401);
if (userResult.two_factor_enabled && userResult.two_factor_secret) {
return jsonResponse({ success: true, twoFactorRequired: true, email: userResult.email });
}
return createSessionAndSetCookie(env, userResult.email, userAgent, protocol, hostname);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `处理登录请求时发生内部错误: ${errorMessage}` }, 500);
}
}
export async function handleLogin2FAVerify(request, env, protocol, hostname) {
try {
const reqBody = await request.json();
const { email, totpCode } = reqBody;
const userAgent = request.headers.get('User-Agent') || 'Unknown';
if (!email || !totpCode) return jsonResponse({ error: '需要邮箱和两步验证码' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
const userStmt = env.DB.prepare('SELECT email, two_factor_secret, two_factor_enabled, is_active FROM users WHERE email = ?');
const userResult = await userStmt.bind(email).first();
if (!userResult || !userResult.two_factor_enabled || !userResult.two_factor_secret) {
return jsonResponse({ error: '用户未找到或未启用两步验证' }, 401);
}
if (!userResult.is_active) return jsonResponse({ error: '您的账户已被禁用,请联系管理员。' }, 403);
const isTotpValid = await verifyTotp(userResult.two_factor_secret, totpCode);
if (!isTotpValid) return jsonResponse({ error: '两步验证码无效' }, 401);
return createSessionAndSetCookie(env, userResult.email, userAgent, protocol, hostname);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `处理两步验证登录时出错: ${errorMessage}` }, 500);
}
}
export async function handleChangePassword(request, env, authenticatedUserEmail) {
if (!authenticatedUserEmail) return jsonResponse({ error: '用户未认证' }, 401);
try {
const reqBody = await request.json();
const { currentPassword, newPassword } = reqBody;
if (!currentPassword || !newPassword || newPassword.length < 6) return jsonResponse({ error: '当前密码和新密码(至少6位)都是必需的' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
const userStmt = env.DB.prepare('SELECT password_hash FROM users WHERE email = ?');
const userResult = await userStmt.bind(authenticatedUserEmail).first();
if (!userResult) return jsonResponse({ error: '用户不存在或认证错误' }, 404);
if (!await constantTimeCompare(userResult.password_hash, await hashPassword(currentPassword))) return jsonResponse({ error: '当前密码不正确' }, 401);
const hashedNewPassword = await hashPassword(newPassword);
const updateStmt = env.DB.prepare('UPDATE users SET password_hash = ? WHERE email = ?');
await updateStmt.bind(hashedNewPassword, authenticatedUserEmail).run();
const deleteSessionsStmt = env.DB.prepare("DELETE FROM sessions WHERE user_email = ?");
await deleteSessionsStmt.bind(authenticatedUserEmail).run();
return jsonResponse({ success: true, message: '密码修改成功,所有旧会话已失效' });
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `修改密码时发生错误: ${errorMessage}` }, 500);
}
}
export async function handleUpdateProfile(request, env, authenticatedUserEmail) {
if (!authenticatedUserEmail) return jsonResponse({ error: '用户未认证' }, 401);
try {
const reqBody = await request.json();
const { username, phoneNumber } = reqBody;
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
const currentUserStmt = env.DB.prepare("SELECT username, phone_number FROM users WHERE email = ?");
const currentUser = await currentUserStmt.bind(authenticatedUserEmail).first();
if (!currentUser) return jsonResponse({ error: '用户未找到' }, 404);
const updates = [];
const bindings = [];
if (username !== undefined && username !== currentUser.username) {
if (username.length < 3 || username.length > 30 || !/^[a-zA-Z0-9_-]+$/.test(username)) return jsonResponse({ error: '用户名必须为3-30位,可包含字母、数字、下划线和连字符' }, 400);
const usernameExistsStmt = env.DB.prepare("SELECT username FROM users WHERE username = ? AND email != ?");
const usernameExists = await usernameExistsStmt.bind(username, authenticatedUserEmail).first();
if (usernameExists) return jsonResponse({ error: '该用户名已被占用' }, 409);
updates.push("username = ?");
bindings.push(username);
}
const newPhoneNumber = (phoneNumber && phoneNumber.trim() !== '') ? phoneNumber.trim() : null;
if (newPhoneNumber !== currentUser.phone_number) {
if (newPhoneNumber && !/^\+?[0-9\s-]{7,20}$/.test(newPhoneNumber)) return jsonResponse({ error: '手机号码格式无效' }, 400);
updates.push("phone_number = ?");
bindings.push(newPhoneNumber);
}
if (updates.length === 0) return jsonResponse({ success: true, message: '未检测到任何更改' });
bindings.push(authenticatedUserEmail);
const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE email = ?`;
await env.DB.prepare(updateQuery).bind(...bindings).run();
return jsonResponse({ success: true, message: '个人信息更新成功' });
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMsg = e.cause?.message || e.message || "未知数据库错误";
if (errorMsg.includes("UNIQUE constraint failed")) return jsonResponse({ error: '该用户名已被占用 (DB)' }, 409);
return jsonResponse({ error: `更新信息时数据库发生错误: ${errorMsg}` }, 500);
}
}
export async function handleLogout(request, env, sessionIdFromCookie, hostname) {
if (sessionIdFromCookie && env.DB) {
try {
const result = await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionIdFromCookie).run();
if (!(result && result.success && result.meta.changes > 0)) {
}
} catch (dbError) {
}
}
const cookieDomain = (hostname === 'localhost' || hostname.endsWith('.workers.dev') || hostname.endsWith('pages.dev')) ? '' : `Domain=${hostname}; `;
return new Response(JSON.stringify({ success: true, message: '已登出' }), {
status: 200,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Set-Cookie': `AuthToken=; ${cookieDomain}Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Strict`
}
});
}
export async function handle2FAGenerateSecret(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
const userStmt = env.DB.prepare("SELECT username, two_factor_enabled FROM users WHERE email = ?");
const user = await userStmt.bind(userEmailFromSession).first();
if (!user) return jsonResponse({ error: '用户未找到' }, 404);
if (user.two_factor_enabled) return jsonResponse({ error: '两步验证已启用' }, 400);
const secret = await generateTotpSecret();
const issuer = env.OAUTH_ISSUER_NAME || "UserCenterDemo";
const otpauthUri = `otpauth://totp/${issuer}:${encodeURIComponent(user.username || userEmailFromSession)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
return jsonResponse({ success: true, secret: secret, otpauthUri: otpauthUri });
}
export async function handle2FAEnable(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
try {
const reqBody = await request.json();
const { secret, totpCode } = reqBody;
if (!secret || !totpCode) return jsonResponse({ error: '需要密钥和两步验证码' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
const isTotpValid = await verifyTotp(secret, totpCode);
if (!isTotpValid) return jsonResponse({ error: '两步验证码无效' }, 400);
const stmt = env.DB.prepare("UPDATE users SET two_factor_secret = ?, two_factor_enabled = 1 WHERE email = ?");
const result = await stmt.bind(secret, userEmailFromSession).run();
if (result.success && result.meta.changes > 0) return jsonResponse({ success: true, message: '两步验证已成功启用' });
else if (result.success && result.meta.changes === 0) return jsonResponse({ error: '启用两步验证失败,用户不存在或2FA未更新' }, 400);
else return jsonResponse({ error: `启用两步验证时数据库操作失败: ${result.error || '未知D1错误'}` }, 500);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `启用两步验证时出错: ${errorMessage}` }, 500);
}
}
export async function handle2FADisable(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户会话无效,无法禁用两步验证' }, 401);
try {
if (!env.DB) return jsonResponse({ error: '服务器配置错误 (DB_NOT_CONFIGURED)' }, 500);
const userCheckStmt = env.DB.prepare("SELECT two_factor_enabled FROM users WHERE email = ?");
const user = await userCheckStmt.bind(userEmailFromSession).first();
if (!user) return jsonResponse({ error: '用户不存在,无法禁用两步验证' }, 404);
if (!user.two_factor_enabled) return jsonResponse({ success: true, message: '两步验证已经处于禁用状态' });
const stmt = env.DB.prepare("UPDATE users SET two_factor_secret = NULL, two_factor_enabled = 0 WHERE email = ? AND two_factor_enabled = 1");
const result = await stmt.bind(userEmailFromSession).run();
if (result.success && result.meta.changes > 0) return jsonResponse({ success: true, message: '两步验证已成功禁用' });
else if (result.success && result.meta.changes === 0) return jsonResponse({ success: true, message: '两步验证未被禁用,可能之前未启用或用户不匹配' });
else return jsonResponse({ error: `禁用两步验证时数据库操作失败: ${result.error || '未知D1错误'}` }, 500);
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `禁用两步验证时发生意外错误: ${errorMessage}` }, 500);
}
}
export async function handleCreatePasteApiKey(request, env, userEmailFromSession, clientIp) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
try {
const bearerToken = await getPasteApiBearerToken(env);
const reqBody = await request.json();
const { turnstileToken } = reqBody;
const turnstileVerification = await verifyTurnstileToken(turnstileToken, env.TURNSTILE_SECRET_KEY, clientIp);
if (!turnstileVerification.success) {
return jsonResponse({ error: turnstileVerification.error || '人机验证失败', details: turnstileVerification['error-codes'] }, 403);
}
if (!env.DB) return jsonResponse({ error: '服务器配置错误 (DB_NOT_CONFIGURED)' }, 500);
const userStmt = env.DB.prepare("SELECT username FROM users WHERE email = ?");
const user = await userStmt.bind(userEmailFromSession).first();
if (!user || !user.username) {
return jsonResponse({ error: '无法获取用户名以生成API密钥名称' }, 500);
}
const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(3)))
.map(b => b.toString(16).padStart(2, '0')).join('');
const apiKeyName = `${user.username}的云剪贴板密钥${randomHex}`;
const externalApiBody = {
name: apiKeyName, expires_at: null, text_permission: true,
file_permission: true, mount_permission: false, custom_key: null,
};
const response = await fetch(`${EXTERNAL_PASTE_API_BASE_URL}/api/admin/api-keys`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${bearerToken}`, 'Content-Type': 'application/json', 'Accept': 'application/json',
},
body: JSON.stringify(externalApiBody)
});
const responseDataText = await response.text();
let data;
try {
data = JSON.parse(responseDataText);
} catch (e) {
return jsonResponse({ error: `创建外部API密钥失败:无法解析响应 (状态 ${response.status})`, responseText: responseDataText }, 502);
}
if (!response.ok) {
return jsonResponse({ error: data.message || data.error || `创建外部API密钥失败 (状态 ${response.status})` }, response.status);
}
return jsonResponse(data, response.status);
} catch (error) {
if (error.message === "External API authentication token is not configured.") {
return jsonResponse({ error: error.message }, 500);
}
if (error instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = error.message || '未知错误';
return jsonResponse({ error: `代理创建外部API密钥时出错: ${errorMessage}` }, 500);
}
}
export async function handleGetCloudPcKey(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.cloudpc) {
return jsonResponse({ error: "服务器配置错误 (KV_NOT_BOUND)" }, 500);
}
try {
const userKeyRecord = await env.cloudpc.get(userEmailFromSession, { type: "json" });
if (userKeyRecord && userKeyRecord.apiKey) {
const actualApiKey = userKeyRecord.apiKey;
const usageCountStr = await env.cloudpc.get(actualApiKey);
let currentUsage = 0;
if (usageCountStr !== null) {
const parsedCount = parseInt(usageCountStr, 10);
if (!isNaN(parsedCount)) currentUsage = parsedCount;
else currentUsage = 0;
} else {
currentUsage = (typeof userKeyRecord.usageCount === 'number') ? userKeyRecord.usageCount : 0;
}
return jsonResponse({ apiKey: actualApiKey, usageCount: currentUsage, message: "Cloud PC 密钥已存在" }, 200);
} else {
return jsonResponse({ apiKey: null, usageCount: 0, message: "尚未创建 Cloud PC 密钥" }, 200);
}
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取 Cloud PC 密钥时出错: ${errorMessage}` }, 500);
}
}
export async function handleCreateCloudPcKey(request, env, userEmailFromSession, clientIp) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.cloudpc) {
return jsonResponse({ error: "服务器配置错误 (KV_NOT_BOUND)" }, 500);
}
try {
const existingKvEntry = await env.cloudpc.get(userEmailFromSession);
if (existingKvEntry !== null) {
return jsonResponse({ error: "Cloud PC 密钥已存在,每位用户只能创建一个" }, 409);
}
const reqBody = await request.json();
const { turnstileToken } = reqBody;
const turnstileVerification = await verifyTurnstileToken(turnstileToken, env.TURNSTILE_SECRET_KEY, clientIp);
if (!turnstileVerification.success) {
return jsonResponse({ error: turnstileVerification.error || '人机验证失败', details: turnstileVerification['error-codes'] }, 403);
}
const newApiKey = crypto.randomUUID();
const initialUsageCount = 1;
await env.cloudpc.put(userEmailFromSession, JSON.stringify({ apiKey: newApiKey, usageCount: initialUsageCount }));
await env.cloudpc.put(newApiKey, String(initialUsageCount));
return jsonResponse({
success: true, message: "Cloud PC 密钥创建成功", apiKey: newApiKey, usageCount: initialUsageCount
}, 201);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `创建 Cloud PC 密钥时出错: ${errorMessage}` }, 500);
}
}
export async function handleGetGreenHubKeys(request, env, userEmailFromSession) {
if (!userEmailFromSession) {
return jsonResponse({ error: '用户未认证,无法获取 GreenHub 激活码' }, 401);
}
const preEncodedStripeKey = env.STRIPE_SECRET_KEY;
if (!preEncodedStripeKey) {
return jsonResponse({ error: '服务器配置错误,无法连接到支付服务 (Stripe Key Missing)' }, 500);
}
try {
const stripeApiUrl = 'https://api.stripe.com/v1/customers';
const response = await fetch(stripeApiUrl, {
method: 'GET',
headers: {
'Authorization': `Basic ${preEncodedStripeKey}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: '未知 Stripe API 错误' } }));
return jsonResponse({ error: `获取客户数据失败 (Stripe: ${errorData.error?.message || response.statusText})` }, response.status);
}
const result = await response.json();
const licenseCodes = [];
if (result.data && Array.isArray(result.data)) {
for (const customer of result.data) {
if (customer.metadata && customer.metadata.license_code) {
licenseCodes.push(customer.metadata.license_code);
} else if (customer.metadata && Object.keys(customer.metadata).length > 0) {
}
}
}
if (licenseCodes.length > 0) {
return jsonResponse({ success: true, license_codes: licenseCodes });
} else {
return jsonResponse({ success: true, license_codes: [], message: '未找到与您的账户关联的 GreenHub 激活码。' });
}
} catch (error) {
const errorMessage = error.message || '未知错误';
return jsonResponse({ error: `请求 GreenHub 激活码时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
async function getOrCreateConversation(user1Email, user2Email, env) {
const p1 = user1Email < user2Email ? user1Email : user2Email;
const p2 = user1Email < user2Email ? user2Email : user1Email;
let conversation = await env.DB.prepare(
"SELECT conversation_id FROM conversations WHERE participant1_email = ? AND participant2_email = ?"
).bind(p1, p2).first();
if (conversation) {
return conversation.conversation_id;
} else {
const newConversationId = crypto.randomUUID();
const now = Date.now();
try {
await env.DB.prepare(
"INSERT INTO conversations (conversation_id, participant1_email, participant2_email, last_message_at, created_at) VALUES (?, ?, ?, ?, ?)"
).bind(newConversationId, p1, p2, now, now).run();
return newConversationId;
} catch (e) {
return null;
}
}
}
export async function handleSendMessage(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const reqBody = await request.json();
const { receiverEmail, content } = reqBody;
if (!receiverEmail || !content || content.trim() === "") {
return jsonResponse({ error: '接收者邮箱和消息内容不能为空' }, 400);
}
if (receiverEmail === userEmailFromSession) {
return jsonResponse({ error: '不能给自己发送消息' }, 400);
}
if (!isValidEmail(receiverEmail)) {
return jsonResponse({ error: '接收者邮箱格式无效' }, 400);
}
const receiverUser = await env.DB.prepare("SELECT email FROM users WHERE email = ?").bind(receiverEmail).first();
if (!receiverUser) {
return jsonResponse({ error: '接收者用户不存在' }, 404);
}
const conversationId = await getOrCreateConversation(userEmailFromSession, receiverEmail, env);
if (!conversationId) {
return jsonResponse({ error: '无法创建或获取对话' }, 500);
}
const now = Date.now();
await env.DB.prepare("UPDATE conversations SET last_message_at = ? WHERE conversation_id = ?")
.bind(now, conversationId).run();
const messageId = crypto.randomUUID();
await env.DB.prepare(
"INSERT INTO messages (message_id, conversation_id, sender_email, receiver_email, content, sent_at, is_read) VALUES (?, ?, ?, ?, ?, ?, 0)"
).bind(messageId, conversationId, userEmailFromSession, receiverEmail, content.trim(), now).run();
return jsonResponse({ success: true, message: '消息已发送', messageId: messageId, conversationId: conversationId }, 201);
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `发送消息时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleGetConversations(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const conversations = await env.DB.prepare(
`SELECT
c.conversation_id,
c.last_message_at,
CASE
WHEN c.participant1_email = ? THEN u2.username
ELSE u1.username
END as other_participant_username,
CASE
WHEN c.participant1_email = ? THEN c.participant2_email
ELSE c.participant1_email
END as other_participant_email,
(SELECT content FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_content,
(SELECT sender_email FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_sender,
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.conversation_id AND m.receiver_email = ? AND m.is_read = 0) as unread_count
FROM conversations c
LEFT JOIN users u1 ON c.participant1_email = u1.email
LEFT JOIN users u2 ON c.participant2_email = u2.email
WHERE c.participant1_email = ? OR c.participant2_email = ?
ORDER BY c.last_message_at DESC`
).bind(userEmailFromSession, userEmailFromSession, userEmailFromSession, userEmailFromSession, userEmailFromSession).all();
return jsonResponse({ success: true, conversations: conversations.results || [] });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取对话列表时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleGetMessagesForConversation(request, env, userEmailFromSession, conversationId) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!conversationId) return jsonResponse({ error: '缺少对话ID' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const conversationCheck = await env.DB.prepare(
"SELECT conversation_id FROM conversations WHERE conversation_id = ? AND (participant1_email = ? OR participant2_email = ?)"
).bind(conversationId, userEmailFromSession, userEmailFromSession).first();
if (!conversationCheck) {
return jsonResponse({ error: '无权访问此对话或对话不存在' }, 403);
}
const messages = await env.DB.prepare(
`SELECT m.message_id, m.sender_email, u_sender.username as sender_username, m.content, m.sent_at, m.is_read
FROM messages m
JOIN users u_sender ON m.sender_email = u_sender.email
WHERE m.conversation_id = ?
ORDER BY m.sent_at ASC`
).bind(conversationId).all();
await env.DB.prepare(
"UPDATE messages SET is_read = 1 WHERE conversation_id = ? AND receiver_email = ? AND is_read = 0"
).bind(conversationId, userEmailFromSession).run();
return jsonResponse({ success: true, messages: messages.results || [] });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取消息时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleMarkConversationAsRead(request, env, userEmailFromSession, conversationId) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!conversationId) return jsonResponse({ error: '缺少对话ID' }, 400);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const conversationCheck = await env.DB.prepare(
"SELECT conversation_id FROM conversations WHERE conversation_id = ? AND (participant1_email = ? OR participant2_email = ?)"
).bind(conversationId, userEmailFromSession, userEmailFromSession).first();
if (!conversationCheck) {
return jsonResponse({ error: '无权操作此对话或对话不存在' }, 403);
}
const result = await env.DB.prepare(
"UPDATE messages SET is_read = 1 WHERE conversation_id = ? AND receiver_email = ? AND is_read = 0"
).bind(conversationId, userEmailFromSession).run();
return jsonResponse({ success: true, message: '对话已标记为已读', updated_count: result.meta.changes || 0 });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `标记已读时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleGetUnreadMessageCount(request, env, userEmailFromSession) {
if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const result = await env.DB.prepare(
"SELECT COUNT(*) as unread_count FROM messages WHERE receiver_email = ? AND is_read = 0"
).bind(userEmailFromSession).first();
return jsonResponse({ success: true, unread_count: result ? result.unread_count : 0 });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取未读消息数时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminListUsers(request, env) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const { results } = await env.DB.prepare("SELECT email, username, phone_number, two_factor_enabled, is_active, created_at FROM users ORDER BY created_at DESC").all();
return jsonResponse({ success: true, users: results || [] });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取用户列表时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminGetUser(request, env, targetUserEmail) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const user = await env.DB.prepare("SELECT email, username, phone_number, two_factor_enabled, is_active, created_at FROM users WHERE email = ?").bind(targetUserEmail).first();
if (!user) return jsonResponse({ error: '用户未找到' }, 404);
return jsonResponse({ success: true, user: user });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取用户信息时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminUpdateUser(request, env, targetUserEmail) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const reqBody = await request.json();
const { is_active, username, phoneNumber, newPassword } = reqBody;
const updates = [];
const bindings = [];
if (typeof is_active === 'boolean') {
updates.push("is_active = ?");
bindings.push(is_active ? 1 : 0);
}
if (username !== undefined) {
if (username.length < 3 || username.length > 30 || !/^[a-zA-Z0-9_-]+$/.test(username)) return jsonResponse({ error: '用户名必须为3-30位,可包含字母、数字、下划线和连字符' }, 400);
const usernameExistsStmt = env.DB.prepare("SELECT username FROM users WHERE username = ? AND email != ?");
const usernameExists = await usernameExistsStmt.bind(username, targetUserEmail).first();
if (usernameExists) return jsonResponse({ error: '该用户名已被占用' }, 409);
updates.push("username = ?");
bindings.push(username);
}
if (phoneNumber !== undefined) {
const newPhoneNumber = (phoneNumber && phoneNumber.trim() !== '') ? phoneNumber.trim() : null;
if (newPhoneNumber && !/^\+?[0-9\s-]{7,20}$/.test(newPhoneNumber)) return jsonResponse({ error: '手机号码格式无效' }, 400);
updates.push("phone_number = ?");
bindings.push(newPhoneNumber);
}
if (newPassword !== undefined && newPassword.length > 0) {
if (newPassword.length < 6) return jsonResponse({ error: '新密码至少需要6个字符。' }, 400);
const hashedNewPassword = await hashPassword(newPassword);
updates.push("password_hash = ?");
bindings.push(hashedNewPassword);
const deleteSessionsStmt = env.DB.prepare("DELETE FROM sessions WHERE user_email = ?");
await deleteSessionsStmt.bind(targetUserEmail).run();
}
if (updates.length === 0) {
return jsonResponse({ success: true, message: '未检测到任何更改' });
}
bindings.push(targetUserEmail);
const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE email = ?`;
const result = await env.DB.prepare(updateQuery).bind(...bindings).run();
if (result.success && result.meta.changes > 0) {
return jsonResponse({ success: true, message: `用户 ${targetUserEmail} 信息已更新。` });
} else if (result.success && result.meta.changes === 0) {
return jsonResponse({ error: '用户未找到或状态未改变' }, 404);
} else {
return jsonResponse({ error: '更新用户状态时数据库操作失败' }, 500);
}
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.cause?.message || e.message || "未知数据库错误";
return jsonResponse({ error: `更新用户状态时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminSwitchUser(request, env, targetUserEmail, hostname, protocol) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const user = await env.DB.prepare("SELECT email, username, is_active FROM users WHERE email = ?").bind(targetUserEmail).first();
if (!user) return jsonResponse({ error: '用户未找到' }, 404);
if (!user.is_active) return jsonResponse({ error: '目标账户已被禁用,无法切换。' }, 403);
const userAgent = request.headers.get('User-Agent') || 'Unknown (Admin Switch)';
return createSessionAndSetCookie(env, user.email, userAgent, protocol, hostname);
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `切换用户时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminListOauthClients(request, env) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const { results } = await env.DB.prepare("SELECT client_id, client_name, owner_email, redirect_uris, created_at, status FROM oauth_clients ORDER BY created_at DESC").all();
return jsonResponse({ success: true, clients: results || [] });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取OAuth应用列表时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminGetOauthClient(request, env, targetClientId) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const client = await env.DB.prepare("SELECT client_id, client_name, owner_email, client_website, client_description, redirect_uris, allowed_scopes, grant_types_allowed, created_at, status FROM oauth_clients WHERE client_id = ?").bind(targetClientId).first();
if (!client) return jsonResponse({ error: 'OAuth应用未找到' }, 404);
return jsonResponse({ success: true, client: client });
} catch (e) {
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `获取OAuth应用信息时发生服务器内部错误: ${errorMessage}` }, 500);
}
}
export async function handleAdminUpdateOauthClient(request, env, targetClientId) {
if (!env.DB) return jsonResponse({ error: '服务器配置错误' }, 500);
try {
const reqBody = await request.json();
const { status } = reqBody;
if (typeof status !== 'string' || !['active', 'suspended'].includes(status.toLowerCase())) {
return jsonResponse({ error: '请求体中缺少有效的 status 字段 (active/suspended)' }, 400);
}
const result = await env.DB.prepare("UPDATE oauth_clients SET status = ? WHERE client_id = ?").bind(status.toLowerCase(), targetClientId).run();
if (result.success && result.meta.changes > 0) {
return jsonResponse({ success: true, message: `OAuth应用 ${targetClientId} 状态已更新为 ${status}` });
} else if (result.success && result.meta.changes === 0) {
return jsonResponse({ error: 'OAuth应用未找到或状态未改变' }, 404);
} else {
return jsonResponse({ error: '更新OAuth应用状态时数据库操作失败' }, 500);
}
} catch (e) {
if (e instanceof SyntaxError) return jsonResponse({ error: '无效的 JSON 请求体' }, 400);
const errorMessage = e.message || '未知错误';
return jsonResponse({ error: `更新OAuth应用状态时发生服务器内部错误: ${errorMessage}` }, 500);
}
}

my/src/html-ui.js

这个文件将进行以下修改:

  1. 在用户管理面板添加搜索输入框和每页显示数量选择框。
  2. 在用户管理面板添加分页容器。
  3. 添加编辑用户信息的模态框。
export function generateHtmlUi(currentPath = '/', env = {}) {const showLogin = currentPath === '/' || currentPath === '/user/login';const showRegister = currentPath === '/user/register';const showAccountSection = currentPath.startsWith('/user/') && !showLogin && !showRegister && currentPath !== '/user/help' && !currentPath.startsWith('/user/admin');const showAdminSection = currentPath.startsWith('/user/admin');const authSectionVisible = showLogin || showRegister;const siteName = "用户中心";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const cdnBaseUrl = env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";// Standard Icons (some might be replaced by user's new SVGs)const menuIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>`;const userIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 mr-1"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-5.5-2.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zM10 12a5.99 5.99 0 00-4.793 2.39A6.483 6.483 0 0010 16.5a6.483 6.483 0 004.793-2.11A5.99 5.99 0 0010 12z" clip-rule="evenodd" /></svg>`;const moonIcon = `<svg id="theme-toggle-dark-icon" class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>`;const messageIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" /></svg>`;const warningIconLarge = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 text-yellow-500"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.008v.008H12v-.008z" /></svg>`;const mailIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>`;// User provided SVGs (replacing class="size-6" with "h-5 w-5 mr-2" or "h-5 w-5")const adminIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"><path fill-rule="evenodd" d="M6.455 1.45A.5.5 0 0 1 6.952 1h2.096a.5.5 0 0 1 .497.45l.186 1.858a4.996 4.996 0 0 1 1.466.848l1.703-.769a.5.5 0 0 1 .639.206l1.047 1.814a.5.5 0 0 1-.14.656l-1.517 1.09a5.026 5.026 0 0 1 0 1.694l1.516 1.09a.5.5 0 0 1 .141.656l-1.047 1.814a.5.5 0 0 1-.639.206l-1.703-.768c-.433.36-.928.649-1.466.847l-.186 1.858a.5.5 0 0 1-.497.45H6.952a.5.5 0 0 1-.497-.45l-.186-1.858a4.993 4.993 0 0 1-1.466-.848l-1.703.769a.5.5 0 0 1-.639-.206l-1.047-1.814a.5.5 0 0 1 .14-.656l1.517-1.09a5.033 5.033 0 0 1 0-1.694l-1.516-1.09a.5.5 0 0 1-.141-.656L2.46 3.593a.5.5 0 0 1 .639-.206l1.703.769c.433-.36.928-.65 1.466-.848l.186-1.858Zm-.177 7.567-.022-.037a2 2 0 0 1 3.466-1.997l.022.037a2 2 0 0 1-3.466 1.997Z" clip-rule="evenodd" /></svg>`; // This is Question Mark Circle, per user's "设置的svg"const sunIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /></svg>`;const helpIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>`;const oauthIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 mr-2"><path fill-rule="evenodd" d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" /></svg>`;const apiKeyIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 mr-2"><path fill-rule="evenodd" d="M15.75 1.5a6.75 6.75 0 0 0-6.651 7.906c.067.39-.032.717-.221.906l-6.5 6.499a3 3 0 0 0-.878 2.121v2.818c0 .414.336.75.75.75H6a.75.75 0 0 0 .75-.75v-1.5h1.5A.75.75 0 0 0 9 19.5V18h1.5a.75.75 0 0 0 .53-.22l2.658-2.658c.19-.189.517-.288.906-.22A6.75 6.75 0 1 0 15.75 1.5Zm0 3a.75.75 0 0 0 0 1.5A2.25 2.25 0 0 1 18 8.25a.75.75 0 0 0 1.5 0 3.75 3.75 0 0 0-3.75-3.75Z" clip-rule="evenodd" /></svg>`;const personalInfoIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 mr-2"><path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" /></svg>`;const securityIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 mr-2"><path fill-rule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clip-rule="evenodd" /></svg>`;return `
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${siteName}</title><link rel="icon" href="${faviconDataUri}"><link rel="stylesheet" href="${cdnBaseUrl}/css/style.css"><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script><script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js" defer></script><style>.hidden { display: none !important; }.modal {position: fixed; top: 0; left: 0; width: 100%; height: 100%;background-color: rgba(0,0,0,0.6); display: flex;justify-content: center; align-items: center; z-index: 2000;opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0s linear 0.3s;}.modal.active { opacity: 1; visibility: visible; transition: opacity 0.3s ease; }.modal-content {background-color: var(--current-surface-color); color: var(--current-text-color);padding: 25px 30px; border-radius: var(--border-radius);box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 90%; max-width: 500px;transform: translateY(-20px); transition: transform 0.3s ease;}.modal.active .modal-content { transform: translateY(0); }.modal-content h4 { margin-top: 0; text-align: left; font-size: 1.4em; color: var(--current-heading-color); }.modal-buttons { margin-top: 25px; text-align: right; }.modal-buttons button { margin-left: 10px; }.license-code-list { list-style-type: none; padding-left: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--current-border-color); border-radius: var(--border-radius); padding: 10px; background-color: var(--current-bg-color); }.license-code-list li { padding: 5px 0; border-bottom: 1px dashed var(--current-border-color); font-family: var(--font-family-mono); font-size: 0.9em;}.license-code-list li:last-child { border-bottom: none; }.admin-panel-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9em; }.admin-panel-table th, .admin-panel-table td { border: 1px solid var(--current-border-color); padding: 8px 12px; text-align: left; }.admin-panel-table th { background-color: var(--current-surface-color); font-weight: 600; }.admin-panel-table tr:nth-child(even) { background-color: color-mix(in srgb, var(--current-surface-color) 95%, var(--current-bg-color));}.admin-panel-table td code { background-color: color-mix(in srgb, var(--current-text-color) 10%, transparent); padding: 2px 4px; border-radius: 3px; font-family: var(--font-family-mono); }.status-active { color: var(--success-color); font-weight: bold; }.status-inactive, .status-suspended { color: var(--danger-color); font-weight: bold; }.admin-actions button { margin-right: 5px; }.pagination { display: flex; justify-content: center; margin-top: 20px; }.pagination ul { list-style: none; padding: 0; margin: 0; display: flex; gap: 5px; }.pagination li a, .pagination li span {display: block; padding: 8px 12px; border: 1px solid var(--current-border-color);border-radius: var(--border-radius); color: var(--primary-color); text-decoration: none;transition: background-color 0.2s ease, color 0.2s ease;}.pagination li a:hover { background-color: color-mix(in srgb, var(--primary-color) 10%, transparent); }.pagination li.active a { background-color: var(--primary-color); color: var(--text-color-inverted); border-color: var(--primary-color); }.pagination li.disabled a, .pagination li.disabled span { color: var(--text-color-muted); cursor: not-allowed; opacity: 0.6; }.pagination li.active a:hover { background-color: var(--primary-color); }.pagination li.ellipsis span { border-color: transparent; }.admin-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }.admin-controls label { margin-right: 10px; }.admin-controls select { padding: 5px 8px; border-radius: var(--border-radius); border: 1px solid var(--current-border-color); background-color: var(--current-input-bg); color: var(--current-text-color); }.admin-controls input[type="search"] { flex-grow: 1; margin-right: 10px; max-width: 250px; }</style>
</head>
<body class="font-sans antialiased"><div id="app-wrapper" class="app-wrapper"><header id="top-bar" class="top-bar"><div class="top-bar-left"><button id="sidebar-toggle" class="sidebar-toggle-button" aria-label="切换导航菜单">${menuIcon}</button><span class="site-title">${siteName}</span></div><div class="top-bar-right"><button type="button" id="theme-toggle-button" aria-label="切换主题" class="theme-toggle-button">${sunIcon}${moonIcon}</button><button type="button" id="top-bar-messaging-button" aria-label="私信" class="messaging-button hidden">${messageIcon}<span id="unread-messages-indicator" class="unread-badge hidden"></span></button><div id="top-bar-auth-buttons" class="auth-actions hidden"><a href="/user/register" class="button secondary small">注册</a><a href="/user/login" class="button primary small">登录</a></div><div id="top-bar-user-info" class="user-info-dropdown hidden"><button id="user-menu-button" class="user-menu-button">${userIcon}<span class="username-text" id="top-bar-user-username"></span></button><div id="user-dropdown-menu" class="dropdown-menu hidden"><div class="dropdown-user-email" id="top-bar-user-email"></div><hr class="dropdown-divider"><a href="/user/profile" id="top-bar-account-link" class="dropdown-item">${personalInfoIcon}账户设置</a><a href="/user/admin" id="top-bar-admin-link" class="dropdown-item hidden">${adminIcon}管理员面板</a><a href="/user/help" class="dropdown-item">${helpIcon}帮助与API示例</a><button type="button" class="dropdown-item logout" id="top-bar-logout-button">安全登出</button></div></div></div></header><aside id="sidebar" class="sidebar"><nav class="sidebar-nav"><ul id="account-tabs"><li><a href="/user/profile" id="nav-profile" class="sidebar-link" data-pane-id="tab-content-personal-info">${personalInfoIcon}个人信息</a></li><li><a href="/user/security" id="nav-security" class="sidebar-link" data-pane-id="tab-content-security-settings">${securityIcon}安全设置</a></li><li><a href="/user/api-keys" id="nav-api-keys" class="sidebar-link" data-pane-id="tab-content-api-keys">${apiKeyIcon}获取/查看密钥</a></li><li><a href="/user/applications" id="nav-applications" class="sidebar-link" data-pane-id="tab-content-my-applications">${oauthIcon}我的应用</a></li></ul><ul id="admin-tabs" class="hidden" style="margin-top: 20px; border-top: 1px solid var(--current-border-color); padding-top:10px;"><li><a href="/user/admin/users" id="nav-admin-users" class="sidebar-link" data-pane-id="tab-content-admin-users">${adminIcon}用户管理</a></li><li><a href="/user/admin/apps" id="nav-admin-apps" class="sidebar-link" data-pane-id="tab-content-admin-apps">${adminIcon}应用管理</a></li></ul></nav></aside><div id="sidebar-overlay" class="sidebar-overlay hidden"></div><main id="main-content" class="main-content"><div class="container"><div id="message-area" class="message hidden" role="alert"></div><div id="auth-section" class="auth-section ${authSectionVisible ? '' : 'hidden'}"><div id="login-form" class="form-container ${showLogin ? '' : 'hidden'}"><h2>用户登录</h2><form id="login-form-el"><div class="form-group"><label for="login-identifier">邮箱或用户名</label><input type="text" id="login-identifier" name="identifier" required autocomplete="username email" placeholder="请输入您的邮箱或用户名"></div><div class="form-group"><label for="login-password">密码</label><input type="password" id="login-password" name="password" required autocomplete="current-password" placeholder="请输入密码"></div><div id="login-2fa-section" class="form-group hidden"><label for="login-totp-code">两步验证码</label><input type="text" id="login-totp-code" name="totpCode" pattern="\\d{6}" maxlength="6" placeholder="请输入6位验证码"></div><div class="cf-turnstile" data-sitekey="${env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA'}" data-callback="turnstileCallbackLogin"></div><button type="submit" class="button primary full-width">登录</button></form><div class="toggle-link">还没有账户? <a href="/user/register">立即注册</a></div></div><div id="register-form" class="form-container ${showRegister ? '' : 'hidden'}"><h2>新用户注册</h2><form id="register-form-el"><div class="form-group"><label for="register-username">用户名</label><input type="text" id="register-username" name="username" required minlength="3" maxlength="30" placeholder="3-30位字符,可包含字母、数字、下划线和连字符"></div><div class="form-group"><label for="register-email">邮箱地址</label><input type="email" id="register-email" name="email" required autocomplete="email" placeholder="例如:user@example.com"></div><div class="form-group"><label for="register-phone">手机号码 (可选)</label><input type="tel" id="register-phone" name="phoneNumber" autocomplete="tel" placeholder="例如:+8613800138000"></div><div class="form-group"><label for="register-password">设置密码 (至少6位)</label><input type="password" id="register-password" name="password" required minlength="6" autocomplete="new-password" placeholder="请输入至少6位密码"></div><div class="form-group"><label for="register-confirm-password">确认密码</label><input type="password" id="register-confirm-password" name="confirmPassword" required minlength="6" autocomplete="new-password" placeholder="请再次输入密码"></div><div class="cf-turnstile" data-sitekey="${env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA'}" data-callback="turnstileCallbackRegister"></div><button type="submit" class="button primary full-width">创建账户</button></form><div class="toggle-link">已经有账户了? <a href="/user/login">返回登录</a></div></div></div><div id="logged-in-section" class="account-content ${showAccountSection ? '' : 'hidden'}"><div id="tab-content-personal-info" class="tab-pane hidden" data-path="/user/profile"><h3>${personalInfoIcon}个人信息修改</h3><form id="update-profile-form" class="content-form"><div class="form-group"><label for="profile-username">用户名</label><input type="text" id="profile-username" name="username" required minlength="3" maxlength="30" placeholder="3-30位字符"></div><div class="form-group"><label for="profile-phone">手机号码 (可选)</label><input type="tel" id="profile-phone" name="phoneNumber" placeholder="例如:+8613800138000"></div><div class="form-group"><label for="profile-email">邮箱地址 (不可修改)</label><input type="email" id="profile-email" name="email" readonly disabled></div><button type="submit" class="button primary">保存个人信息</button></form></div><div id="tab-content-security-settings" class="tab-pane hidden" data-path="/user/security"><h3>${securityIcon}安全设置</h3><ul class="security-settings-list"><li class="security-setting-item"><div class="setting-entry" data-target="change-password-content-panel"><span class="entry-title">修改密码</span><span class="entry-arrow">▼</span></div><div id="change-password-content-panel" class="setting-content-panel hidden"><form id="change-password-form" class="content-form"><div class="form-group"><label for="current-password">当前密码</label><input type="password" id="current-password" name="currentPassword" required autocomplete="current-password" placeholder="请输入您当前的密码"></div><div class="form-group"><label for="new-password">新密码 (至少6位)</label><input type="password" id="new-password" name="newPassword" required minlength="6" autocomplete="new-password" placeholder="请输入新的密码"></div><button type="submit" class="button secondary">确认修改密码</button></form></div></li><li class="security-setting-item"><div class="setting-entry" data-target="2fa-content-panel"><span class="entry-title">两步验证 (2FA)</span><span class="entry-status" id="2fa-entry-status">(未知)</span><span class="entry-arrow">▼</span></div><div id="2fa-content-panel" class="setting-content-panel hidden"><p class="description-text">使用两步验证码可以帮您双重保护账户安全。</p><div id="2fa-status-section" class="status-display">当前状态: <span id="2fa-current-status">未知</span></div><div id="2fa-controls" style="margin-bottom: 15px;"><button type="button" id="btn-init-enable-2fa" class="button success small hidden">启用两步验证</button><button type="button" id="btn-disable-2fa" class="button danger small hidden">禁用两步验证</button></div><div id="2fa-setup-section" class="hidden" style="margin-top:20px;"><p>1. 使用您的身份验证器应用扫描二维码或手动输入密钥。</p><div style="text-align: center; margin: 15px 0;"><div id="qrcode-display"></div></div><p>密钥链接: <code id="otpauth-uri-text-display" class="otpauth-uri-text"></code></p><input type="hidden" id="2fa-temp-secret"><div class="form-group" style="margin-top:10px;"><label for="2fa-setup-code">6位验证码</label><input type="text" id="2fa-setup-code" name="totpCode" pattern="\\d{6}" maxlength="6" placeholder="请输入验证码"></div><div class="form-actions"><button type="button" id="btn-complete-enable-2fa" class="button success">验证并启用</button><button type="button" id="btn-cancel-2fa-setup" class="button secondary">取消</button></div></div></div></li></ul></div><div id="tab-content-api-keys" class="tab-pane hidden" data-path="/user/api-keys"><h3>${apiKeyIcon}获取/查看密钥</h3><ul class="security-settings-list"><li class="security-setting-item"><div class="setting-entry" data-target="paste-api-key-content-panel"><span class="entry-title">云剪贴板 API 密钥</span><span class="entry-arrow">▼</span></div><div id="paste-api-key-content-panel" class="setting-content-panel hidden"><p class="description-text">API 密钥名称将自动生成,仅拥有文本和文件权限。</p><p class="description-text">访问云剪贴板服务:<a href="http://go.qmwneb946.dpdns.org/?LinkId=37" target="_blank" rel="noopener noreferrer" class="external-link">http://go.qmwneb946.dpdns.org/?LinkId=37</a></p><form id="create-paste-api-key-form" class="content-form" style="margin-top:15px;"><div class="cf-turnstile" data-sitekey="${env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA'}" data-callback="turnstileCallbackPasteApi"></div><button type="submit" class="button success">创建云剪贴板 API 密钥</button></form><div id="newly-created-api-key-display" class="hidden api-key-display" style="margin-top:15px;"><h5>新创建的云剪贴板 API 密钥:</h5><div class="api-key-value-container"><input type="text" id="new-api-key-value" readonly><button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-api-key-value').value, '云剪贴板 API 密钥')">复制</button></div></div></div></li><li class="security-setting-item"><div class="setting-entry" data-target="cloud-pc-key-content-panel"><span class="entry-title">Cloud PC 密钥</span><span class="entry-status" id="cloud-pc-key-entry-status">(点击展开查看状态)</span><span class="entry-arrow">▼</span></div><div id="cloud-pc-key-content-panel" class="setting-content-panel hidden"><div id="cloud-pc-key-status-area" class="status-display">正在加载 Cloud PC 密钥状态...</div><p class="description-text">访问 Cloud PC 服务:<a href="http://go.qmwneb946.dpdns.org/?LinkId=17" target="_blank" rel="noopener noreferrer" class="external-link">http://go.qmwneb946.dpdns.org/?LinkId=17</a></p><form id="create-cloud-pc-key-form" class="content-form hidden" style="margin-top:15px;"><p class="description-text">每位用户仅可创建一次,获得 1 次使用次数。</p><div class="cf-turnstile" data-sitekey="${env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA'}" data-callback="turnstileCallbackCloudPc"></div><button type="submit" class="button success">创建 Cloud PC 密钥</button></form><div id="existing-cloud-pc-key-display" class="hidden api-key-display" style="margin-top:15px;"><h5>您的 Cloud PC 密钥:</h5><div class="api-key-value-container"><input type="text" id="cloud-pc-api-key-value" readonly><button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('cloud-pc-api-key-value').value, 'Cloud PC API 密钥')">复制</button></div><p>剩余使用次数: <strong id="cloud-pc-usage-count"></strong></p></div></div></li><li class="security-setting-item"><div class="setting-entry" data-target="greenhub-key-content-panel"><span class="entry-title">GreenHub 激活码</span><span class="entry-arrow">▼</span></div><div id="greenhub-key-content-panel" class="setting-content-panel hidden"><p class="description-text">查看您已获取的 GreenHub 激活码。</p><button type="button" id="btn-fetch-greenhub-keys" class="button primary">获取 GreenHub 激活码</button><div id="greenhub-codes-display" style="margin-top:15px;"><p class="placeholder-text">点击按钮获取激活码。</p></div></div></li></ul></div><div id="tab-content-my-applications" class="tab-pane hidden" data-path="/user/applications"><h3>${oauthIcon}我的 OAuth 应用</h3><div class="setting-block"><h4>注册新应用</h4><form id="register-oauth-client-form" class="content-form"><div class="form-group"><label for="oauth-client-name">应用名称</label><input type="text" id="oauth-client-name" name="clientName" required maxlength="50" placeholder="例如:我的博客评论系统"></div><div class="form-group"><label for="oauth-client-website">应用主页 (可选)</label><input type="url" id="oauth-client-website" name="clientWebsite" maxlength="200" placeholder="例如:https://myblog.com"></div><div class="form-group"><label for="oauth-client-description">应用描述 (可选)</label><input type="text" id="oauth-client-description" name="clientDescription" maxlength="200" placeholder="简要描述您的应用"></div><div class="form-group"><label for="oauth-client-redirect-uri">回调地址 (Redirect URI)</label><input type="url" id="oauth-client-redirect-uri" name="redirectUri" required placeholder="例如:https://myblog.com/oauth/callback"><p class="input-hint">必须是 HTTPS 地址。</p></div><div class="cf-turnstile" data-sitekey="${env.TURNSTILE_SITE_KEY || '1x00000000000000000000AA'}" data-callback="turnstileCallbackOauthClient"></div><button type="submit" class="button success">注册应用</button></form><div id="new-oauth-client-credentials" class="hidden" style="margin-top: 20px;"><h5 style="color: var(--primary-color);">应用注册成功!</h5><p class="description-text">请妥善保管您的应用凭据,特别是 <strong>客户端密钥 (Client Secret)</strong>,它将仅显示这一次。</p><div class="application-card"><p><strong>客户端 ID:</strong> <code id="new-client-id-display"></code> <button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-client-id-display').textContent, '客户端 ID')">复制</button></p><p><strong>客户端密钥:</strong> <code id="new-client-secret-display"></code> <button type="button" class="button small secondary" onclick="copyToClipboard(document.getElementById('new-client-secret-display').textContent, '客户端密钥')">复制</button></p></div><div class="new-client-secret-warning"><strong>重要提示:</strong> 客户端密钥非常敏感,请立即复制并安全存储。</div></div></div><hr class="section-divider"><h4>已注册的应用</h4><div id="registered-oauth-clients-list"><p>正在加载应用列表...</p></div></div><div id="tab-content-messaging" class="tab-pane hidden" data-path="/user/messaging"><div class="messaging-tab-header">${mailIcon}<h3>个人私信</h3></div><div class="new-conversation-trigger" id="new-conversation-area-messaging"><input type="email" id="new-conversation-email" placeholder="输入对方邮箱开始新对话..."><button type="button" id="btn-start-new-conversation" class="button primary small">开始</button></div><div class="messaging-layout-new"><div class="messaging-contacts-panel"><div class="contact-search-bar"><input type="search" id="contact-search-input" placeholder="搜索联系人..."></div><h4 class="recent-contacts-title">最近联系</h4><ul id="conversations-list" class="contact-list"><p class="placeholder-text">正在加载对话...</p></ul></div><div id="messages-area" class="message-display-panel"><div id="messages-list" class="messages-list"><div id="messages-loading-indicator-wrapper" class="messages-loading-indicator-wrapper hidden"><div class="spinner"></div></div><div id="load-more-messages-button-wrapper" class="load-more-messages-button-wrapper hidden"><button type="button" id="load-more-messages-button" class="button secondary small load-more-messages-button">加载更多...</button></div><div class="empty-messages-placeholder">${warningIconLarge}<p>选择一个联系人开始聊天</p><span>或通过上方搜索框发起新的对话。</span></div></div><div id="message-input-area" class="message-input-area hidden"><textarea id="message-input" placeholder="输入消息..." rows="1"></textarea><button type="button" id="btn-send-message" class="button primary">发送</button></div></div></div></div></div><div id="admin-section" class="admin-content ${showAdminSection ? '' : 'hidden'}"><div id="tab-content-admin-users" class="tab-pane hidden" data-path="/user/admin/users"><h3>用户管理</h3><div class="admin-controls"><input type="search" id="admin-users-search-input" placeholder="搜索用户邮箱/用户名/手机号..."><div><label for="admin-users-per-page">每页显示:</label><select id="admin-users-per-page"><option value="10">10</option><option value="20">20</option><option value="50">50</option></select></div></div><div id="admin-users-list-container"><p>正在加载用户列表...</p></div><div id="admin-users-pagination" class="pagination"></div></div><div id="tab-content-admin-apps" class="tab-pane hidden" data-path="/user/admin/apps"><h3>OAuth 应用管理</h3><div id="admin-oauth-clients-list-container"><p>正在加载应用列表...</p></div></div></div></div></main></div><div id="edit-oauth-client-modal" class="modal hidden"><div class="modal-content"><h4 id="edit-oauth-client-modal-title">编辑应用信息</h4><form id="edit-oauth-client-form"><input type="hidden" id="edit-client-id" name="clientId"><div class="form-group"><label for="edit-oauth-client-name">应用名称</label><input type="text" id="edit-oauth-client-name" name="clientName" required maxlength="50"></div><div class="form-group"><label for="edit-oauth-client-website">应用主页 (可选)</label><input type="url" id="edit-oauth-client-website" name="clientWebsite" maxlength="200"></div><div class="form-group"><label for="edit-oauth-client-description">应用描述 (可选)</label><input type="text" id="edit-oauth-client-description" name="clientDescription" maxlength="200"></div><div class="form-group"><label for="edit-oauth-client-redirect-uri">回调地址 (Redirect URI)</label><input type="url" id="edit-oauth-client-redirect-uri" name="redirectUri" required><p class="input-hint">必须是 HTTPS 地址。</p></div><div class="modal-buttons"><button type="button" id="btn-cancel-edit-oauth-client" class="button secondary">取消</button><button type="submit" class="button success">保存更改</button></div></form></div></div><div id="edit-user-modal" class="modal hidden"><div class="modal-content"><h4>编辑用户信息</h4><form id="edit-user-form"><div class="form-group"><label for="edit-user-email">邮箱地址</label><input type="email" id="edit-user-email" name="email" readonly disabled></div><div class="form-group"><label for="edit-username">用户名</label><input type="text" id="edit-username" name="username" required minlength="3" maxlength="30" placeholder="3-30位字符,可包含字母、数字、下划线和连字符"></div><div class="form-group"><label for="edit-phone-number">手机号码 (可选)</label><input type="tel" id="edit-phone-number" name="phoneNumber" placeholder="例如:+8613800138000"></div><div class="form-group"><label for="edit-password">新密码 (留空则不修改)</label><input type="password" id="edit-password" name="newPassword" minlength="6" autocomplete="new-password" placeholder="输入新密码或留空"></div><div class="modal-buttons"><button type="button" id="btn-cancel-edit-user" class="button secondary">取消</button><button type="submit" class="button primary">保存更改</button></div></form></div></div><script src="${cdnBaseUrl}/js/main.js" defer></script><script src="${cdnBaseUrl}/js/ui-personal-info.js" defer></script><script src="${cdnBaseUrl}/js/ui-security-settings.js" defer></script><script src="${cdnBaseUrl}/js/ui-api-keys.js" defer></script><script src="${cdnBaseUrl}/js/ui-oauth-apps.js" defer></script><script src="${cdnBaseUrl}/js/ui-messaging.js" defer></script><script src="${cdnBaseUrl}/js/ui-admin.js" defer></script>
</body>
</html>`;
}
export function generateHelpPageHtml(env = {}) {const siteName = "用户中心";const faviconDataUri = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E👤%3C/text%3E%3C/svg%3E";const cdnBaseUrl = env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const menuIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>`;const userIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 mr-1"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-5.5-2.5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zM10 12a5.99 5.99 0 00-4.793 2.39A6.483 6.483 0 0010 16.5a6.483 6.483 0 004.793-2.11A5.99 5.99 0 0010 12z" clip-rule="evenodd" /></svg>`;const sunIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /></svg>`;const moonIcon = `<svg id="theme-toggle-dark-icon" class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>`;const messageIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" /></svg>`;const adminIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>`; // Question mark circle for admin per userconst helpIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>`;const personalInfoIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5 mr-2"><path fill-rule="evenodd" d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" clip-rule="evenodd" /></svg>`;return `
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>帮助与API示例 - ${siteName}</title><link rel="icon" href="${faviconDataUri}"><link rel="stylesheet" href="${cdnBaseUrl}/css/style.css"><style>.help-content { padding: 20px; }.api-usage-section pre {background-color: color-mix(in srgb, var(--current-bg-color) 95%, var(--current-surface-color));color: var(--current-text-color);padding: 15px; border-radius: var(--border-radius); overflow-x: auto;font-family: var(--font-family-mono); font-size: 0.875em;border: 1px solid var(--current-border-color); margin-bottom: 15px;}.api-usage-section code.inline-code {background-color: color-mix(in srgb, var(--current-text-color) 10%, transparent);padding: 2px 5px; border-radius: 3px; font-family: var(--font-family-mono);}hr.section-divider { border: none; border-top: 1px solid var(--current-border-color); margin: 30px 0; }body.dark-mode .api-usage-section pre {background-color: color-mix(in srgb, var(--current-bg-color) 95%, var(--current-surface-color));}.app-wrapper.help-page-layout .sidebar { display: none; }.app-wrapper.help-page-layout .main-content { margin-left: 0; }</style>
</head>
<body class="font-sans antialiased"><div id="app-wrapper" class="app-wrapper help-page-layout"><header id="top-bar" class="top-bar"><div class="top-bar-left"><button id="sidebar-toggle" class="sidebar-toggle-button" aria-label="切换导航菜单">${menuIcon}</button><a href="/" class="site-title" style="margin-left:0;">${siteName}</a></div><div class="top-bar-right"><button type="button" id="theme-toggle-button" aria-label="切换主题" class="theme-toggle-button">${sunIcon}${moonIcon}</button><button type="button" id="top-bar-messaging-button" aria-label="私信" class="messaging-button hidden">${messageIcon}<span id="unread-messages-indicator" class="unread-badge hidden"></span></button><div id="top-bar-auth-buttons" class="auth-actions hidden"><a href="/user/register" class="button secondary small">注册</a><a href="/user/login" class="button primary small">登录</a></div><div id="top-bar-user-info" class="user-info-dropdown hidden"><button id="user-menu-button" class="user-menu-button">${userIcon}<span class="username-text" id="top-bar-user-username"></span></button><div id="user-dropdown-menu" class="dropdown-menu hidden"><div class="dropdown-user-email" id="top-bar-user-email"></div><hr class="dropdown-divider"><a href="/user/profile" id="top-bar-account-link" class="dropdown-item">${personalInfoIcon}账户设置</a><a href="/user/admin" id="top-bar-admin-link" class="dropdown-item hidden">${adminIcon}管理员面板</a><a href="/user/help" class="dropdown-item">${helpIcon}帮助与API示例</a><button type="button" class="dropdown-item logout" id="top-bar-logout-button">安全登出</button></div></div></div></header><aside id="sidebar" class="sidebar"><nav class="sidebar-nav"><ul id="account-tabs"><li><a href="/user/profile" class="sidebar-link">${personalInfoIcon}返回账户</a></li></ul></nav></aside><div id="sidebar-overlay" class="sidebar-overlay hidden"></div><main id="main-content" class="main-content"><div class="container"><div id="message-area" class="message hidden" role="alert"></div><div id="help-page-content" class="help-content api-usage-section"><h3>OAuth&nbsp;验证流程</h3><h4>1. 获取访问令牌 (Access Token) 和 ID 令牌 (ID Token)</h4><p>在您的客户端应用完成 OAuth 授权码流程后,使用授权码向 <code>/oauth/token</code> 端点发起 POST 请求以交换令牌。</p><pre><code>curl -X POST "${OAUTH_ISSUER_URL(env, {url: 'https://example.com'})}/oauth/token" \\-H "Content-Type: application/x-www-form-urlencoded" \\-d "grant_type=authorization_code" \\-d "code=YOUR_AUTHORIZATION_CODE" \\-d "redirect_uri=YOUR_REGISTERED_REDIRECT_URI" \\-d "client_id=YOUR_CLIENT_ID" \\-d "client_secret=YOUR_CLIENT_SECRET"</code></pre><p>成功的响应将包含 <code>access_token</code>, <code>id_token</code> 等。</p><hr class="section-divider"><h4>2. 使用访问令牌获取用户信息</h4><p>获得访问令牌后,向 <code>/oauth/userinfo</code> 端点请求用户信息。</p><pre><code>curl -X GET "${OAUTH_ISSUER_URL(env, {url: 'https://example.com'})}/oauth/userinfo" \\-H "Authorization: Bearer YOUR_ACCESS_TOKEN"</code></pre><p>响应内容取决于授予的权限范围 (scopes)。</p></div></div></main></div><script src="${cdnBaseUrl}/js/main.js" defer></script><script>(function() {const themeToggleButton = document.getElementById('theme-toggle-button');const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');let isDarkMode = localStorage.getItem('theme') === 'dark' ||(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);function applyTheme(dark) {document.body.classList.toggle('dark-mode', dark);if (themeToggleDarkIcon) themeToggleDarkIcon.style.display = dark ? 'block' : 'none';if (themeToggleLightIcon) themeToggleLightIcon.style.display = dark ? 'none' : 'block';}applyTheme(isDarkMode);if (themeToggleButton) {themeToggleButton.addEventListener('click', () => {isDarkMode = !isDarkMode;localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');applyTheme(isDarkMode);});}window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {if (localStorage.getItem('theme') === null) {isDarkMode = e.matches;applyTheme(isDarkMode);}});})();</script>
</body>
</html>`;
}
export function generateConsentScreenHtml(data) {const { clientName, requestedScopes, user, formAction, clientId, redirectUri, scope, state, nonce, responseType, issuerUrl: consentIssuerUrl, cdnBaseUrl: consentCdnBaseUrl, env = {} } = data;const finalCdnBaseUrl = consentCdnBaseUrl || env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心授权";const scopesHtml = requestedScopes.map(s => `<li>${escapeHtml(s)}</li>`).join('');return `
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>授权请求 - ${siteName}</title><link rel="stylesheet" href="${finalCdnBaseUrl}/css/style.css"><style>body { display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; background-color: var(--current-bg-color); color: var(--current-text-color); }.consent-container { background-color: var(--current-surface-color); padding: 30px 40px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-md); max-width: 480px; width:100%; text-align: center; }.consent-container h2 { margin-top: 0; color: var(--current-heading-color); font-size: 1.8em; margin-bottom: 20px; }.consent-container p { margin-bottom: 15px; line-height: 1.6; font-size: 1.05em; }.client-name { font-weight: bold; color: var(--primary-color); }.user-identifier { font-weight: bold; }.scopes-list { list-style: inside disc; text-align: left; margin: 20px auto; padding-left: 25px; max-width: fit-content; }.scopes-list li { margin-bottom: 8px; }.consent-buttons { margin-top: 30px; display: flex; justify-content: space-between; gap: 15px; }.consent-buttons button { flex-grow: 1; }</style>
</head>
<body class="font-sans antialiased"><div class="consent-container"><h2>授权请求</h2><p>应用 <strong class="client-name">${escapeHtml(clientName)}</strong> (${escapeHtml(clientId)})</p><p>正在请求访问您 <strong class="user-identifier">(${escapeHtml(user.username || user.email)})</strong> 的以下信息:</p><ul class="scopes-list">${scopesHtml}</ul><p>您是否允许此应用访问?</p><form method="POST" action="${escapeHtml(formAction)}"><input type="hidden" name="client_id" value="${escapeHtml(clientId)}"><input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}"><input type="hidden" name="scope" value="${escapeHtml(scope)}"><input type="hidden" name="state" value="${escapeHtml(state || '')}"><input type="hidden" name="nonce" value="${escapeHtml(nonce || '')}"><input type="hidden" name="response_type" value="${escapeHtml(responseType)}"><div class="consent-buttons"><button type="submit" name="decision" value="deny" class="button secondary">拒绝</button><button type="submit" name="decision" value="allow" class="button primary">允许</button></div></form></div><script>function escapeHtml(unsafe) {if (typeof unsafe !== 'string') return '';return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");}if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {document.body.classList.add('dark-mode');}</script>
</body>
</html>`;
}
export function generateErrorPageHtml(data) {const { title, message, issuerUrl, cdnBaseUrl: dataCdnBaseUrl, env = {} } = data;const cdnBaseUrl = dataCdnBaseUrl || env.CDN_BASE_URL || "https://cdn.qmwneb946.dpdns.org";const siteName = "用户中心";const finalIssuerUrl = issuerUrl || OAUTH_ISSUER_URL(env, {url: 'https://example.com'});return `
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>错误 - ${siteName}</title><link rel="stylesheet" href="${cdnBaseUrl}/css/style.css"><style>body { display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; background-color: var(--current-bg-color); color: var(--current-text-color); }.error-container { background-color: var(--current-surface-color); padding: 30px 40px; border-radius: var(--border-radius); box-shadow: var(--box-shadow-md); max-width: 450px; width: 100%; text-align: center;}.error-container h1 { color: var(--danger-color); margin-top: 0; font-size: 2em; margin-bottom: 15px;}.error-container p { font-size: 1.1em; margin-bottom: 25px; }.error-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }.error-container a:hover { text-decoration: underline; }</style>
</head>
<body class="font-sans antialiased"><div class="error-container"><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p><p><a href="${escapeHtml(finalIssuerUrl || '/user/login')}">返回登录或首页</a></p></div><script>function escapeHtml(unsafe) {if (typeof unsafe !== 'string') return '';return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");}if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {document.body.classList.add('dark-mode');}</script>
</body>
</html>`;
}
function escapeHtml(unsafe) {if (typeof unsafe !== 'string') return '';return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
const OAUTH_ISSUER_URL = (env, request) => {if (request && request.url) {try {const url = new URL(request.url);return `${url.protocol}//${url.hostname}`;} catch (e) {}}if (env && env.EXPECTED_ISSUER_URL) {return env.EXPECTED_ISSUER_URL;}const currentHost = (typeof window !== 'undefined' && window.location) ? window.location.origin : 'https://my.qmwneb946.dpdns.org';return currentHost;
};

my/src/worker.js

这个文件将进行以下修改:

  1. 添加管理员切换用户的路由。
export class ConversationDurableObject {constructor(state, env) {this.state = state;this.env = env;this.sessions = [];this.allMessages = [];this.conversationId = null;this.participants = [];this.initialized = false;this.pageSize = 20;this.state.blockConcurrencyWhile(async () => {let stored = await this.state.storage.get(["conversationId", "participants", "allMessages"]);this.conversationId = stored.get("conversationId");this.participants = stored.get("participants") || [];this.allMessages = stored.get("allMessages") || [];if (this.conversationId && this.participants.length > 0) {this.initialized = true;if (this.allMessages.length === 0 && this.env.DB) {await this.loadAllMessagesFromD1();}}});}async loadAllMessagesFromD1() {if (!this.env.DB || !this.conversationId) return;try {const { results } = await this.env.DB.prepare(`SELECT m.message_id, m.conversation_id, m.sender_email, u_sender.username as sender_username, m.content, m.sent_at, m.is_readFROM messages mJOIN users u_sender ON m.sender_email = u_sender.emailWHERE m.conversation_id = ?ORDER BY m.sent_at ASC`).bind(this.conversationId).all();if (results) {this.allMessages = results;await this.state.storage.put("allMessages", this.allMessages);}} catch (e) {}}async initialize(conversationId, participant1Email, participant2Email) {if (this.initialized) return;this.conversationId = conversationId;this.participants = [participant1Email, participant2Email].sort();await this.loadAllMessagesFromD1();await this.state.storage.put({"conversationId": this.conversationId,"participants": this.participants,"allMessages": this.allMessages});this.initialized = true;}static getConversationDOName(user1Email, user2Email) {const participants = [user1Email, user2Email].sort();return `conv-${participants[0]}-${participants[1]}`;}async fetch(request) {const url = new URL(request.url);const userEmail = request.headers.get("X-User-Email");const requestD1ConversationId = request.headers.get("X-Conversation-D1-Id");const requestP1 = request.headers.get("X-Participant1-Email");const requestP2 = request.headers.get("X-Participant2-Email");if (!this.initialized && requestD1ConversationId && requestP1 && requestP2) {await this.initialize(requestD1ConversationId, requestP1, requestP2);}if (!this.initialized) {return new Response("DO 未初始化。请调用 /initialize 或提供头部信息。", { status: 500 });}if (!userEmail) {return new Response("需要 X-User-Email 头部信息。", { status: 400 });}if (!this.participants.includes(userEmail)) {return new Response("禁止访问:用户不是参与者。", { status: 403 });}if (url.pathname === "/websocket") {if (request.headers.get("Upgrade") !== "websocket") {return new Response("期望 WebSocket 升级", { status: 426 });}const pair = new WebSocketPair();const [client, server] = Object.values(pair);server.accept();this.sessions.push({ ws: server, userEmail: userEmail, lastSentMessageIndex: this.allMessages.length });const initialMessagesToSend = this.allMessages.slice(-this.pageSize);initialMessagesToSend.forEach(msg => {server.send(JSON.stringify({ type: "HISTORICAL_MESSAGE", data: msg }));});if (this.allMessages.length > this.pageSize) {server.send(JSON.stringify({ type: "MORE_MESSAGES_AVAILABLE", data: { oldestMessageTimestamp: this.allMessages[0].sent_at } }));}server.send(JSON.stringify({ type: "CONNECTION_ESTABLISHED", data: { conversationId: this.conversationId, participants: this.participants }}));server.addEventListener("message", async event => {try {const messageData = JSON.parse(event.data);if (messageData.type === "REQUEST_MORE_MESSAGES") {const currentSession = this.sessions.find(s => s.ws === server);if (currentSession) {const currentOldestLoadedIndex = currentSession.lastSentMessageIndex - initialMessagesToSend.length;const nextBatchStartIndex = Math.max(0, currentOldestLoadedIndex - this.pageSize);const messagesToLoad = this.allMessages.slice(nextBatchStartIndex, currentOldestLoadedIndex);messagesToLoad.reverse().forEach(msg => {server.send(JSON.stringify({ type: "HISTORICAL_MESSAGE", data: msg, prepended: true }));});currentSession.lastSentMessageIndex = nextBatchStartIndex;if (nextBatchStartIndex > 0) {server.send(JSON.stringify({ type: "MORE_MESSAGES_AVAILABLE", data: { oldestMessageTimestamp: this.allMessages[0].sent_at } }));} else {server.send(JSON.stringify({ type: "NO_MORE_MESSAGES" }));}}} else if (messageData.type === "NEW_MESSAGE") {const { content } = messageData.data;const senderEmail = userEmail;const receiverEmail = this.participants.find(p => p !== senderEmail);if (!receiverEmail) {server.send(JSON.stringify({ type: "ERROR", data: "无法确定接收者。"}));return;}const now = Date.now();const messageId = crypto.randomUUID();let senderUsername = senderEmail.split('@')[0];try {const userDetails = await this.env.DB.prepare("SELECT username FROM users WHERE email = ?").bind(senderEmail).first();if (userDetails && userDetails.username) senderUsername = userDetails.username;} catch (e) {  }const newMessage = {message_id: messageId,conversation_id: this.conversationId,sender_email: senderEmail,sender_username: senderUsername,content: content,sent_at: now,is_read: 0,};if (this.env.DB) {try {await this.env.DB.prepare("INSERT INTO messages (message_id, conversation_id, sender_email, receiver_email, content, sent_at, is_read) VALUES (?, ?, ?, ?, ?, ?, 0)").bind(messageId, this.conversationId, senderEmail, receiverEmail, content, now).run();await this.env.DB.prepare("UPDATE conversations SET last_message_at = ? WHERE conversation_id = ?").bind(now, this.conversationId).run();} catch (e) {server.send(JSON.stringify({ type: "ERROR", data: "保存消息失败。" }));return;}}this.allMessages.push(newMessage);await this.state.storage.put("allMessages", this.allMessages);this.sessions.forEach(s => {if (s.ws.readyState === WebSocket.OPEN) {s.lastSentMessageIndex = this.allMessages.length;}});this.broadcast({ type: "NEW_MESSAGE", data: newMessage });if (this.env.USER_PRESENCE_DO) {try {const presenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${receiverEmail}`);const presenceStub = this.env.USER_PRESENCE_DO.get(presenceDOId);await presenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({conversationId: this.conversationId,action: 'newMessage',senderEmail: senderEmail,messageTimestamp: now,messageContent: content,otherParticipantEmail: senderEmail,otherParticipantUsername: senderUsername})}));} catch(e) {  }try {const senderPresenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${senderEmail}`);const senderPresenceStub = this.env.USER_PRESENCE_DO.get(senderPresenceDOId);await senderPresenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({conversationId: this.conversationId,action: 'messageSent',receiverEmail: receiverEmail,messageTimestamp: now,messageContent: content,})}));} catch(e) {  }}} else if (messageData.type === "MESSAGE_SEEN") {const { message_id: seenMessageId } = messageData.data;if (this.env.DB && this.conversationId) {const messagesRead = await this.env.DB.prepare("UPDATE messages SET is_read = 1 WHERE conversation_id = ? AND receiver_email = ? AND is_read = 0").bind(this.conversationId, userEmail).run();this.allMessages.forEach(msg => {if (msg.receiver_email === userEmail && msg.conversation_id === this.conversationId && !msg.is_read) {msg.is_read = 1;}});await this.state.storage.put("allMessages", this.allMessages);this.broadcast({ type: "MESSAGES_READ", data: { reader: userEmail, conversationId: this.conversationId, count: messagesRead.meta.changes }});if (this.env.USER_PRESENCE_DO) {try {const presenceDOId = this.env.USER_PRESENCE_DO.idFromName(`user-${userEmail}`);const presenceStub = this.env.USER_PRESENCE_DO.get(presenceDOId);await presenceStub.fetch(new Request(`https://do-internal/updateConversationState`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ conversationId: this.conversationId, action: 'markRead' })}));} catch(e) {  }}}}} catch (e) {server.send(JSON.stringify({ type: "ERROR", data: "无效的消息格式或处理错误。" }));}});server.addEventListener("close", () => {this.sessions = this.sessions.filter(s => s.ws !== server);});server.addEventListener("error", (err) => {this.sessions = this.sessions.filter(s => s.ws !== server);});return new Response(null, { status: 101, webSocket: client });} else if (url.pathname === "/initialize" && request.method === "POST") {try {const { conversationId, participant1Email, participant2Email } = await request.json();if (conversationId && participant1Email && participant2Email) {if (!this.initialized) {await this.initialize(conversationId, participant1Email, participant2Email);}return new Response("DO 已初始化或之前已初始化", { status: 200 });}return new Response("DO初始化缺少数据", { status: 400 });} catch (e) {return new Response(`DO初始化错误: ${e.message}`, { status: 500 });}}return new Response("在ConversationDurableObject中未找到", { status: 404 });}broadcast(message) {const serializedMessage = JSON.stringify(message);this.sessions = this.sessions.filter(session => {try {if (session.ws.readyState === WebSocket.OPEN) {session.ws.send(serializedMessage);return true;} else {return false;}} catch (e) {return false;}});}
}
export class UserPresenceDurableObject {constructor(state, env) {this.state = state;this.env = env;this.userEmail = null;this.webSocket = null;this.conversationsSummary = {};this.initialized = false;this.state.blockConcurrencyWhile(async () => {const storedUserEmail = await this.state.storage.get("userEmail");if (storedUserEmail) {this.userEmail = storedUserEmail;const storedSummary = await this.state.storage.get("conversationsSummary");if (storedSummary) {this.conversationsSummary = storedSummary;}this.initialized = true;}});}async initialize(userEmail) {if (this.initialized && this.userEmail === userEmail) return;this.userEmail = userEmail;await this.state.storage.put("userEmail", this.userEmail);await this.fetchConversationsSummaryFromD1();this.initialized = true;}async fetchConversationsSummaryFromD1() {if (!this.userEmail || !this.env.DB) return;try {const convResults = await this.env.DB.prepare(`SELECTc.conversation_id, c.last_message_at, c.participant1_email, c.participant2_email,u1.username as p1_username, u2.username as p2_username,(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.conversation_id AND m.receiver_email = ?1 AND m.is_read = 0) as unread_count,(SELECT content FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_content,(SELECT sender_email FROM messages m WHERE m.conversation_id = c.conversation_id ORDER BY m.sent_at DESC LIMIT 1) as last_message_senderFROM conversations cLEFT JOIN users u1 ON c.participant1_email = u1.emailLEFT JOIN users u2 ON c.participant2_email = u2.emailWHERE c.participant1_email = ?1 OR c.participant2_email = ?1ORDER BY c.last_message_at DESC`).bind(this.userEmail).all();const newSummary = {};let totalUnread = 0;if (convResults && convResults.results) {convResults.results.forEach(conv => {const otherParticipantEmail = conv.participant1_email === this.userEmail ? conv.participant2_email : conv.participant1_email;const otherParticipantUsername = conv.participant1_email === this.userEmail ? conv.p2_username : conv.p1_username;newSummary[conv.conversation_id] = {conversation_id: conv.conversation_id,unread_count: conv.unread_count || 0,last_message_at: conv.last_message_at,last_message_content: conv.last_message_content,last_message_sender: conv.last_message_sender,other_participant_email: otherParticipantEmail,other_participant_username: otherParticipantUsername || otherParticipantEmail.split('@')[0],};totalUnread += (conv.unread_count || 0);});}this.conversationsSummary = newSummary;await this.state.storage.put("conversationsSummary", this.conversationsSummary);if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {this.webSocket.send(JSON.stringify({ type: "CONVERSATIONS_LIST", data: Object.values(this.conversationsSummary) }));this.webSocket.send(JSON.stringify({ type: "UNREAD_COUNT_TOTAL", data: { unread_count: totalUnread } }));}} catch (e) {}}async fetch(request) {const url = new URL(request.url);const requestUserEmail = request.headers.get("X-User-Email");if (!this.initialized && requestUserEmail) {await this.initialize(requestUserEmail);} else if (!this.initialized && this.state.id && this.state.id.name && this.state.id.name.startsWith("user-")) {await this.initialize(this.state.id.name.substring(5));}if (!this.initialized || !this.userEmail) {return new Response("UserPresenceDO 未正确初始化。", { status: 500 });}if (url.pathname === "/websocket") {if (request.headers.get("Upgrade") !== "websocket") {return new Response("期望 WebSocket 升级", { status: 426 });}if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {this.webSocket.close(1000, "新连接已建立");}const pair = new WebSocketPair();const [client, server] = Object.values(pair);this.webSocket = server;this.webSocket.accept();await this.fetchConversationsSummaryFromD1();this.webSocket.addEventListener("message", async event => {try {const messageData = JSON.parse(event.data);if (messageData.type === "REQUEST_INITIAL_STATE" || messageData.type === "REQUEST_CONVERSATIONS_LIST") {await this.fetchConversationsSummaryFromD1();}} catch(e) {}});this.webSocket.addEventListener("close", () => { this.webSocket = null; });this.webSocket.addEventListener("error", (err) => {this.webSocket = null;});return new Response(null, { status: 101, webSocket: client });} else if (url.pathname === "/updateConversationState" && request.method === "POST") {const updateData = await request.json();const { conversationId, action, senderEmail, messageTimestamp, messageContent, otherParticipantEmail, otherParticipantUsername, receiverEmail } = updateData;let changed = false;if (!this.conversationsSummary[conversationId]) {this.conversationsSummary[conversationId] = {conversation_id: conversationId,unread_count: 0,last_message_at: 0,last_message_content: "",last_message_sender: "",other_participant_email: action === 'newMessage' ? (otherParticipantEmail || senderEmail) : (action === 'messageSent' ? receiverEmail : ""),other_participant_username: action === 'newMessage' ? (otherParticipantUsername || (otherParticipantEmail || senderEmail || "").split('@')[0]) : (action === 'messageSent' ? (receiverEmail || "").split('@')[0] : "")};changed = true;}const currentConv = this.conversationsSummary[conversationId];if (action === 'newMessage') {currentConv.unread_count = (currentConv.unread_count || 0) + 1;currentConv.last_message_at = messageTimestamp;currentConv.last_message_content = messageContent;currentConv.last_message_sender = senderEmail;if (!currentConv.other_participant_email && senderEmail) {currentConv.other_participant_email = senderEmail;currentConv.other_participant_username = otherParticipantUsername || senderEmail.split('@')[0];}changed = true;} else if (action === 'markRead') {if (currentConv.unread_count > 0) {currentConv.unread_count = 0;changed = true;}} else if (action === 'messageSent') {currentConv.last_message_at = messageTimestamp;currentConv.last_message_content = messageContent;currentConv.last_message_sender = this.userEmail;if (!currentConv.other_participant_email && receiverEmail) {currentConv.other_participant_email = receiverEmail;currentConv.other_participant_username = receiverEmail.split('@')[0];}changed = true;}if (changed) {await this.state.storage.put("conversationsSummary", this.conversationsSummary);let totalUnread = 0;Object.values(this.conversationsSummary).forEach(conv => totalUnread += (conv.unread_count || 0));if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {const sortedSummary = Object.values(this.conversationsSummary).sort((a,b) => (b.last_message_at || 0) - (a.last_message_at || 0));this.webSocket.send(JSON.stringify({ type: "CONVERSATIONS_LIST", data: sortedSummary }));this.webSocket.send(JSON.stringify({ type: "UNREAD_COUNT_TOTAL", data: { unread_count: totalUnread } }));}}return new Response("用户状态已更新", { status: 200 });} else if (url.pathname === "/getConversationsList" && request.method === "GET") {await this.fetchConversationsSummaryFromD1();const sortedSummary = Object.values(this.conversationsSummary).sort((a,b) => (b.last_message_at || 0) - (a.last_message_at || 0));return new Response(JSON.stringify(sortedSummary), { headers: { 'Content-Type': 'application/json'}});} else if (url.pathname === "/getTotalUnreadCount" && request.method === "GET") {await this.fetchConversationsSummaryFromD1();let totalUnread = 0;Object.values(this.conversationsSummary).forEach(conv => totalUnread += (conv.unread_count || 0));return new Response(JSON.stringify({ unread_count: totalUnread }), { headers: { 'Content-Type': 'application/json'}});}return new Response("在UserPresenceDurableObject中未找到", { status: 404 });}
}
import {authenticateApiRequest,handleGetMe,handleRegister,handleLogin,handleLogin2FAVerify,handleChangePassword,handleUpdateProfile,handleLogout,handle2FAGenerateSecret,handle2FAEnable,handle2FADisable,handleCreatePasteApiKey,handleGetCloudPcKey,handleCreateCloudPcKey,handleGetGreenHubKeys,handleAdminListUsers,handleAdminGetUser,handleAdminUpdateUser,handleAdminSwitchUser,handleAdminListOauthClients,handleAdminGetOauthClient,handleAdminUpdateOauthClient,
} from './api-handlers.js';
import {handleOpenIDConfiguration,handleJwks,handleOAuthAuthorize,handleOAuthToken,handleOAuthUserInfo,handleRegisterOauthClient,handleListOauthClients,handleUpdateOauthClient,handleDeleteOauthClient
} from './oauth-server.js';
import { generateHtmlUi, generateErrorPageHtml, generateHelpPageHtml } from './html-ui.js';
import { jsonResponse, OAUTH_ISSUER_URL, isAdminUser } from './helpers.js';
export default {async fetch(request, env, ctx) {const url = new URL(request.url);const path = url.pathname;const method = request.method;const rayId = request.headers.get('cf-ray') || crypto.randomUUID();let userEmailFromSession = null;const cookieHeader = request.headers.get('Cookie');let sessionIdFromCookie = null;if (cookieHeader) {const authCookieString = cookieHeader.split(';').find(row => row.trim().startsWith('AuthToken='));if (authCookieString) sessionIdFromCookie = authCookieString.split('=')[1]?.trim();}if (sessionIdFromCookie && env.DB) {userEmailFromSession = await authenticateApiRequest(sessionIdFromCookie, env);}const uiPaths = ['/','/user/login','/user/register','/user/profile','/user/security','/user/api-keys','/user/applications','/user/messaging','/user/admin','/user/admin/users','/user/admin/apps'];try {if (method === 'GET' && path === '/user/account') {return Response.redirect(`${url.origin}/user/profile`, 302);}if (method === 'GET' && uiPaths.includes(path)) {return new Response(generateHtmlUi(path, env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });}if (method === 'GET' && path === '/.well-known/openid-configuration') {return handleOpenIDConfiguration(request, env);}if (method === 'GET' && path === '/.well-known/jwks.json') {return handleJwks(request, env);}if (path.startsWith('/oauth/')) {if (path === '/oauth/authorize') {return handleOAuthAuthorize(request, env);}if (path === '/oauth/token' && method === 'POST') {return handleOAuthToken(request, env);}if (path === '/oauth/userinfo' && (method === 'GET' || method === 'POST')) {return handleOAuthUserInfo(request, env);}return jsonResponse({ error: 'OAuth/OIDC 端点未找到或方法不允许' }, 404);}if (path.startsWith('/api/ws/user')) {if (!userEmailFromSession) return jsonResponse({ error: 'WebSocket 需要身份验证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/websocket`, new Request(request, {headers: forwardRequestHeaders})));}const wsConversationMatch = path.match(/^\/api\/ws\/conversation\/([a-fA-F0-9-]+)$/);if (wsConversationMatch) {if (!userEmailFromSession) return jsonResponse({ error: 'WebSocket 需要身份验证' }, 401);const conversationD1Id = wsConversationMatch[1];const convDetails = await env.DB.prepare("SELECT participant1_email, participant2_email FROM conversations WHERE conversation_id = ?").bind(conversationD1Id).first();if (!convDetails) return jsonResponse({ error: '对话未找到' }, 404);if (userEmailFromSession !== convDetails.participant1_email && userEmailFromSession !== convDetails.participant2_email) {return jsonResponse({ error: '禁止访问' }, 403);}const doName = ConversationDurableObject.getConversationDOName(convDetails.participant1_email, convDetails.participant2_email);const doId = env.CONVERSATION_DO.idFromName(doName);const stub = env.CONVERSATION_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);forwardRequestHeaders.set('X-Conversation-D1-Id', conversationD1Id);forwardRequestHeaders.set('X-Participant1-Email', convDetails.participant1_email);forwardRequestHeaders.set('X-Participant2-Email', convDetails.participant2_email);return stub.fetch(new Request(`${url.origin}/websocket`, new Request(request, {headers: forwardRequestHeaders})));}if (path.startsWith('/api/')) {if (path === '/api/config' && method === 'GET') {return jsonResponse({ turnstileSiteKey: env.TURNSTILE_SITE_KEY });}if (method === 'POST' && path === '/api/logout') {return handleLogout(request, env, sessionIdFromCookie, url.hostname);}const authenticatedRoutes = ['/api/me', '/api/change-password', '/api/update-profile','/api/2fa/', '/api/paste-keys', '/api/cloudpc-key','/api/greenhub-keys', '/api/oauth/clients', '/api/messages', '/api/conversations','/api/admin/'];let needsAuth = authenticatedRoutes.some(p => path.startsWith(p) || (p.endsWith('/') && path.startsWith(p.slice(0,-1))) || path === p );const oauthClientSpecificPathMatchForAuth = path.match(/^\/api\/oauth\/clients\/([\w-]+)$/);if (oauthClientSpecificPathMatchForAuth && (method === 'PUT' || method === 'DELETE')) {needsAuth = true;}if (needsAuth && !userEmailFromSession) {return jsonResponse({ error: '未授权或会话无效' }, 401);}if (path.startsWith('/api/admin/')) {if (!isAdminUser(userEmailFromSession, env)) {return jsonResponse({ error: '禁止访问管理员接口' }, 403);}if (path === '/api/admin/users' && method === 'GET') {return handleAdminListUsers(request, env);}const adminUserEmailRegex = /^\/api\/admin\/users\/([^/]+)$/; // Simplified regexconst adminUserDetailMatch = path.match(adminUserEmailRegex);if (adminUserDetailMatch) {const targetUserEmail = decodeURIComponent(adminUserDetailMatch[1]);if (method === 'GET') {return handleAdminGetUser(request, env, targetUserEmail);} else if (method === 'PUT') {return handleAdminUpdateUser(request, env, targetUserEmail);} else {return jsonResponse({ error: `方法 ${method} 不允许用于此用户管理端点` }, 405);}}if (path === '/api/admin/switch-user' && method === 'POST') {const reqBody = await request.json();const { targetUserEmail } = reqBody;return handleAdminSwitchUser(request, env, targetUserEmail, url.hostname, url.protocol);}if (path === '/api/admin/oauth/clients' && method === 'GET') {return handleAdminListOauthClients(request, env);}const adminClientDetailMatch = path.match(/^\/api\/admin\/oauth\/clients\/([\w-]+)$/);if (adminClientDetailMatch) {const targetClientId = adminClientDetailMatch[1];if (method === 'GET') {return handleAdminGetOauthClient(request, env, targetClientId);} else if (method === 'PUT') {return handleAdminUpdateOauthClient(request, env, targetClientId);} else {return jsonResponse({ error: `方法 ${method} 不允许用于此应用管理端点` }, 405);}}return jsonResponse({ error: '管理员 API 端点未找到' }, 404);}if (method === 'POST' && path === '/api/register') {return handleRegister(request, env, request.headers.get('CF-Connecting-IP'));}if (method === 'POST' && path === '/api/login') {return handleLogin(request, env, url.protocol, url.hostname, request.headers.get('CF-Connecting-IP'));}if (method === 'POST' && path === '/api/login/2fa-verify') {return handleLogin2FAVerify(request, env, url.protocol, url.hostname);}if (path === '/api/me' && method === 'GET') {return handleGetMe(request, env, userEmailFromSession);}if (path === '/api/change-password' && method === 'POST') {return handleChangePassword(request, env, userEmailFromSession);}if (path === '/api/update-profile' && method === 'POST') {return handleUpdateProfile(request, env, userEmailFromSession);}if (path.startsWith('/api/2fa/')) {if (path === '/api/2fa/generate-secret' && method === 'GET') {return handle2FAGenerateSecret(request, env, userEmailFromSession);}if (path === '/api/2fa/enable' && method === 'POST') {return handle2FAEnable(request, env, userEmailFromSession);}if (path === '/api/2fa/disable' && method === 'POST') {return handle2FADisable(request, env, userEmailFromSession);}}if (path === '/api/paste-keys' && method === 'POST') {return handleCreatePasteApiKey(request, env, userEmailFromSession, request.headers.get('CF-Connecting-IP'));}if (path.startsWith('/api/cloudpc-key')) {if (method === 'GET') {return handleGetCloudPcKey(request, env, userEmailFromSession);}if (method === 'POST') {return handleCreateCloudPcKey(request, env, userEmailFromSession, request.headers.get('CF-Connecting-IP'));}}if (path === '/api/greenhub-keys' && method === 'GET') {return handleGetGreenHubKeys(request, env, userEmailFromSession);}if (path === '/api/oauth/clients') {if (method === 'POST') {return handleRegisterOauthClient(request, env, userEmailFromSession);}if (method === 'GET') {return handleListOauthClients(request, env, userEmailFromSession);}}if (oauthClientSpecificPathMatchForAuth) {const clientIdFromPath = oauthClientSpecificPathMatchForAuth[1];if (method === 'PUT') {return handleUpdateOauthClient(request, env, userEmailFromSession, clientIdFromPath);}if (method === 'DELETE') {return handleDeleteOauthClient(request, env, userEmailFromSession, clientIdFromPath);}}if (path === '/api/messages' && method === 'POST') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const reqBody = await request.json();const { receiverEmail, content } = reqBody;if (!receiverEmail || !content) return jsonResponse({ error: '接收者和内容为必填项' }, 400);if (receiverEmail === userEmailFromSession) return jsonResponse({ error: '不能给自己发送消息' }, 400);const receiverUser = await env.DB.prepare("SELECT email FROM users WHERE email = ?").bind(receiverEmail).first();if (!receiverUser) return jsonResponse({ error: '接收用户不存在' }, 404);const p1Sorted = [userEmailFromSession, receiverEmail].sort()[0];const p2Sorted = [userEmailFromSession, receiverEmail].sort()[1];let conversation = await env.DB.prepare("SELECT conversation_id FROM conversations WHERE participant1_email = ? AND participant2_email = ?").bind(p1Sorted, p2Sorted).first();let conversationD1Id;if (!conversation) {conversationD1Id = crypto.randomUUID();const nowDb = Date.now();await env.DB.prepare("INSERT INTO conversations (conversation_id, participant1_email, participant2_email, last_message_at, created_at) VALUES (?, ?, ?, ?, ?)").bind(conversationD1Id, p1Sorted, p2Sorted, nowDb, nowDb).run();} else {conversationD1Id = conversation.conversation_id;}const doName = ConversationDurableObject.getConversationDOName(p1Sorted, p2Sorted);const doId = env.CONVERSATION_DO.idFromName(doName);const stub = env.CONVERSATION_DO.get(doId);const initResponse = await stub.fetch(new Request(`${url.origin}/initialize`, {method: 'POST',headers: {'Content-Type': 'application/json', 'X-User-Email': userEmailFromSession},body: JSON.stringify({conversationId: conversationD1Id,participant1Email: p1Sorted,participant2Email: p2Sorted})}));if (!initResponse.ok) {}return jsonResponse({success: true,message: '对话已就绪。请通过WebSocket发送实际消息。',conversationId: conversationD1Id});}if (path === '/api/conversations' && method === 'GET') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/getConversationsList`, new Request(request, {headers: forwardRequestHeaders})));}if (path === '/api/messages/unread-count' && method === 'GET') {if (!userEmailFromSession) return jsonResponse({ error: '用户未认证' }, 401);const doId = env.USER_PRESENCE_DO.idFromName(`user-${userEmailFromSession}`);const stub = env.USER_PRESENCE_DO.get(doId);const forwardRequestHeaders = new Headers(request.headers);forwardRequestHeaders.set('X-User-Email', userEmailFromSession);return stub.fetch(new Request(`${url.origin}/getTotalUnreadCount`, new Request(request, {headers: forwardRequestHeaders})));}return jsonResponse({ error: 'API 端点未找到或请求方法不允许' }, 404);}if (method === 'GET' && path === '/user/help') {return new Response(generateHelpPageHtml(env), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });}let errorPageIssuerUrl = '/';try { errorPageIssuerUrl = OAUTH_ISSUER_URL(env, request); } catch(e) {  }return new Response(generateErrorPageHtml({title: "页面未找到", message: `请求的资源 ${path} 不存在。`, issuerUrl: errorPageIssuerUrl, env}), { status: 404, headers: { 'Content-Type': 'text/html;charset=UTF-8' }});} catch (error) {let errorPageIssuerUrl = '/';try { errorPageIssuerUrl = OAUTH_ISSUER_URL(env, request); } catch (e) {  }return new Response(generateErrorPageHtml({title: "服务器内部错误", message: "处理您的请求时发生意外错误。", issuerUrl: errorPageIssuerUrl, env}), { status: 500, headers: { 'Content-Type': 'text/html;charset=UTF-8' }});}},
};

相关文章:

  • CSS相关知识
  • 6个月Python学习计划 Day 4
  • 前端流行框架Vue3教程:26. 异步组件
  • 【25软考网工】第八章 (1)交换机基础
  • springboot 控制层调用业务逻辑层,注入报错,无法自动装配 解决办法
  • 在机器学习中,L2正则化为什么能够缓过拟合?为何正则化等机制能够使一个“过度拟合训练集”的模型展现出更优的泛化性能?正则化
  • c++总结-04-智能指针
  • 奈雪小程序任务脚本
  • Python与C++中浮点数的精度与计算误差(易忽略易错)
  • C++11(2):
  • 历年华东师范大学保研上机真题
  • 计算机病毒的发展历程及其分类
  • 审计报告附注救星!实现Word表格纵向求和+横向计算及其对应的智能校验
  • JavaScript 中的 structuredClone() 如何彻底改变你的对象复制方式
  • 制造业主要管理哪些主数据范围
  • 智能办公系统 — 审批管理模块 · 开发日志
  • 理解HTTP基本认证与表单登录认证
  • [创业之路-381]:企业战略管理案例分析-战略制定/设计-市场洞察“五看”:看宏观-经济-如何获得国家经济政策与愿景规划,以及技术发展趋势、技术成熟度
  • Windows 开始菜单快捷方式路径说明
  • Cygwin:在Windows上搭建类Linux环境的桥梁
  • 买了一个域名如何做网站/爱站网ip反域名查询
  • 母婴网站 模板/网站关键词seo排名
  • 品牌建设是指/seo优化标题 关键词
  • 铜川免费做网站/专业黑帽seo
  • 做百度网站还是安居客网站/百度推广找谁做靠谱
  • 网站搭建怎么收费/国外域名注册网站