mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
fix: require master password for admin and wipe-device actions
Gate invite management, user ban/delete, and delete-all-devices behind masterPasswordHash verification, matching backup step-up auth. The web UI prompts for the master password in the shared confirm dialog.
This commit is contained in:
+47
-14
@@ -9,6 +9,34 @@ function isAdmin(user: User): boolean {
|
||||
return user.role === 'admin' && user.status === 'active';
|
||||
}
|
||||
|
||||
async function requireMasterPasswordHash(
|
||||
env: Env,
|
||||
actorUser: User,
|
||||
masterPasswordHash: unknown
|
||||
): Promise<Response | null> {
|
||||
const normalized = String(masterPasswordHash || '').trim();
|
||||
if (!normalized) {
|
||||
return errorResponse('masterPasswordHash is required', 400);
|
||||
}
|
||||
const auth = new AuthService(env);
|
||||
const valid = await auth.verifyPassword(normalized, actorUser.masterPasswordHash, actorUser.email);
|
||||
if (!valid) {
|
||||
return errorResponse('Invalid password', 400);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readJsonBody(request: Request): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const body = await request.json();
|
||||
return body && typeof body === 'object' && !Array.isArray(body)
|
||||
? body as Record<string, unknown>
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function randomHex(bytes: number): string {
|
||||
const data = crypto.getRandomValues(new Uint8Array(bytes));
|
||||
return Array.from(data).map(v => v.toString(16).padStart(2, '0')).join('');
|
||||
@@ -204,14 +232,11 @@ export async function handleAdminCreateInvite(
|
||||
}
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
let body: { expiresInHours?: number } = {};
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
const body = await readJsonBody(request);
|
||||
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||
if (passwordError) return passwordError;
|
||||
|
||||
const expiresInHours = Number.isFinite(body.expiresInHours)
|
||||
const expiresInHours = Number.isFinite(Number(body.expiresInHours))
|
||||
? Math.max(1, Math.min(24 * 30, Math.floor(Number(body.expiresInHours))))
|
||||
: 24 * 7;
|
||||
const now = new Date();
|
||||
@@ -266,6 +291,10 @@ export async function handleAdminDeleteInvite(
|
||||
return errorResponse('Forbidden', 403);
|
||||
}
|
||||
|
||||
const body = await readJsonBody(request);
|
||||
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||
if (passwordError) return passwordError;
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const deleted = await storage.deleteInvite(code);
|
||||
if (!deleted) {
|
||||
@@ -288,6 +317,10 @@ export async function handleAdminDeleteAllInvites(
|
||||
return errorResponse('Forbidden', 403);
|
||||
}
|
||||
|
||||
const body = await readJsonBody(request);
|
||||
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||
if (passwordError) return passwordError;
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const url = new URL(request.url);
|
||||
if (url.searchParams.get('scope') === 'invalid') {
|
||||
@@ -318,12 +351,9 @@ export async function handleAdminSetUserStatus(
|
||||
return errorResponse('Forbidden', 403);
|
||||
}
|
||||
|
||||
let body: { status?: string };
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return errorResponse('Invalid JSON', 400);
|
||||
}
|
||||
const body = await readJsonBody(request);
|
||||
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||
if (passwordError) return passwordError;
|
||||
|
||||
const nextStatus = body.status === 'banned' ? 'banned' : body.status === 'active' ? 'active' : null;
|
||||
if (!nextStatus) {
|
||||
@@ -366,7 +396,6 @@ export async function handleAdminDeleteUser(
|
||||
actorUser: User,
|
||||
targetUserId: string
|
||||
): Promise<Response> {
|
||||
void request;
|
||||
if (!isAdmin(actorUser)) {
|
||||
return errorResponse('Forbidden', 403);
|
||||
}
|
||||
@@ -374,6 +403,10 @@ export async function handleAdminDeleteUser(
|
||||
return errorResponse('You cannot delete yourself', 400);
|
||||
}
|
||||
|
||||
const body = await readJsonBody(request);
|
||||
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||
if (passwordError) return passwordError;
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const target = await storage.getUserById(targetUserId);
|
||||
if (!target) {
|
||||
|
||||
+16
-1
@@ -464,11 +464,26 @@ export async function handleUpdateDeviceName(
|
||||
|
||||
// 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);
|
||||
|
||||
let masterPasswordHash = '';
|
||||
try {
|
||||
const body = await request.json() as { masterPasswordHash?: string };
|
||||
masterPasswordHash = String(body?.masterPasswordHash || '').trim();
|
||||
} catch {
|
||||
masterPasswordHash = '';
|
||||
}
|
||||
if (!masterPasswordHash) {
|
||||
return errorResponse('masterPasswordHash is required', 400);
|
||||
}
|
||||
const auth = new AuthService(env);
|
||||
const passwordValid = await auth.verifyPassword(masterPasswordHash, user.masterPasswordHash, user.email);
|
||||
if (!passwordValid) {
|
||||
return errorResponse('Invalid password', 400);
|
||||
}
|
||||
|
||||
const [removedTrusted, removedSessions, removedDevices] = await Promise.all([
|
||||
storage.deleteTrustedTwoFactorTokensByUserId(userId),
|
||||
storage.deleteRefreshTokensByUserId(userId),
|
||||
|
||||
Reference in New Issue
Block a user