Files
nodewarden/src/handlers/notifications.ts
T
shuaiplus c652cc1533 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>
2026-06-12 13:12:11 +08:00

74 lines
2.8 KiB
TypeScript

import { AuthService } from '../services/auth';
import type { Env, JWTPayload } from '../types';
import { errorResponse, jsonResponse } from '../utils/response';
import { generateUUID } from '../utils/uuid';
function extractAccessToken(request: Request): string | null {
const url = new URL(request.url);
const queryToken = String(url.searchParams.get('access_token') || '').trim();
if (queryToken) return queryToken;
const authHeader = String(request.headers.get('Authorization') || '').trim();
const match = authHeader.match(/^Bearer\s+(.+)$/i);
return match?.[1]?.trim() || null;
}
async function authenticateNotificationsRequest(request: Request, env: Env): Promise<JWTPayload | null> {
const accessToken = extractAccessToken(request);
if (!accessToken) return null;
const auth = new AuthService(env);
return auth.verifyAccessToken(`Bearer ${accessToken}`);
}
export async function handleNotificationsNegotiate(request: Request, env: Env): Promise<Response> {
const payload = await authenticateNotificationsRequest(request, env);
if (!payload?.sub) return errorResponse('Unauthorized', 401);
const connectionId = generateUUID();
return jsonResponse({
connectionId,
connectionToken: connectionId,
negotiateVersion: 1,
availableTransports: [
{
transport: 'WebSockets',
transferFormats: ['Text', 'Binary'],
},
],
});
}
export async function handleNotificationsHub(request: Request, env: Env): Promise<Response> {
const payload = await authenticateNotificationsRequest(request, env);
if (!payload?.sub) return errorResponse('Unauthorized', 401);
if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
return errorResponse('Expected websocket', 426);
}
const userId = payload.sub;
const id = env.NOTIFICATIONS_HUB.idFromName(userId);
const stub = env.NOTIFICATIONS_HUB.get(id);
const forwardedUrl = new URL(request.url);
forwardedUrl.searchParams.set('nw_uid', userId);
if (payload.did) {
forwardedUrl.searchParams.set('nw_did', payload.did);
}
return stub.fetch(new Request(forwardedUrl.toString(), request));
}
export async function handleAnonymousNotificationsHub(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const authRequestId = String(url.searchParams.get('Token') || url.searchParams.get('token') || '').trim();
if (!authRequestId) return errorResponse('Token is required', 400);
if (request.headers.get('Upgrade')?.toLowerCase() !== 'websocket') {
return errorResponse('Expected websocket', 426);
}
const id = env.NOTIFICATIONS_HUB.idFromName(authRequestId);
const stub = env.NOTIFICATIONS_HUB.get(id);
const forwardedUrl = new URL(request.url);
forwardedUrl.searchParams.set('nw_auth_request_id', authRequestId);
return stub.fetch(new Request(forwardedUrl.toString(), request));
}