mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-21 05:10:41 +00:00
185 lines
6.3 KiB
TypeScript
185 lines
6.3 KiB
TypeScript
import { Env } from '../types';
|
|
import { getOnlineUserDevices, notifyUserLogout } from '../durable/notifications-hub';
|
|
import { StorageService } from '../services/storage';
|
|
import { errorResponse, jsonResponse } from '../utils/response';
|
|
import { readKnownDeviceProbe } from '../utils/device';
|
|
import { generateUUID } from '../utils/uuid';
|
|
|
|
// GET /api/devices/knowndevice
|
|
// Compatible with Bitwarden/Vaultwarden behavior:
|
|
// - X-Request-Email: base64url(email) without padding
|
|
// - X-Device-Identifier: client device identifier
|
|
export async function handleKnownDevice(request: Request, env: Env): Promise<Response> {
|
|
const storage = new StorageService(env.DB);
|
|
const { email, deviceIdentifier } = readKnownDeviceProbe(request);
|
|
|
|
if (!email || !deviceIdentifier) {
|
|
return jsonResponse(false);
|
|
}
|
|
|
|
const known = await storage.isKnownDeviceByEmail(email, deviceIdentifier);
|
|
return jsonResponse(known);
|
|
}
|
|
|
|
// GET /api/devices
|
|
export async function handleGetDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
|
void request;
|
|
const storage = new StorageService(env.DB);
|
|
const devices = await storage.getDevicesByUserId(userId);
|
|
|
|
return jsonResponse({
|
|
data: devices.map(device => ({
|
|
id: device.deviceIdentifier,
|
|
name: device.name,
|
|
identifier: device.deviceIdentifier,
|
|
type: device.type,
|
|
creationDate: device.createdAt,
|
|
revisionDate: device.updatedAt,
|
|
object: 'device',
|
|
})),
|
|
object: 'list',
|
|
continuationToken: null,
|
|
});
|
|
}
|
|
|
|
// GET /api/devices/authorized
|
|
// Returns known devices together with active 2FA remember-token expiry.
|
|
export async function handleGetAuthorizedDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
|
void request;
|
|
const storage = new StorageService(env.DB);
|
|
const [devices, trusted, onlineDeviceIdentifiers] = await Promise.all([
|
|
storage.getDevicesByUserId(userId),
|
|
storage.getTrustedDeviceTokenSummariesByUserId(userId),
|
|
getOnlineUserDevices(env, userId),
|
|
]);
|
|
const onlineSet = new Set(onlineDeviceIdentifiers);
|
|
|
|
const trustedByIdentifier = new Map<string, { expiresAt: number; tokenCount: number }>();
|
|
for (const row of trusted) {
|
|
trustedByIdentifier.set(row.deviceIdentifier, { expiresAt: row.expiresAt, tokenCount: row.tokenCount });
|
|
}
|
|
|
|
const knownIdentifiers = new Set<string>();
|
|
const data = devices.map(device => {
|
|
knownIdentifiers.add(device.deviceIdentifier);
|
|
const trustedInfo = trustedByIdentifier.get(device.deviceIdentifier);
|
|
return {
|
|
id: device.deviceIdentifier,
|
|
name: device.name,
|
|
identifier: device.deviceIdentifier,
|
|
type: device.type,
|
|
creationDate: device.createdAt,
|
|
revisionDate: device.updatedAt,
|
|
online: onlineSet.has(device.deviceIdentifier),
|
|
trusted: !!trustedInfo,
|
|
trustedTokenCount: trustedInfo?.tokenCount || 0,
|
|
trustedUntil: trustedInfo?.expiresAt ? new Date(trustedInfo.expiresAt).toISOString() : null,
|
|
object: 'device',
|
|
};
|
|
});
|
|
|
|
for (const row of trusted) {
|
|
if (knownIdentifiers.has(row.deviceIdentifier)) continue;
|
|
data.push({
|
|
id: row.deviceIdentifier,
|
|
name: 'Unknown device',
|
|
identifier: row.deviceIdentifier,
|
|
type: 14,
|
|
creationDate: '',
|
|
revisionDate: '',
|
|
online: onlineSet.has(row.deviceIdentifier),
|
|
trusted: true,
|
|
trustedTokenCount: row.tokenCount,
|
|
trustedUntil: row.expiresAt ? new Date(row.expiresAt).toISOString() : null,
|
|
object: 'device',
|
|
});
|
|
}
|
|
|
|
return jsonResponse({
|
|
data,
|
|
object: 'list',
|
|
continuationToken: null,
|
|
});
|
|
}
|
|
|
|
// DELETE /api/devices/authorized
|
|
export async function handleRevokeAllTrustedDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
|
void request;
|
|
const storage = new StorageService(env.DB);
|
|
const removed = await storage.deleteTrustedTwoFactorTokensByUserId(userId);
|
|
return jsonResponse({ success: true, removed });
|
|
}
|
|
|
|
// DELETE /api/devices/authorized/:deviceIdentifier
|
|
export async function handleRevokeTrustedDevice(
|
|
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 removed = await storage.deleteTrustedTwoFactorTokensByDevice(userId, normalized);
|
|
return jsonResponse({ success: true, removed });
|
|
}
|
|
|
|
// DELETE /api/devices/:deviceIdentifier
|
|
export async function handleDeleteDevice(
|
|
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);
|
|
await storage.deleteTrustedTwoFactorTokensByDevice(userId, normalized);
|
|
await storage.deleteRefreshTokensByDevice(userId, normalized);
|
|
const deleted = await storage.deleteDevice(userId, normalized);
|
|
if (deleted) {
|
|
await notifyUserLogout(env, userId, normalized);
|
|
}
|
|
return jsonResponse({ success: deleted });
|
|
}
|
|
|
|
// DELETE /api/devices
|
|
export async function handleDeleteAllDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
|
void request;
|
|
const storage = new StorageService(env.DB);
|
|
const user = await storage.getUserById(userId);
|
|
if (!user) return errorResponse('User not found', 404);
|
|
|
|
const [removedTrusted, removedSessions, removedDevices] = await Promise.all([
|
|
storage.deleteTrustedTwoFactorTokensByUserId(userId),
|
|
storage.deleteRefreshTokensByUserId(userId),
|
|
storage.deleteDevicesByUserId(userId),
|
|
]);
|
|
user.securityStamp = generateUUID();
|
|
user.updatedAt = new Date().toISOString();
|
|
await storage.saveUser(user);
|
|
await notifyUserLogout(env, userId, null);
|
|
return jsonResponse({ success: true, removedTrusted, removedSessions: removedSessions ?? 0, removedDevices });
|
|
}
|
|
|
|
// PUT /api/devices/identifier/{deviceIdentifier}/token
|
|
// Bitwarden mobile reports push token updates to this endpoint.
|
|
// NodeWarden does not implement push notifications, so accept and no-op.
|
|
export async function handleUpdateDeviceToken(
|
|
request: Request,
|
|
env: Env,
|
|
userId: string,
|
|
deviceIdentifier: string
|
|
): Promise<Response> {
|
|
void request;
|
|
void env;
|
|
void userId;
|
|
void deviceIdentifier;
|
|
return new Response(null, { status: 200 });
|
|
}
|
|
|