feat: add permanent trust functionality for devices with corresponding API and UI updates

This commit is contained in:
shuaiplus
2026-05-12 18:01:04 +08:00
parent 83a1fc2376
commit 2685741386
15 changed files with 140 additions and 2 deletions
+25
View File
@@ -6,6 +6,8 @@ import { errorResponse, jsonResponse } from '../utils/response';
import { readKnownDeviceProbe } from '../utils/device';
import { generateUUID } from '../utils/uuid';
const PERMANENT_TRUST_EXPIRES_AT_MS = Date.UTC(2099, 11, 31, 23, 59, 59);
function normalizeIdentifier(value: string | null | undefined): string {
return String(value || '').trim();
}
@@ -268,6 +270,29 @@ export async function handleRevokeTrustedDevice(
return jsonResponse({ success: true, removed });
}
// POST /api/devices/authorized/:deviceIdentifier/permanent
// Upgrades an existing active 2FA remember-token record to permanent trust.
export async function handleTrustDevicePermanently(
request: Request,
env: Env,
userId: string,
deviceIdentifier: string
): Promise<Response> {
void request;
const normalized = String(deviceIdentifier || '').trim();
if (!normalized) return errorResponse('Invalid device identifier', 400);
const storage = new StorageService(env.DB);
const updated = await storage.updateTrustedTwoFactorTokensExpiryByDevice(userId, normalized, PERMANENT_TRUST_EXPIRES_AT_MS);
if (!updated) return errorResponse('Device is not currently trusted', 409);
return jsonResponse({
success: true,
updated,
trustedUntil: new Date(PERMANENT_TRUST_EXPIRES_AT_MS).toISOString(),
});
}
// DELETE /api/devices/:deviceIdentifier
export async function handleDeleteDevice(
request: Request,
+7
View File
@@ -11,6 +11,7 @@ import {
handleDeactivateDevice,
handleRevokeAllTrustedDevices,
handleRevokeTrustedDevice,
handleTrustDevicePermanently,
handleDeleteAllDevices,
handleDeleteDevice,
handleUpdateDeviceName,
@@ -44,6 +45,12 @@ export async function handleAuthenticatedDeviceRoute(
return handleRevokeTrustedDevice(request, env, userId, deviceIdentifier);
}
const permanentAuthorizedDeviceMatch = path.match(/^\/api\/devices\/authorized\/([^/]+)\/permanent$/i);
if (permanentAuthorizedDeviceMatch && method === 'POST') {
const deviceIdentifier = decodeURIComponent(permanentAuthorizedDeviceMatch[1]);
return handleTrustDevicePermanently(request, env, userId, deviceIdentifier);
}
const deleteDeviceMatch = path.match(/^\/api\/devices\/([^/]+)$/i);
if (deleteDeviceMatch && method === 'GET') {
const deviceIdentifier = decodeURIComponent(deleteDeviceMatch[1]);
+15
View File
@@ -233,6 +233,21 @@ export async function deleteTrustedTwoFactorTokensByUserId(db: D1Database, userI
return Number(result.meta.changes ?? 0);
}
export async function updateTrustedTwoFactorTokensExpiryByDevice(
db: D1Database,
userId: string,
deviceIdentifier: string,
expiresAtMs: number
): Promise<number> {
const now = Date.now();
await db.prepare('DELETE FROM trusted_two_factor_device_tokens WHERE expires_at < ?').bind(now).run();
const result = await db
.prepare('UPDATE trusted_two_factor_device_tokens SET expires_at = ? WHERE user_id = ? AND device_identifier = ? AND expires_at >= ?')
.bind(expiresAtMs, userId, deviceIdentifier, now)
.run();
return Number(result.meta.changes ?? 0);
}
export async function saveTrustedTwoFactorDeviceToken(
db: D1Database,
trustedTokenKey: TrustedTokenKeyFn,
+5
View File
@@ -96,6 +96,7 @@ import {
upsertDevice as saveStoredDevice,
updateDeviceName as updateStoredDeviceName,
updateDeviceKeys as updateStoredDeviceKeys,
updateTrustedTwoFactorTokensExpiryByDevice as updateStoredTrustedTokensExpiryByDevice,
} from './storage-device-repo';
import {
ensureUsedAttachmentDownloadTokenTable as ensureStoredAttachmentTokenTable,
@@ -614,6 +615,10 @@ export class StorageService {
return deleteStoredTrustedTokensByUserId(this.db, userId);
}
async updateTrustedTwoFactorTokensExpiryByDevice(userId: string, deviceIdentifier: string, expiresAtMs: number): Promise<number> {
return updateStoredTrustedTokensExpiryByDevice(this.db, userId, deviceIdentifier, expiresAtMs);
}
// --- Trusted 2FA remember tokens (device-bound) ---
async saveTrustedTwoFactorDeviceToken(