feat: implement device login approval system

Add a complete device authentication approval flow that allows users to approve login requests from new devices on their already-authenticated devices.

Core features:
- Create authentication requests when logging in from new devices
- Display pending requests with device info, IP address, and fingerprint phrases
- Approve or deny requests from web interface with real-time notifications
- Support multiple auth request types (authenticate & unlock, unlock only)
- Automatic expiration and cleanup of stale requests

Backend changes:
- Add auth_requests table with proper indexes for efficient queries
- Implement full CRUD API for authentication requests
- Add notification hub integration for real-time updates
- Add device fingerprint phrase generation for security verification

Frontend changes:
- Add AuthRequestApprovalDialog component for approving/denying requests
- Add PendingAuthRequestsPanel component to display and manage pending requests
- Integrate panels into Security and Settings pages
- Add fingerprint wordlist for generating human-readable verification phrases
- Update i18n translations for all supported languages

Security considerations:
- Access code verification to prevent unauthorized access
- Device fingerprint validation for additional security layer
- IP address and country tracking for audit purposes
- Automatic expiration of old requests (15 minutes)
- Only most recent request per device can be approved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
shuaiplus
2026-06-12 13:12:11 +08:00
parent e9aef72df7
commit c652cc1533
27 changed files with 9187 additions and 92 deletions
+22
View File
@@ -15,9 +15,14 @@ import {
handleGetPasswordHint,
handleRecoverTwoFactor,
} from './handlers/accounts';
import {
handleCreateAuthRequest,
handleGetAuthRequestResponse,
} from './handlers/auth-requests';
import { handlePublicDownloadAttachment } from './handlers/attachments';
import { handlePublicUploadAttachment } from './handlers/attachments';
import {
handleAnonymousNotificationsHub,
handleNotificationsHub,
handleNotificationsNegotiate,
} from './handlers/notifications';
@@ -390,6 +395,19 @@ export async function handlePublicRoute(
return handleDownloadSendFile(request, env, sendDownloadMatch[1], sendDownloadMatch[2]);
}
if ((path === '/api/auth-requests' || path === '/api/auth-requests/') && method === 'POST') {
const blocked = await enforcePublicRateLimit('public-sensitive', LIMITS.rateLimit.sensitivePublicRequestsPerMinute);
if (blocked) return blocked;
return handleCreateAuthRequest(request, env);
}
const authRequestResponseMatch = path.match(/^\/api\/auth-requests\/([a-f0-9-]+)\/response$/i);
if (authRequestResponseMatch && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-sensitive', LIMITS.rateLimit.sensitivePublicRequestsPerMinute);
if (blocked) return blocked;
return handleGetAuthRequestResponse(request, env, authRequestResponseMatch[1]);
}
if (path === '/identity/connect/token' && method === 'POST') {
return handleToken(request, env);
}
@@ -477,5 +495,9 @@ export async function handlePublicRoute(
if (path === '/notifications/hub' && method === 'GET') {
return handleNotificationsHub(request, env);
}
if (path === '/notifications/anonymous-hub' && method === 'GET') {
return handleAnonymousNotificationsHub(request, env);
}
return null;
}