mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-06 15:10:13 +00:00
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>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
import { ShieldCheck, ShieldX } from 'lucide-preact';
|
|
import ConfirmDialog from '@/components/ConfirmDialog';
|
|
import { t } from '@/lib/i18n';
|
|
import type { AuthRequest } from '@/lib/types';
|
|
|
|
interface AuthRequestApprovalDialogProps {
|
|
open: boolean;
|
|
authRequest: AuthRequest | null;
|
|
submitting: boolean;
|
|
onApprove: () => void;
|
|
onDeny: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function formatDateTime(value: string | null | undefined): string {
|
|
if (!value) return t('txt_dash');
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) return value;
|
|
return parsed.toLocaleString();
|
|
}
|
|
|
|
export default function AuthRequestApprovalDialog(props: AuthRequestApprovalDialogProps) {
|
|
const authRequest = props.authRequest;
|
|
return (
|
|
<ConfirmDialog
|
|
open={props.open && !!authRequest}
|
|
title={t('txt_approve_device_login')}
|
|
message={t('txt_auth_request_approve_message')}
|
|
confirmText={props.submitting ? t('txt_approving') : t('txt_approve')}
|
|
cancelText={t('txt_later')}
|
|
confirmDisabled={props.submitting || !authRequest}
|
|
cancelDisabled={props.submitting}
|
|
onConfirm={props.onApprove}
|
|
onCancel={props.onClose}
|
|
afterActions={(
|
|
<button
|
|
type="button"
|
|
className="btn btn-danger dialog-btn"
|
|
disabled={props.submitting || !authRequest}
|
|
onClick={props.onDeny}
|
|
>
|
|
<ShieldX size={14} className="btn-icon" />
|
|
{t('txt_deny')}
|
|
</button>
|
|
)}
|
|
>
|
|
{authRequest && (
|
|
<div className="auth-request-details">
|
|
<div className="auth-request-device">
|
|
<ShieldCheck size={18} />
|
|
<div>
|
|
<strong>{authRequest.requestDeviceType || t('txt_unknown_device')}</strong>
|
|
<small>{authRequest.requestDeviceIdentifier}</small>
|
|
</div>
|
|
</div>
|
|
<div className="auth-request-kv">
|
|
<span>{t('txt_created')}</span>
|
|
<strong>{formatDateTime(authRequest.creationDate)}</strong>
|
|
</div>
|
|
{authRequest.requestIpAddress && (
|
|
<div className="auth-request-kv">
|
|
<span>{t('txt_ip_address')}</span>
|
|
<strong>{authRequest.requestIpAddress}</strong>
|
|
</div>
|
|
)}
|
|
<div className="auth-request-fingerprint">
|
|
<span>{t('txt_fingerprint_phrase')}</span>
|
|
<strong>{authRequest.fingerprintPhrase || t('txt_dash')}</strong>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ConfirmDialog>
|
|
);
|
|
}
|