diff --git a/package-lock.json b/package-lock.json index ca656b4..6d238ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -507,12 +507,6 @@ "vue": "^3.0.0" } }, - "node_modules/@hexagon/base64": { - "version": "1.1.28", - "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", - "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", - "license": "MIT" - }, "node_modules/@intlify/core-base": { "version": "9.14.4", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.4.tgz", @@ -584,12 +578,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@levischuck/tiny-cbor": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", - "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", - "license": "MIT" - }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -745,64 +733,6 @@ "node": ">=18.12.0" } }, - "node_modules/@peculiar/asn1-android": { - "version": "2.3.16", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz", - "integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz", - "integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz", - "integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", - "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", - "license": "MIT", - "dependencies": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz", - "integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", @@ -1063,30 +993,6 @@ "win32" ] }, - "node_modules/@simplewebauthn/browser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz", - "integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==", - "license": "MIT" - }, - "node_modules/@simplewebauthn/server": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz", - "integrity": "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==", - "license": "MIT", - "dependencies": { - "@hexagon/base64": "^1.1.27", - "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.3.10", - "@peculiar/asn1-ecc": "^2.3.8", - "@peculiar/asn1-rsa": "^2.3.8", - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/asn1-x509": "^2.3.8" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -2123,20 +2029,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/asn1js": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", - "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", - "license": "BSD-3-Clause", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5592,24 +5484,6 @@ "once": "^1.3.1" } }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -6816,7 +6690,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -7537,7 +7412,6 @@ "name": "@nexus-terminal/backend", "version": "0.1.0", "dependencies": { - "@simplewebauthn/server": "^13.1.1", "@types/multer": "^1.4.12", "@types/session-file-store": "^1.2.5", "@types/uuid": "^10.0.0", @@ -7586,7 +7460,6 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.7.2", "@hcaptcha/vue3-hcaptcha": "^1.3.0", - "@simplewebauthn/browser": "^13.1.0", "@tailwindcss/vite": "^4.1.4", "@xterm/addon-search": "^0.15.0", "axios": "^1.8.4", diff --git a/packages/backend/package.json b/packages/backend/package.json index ede1508..33d3585 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -9,7 +9,6 @@ "dev": "cross-env NODE_ENV=development npx ts-node-dev --respawn --transpile-only src/index.ts" }, "dependencies": { - "@simplewebauthn/server": "^13.1.1", "@types/multer": "^1.4.12", "@types/session-file-store": "^1.2.5", "@types/uuid": "^10.0.0", diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index b992f98..fc84f92 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -4,8 +4,6 @@ import bcrypt from 'bcrypt'; import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; import speakeasy from 'speakeasy'; import qrcode from 'qrcode'; -import { PasskeyService } from '../services/passkey.service'; -import type { RegistrationResponseJSON } from '@simplewebauthn/server'; // 添加类型导入 import { NotificationService } from '../services/notification.service'; import { AuditLogService } from '../services/audit.service'; import { ipBlacklistService } from '../services/ip-blacklist.service'; @@ -13,8 +11,7 @@ import { captchaService } from '../services/captcha.service'; import { settingsService } from '../services/settings.service'; -const passkeyService = new PasskeyService(); -const notificationService = new NotificationService(); +const notificationService = new NotificationService(); const auditLogService = new AuditLogService(); export interface User { // Add export keyword @@ -30,7 +27,7 @@ declare module 'express-session' { username?: string; tempTwoFactorSecret?: string; requiresTwoFactor?: boolean; - currentChallenge?: string; + // currentChallenge?: string; // Removed Passkey challenge storage rememberMe?: boolean; } } @@ -410,294 +407,7 @@ export const setup2FA = async (req: Request, res: Response): Promise => { }; -// --- Passkey 相关方法 --- -/** - * 生成 Passkey 注册选项 (POST /api/v1/auth/passkey/register-options) - */ -export const generatePasskeyRegistrationOptions = async (req: Request, res: Response): Promise => { - const userId = req.session.userId; - const username = req.session.username; // Passkey 需要用户名 - - if (!userId || !username || req.session.requiresTwoFactor) { - res.status(401).json({ message: '用户未认证或认证未完成。' }); - return; - } - - try { - // 从请求中获取 hostname - const hostname = req.hostname; - // 注意: 确保 Express 配置了 'trust proxy' 如果应用在反向代理后面, - // 否则 req.hostname 可能返回不正确的值 (例如 'localhost')。 - // 可以在 Express 初始化时设置 app.set('trust proxy', true); - - const options = await passkeyService.generateRegistrationOptions(hostname, username); - - // 将 challenge 存储在 session 中,用于后续验证 - req.session.currentChallenge = options.challenge; - - res.json(options); - } catch (error: any) { - console.error(`用户 ${userId} 生成 Passkey 注册选项时出错:`, error); - res.status(500).json({ message: '生成 Passkey 注册选项失败。', error: error.message }); - } -}; - -/** - * 验证 Passkey 注册响应 (POST /api/v1/auth/passkey/verify-registration) - */ -export const verifyPasskeyRegistration = async (req: Request, res: Response): Promise => { - const userId = req.session.userId; - const expectedChallenge = req.session.currentChallenge; - // 将 name 提取出来,其余部分作为 registrationData 对象 - const { name, ...registrationData } = req.body; - console.log(`[AuthController VerifyReg] Received request body: name=${name}, registrationData=${JSON.stringify(registrationData)}`); // Log received data - - if (!userId || req.session.requiresTwoFactor) { - res.status(401).json({ message: '用户未认证或认证未完成。' }); - return; - } - - if (!expectedChallenge) { - res.status(400).json({ message: '未找到预期的挑战,请重新生成注册选项。' }); - return; - } - - // 检查 registrationData 是否存在且不为空对象 - if (!registrationData || Object.keys(registrationData).length === 0) { - res.status(400).json({ message: '缺少注册响应数据。' }); - return; - } - - // 清除 session 中的 challenge,无论成功与否 - delete req.session.currentChallenge; - - try { - // 从请求中获取 hostname 和 origin - const hostname = req.hostname; - // 尝试从 Origin header 获取,如果不存在,则根据协议和主机名构造 - const originHeader = req.get('origin'); - const origin = originHeader || `${req.protocol}://${req.get('host')}`; // req.get('host') 包含端口 - - // 再次提醒: 确保 Express 配置了 'trust proxy' - - // 从 session 获取 userId - const userId = req.session.userId; - if (!userId) { - // 这个检查理论上在函数开头已经做过,但为了类型安全和明确性再次检查 - throw new Error('无法获取用户 ID,无法验证 Passkey。'); - } - console.log(`[AuthController VerifyReg] Calling passkeyService.verifyRegistration with: userId=${userId}, expectedChallenge=${expectedChallenge}, hostname=${hostname}, origin=${origin}, name=${name}`); // Log parameters before calling service - - const verification = await passkeyService.verifyRegistration( - userId, - registrationData as RegistrationResponseJSON, // 将收集到的字段重新构造成符合类型的对象 - expectedChallenge, - hostname, - origin, - name - ); - console.log(`[AuthController VerifyReg] Received verification result from service: verified=${verification.verified}`); // Log service result - - if (verification.verified && verification.registrationInfo) { - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; - // 记录审计日志 (添加 IP) - const regInfo: any = verification.registrationInfo; - auditLogService.logAction('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp }); - notificationService.sendNotification('PASSKEY_REGISTERED', { userId, passkeyId: regInfo.credentialID, name, ip: clientIp }); // 添加通知调用 - res.status(201).json({ message: 'Passkey 注册成功!', verified: true }); - } else { - console.error(`用户 ${userId} Passkey 注册验证失败:`, verification); - res.status(400).json({ message: 'Passkey 注册验证失败。', verified: false }); - } - } catch (error: any) { - console.error(`用户 ${userId} 验证 Passkey 注册时出错:`, error); - res.status(500).json({ message: '验证 Passkey 注册失败。', error: error.message }); - } -}; -/** - * 获取当前用户已注册的所有 Passkey (GET /api/v1/auth/passkeys) - */ -export const listUserPasskeys = async (req: Request, res: Response): Promise => { - const userId = req.session.userId; - - if (!userId || req.session.requiresTwoFactor) { - res.status(401).json({ message: '用户未认证或认证未完成。' }); - return; - } - - try { - // 注意:PasskeyService 的 listPasskeys 目前是获取所有用户的, - // 实际应用中应该只获取当前用户的。这里暂时调用, - // 但 PasskeyRepository 和 Service 可能需要调整以支持按用户过滤。 - // 假设 PasskeyRepository.getAllPasskeys() 可以接受 userId 过滤 - // 或者 PasskeyService.listPasskeys() 内部处理过滤逻辑。 - // **临时简化处理:** 假设 PasskeyService.listPasskeys() 返回所有密钥, - // 在实际应用中需要根据 userId 过滤。 - // TODO: Refactor PasskeyRepository/Service to filter by userId - const passkeys = await passkeyService.listPasskeys(); // 假设这会返回适合前端展示的数据结构 - - res.status(200).json(passkeys); - } catch (error: any) { - console.error(`用户 ${userId} 获取 Passkey 列表时出错:`, error); - res.status(500).json({ message: '获取 Passkey 列表失败。', error: error.message }); - } -}; - -/** - * 删除指定的 Passkey (DELETE /api/v1/auth/passkeys/:id) - */ -export const deleteUserPasskey = async (req: Request, res: Response): Promise => { - const userId = req.session.userId; - const passkeyIdToDelete = parseInt(req.params.id, 10); // 从路由参数获取 ID - - if (!userId || req.session.requiresTwoFactor) { - res.status(401).json({ message: '用户未认证或认证未完成。' }); - return; - } - - if (isNaN(passkeyIdToDelete)) { - res.status(400).json({ message: '无效的 Passkey ID。' }); - return; - } - - try { - // TODO: 在删除前,应该验证这个 Passkey ID 是否属于当前登录用户。 - // 这需要调整 PasskeyRepository.deletePasskeyById 或增加一个验证步骤。 - // **临时简化处理:** 直接尝试删除。 - - await passkeyService.deletePasskey(passkeyIdToDelete); - - console.log(`用户 ${userId} 删除了 Passkey ID: ${passkeyIdToDelete}`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; - // 记录审计日志 - auditLogService.logAction('PASSKEY_DELETED', { userId, passkeyId: passkeyIdToDelete, ip: clientIp }); - notificationService.sendNotification('PASSKEY_DELETED', { userId, passkeyId: passkeyIdToDelete, ip: clientIp }); - - res.status(200).json({ message: 'Passkey 删除成功。' }); - } catch (error: any) { - console.error(`用户 ${userId} 删除 Passkey ID ${passkeyIdToDelete} 时出错:`, error); - // 可以根据错误类型返回不同状态码,例如找不到资源返回 404 - if (error.message?.includes('未找到')) { // 简单的错误检查 - res.status(404).json({ message: '未找到要删除的 Passkey。', error: error.message }); - } else { - res.status(500).json({ message: '删除 Passkey 失败。', error: error.message }); - } - } -}; -/** - * 生成 Passkey 认证选项 (POST /api/v1/auth/passkey/authenticate-options) - * 用于登录流程 - */ -export const generatePasskeyAuthenticationOptions = async (req: Request, res: Response): Promise => { - try { - // 从请求中获取 hostname - const hostname = req.hostname; - // 确保 Express 配置了 'trust proxy' - - const options = await passkeyService.generateAuthenticationOptions(hostname); - - // 将 challenge 存储在 session 中,用于后续验证 - req.session.currentChallenge = options.challenge; - // 可以在这里添加一个标记,表明正在进行 passkey 认证 - // req.session.passkeyAuthInProgress = true; - - console.log(`[AuthController] 为 Passkey 登录生成认证选项,Challenge: ${options.challenge}`); - res.json(options); - } catch (error: any) { - console.error(`生成 Passkey 认证选项时出错:`, error); - res.status(500).json({ message: '生成 Passkey 认证选项失败。', error: error.message }); - } -}; - -/** - * 验证 Passkey 认证响应并登录 (POST /api/v1/auth/passkey/verify-authentication) - */ -export const verifyPasskeyAuthentication = async (req: Request, res: Response): Promise => { - const expectedChallenge = req.session.currentChallenge; - const { authenticationResponse, rememberMe } = req.body; // 获取认证响应和 rememberMe 状态 - - if (!expectedChallenge) { - res.status(400).json({ message: '未找到预期的挑战,请重新开始登录流程。' }); - return; - } - - if (!authenticationResponse) { - res.status(400).json({ message: '缺少认证响应数据。' }); - return; - } - - // 清除 session 中的 challenge,无论成功与否 - delete req.session.currentChallenge; - // delete req.session.passkeyAuthInProgress; // 清除标记 - - try { - // 从请求中获取 hostname 和 origin - const hostname = req.hostname; - const originHeader = req.get('origin'); - const origin = originHeader || `${req.protocol}://${req.get('host')}`; - // 确保 Express 配置了 'trust proxy' - - const verification = await passkeyService.verifyAuthentication( - authenticationResponse, - expectedChallenge, - hostname, - origin - ); - - if (verification.verified && verification.userInfo) { - const { userId, username } = verification.userInfo; - console.log(`Passkey 认证成功,用户: ${username} (ID: ${userId})`); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; - - // --- 认证成功,建立会话 --- - ipBlacklistService.resetAttempts(clientIp); // 重置 IP 失败尝试 - auditLogService.logAction('LOGIN_SUCCESS', { userId, username, ip: clientIp, method: 'passkey' }); - notificationService.sendNotification('LOGIN_SUCCESS', { userId, username, ip: clientIp, method: 'passkey' }); - - req.session.userId = userId; - req.session.username = username; - req.session.requiresTwoFactor = false; // Passkey 本身包含验证,通常视为已完成 2FA - - // 根据 rememberMe 设置 cookie maxAge - if (rememberMe) { - req.session.cookie.maxAge = 315360000000; // 10 years - } else { - req.session.cookie.maxAge = undefined; // Session cookie - } - - res.status(200).json({ - message: 'Passkey 登录成功。', - user: { id: userId, username: username } - }); - // --- 会话建立结束 --- - - } else { - console.error(`Passkey 认证验证失败:`, verification); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; - ipBlacklistService.recordFailedAttempt(clientIp); // 记录失败尝试 - // 尝试从响应中获取用户 ID (如果可能) 用于日志记录 - const credentialId = authenticationResponse?.id; - let potentialUserId: number | string = 'unknown'; - if (credentialId) { - try { - const authenticator = await passkeyService.getPasskeyByCredentialId(credentialId); - if (authenticator) potentialUserId = authenticator.user_id; - } catch { /* ignore error */ } - } - auditLogService.logAction('LOGIN_FAILURE', { userId: potentialUserId, reason: 'Passkey verification failed', ip: clientIp, method: 'passkey' }); - notificationService.sendNotification('LOGIN_FAILURE', { userId: potentialUserId, reason: 'Passkey verification failed', ip: clientIp, method: 'passkey' }); - res.status(401).json({ message: 'Passkey 认证失败。', verified: false }); - } - } catch (error: any) { - console.error(`验证 Passkey 认证时出错:`, error); - const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; - ipBlacklistService.recordFailedAttempt(clientIp); // 记录失败尝试 - auditLogService.logAction('LOGIN_FAILURE', { reason: `Passkey verification error: ${error.message}`, ip: clientIp, method: 'passkey' }); - notificationService.sendNotification('LOGIN_FAILURE', { reason: `Passkey verification error: ${error.message}`, ip: clientIp, method: 'passkey' }); - res.status(500).json({ message: '验证 Passkey 认证失败。', error: error.message }); - } -}; /** * 验证并激活 2FA (POST /api/v1/auth/2fa/verify) diff --git a/packages/backend/src/auth/auth.routes.ts b/packages/backend/src/auth/auth.routes.ts index ced1c50..4ae057b 100644 --- a/packages/backend/src/auth/auth.routes.ts +++ b/packages/backend/src/auth/auth.routes.ts @@ -7,12 +7,7 @@ import { verifyAndActivate2FA, disable2FA, getAuthStatus, - generatePasskeyRegistrationOptions, - verifyPasskeyRegistration, - generatePasskeyAuthenticationOptions, // <-- 添加导入 - verifyPasskeyAuthentication, // <-- 添加导入 - listUserPasskeys, - deleteUserPasskey, + // Removed Passkey imports needsSetup, setupAdmin, logout, @@ -57,24 +52,7 @@ router.delete('/2fa', isAuthenticated, disable2FA); // GET /api/v1/auth/status - 获取当前认证状态 (需要认证) router.get('/status', isAuthenticated, getAuthStatus); -// --- Passkey 管理接口 (都需要认证) --- -// POST /api/v1/auth/passkey/register-options - 生成 Passkey 注册选项 -router.post('/passkey/register-options', isAuthenticated, generatePasskeyRegistrationOptions); - -// POST /api/v1/auth/passkey/verify-registration - 验证 Passkey 注册响应 -router.post('/passkey/verify-registration', isAuthenticated, verifyPasskeyRegistration); -// GET /api/v1/auth/passkeys - 获取当前用户的所有 Passkey -router.get('/passkeys', isAuthenticated, listUserPasskeys); - -// DELETE /api/v1/auth/passkeys/:id - 删除指定的 Passkey -router.delete('/passkeys/:id', isAuthenticated, deleteUserPasskey); - -// --- Passkey 认证接口 (公开访问,添加黑名单检查) --- -// POST /api/v1/auth/passkey/authenticate-options - 生成 Passkey 认证选项 (用于登录) -router.post('/passkey/authenticate-options', ipBlacklistCheckMiddleware, generatePasskeyAuthenticationOptions); - -// POST /api/v1/auth/passkey/verify-authentication - 验证 Passkey 认证响应并登录 -router.post('/passkey/verify-authentication', ipBlacklistCheckMiddleware, verifyPasskeyAuthentication); +// --- Passkey routes removed --- // POST /api/v1/auth/logout - 用户登出接口 (公开访问) router.post('/logout', logout); diff --git a/packages/backend/src/database/schema.ts b/packages/backend/src/database/schema.ts index 72de254..b97af0e 100644 --- a/packages/backend/src/database/schema.ts +++ b/packages/backend/src/database/schema.ts @@ -28,20 +28,7 @@ CREATE TABLE IF NOT EXISTS audit_logs ( // ); // `; -export const createPasskeysTableSQL = ` -CREATE TABLE IF NOT EXISTS passkeys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, -- 新增:关联到用户 ID - credential_id TEXT UNIQUE NOT NULL, -- Base64URL encoded - public_key TEXT NOT NULL, -- Base64URL encoded - counter INTEGER NOT NULL, - transports TEXT, -- JSON array as string, e.g., '["internal", "usb"]' - name TEXT, -- User-provided name for the key - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -- 新增:外键约束 -); -`; +// Removed Passkeys table definition (lines 31-44 from original) export const createNotificationSettingsTableSQL = ` CREATE TABLE IF NOT EXISTS notification_settings ( diff --git a/packages/backend/src/locales/en-US.json b/packages/backend/src/locales/en-US.json index ad95d29..27ece61 100644 --- a/packages/backend/src/locales/en-US.json +++ b/packages/backend/src/locales/en-US.json @@ -20,8 +20,6 @@ "PASSWORD_CHANGED": "Password Changed", "2FA_ENABLED": "2FA Enabled", "2FA_DISABLED": "2FA Disabled", - "PASSKEY_REGISTERED": "Passkey Registered", - "PASSKEY_DELETED": "Passkey Deleted", "CONNECTION_CREATED": "Connection Created", "CONNECTION_UPDATED": "Connection Updated", "CONNECTION_DELETED": "Connection Deleted", diff --git a/packages/backend/src/locales/ja-JP.json b/packages/backend/src/locales/ja-JP.json index 5f242c9..87ea913 100644 --- a/packages/backend/src/locales/ja-JP.json +++ b/packages/backend/src/locales/ja-JP.json @@ -20,8 +20,6 @@ "PASSWORD_CHANGED": "パスワード変更", "2FA_ENABLED": "2段階認証有効", "2FA_DISABLED": "2段階認証無効", - "PASSKEY_REGISTERED": "パスキー登録", - "PASSKEY_DELETED": "パスキー削除", "CONNECTION_CREATED": "接続を作成しました", "CONNECTION_UPDATED": "接続を更新しました", "CONNECTION_DELETED": "接続を削除しました", diff --git a/packages/backend/src/locales/zh-CN.json b/packages/backend/src/locales/zh-CN.json index 2a744cb..208018f 100644 --- a/packages/backend/src/locales/zh-CN.json +++ b/packages/backend/src/locales/zh-CN.json @@ -6,8 +6,6 @@ "PASSWORD_CHANGED": "密码已更改", "2FA_ENABLED": "两步验证已启用", "2FA_DISABLED": "两步验证已禁用", - "PASSKEY_REGISTERED": "通行密钥已注册", - "PASSKEY_DELETED": "通行密钥已删除", "CONNECTION_CREATED": "连接已创建", "CONNECTION_UPDATED": "连接已更新", "CONNECTION_DELETED": "连接已删除", diff --git a/packages/backend/src/repositories/passkey.repository.ts b/packages/backend/src/repositories/passkey.repository.ts index 9db3b9d..6972061 100644 --- a/packages/backend/src/repositories/passkey.repository.ts +++ b/packages/backend/src/repositories/passkey.repository.ts @@ -1,186 +1 @@ -import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; - -// 定义 Passkey 数据库记录的接口 -export interface PasskeyRecord { - id: number; - user_id: number; // 新增:关联的用户 ID - credential_id: string; // Base64URL encoded - public_key: string; // Base64URL encoded - counter: number; - transports: string | null; - name: string | null; - created_at: number; - updated_at: number; -} - - -type DbPasskeyRow = PasskeyRecord; // 类型别名保持不变,因为接口已更新 - -export class PasskeyRepository { - - /** - * 保存新的 Passkey 凭证 - * @param userId 关联的用户 ID - * @returns Promise 新插入记录的 ID - */ - async savePasskey( - userId: number, // 新增 userId 参数 - credentialId: string, - publicKey: string, - counter: number, - transports: string | null, - name?: string - ): Promise { - const sql = ` - INSERT INTO passkeys (user_id, credential_id, public_key, counter, transports, name, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')) - `; - // 新增 userId 到参数列表 - const params = [userId, credentialId, publicKey, counter, transports, name ?? null]; - try { - const db = await getDbInstance(); - const result = await runDb(db, sql, params); - // Ensure lastID is valid before returning - if (typeof result.lastID !== 'number' || result.lastID <= 0) { - throw new Error('保存 Passkey 后未能获取有效的 lastID'); - } - return result.lastID; - } catch (err: any) { - console.error('保存 Passkey 时出错:', err.message); - if (err.message.includes('UNIQUE constraint failed')) { - throw new Error(`Credential ID "${credentialId}" 已存在。`); - } - // 检查外键约束错误 - if (err.message.includes('FOREIGN KEY constraint failed')) { - throw new Error(`关联的用户 ID ${userId} 不存在。`); - } - throw new Error(`保存 Passkey 时出错: ${err.message}`); - } - } - - /** - * 根据 Credential ID 获取 Passkey 记录 (包含 user_id) - * @returns Promise 找到的记录或 null - */ - async getPasskeyByCredentialId(credentialId: string): Promise { - // 确保查询包含 user_id - const sql = `SELECT * FROM passkeys WHERE credential_id = ?`; - try { - const db = await getDbInstance(); - // 使用更新后的 DbPasskeyRow 类型 - const row = await getDbRow(db, sql, [credentialId]); - return row || null; - } catch (err: any) { - console.error('按 Credential ID 获取 Passkey 时出错:', err.message); - throw new Error(`按 Credential ID 获取 Passkey 时出错: ${err.message}`); - } - } - - /** - * 获取指定用户或所有已注册的 Passkey 记录 (仅选择必要字段) - * @param userId 可选,如果提供,则只获取该用户的 Passkey - * @returns Promise[]> 记录的部分信息的数组 - */ - async getAllPasskeys(userId?: number): Promise>> { - let sql = `SELECT id, user_id, credential_id, name, transports, created_at FROM passkeys`; - const params: any[] = []; - if (userId !== undefined) { - sql += ` WHERE user_id = ?`; - params.push(userId); - } - sql += ` ORDER BY created_at DESC`; - - try { - const db = await getDbInstance(); - // 更新返回类型以包含 user_id - const rows = await allDb>(db, sql, params); - return rows; - } catch (err: any) { - console.error('获取 Passkey 列表时出错:', err.message); - throw new Error(`获取 Passkey 列表时出错: ${err.message}`); - } - } - - /** - * 更新 Passkey 的签名计数器 - * @returns Promise - */ - async updatePasskeyCounter(credentialId: string, newCounter: number): Promise { - const sql = `UPDATE passkeys SET counter = ?, updated_at = strftime('%s', 'now') WHERE credential_id = ?`; - try { - const db = await getDbInstance(); - const result = await runDb(db, sql, [newCounter, credentialId]); - if (result.changes === 0) { - // Consider if this should be an error or just a warning/no-op - console.warn(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行计数器更新`); - // throw new Error(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行更新`); - } - } catch (err: any) { - console.error('更新 Passkey 计数器时出错:', err.message); - throw new Error(`更新 Passkey 计数器时出错: ${err.message}`); - } - } - - /** - * 根据 ID 删除 Passkey - * @returns Promise 是否成功删除 - */ - async deletePasskeyById(id: number): Promise { - const sql = `DELETE FROM passkeys WHERE id = ?`; - try { - const db = await getDbInstance(); - const result = await runDb(db, sql, [id]); - if (result.changes > 0) { - console.log(`ID 为 ${id} 的 Passkey 已删除。`); - return true; - } else { - console.warn(`尝试删除不存在的 Passkey ID: ${id}`); - return false; - } - } catch (err: any) { - console.error('按 ID 删除 Passkey 时出错:', err.message); - throw new Error(`按 ID 删除 Passkey 时出错: ${err.message}`); - } - } - - /** - * 根据 Credential ID 删除 Passkey - * @returns Promise 是否成功删除 - */ - async deletePasskeyByCredentialId(credentialId: string): Promise { - const sql = `DELETE FROM passkeys WHERE credential_id = ?`; - try { - const db = await getDbInstance(); - const result = await runDb(db, sql, [credentialId]); - if (result.changes > 0) { - console.log(`Credential ID 为 ${credentialId} 的 Passkey 已删除。`); - return true; - } else { - console.warn(`尝试删除不存在的 Credential ID: ${credentialId}`); - return false; - } - } catch (err: any) { - console.error('按 Credential ID 删除 Passkey 时出错:', err.message); - throw new Error(`按 Credential ID 删除 Passkey 时出错: ${err.message}`); - } - } - - /** - * 根据 credential_id 或 name 前缀模糊查找 Passkey 记录(自动补全) - * @returns Promise 匹配的记录数组 - */ - // Adjust return type based on selected columns if not selecting all (*) - async searchPasskeyByPrefix(prefix: string): Promise { - const sql = `SELECT * FROM passkeys WHERE credential_id LIKE ? OR name LIKE ? ORDER BY created_at DESC`; - const likePrefix = `${prefix}%`; - try { - const db = await getDbInstance(); - const rows = await allDb(db, sql, [likePrefix, likePrefix]); - return rows; - } catch (err: any) { - console.error('模糊查找 Passkey 时出错:', err.message); - throw new Error(`模糊查找 Passkey 时出错: ${err.message}`); - } - } -} - +// This file is intentionally left empty as Passkey functionality has been removed. diff --git a/packages/backend/src/services/event.service.ts b/packages/backend/src/services/event.service.ts index fd0ce97..454468d 100644 --- a/packages/backend/src/services/event.service.ts +++ b/packages/backend/src/services/event.service.ts @@ -10,8 +10,6 @@ export enum AppEventType { PasswordChanged = 'PASSWORD_CHANGED', TwoFactorEnabled = '2FA_ENABLED', TwoFactorDisabled = '2FA_DISABLED', - PasskeyRegistered = 'PASSKEY_REGISTERED', - PasskeyDeleted = 'PASSKEY_DELETED', ConnectionCreated = 'CONNECTION_CREATED', ConnectionUpdated = 'CONNECTION_UPDATED', ConnectionDeleted = 'CONNECTION_DELETED', diff --git a/packages/backend/src/services/passkey.service.ts b/packages/backend/src/services/passkey.service.ts deleted file mode 100644 index ef71da1..0000000 --- a/packages/backend/src/services/passkey.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { - generateRegistrationOptions, - verifyRegistrationResponse, - generateAuthenticationOptions, - verifyAuthenticationResponse, - VerifiedRegistrationResponse, - // VerifiedAuthenticationResponse, // Remove original import -} from '@simplewebauthn/server'; -import type { VerifiedAuthenticationResponse as SimpleVerifiedAuthenticationResponse } from '@simplewebauthn/server'; // Import with alias -import type { - GenerateRegistrationOptionsOpts, - GenerateAuthenticationOptionsOpts, - VerifyRegistrationResponseOpts, - VerifyAuthenticationResponseOpts, - RegistrationResponseJSON, - AuthenticationResponseJSON, -} from '@simplewebauthn/server'; -import { PasskeyRepository, PasskeyRecord } from '../repositories/passkey.repository'; -import { getDbInstance, getDb } from '../database/connection'; // Import database functions -import type { User } from '../auth/auth.controller'; // Import User type (assuming it's defined or importable from auth.controller) - -// Define extended verification response type including user info -export interface VerifiedAuthenticationResponse extends SimpleVerifiedAuthenticationResponse { - userInfo?: { - userId: number; - username: string; - }; -} -// 定义 Relying Party (RP) 信息 - 这些应该来自配置或设置 -const rpName = 'Nexus Terminal'; -// rpID 和 expectedOrigin 将从请求动态获取,不再在此处硬编码 -// const rpID = process.env.NODE_ENV === 'development' ? 'localhost' : 'YOUR_PRODUCTION_DOMAIN'; -// const expectedOrigin = process.env.FRONTEND_URL || 'http://localhost:5173'; - -export class PasskeyService { - private passkeyRepository: PasskeyRepository; - - - constructor() { - this.passkeyRepository = new PasskeyRepository(); - - } - - /** - * 生成 Passkey 注册选项 (挑战) - * @param hostname 请求的主机名 (例如 'myapp.example.com' 或 'localhost') - * @param userName WebAuthn 需要的用户名 - */ - async generateRegistrationOptions(hostname: string, userName: string = 'nexus-user') { - // 暂时不获取已存在的凭证,允许同一用户注册多个设备 - - const rpID = hostname; // 使用请求的主机名作为 RP ID - - const options: GenerateRegistrationOptionsOpts = { - rpName, - rpID, - userID: Buffer.from(userName), // userID should be a Buffer/Uint8Array - userName: userName, - - authenticatorSelection: { - userVerification: 'preferred', // 倾向于需要用户验证 (PIN, 生物识别) - residentKey: 'preferred', // 倾向于创建可发现凭证 (存储在认证器上) - }, - // 可选:增加超时时间 - timeout: 60000, // 60 秒 - }; - - const registrationOptions = await generateRegistrationOptions(options); - - return registrationOptions; - } - - /** - * 验证 Passkey 注册响应 - * @param userId 当前登录用户的 ID - * @param registrationResponse 来自客户端的注册响应 - * @param expectedChallenge 之前生成的、临时存储的挑战 - * @param hostname 请求的主机名 - * @param origin 请求的源 (例如 'https://myapp.example.com' 或 'http://localhost:5173') - * @param passkeyName 用户为这个 Passkey 起的名字 (可选) - */ - async verifyRegistration( - userId: number, // 新增 userId 参数 - registrationResponse: RegistrationResponseJSON, - expectedChallenge: string, - hostname: string, - origin: string, - passkeyName?: string - ): Promise { - - console.log(`[PasskeyService VerifyReg] Received parameters: userId=${userId}, expectedChallenge=${expectedChallenge}, hostname=${hostname}, origin=${origin}, name=${passkeyName}`); // Log received parameters - console.log(`[PasskeyService VerifyReg] Received registrationResponse: ${JSON.stringify(registrationResponse)}`); // Log the raw registrationResponse - - const expectedRPID = hostname; - const expectedOrigin = origin; - - const verificationOptions: VerifyRegistrationResponseOpts = { - response: registrationResponse, - expectedChallenge: expectedChallenge, - expectedOrigin: expectedOrigin, - expectedRPID: expectedRPID, - requireUserVerification: true, // 强制要求用户验证, simplewebauthn defaults this to true now - }; - console.log(`[PasskeyService VerifyReg] Constructed verificationOptions: ${JSON.stringify(verificationOptions)}`); // Log options before verification - - let verification: VerifiedRegistrationResponse; - try { - console.log('[PasskeyService VerifyReg] Calling @simplewebauthn/server verifyRegistrationResponse...'); - verification = await verifyRegistrationResponse(verificationOptions); - console.log(`[PasskeyService VerifyReg] verifyRegistrationResponse returned: verified=${verification.verified}, registrationInfo exists=${!!verification.registrationInfo}`); // Log verification result - } catch (error: any) { - console.error('Passkey 注册验证时发生异常:', error); - // Provide more context in the error - const err = error as Error; - throw new Error(`Passkey registration verification failed: ${err.message || err}`); - } - - - // --- 移除日志记录 --- - // console.log('[PasskeyService] Verification result:', JSON.stringify(verification, null, 2)); - // --- 结束日志记录 --- - - if (verification.verified && verification.registrationInfo) { - const registrationInfo = verification.registrationInfo as any; // Keep type assertion for now - console.log(`[PasskeyService VerifyReg] Verification successful. Extracted registrationInfo: ${JSON.stringify(registrationInfo)}`); // Log extracted info - - // Log the critical fields BEFORE using them - // 从嵌套的 credential 对象中获取 id 和 publicKey - // 从嵌套的 credential 对象中获取 id, publicKey 和 counter - const credentialId = registrationInfo.credential?.id; - const credentialPublicKey = registrationInfo.credential?.publicKey; - const counter = registrationInfo.credential?.counter; // counter 也在 credential 内部 - - console.log(`[PasskeyService VerifyReg] BEFORE Buffer.from(credential.id): Type=${typeof credentialId}, Value=${credentialId}`); - console.log(`[PasskeyService VerifyReg] BEFORE Buffer.from(credential.publicKey): Type=${typeof credentialPublicKey}, Value=${credentialPublicKey}`); - console.log(`[PasskeyService VerifyReg] Extracted counter: Type=${typeof counter}, Value=${counter}`); // Log counter - - // 检查所有必要字段 - if (!credentialId || !credentialPublicKey || counter === undefined || counter === null) { - console.error('[PasskeyService VerifyReg] Error: credential.id, credential.publicKey, or counter is missing or invalid in registrationInfo.'); - throw new Error('Verification successful, but credential ID, Public Key, or Counter is missing or invalid in registration info.'); - } - - - // --- credentialId is already a Base64URL string, use directly --- - // --- publicKey needs conversion from ArrayBuffer/object --- - const credentialIdBase64Url = credentialId; // Use the string directly - const credentialPublicKeyUint8Array = new Uint8Array(credentialPublicKey); // Convert public key - const publicKeyBase64Url = Buffer.from(credentialPublicKeyUint8Array).toString('base64url'); - console.log(`[PasskeyService VerifyReg] Using credentialId (already Base64URL): ${credentialIdBase64Url}`); // Log the ID being used - console.log(`[PasskeyService VerifyReg] Converted publicKey to Base64URL: ${publicKeyBase64Url}`); // Log the converted public key - - // 获取 transports 信息 - const transports = registrationResponse.response.transports ?? null; - - // 保存到数据库,传入 userId - await this.passkeyRepository.savePasskey( - userId, // 传递 userId - credentialIdBase64Url, - publicKeyBase64Url, - counter, - transports ? JSON.stringify(transports) : null, - passkeyName - ); - console.log(`用户 ${userId} Passkey 注册成功: ${credentialIdBase64Url}, Name: ${passkeyName ?? 'N/A'}`); - } else { - console.error('Passkey 注册验证失败:', verification); - } - - return verification; - } - - /** - * 生成 Passkey 认证选项 (挑战) - * @param hostname 请求的主机名 - */ - async generateAuthenticationOptions(hostname: string): Promise> { - - const rpID = hostname; - - const options: GenerateAuthenticationOptionsOpts = { - rpID, - - userVerification: 'preferred', // 倾向于需要用户验证 - timeout: 60000, // 60 秒 - }; - - const authenticationOptions = await generateAuthenticationOptions(options); - - // TODO: 需要将生成的 challenge 临时存储起来,以便后续验证 - // 这里暂时返回 challenge,让 Controller 处理存储 - return authenticationOptions; - } - - /** - * 验证 Passkey 认证响应 - * @param authenticationResponse 来自客户端的认证响应 - * @param expectedChallenge 之前生成的、临时存储的挑战 - * @param hostname 请求的主机名 - * @param origin 请求的源 - */ - async verifyAuthentication( - authenticationResponse: AuthenticationResponseJSON, - expectedChallenge: string, - hostname: string, - origin: string - ): Promise { // Return our extended type - - const credentialIdBase64Url = authenticationResponse.id; // 客户端传回的 ID 已经是 Base64URL - console.log(`[PasskeyService VerifyAuth] Received credentialId from client: ${credentialIdBase64Url}`); // Log received ID - - console.log(`[PasskeyService VerifyAuth] Calling passkeyRepository.getPasskeyByCredentialId with: ${credentialIdBase64Url}`); - const authenticator = await this.passkeyRepository.getPasskeyByCredentialId(credentialIdBase64Url); - console.log(`[PasskeyService VerifyAuth] Result from getPasskeyByCredentialId: ${authenticator ? 'Found' : 'Not Found'}`); // Log lookup result - // Log the raw authenticator object fetched from DB - console.log(`[PasskeyService VerifyAuth] Authenticator data from DB: ${JSON.stringify(authenticator)}`); - - if (!authenticator) { - throw new Error(`未找到 Credential ID 为 ${credentialIdBase64Url} 的认证器`); - } - - const expectedRPID = hostname; - const expectedOrigin = origin; - - const verificationOptions: VerifyAuthenticationResponseOpts = { - response: authenticationResponse, - expectedChallenge: expectedChallenge, - expectedOrigin: expectedOrigin, - expectedRPID: expectedRPID, - - authenticator: { - credentialID: Buffer.from(authenticator.credential_id, 'base64url'), - credentialPublicKey: Buffer.from(authenticator.public_key, 'base64url'), - counter: authenticator.counter, - // Temporarily remove transports to test if it causes issues - // transports: authenticator.transports ? JSON.parse(authenticator.transports) : undefined, - }, - requireUserVerification: true, // Keep user verification requirement - } as any; - // Log the constructed verificationOptions, especially the authenticator part - console.log(`[PasskeyService VerifyAuth] Full authenticationResponse from client: ${JSON.stringify(authenticationResponse, null, 2)}`); // Added log - console.log(`[PasskeyService VerifyAuth] Authenticator Data (Base64URL): ${authenticationResponse.response.authenticatorData}`); // Added log - console.log(`[PasskeyService VerifyAuth] Client Data JSON (Base64URL): ${authenticationResponse.response.clientDataJSON}`); // Added log - console.log(`[PasskeyService VerifyAuth] Constructed verificationOptions for library: ${JSON.stringify(verificationOptions, null, 2)}`); - - let verification: VerifiedAuthenticationResponse; - try { - verification = await verifyAuthenticationResponse(verificationOptions); - } catch (error: any) { - console.error('Passkey 认证验证时发生异常:', error); - const err = error as Error; - if (!err.message.includes(credentialIdBase64Url)) { - throw new Error(`Passkey authentication verification failed: ${err.message || err}`); - } - throw error; - } - - if (verification.verified && verification.authenticationInfo) { - const { newCounter } = verification.authenticationInfo; - // 更新数据库中的计数器 - await this.passkeyRepository.updatePasskeyCounter(authenticator.credential_id, newCounter); - console.log(`Passkey 认证成功: ${authenticator.credential_id}`); - - // --- Added: Fetch user information --- - const db = await getDbInstance(); - // Assuming PasskeyRecord has user_id - const user = await getDb(db, 'SELECT id, username FROM users WHERE id = ?', [authenticator.user_id]); - if (!user) { - // This theoretically shouldn't happen if the authenticator exists - console.error(`Passkey authentication successful but associated user not found: UserID ${authenticator.user_id}, CredentialID ${authenticator.credential_id}`); - throw new Error('Passkey authentication successful but failed to find associated user information.'); - } - // Attach user info to the verification result - (verification as VerifiedAuthenticationResponse).userInfo = { - userId: user.id, - username: user.username, - }; - // --- End: Fetch user information --- - - } else { - console.error('Passkey 认证验证失败:', verification); - } - - return verification; - } - - /** - * 获取所有已注册 Passkey 的简要信息 (用于管理) - */ - async listPasskeys(): Promise[]> { - // 只返回 ID, Name, Transports, CreatedAt 以减少暴露敏感信息 - const keys = await this.passkeyRepository.getAllPasskeys(); - return keys.map(k => ({ - id: k.id, - name: k.name, - transports: k.transports, - created_at: k.created_at - })); - } - - /** - * 根据 ID 删除 Passkey - * @param id Passkey 记录的 ID - */ - async deletePasskey(id: number): Promise { - await this.passkeyRepository.deletePasskeyById(id); - } - - /** - * 根据 Credential ID 获取 Passkey 记录 (供认证验证使用) - * @param credentialIdBase64Url Base64URL 编码的 Credential ID - */ - async getPasskeyByCredentialId(credentialIdBase64Url: string): Promise { - // 注意:PasskeyRepository 需要有 getPasskeyByCredentialId 方法 - // 并且 PasskeyRecord 需要包含 user_id 以便后续查找用户 - return this.passkeyRepository.getPasskeyByCredentialId(credentialIdBase64Url); - } -} diff --git a/packages/backend/src/types/audit.types.ts b/packages/backend/src/types/audit.types.ts index a012df0..fcec5d4 100644 --- a/packages/backend/src/types/audit.types.ts +++ b/packages/backend/src/types/audit.types.ts @@ -7,8 +7,7 @@ export type AuditLogActionType = | 'PASSWORD_CHANGED' | '2FA_ENABLED' | '2FA_DISABLED' - | 'PASSKEY_REGISTERED' - | 'PASSKEY_DELETED' + // Removed Passkey events // Connections | 'CONNECTION_CREATED' diff --git a/packages/backend/src/types/notification.types.ts b/packages/backend/src/types/notification.types.ts index c5173f3..72183ae 100644 --- a/packages/backend/src/types/notification.types.ts +++ b/packages/backend/src/types/notification.types.ts @@ -3,7 +3,7 @@ export type NotificationChannelType = 'webhook' | 'email' | 'telegram'; // Align NotificationEvent with AuditLogActionType as requested export type NotificationEvent = | 'LOGIN_SUCCESS' | 'LOGIN_FAILURE' | 'LOGOUT' | 'PASSWORD_CHANGED' - | '2FA_ENABLED' | '2FA_DISABLED' | 'PASSKEY_REGISTERED' | 'PASSKEY_DELETED' + | '2FA_ENABLED' | '2FA_DISABLED' // Removed Passkey events | 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' | 'CONNECTION_DELETED' | 'CONNECTION_TESTED' | 'PROXY_CREATED' | 'PROXY_UPDATED' | 'PROXY_DELETED' | 'TAG_CREATED' | 'TAG_UPDATED' | 'TAG_DELETED' diff --git a/packages/backend/src/utils/crypto.ts b/packages/backend/src/utils/crypto.ts index 81cc905..f863805 100644 --- a/packages/backend/src/utils/crypto.ts +++ b/packages/backend/src/utils/crypto.ts @@ -1,80 +1,41 @@ import crypto from 'crypto'; +import bcrypt from 'bcrypt'; -const algorithm = 'aes-256-gcm'; -const ivLength = 16; // GCM 推荐的 IV 长度为 12 或 16 字节 -const tagLength = 16; // GCM 认证标签长度 +// Function to hash a password +export async function hashPassword(password: string): Promise { + const saltRounds = 10; // Adjust salt rounds as needed for security/performance balance + return bcrypt.hash(password, saltRounds); +} + +// Function to compare a password with a hash +export async function comparePassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); +} + +// Function to generate a secure random string (e.g., for session secrets, tokens) +export function generateSecureRandomString(length: number = 32): string { + return crypto.randomBytes(length).toString('hex'); +} + +// --- Added for WebAuthn --- /** - * Internal helper to get and validate the encryption key buffer on demand. + * Converts an ArrayBuffer or Buffer to a Base64URL-encoded string. + * @param buffer The ArrayBuffer or Buffer to convert. + * @returns The Base64URL-encoded string. */ -const getEncryptionKeyBuffer = (): Buffer => { - const keyEnv = process.env.ENCRYPTION_KEY; - if (!keyEnv) { - // This should ideally not happen due to initializeEnvironment in index.ts - console.error('错误:ENCRYPTION_KEY 环境变量未设置!'); - throw new Error('ENCRYPTION_KEY is not set.'); - } - try { - const keyBuffer = Buffer.from(keyEnv, 'hex'); - if (keyBuffer.length !== 32) { - console.error(`错误:加密密钥长度必须是 32 字节,当前长度为 ${keyBuffer.length}。`); - throw new Error('Invalid ENCRYPTION_KEY length.'); - } - return keyBuffer; - } catch (error) { - console.error('错误:无法将 ENCRYPTION_KEY 从 hex 解码为 Buffer:', error); - throw new Error('Failed to decode ENCRYPTION_KEY.'); - } -}; - +export function bufferToBase64url(buffer: ArrayBuffer | Buffer): string { + const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); + return buf.toString('base64url'); +} /** - * 加密文本 (例如连接密码) - * @param text - 需要加密的明文 - * @returns Base64 编码的字符串,格式为 "iv:encrypted:tag" + * Converts a Base64URL-encoded string back to a Buffer. + * @param base64urlString The Base64URL-encoded string. + * @returns The corresponding Buffer. */ -export const encrypt = (text: string): string => { - try { - const encryptionKey = getEncryptionKeyBuffer(); // Get key on demand - const iv = crypto.randomBytes(ivLength); - const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv); - const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); - const tag = cipher.getAuthTag(); - // 将 iv、密文和认证标签组合并编码 - return Buffer.concat([iv, encrypted, tag]).toString('base64'); - } catch (error) { - console.error('加密失败:', error); - throw new Error('加密过程中发生错误'); - } -}; - -/** - * 解密文本 - * @param encryptedText - Base64 编码的加密字符串 ("iv:encrypted:tag") - * @returns 解密后的明文 - */ -export const decrypt = (encryptedText: string): string => { - try { - const encryptionKey = getEncryptionKeyBuffer(); // Get key on demand - const data = Buffer.from(encryptedText, 'base64'); - if (data.length < ivLength + tagLength) { - throw new Error('无效的加密数据格式'); - } - - // 从组合数据中提取 iv、密文和认证标签 - const iv = data.slice(0, ivLength); - const encrypted = data.slice(ivLength, data.length - tagLength); - const tag = data.slice(data.length - tagLength); - - const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv); - decipher.setAuthTag(tag); // 设置认证标签以供验证 - - const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); - return decrypted.toString('utf8'); - } catch (error) { - console.error('解密失败:', error); - // 在实际应用中,解密失败通常意味着数据被篡改或密钥错误 - // 不应向客户端泄露具体错误细节 - throw new Error('解密过程中发生错误或数据无效'); - } -}; +export function base64urlToBuffer(base64urlString: string): Buffer { + // Pad the string if necessary, as base64url might omit padding + // Node.js Buffer.from handles base64url directly + return Buffer.from(base64urlString, 'base64url'); +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index fc4af01..c15f670 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -11,7 +11,6 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.7.2", "@hcaptcha/vue3-hcaptcha": "^1.3.0", - "@simplewebauthn/browser": "^13.1.0", "@tailwindcss/vite": "^4.1.4", "@xterm/addon-search": "^0.15.0", "axios": "^1.8.4", diff --git a/packages/frontend/src/components/NotificationSettingForm.vue b/packages/frontend/src/components/NotificationSettingForm.vue index 40e227e..08d3618 100644 --- a/packages/frontend/src/components/NotificationSettingForm.vue +++ b/packages/frontend/src/components/NotificationSettingForm.vue @@ -269,7 +269,7 @@ const canTestUnsaved = computed(() => { // Define all possible events (aligned with AuditLogView's allActionTypes) const allNotificationEvents: NotificationEvent[] = [ 'LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT', 'PASSWORD_CHANGED', // Added LOGOUT, PASSWORD_CHANGED - '2FA_ENABLED', '2FA_DISABLED', 'PASSKEY_REGISTERED', 'PASSKEY_DELETED', // Added 2FA, changed PASSKEY_ADDED + '2FA_ENABLED', '2FA_DISABLED', // Added 2FA, 'CONNECTION_CREATED', 'CONNECTION_UPDATED', 'CONNECTION_DELETED', 'CONNECTION_TESTED', // Changed _ADDED, added _TESTED 'PROXY_CREATED', 'PROXY_UPDATED', 'PROXY_DELETED', // Changed _ADDED 'TAG_CREATED', 'TAG_UPDATED', 'TAG_DELETED', // Changed _ADDED diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 3d4cc95..ea50f71 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -497,8 +497,6 @@ "PASSWORD_CHANGED": "Password Changed", "2FA_ENABLED": "2FA Enabled", "2FA_DISABLED": "2FA Disabled", - "PASSKEY_REGISTERED": "Passkey Registered", - "PASSKEY_DELETED": "Passkey Deleted", "CONNECTION_CREATED": "Connection Created", "CONNECTION_UPDATED": "Connection Updated", "CONNECTION_DELETED": "Connection Deleted", @@ -717,8 +715,6 @@ "PASSWORD_CHANGED": "Password Changed", "2FA_ENABLED": "2FA Enabled", "2FA_DISABLED": "2FA Disabled", - "PASSKEY_REGISTERED": "Passkey Registered", - "PASSKEY_DELETED": "Passkey Deleted", "CONNECTION_CREATED": "Connection Created", "CONNECTION_UPDATED": "Connection Updated", "CONNECTION_DELETED": "Connection Deleted", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index e9f9fa0..e2631b0 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -497,8 +497,6 @@ "PASSWORD_CHANGED": "パスワード変更", "2FA_ENABLED": "2段階認証有効", "2FA_DISABLED": "2段階認証無効", - "PASSKEY_REGISTERED": "Passkey 登録", - "PASSKEY_DELETED": "Passkey 削除", "CONNECTION_CREATED": "接続作成", "CONNECTION_UPDATED": "接続更新", "CONNECTION_DELETED": "接続削除", @@ -720,8 +718,6 @@ "PASSWORD_CHANGED": "パスワード変更", "2FA_ENABLED": "2段階認証有効", "2FA_DISABLED": "2段階認証無効", - "PASSKEY_REGISTERED": "Passkey 登録", - "PASSKEY_DELETED": "Passkey 削除", "CONNECTION_CREATED": "接続作成", "CONNECTION_UPDATED": "接続更新", "CONNECTION_DELETED": "接続削除", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index cda0225..1f34e7e 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -497,8 +497,6 @@ "PASSWORD_CHANGED": "密码已修改", "2FA_ENABLED": "两步验证已启用", "2FA_DISABLED": "两步验证已禁用", - "PASSKEY_REGISTERED": "Passkey 已注册", - "PASSKEY_DELETED": "Passkey 已删除", "CONNECTION_CREATED": "连接已创建", "CONNECTION_UPDATED": "连接已更新", "CONNECTION_DELETED": "连接已删除", @@ -720,8 +718,6 @@ "PASSWORD_CHANGED": "密码已修改", "2FA_ENABLED": "两步验证已启用", "2FA_DISABLED": "两步验证已禁用", - "PASSKEY_REGISTERED": "Passkey 已注册", - "PASSKEY_DELETED": "Passkey 已删除", "CONNECTION_CREATED": "连接已创建", "CONNECTION_UPDATED": "连接已更新", "CONNECTION_DELETED": "连接已删除", diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 1a14040..665829e 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -36,13 +36,7 @@ interface FullCaptchaSettings { recaptchaSecretKey?: string; // We won't use this in authStore } -// 新增:Passkey 信息接口 (根据后端返回调整) -interface PasskeyInfo { - id: number; // 数据库中的 ID,用于删除 - name?: string; // 用户设置的名称 - transports?: string; // JSON string of transports like ["internal", "usb"] - created_at?: number; // Unix timestamp -} +// Removed PasskeyInfo interface // Auth Store State 接口 @@ -59,9 +53,7 @@ interface AuthState { }; needsSetup: boolean; // 新增:是否需要初始设置 publicCaptchaConfig: PublicCaptchaConfig | null; // NEW: Public CAPTCHA config - passkeys: PasskeyInfo[]; // 新增:存储 Passkey 列表 - passkeysLoading: boolean; // 新增:Passkey 列表加载状态 - passkeysError: string | null; // 新增:Passkey 列表错误状态 + // Removed Passkey state properties } export const useAuthStore = defineStore('auth', { @@ -74,9 +66,7 @@ export const useAuthStore = defineStore('auth', { ipBlacklist: { entries: [], total: 0 }, // 初始化黑名单状态 needsSetup: false, // 初始假设不需要设置 publicCaptchaConfig: null, // NEW: Initialize CAPTCHA config as null - passkeys: [], // 初始化 Passkey 列表为空 - passkeysLoading: false, - passkeysError: null, + // Removed Passkey state initialization }), getters: { // 可以添加一些 getter,例如获取用户名 @@ -180,7 +170,7 @@ export const useAuthStore = defineStore('auth', { // 清除本地状态 this.isAuthenticated = false; this.user = null; - this.passkeys = []; // 登出时清空 Passkey 列表 + // Removed passkeys clear on logout console.log('已登出'); // 登出后重定向到登录页 await router.push({ name: 'Login' }); @@ -210,7 +200,7 @@ export const useAuthStore = defineStore('auth', { this.isAuthenticated = false; this.user = null; this.loginRequires2FA = false; - this.passkeys = []; // 未认证时清空 Passkey 列表 + // Removed passkeys clear on unauthenticated } } catch (error: any) { // 如果获取状态失败 (例如 session 过期),则认为未认证 @@ -218,7 +208,7 @@ export const useAuthStore = defineStore('auth', { this.isAuthenticated = false; this.user = null; this.loginRequires2FA = false; - this.passkeys = []; // 失败时也清空 Passkey 列表 + // Removed passkeys clear on error // 可选:如果不是 401 错误,可以记录更详细的日志 } finally { this.isLoading = false; @@ -353,111 +343,7 @@ export const useAuthStore = defineStore('auth', { } }, - // --- Passkey Actions --- - /** - * 获取当前用户的 Passkey 列表 - */ - async fetchPasskeys() { - if (!this.isAuthenticated) return; // 确保用户已登录 - this.passkeysLoading = true; - this.passkeysError = null; - try { - const response = await apiClient.get('/auth/passkeys'); - this.passkeys = response.data; - console.log('获取 Passkey 列表成功:', this.passkeys); - } catch (err: any) { - console.error('获取 Passkey 列表失败:', err); - this.passkeysError = err.response?.data?.message || err.message || '获取 Passkey 列表时发生未知错误。'; - this.passkeys = []; // 出错时清空列表 - } finally { - this.passkeysLoading = false; - } - }, - - /** - * 删除指定的 Passkey - * @param passkeyId 要删除的 Passkey 的 ID - */ - async deletePasskey(passkeyId: number) { - if (!this.isAuthenticated) throw new Error('用户未登录'); - // 可以添加一个 loading 状态 specific to deletion if needed - this.passkeysError = null; // Clear previous errors - try { - await apiClient.delete(`/auth/passkeys/${passkeyId}`); - console.log(`Passkey ID ${passkeyId} 已删除`); - // 从本地状态中移除 - this.passkeys = this.passkeys.filter(key => key.id !== passkeyId); - return true; // Indicate success - } catch (err: any) { - console.error(`删除 Passkey ID ${passkeyId} 失败:`, err); - this.passkeysError = err.response?.data?.message || err.message || '删除 Passkey 时发生未知错误。'; - // 抛出错误以便 UI 显示 - throw new Error(this.passkeysError ?? '删除 Passkey 时发生未知错误。'); - } - }, - - // --- Passkey Authentication Actions --- - /** - * 从后端获取 Passkey 认证选项 - */ - async getPasskeyAuthenticationOptions() { - this.isLoading = true; - this.error = null; - try { - // 调用后端 API 获取选项 - const response = await apiClient.post('/auth/passkey/authenticate-options'); - console.log('获取 Passkey 认证选项成功:', response.data); - return response.data; // 返回选项给调用者 (LoginView) - } catch (err: any) { - console.error('获取 Passkey 认证选项失败:', err); - this.error = err.response?.data?.message || err.message || '获取 Passkey 认证选项时发生未知错误。'; - // 返回 null 或抛出错误,让调用者知道失败了 - return null; - } finally { - this.isLoading = false; - } - }, - - /** - * 验证 Passkey 认证响应并登录 - * @param authenticationResponse 从 @simplewebauthn/browser 获取的响应 - * @param rememberMe 用户是否勾选了“记住我” - */ - async verifyPasskeyAuthentication(authenticationResponse: any, rememberMe: boolean) { - this.isLoading = true; - this.error = null; - try { - // 调用后端 API 验证响应 - const response = await apiClient.post<{ message: string; user: UserInfo }>('/auth/passkey/verify-authentication', { - authenticationResponse, - rememberMe // 将 rememberMe 状态传递给后端 - }); - - // Passkey 认证和登录成功 - this.isAuthenticated = true; - this.user = response.data.user; - this.loginRequires2FA = false; // Passkey 登录通常不需要额外 2FA - console.log('Passkey 登录成功:', this.user); - - // 设置语言 - if (this.user?.language) { - setLocale(this.user.language); - } - - // 跳转到工作区并刷新 - window.location.href = '/'; - return { success: true }; - - } catch (err: any) { - console.error('Passkey 认证验证失败:', err); - this.isAuthenticated = false; - this.user = null; - this.error = err.response?.data?.message || err.message || 'Passkey 登录时发生未知错误。'; - return { success: false, error: this.error }; - } finally { - this.isLoading = false; - } - }, + // --- Passkey Actions Removed --- }, persist: true, // Revert to simple persistence to fix TS error for now }); diff --git a/packages/frontend/src/types/audit.types.ts b/packages/frontend/src/types/audit.types.ts index 2828986..4692fe4 100644 --- a/packages/frontend/src/types/audit.types.ts +++ b/packages/frontend/src/types/audit.types.ts @@ -9,8 +9,6 @@ export type AuditLogActionType = | 'PASSWORD_CHANGED' | '2FA_ENABLED' | '2FA_DISABLED' - | 'PASSKEY_REGISTERED' - | 'PASSKEY_DELETED' // Connections | 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' diff --git a/packages/frontend/src/types/server.types.ts b/packages/frontend/src/types/server.types.ts index bf857e3..e3ad1ab 100644 --- a/packages/frontend/src/types/server.types.ts +++ b/packages/frontend/src/types/server.types.ts @@ -21,7 +21,7 @@ export type NotificationChannelType = 'webhook' | 'email' | 'telegram'; // Align NotificationEvent with AuditLogActionType as requested export type NotificationEvent = | 'LOGIN_SUCCESS' | 'LOGIN_FAILURE' | 'LOGOUT' | 'PASSWORD_CHANGED' - | '2FA_ENABLED' | '2FA_DISABLED' | 'PASSKEY_REGISTERED' | 'PASSKEY_DELETED' + | '2FA_ENABLED' | '2FA_DISABLED' | 'CONNECTION_CREATED' | 'CONNECTION_UPDATED' | 'CONNECTION_DELETED' | 'CONNECTION_TESTED' | 'PROXY_CREATED' | 'PROXY_UPDATED' | 'PROXY_DELETED' | 'TAG_CREATED' | 'TAG_UPDATED' | 'TAG_DELETED' @@ -74,7 +74,7 @@ export type NotificationSettingData = Omit(''); // Allow empty stri // Define all possible action types for the dropdown const allActionTypes: AuditLogActionType[] = [ 'LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT', 'PASSWORD_CHANGED', - '2FA_ENABLED', '2FA_DISABLED', 'PASSKEY_REGISTERED', 'PASSKEY_DELETED', + '2FA_ENABLED', '2FA_DISABLED', 'CONNECTION_CREATED', 'CONNECTION_UPDATED', 'CONNECTION_DELETED', 'CONNECTION_TESTED', 'PROXY_CREATED', 'PROXY_UPDATED', 'PROXY_DELETED', 'TAG_CREATED', 'TAG_UPDATED', 'TAG_DELETED', diff --git a/packages/frontend/src/views/LoginView.vue b/packages/frontend/src/views/LoginView.vue index 2877604..00cf2be 100644 --- a/packages/frontend/src/views/LoginView.vue +++ b/packages/frontend/src/views/LoginView.vue @@ -2,7 +2,7 @@ import { reactive, ref, onMounted } from 'vue'; // computed 不再直接使用,移除 import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; -import { startAuthentication } from '@simplewebauthn/browser'; // <-- 导入 Passkey 函数 +// Removed Passkey import: import { startAuthentication } from '@simplewebauthn/browser'; import { useAuthStore } from '../stores/auth.store'; import VueHcaptcha from '@hcaptcha/vue3-hcaptcha'; import VueRecaptcha from 'vue3-recaptcha2'; // 使用默认导入 @@ -98,46 +98,7 @@ onMounted(() => { authStore.fetchCaptchaConfig(); }); -// --- Passkey Login Handler --- -const handlePasskeyLogin = async () => { - authStore.clearError(); // 清除之前的错误 - captchaError.value = null; // 清除 CAPTCHA 错误 - try { - // 1. 从后端获取认证选项 (包含 challenge) - // 需要 authStore 中添加 getPasskeyAuthenticationOptions action - const options = await authStore.getPasskeyAuthenticationOptions(); - if (!options) { - // 错误已在 store action 中处理 - return; - } - - // 2. 使用浏览器 API 开始认证 - let authenticationResponse; - try { - authenticationResponse = await startAuthentication(options); - } catch (err: any) { - console.error('Passkey authentication failed (startAuthentication):', err); - // 用户取消或浏览器不支持等情况 - if (err.name === 'NotAllowedError') { - authStore.setError(t('login.error.passkeyCancelled')); - } else { - authStore.setError(t('login.error.passkeyFailed', { error: err.message || err.name || 'Unknown error' })); - } - return; - } - - // 3. 将认证响应发送到后端进行验证 - // 需要 authStore 中添加 verifyPasskeyAuthentication action - await authStore.verifyPasskeyAuthentication(authenticationResponse, rememberMe.value); - // 成功后的重定向由 store action 处理 - // 失败会更新 error 状态并在模板中显示 - - } catch (err) { - // Store action 中的错误已处理,这里无需额外操作 - console.error('Error during passkey login flow:', err); - } -}; -// --- End Passkey Login Handler --- +// --- Passkey Login Handler Removed ---