mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +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';
|
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 {
|
function randomHex(bytes: number): string {
|
||||||
const data = crypto.getRandomValues(new Uint8Array(bytes));
|
const data = crypto.getRandomValues(new Uint8Array(bytes));
|
||||||
return Array.from(data).map(v => v.toString(16).padStart(2, '0')).join('');
|
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);
|
const storage = new StorageService(env.DB);
|
||||||
let body: { expiresInHours?: number } = {};
|
const body = await readJsonBody(request);
|
||||||
try {
|
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||||
body = await request.json();
|
if (passwordError) return passwordError;
|
||||||
} catch {
|
|
||||||
body = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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))))
|
? Math.max(1, Math.min(24 * 30, Math.floor(Number(body.expiresInHours))))
|
||||||
: 24 * 7;
|
: 24 * 7;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -266,6 +291,10 @@ export async function handleAdminDeleteInvite(
|
|||||||
return errorResponse('Forbidden', 403);
|
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 storage = new StorageService(env.DB);
|
||||||
const deleted = await storage.deleteInvite(code);
|
const deleted = await storage.deleteInvite(code);
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
@@ -288,6 +317,10 @@ export async function handleAdminDeleteAllInvites(
|
|||||||
return errorResponse('Forbidden', 403);
|
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 storage = new StorageService(env.DB);
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
if (url.searchParams.get('scope') === 'invalid') {
|
if (url.searchParams.get('scope') === 'invalid') {
|
||||||
@@ -318,12 +351,9 @@ export async function handleAdminSetUserStatus(
|
|||||||
return errorResponse('Forbidden', 403);
|
return errorResponse('Forbidden', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: { status?: string };
|
const body = await readJsonBody(request);
|
||||||
try {
|
const passwordError = await requireMasterPasswordHash(env, actorUser, body.masterPasswordHash);
|
||||||
body = await request.json();
|
if (passwordError) return passwordError;
|
||||||
} catch {
|
|
||||||
return errorResponse('Invalid JSON', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStatus = body.status === 'banned' ? 'banned' : body.status === 'active' ? 'active' : null;
|
const nextStatus = body.status === 'banned' ? 'banned' : body.status === 'active' ? 'active' : null;
|
||||||
if (!nextStatus) {
|
if (!nextStatus) {
|
||||||
@@ -366,7 +396,6 @@ export async function handleAdminDeleteUser(
|
|||||||
actorUser: User,
|
actorUser: User,
|
||||||
targetUserId: string
|
targetUserId: string
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
void request;
|
|
||||||
if (!isAdmin(actorUser)) {
|
if (!isAdmin(actorUser)) {
|
||||||
return errorResponse('Forbidden', 403);
|
return errorResponse('Forbidden', 403);
|
||||||
}
|
}
|
||||||
@@ -374,6 +403,10 @@ export async function handleAdminDeleteUser(
|
|||||||
return errorResponse('You cannot delete yourself', 400);
|
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 storage = new StorageService(env.DB);
|
||||||
const target = await storage.getUserById(targetUserId);
|
const target = await storage.getUserById(targetUserId);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
|
|||||||
+16
-1
@@ -464,11 +464,26 @@ export async function handleUpdateDeviceName(
|
|||||||
|
|
||||||
// DELETE /api/devices
|
// DELETE /api/devices
|
||||||
export async function handleDeleteAllDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
export async function handleDeleteAllDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
||||||
void request;
|
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const user = await storage.getUserById(userId);
|
const user = await storage.getUserById(userId);
|
||||||
if (!user) return errorResponse('User not found', 404);
|
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([
|
const [removedTrusted, removedSessions, removedDevices] = await Promise.all([
|
||||||
storage.deleteTrustedTwoFactorTokensByUserId(userId),
|
storage.deleteTrustedTwoFactorTokensByUserId(userId),
|
||||||
storage.deleteRefreshTokensByUserId(userId),
|
storage.deleteRefreshTokensByUserId(userId),
|
||||||
|
|||||||
@@ -1856,6 +1856,8 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
const adminActions = useAdminActions({
|
const adminActions = useAdminActions({
|
||||||
authedFetch,
|
authedFetch,
|
||||||
|
email: String(profile?.email || session?.email || ''),
|
||||||
|
defaultKdfIterations,
|
||||||
onNotify: pushToast,
|
onNotify: pushToast,
|
||||||
onSetConfirm: setConfirm,
|
onSetConfirm: setConfirm,
|
||||||
refetchUsers: usersQuery.refetch,
|
refetchUsers: usersQuery.refetch,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export interface AppConfirmState {
|
|||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
hideCancel?: boolean;
|
hideCancel?: boolean;
|
||||||
onConfirm: () => void;
|
/** When true, dialog shows a master-password field and passes it to onConfirm. */
|
||||||
|
requireMasterPassword?: boolean;
|
||||||
|
onConfirm: (masterPassword?: string) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ function twoFactorProviderLabel(providerType: number): string {
|
|||||||
|
|
||||||
export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) {
|
export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) {
|
||||||
const [methodChooserOpen, setMethodChooserOpen] = useState(false);
|
const [methodChooserOpen, setMethodChooserOpen] = useState(false);
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const availableProviders = useMemo(
|
const availableProviders = useMemo(
|
||||||
() => uniqueSupportedProviders(props.pendingTotpAvailableProviders),
|
() => uniqueSupportedProviders(props.pendingTotpAvailableProviders),
|
||||||
[props.pendingTotpAvailableProviders]
|
[props.pendingTotpAvailableProviders]
|
||||||
@@ -70,11 +73,16 @@ export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) {
|
|||||||
const alternateProviders = availableProviders.filter((provider) => provider !== props.pendingTotpProviderType);
|
const alternateProviders = availableProviders.filter((provider) => provider !== props.pendingTotpProviderType);
|
||||||
const isYubiKeyOtp = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_YUBIKEY;
|
const isYubiKeyOtp = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_YUBIKEY;
|
||||||
const isWebAuthn = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_WEBAUTHN;
|
const isWebAuthn = props.pendingTotpProviderType === TWO_FACTOR_PROVIDER_WEBAUTHN;
|
||||||
|
const requireMasterPassword = !!props.confirm?.requireMasterPassword;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMethodChooserOpen(false);
|
setMethodChooserOpen(false);
|
||||||
}, [props.pendingTotpOpen, props.pendingTotpProviderType]);
|
}, [props.pendingTotpOpen, props.pendingTotpProviderType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setConfirmPassword('');
|
||||||
|
}, [props.confirm?.title, props.confirm?.message, requireMasterPassword]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -86,9 +94,30 @@ export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) {
|
|||||||
confirmText={props.confirm?.confirmText}
|
confirmText={props.confirm?.confirmText}
|
||||||
cancelText={props.confirm?.cancelText}
|
cancelText={props.confirm?.cancelText}
|
||||||
hideCancel={props.confirm?.hideCancel}
|
hideCancel={props.confirm?.hideCancel}
|
||||||
onConfirm={() => props.confirm?.onConfirm()}
|
confirmDisabled={requireMasterPassword && !confirmPassword.trim()}
|
||||||
onCancel={props.confirm?.onCancel || props.onCancelConfirm}
|
onConfirm={() => {
|
||||||
/>
|
if (requireMasterPassword && !confirmPassword.trim()) return;
|
||||||
|
props.confirm?.onConfirm(requireMasterPassword ? confirmPassword : undefined);
|
||||||
|
setConfirmPassword('');
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setConfirmPassword('');
|
||||||
|
(props.confirm?.onCancel || props.onCancelConfirm)();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{requireMasterPassword && (
|
||||||
|
<label className="field">
|
||||||
|
<span>{t('txt_master_password')}</span>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onInput={(e) => setConfirmPassword((e.currentTarget as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={props.pendingTotpOpen}
|
open={props.pendingTotpOpen}
|
||||||
|
|||||||
@@ -561,13 +561,18 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
|||||||
openRemoveAllDevices() {
|
openRemoveAllDevices() {
|
||||||
onSetConfirm({
|
onSetConfirm({
|
||||||
title: t('txt_remove_all_devices'),
|
title: t('txt_remove_all_devices'),
|
||||||
message: t('txt_remove_all_devices_and_sign_out_all_sessions'),
|
message: `${t('txt_remove_all_devices_and_sign_out_all_sessions')}\n${t('txt_enter_master_password_to_continue')}`,
|
||||||
danger: true,
|
danger: true,
|
||||||
onConfirm: () => {
|
requireMasterPassword: true,
|
||||||
|
onConfirm: (masterPassword) => {
|
||||||
onSetConfirm(null);
|
onSetConfirm(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAllAuthorizedDevices(authedFetch);
|
if (!profile) throw new Error(t('txt_profile_unavailable'));
|
||||||
|
const normalizedPassword = String(masterPassword || '');
|
||||||
|
if (!normalizedPassword.trim()) throw new Error(t('txt_master_password_is_required'));
|
||||||
|
const derived = await deriveLoginHash(profile.email, normalizedPassword, defaultKdfIterations);
|
||||||
|
await deleteAllAuthorizedDevices(authedFetch, derived.hash);
|
||||||
onNotify('success', t('txt_all_devices_removed'));
|
onNotify('success', t('txt_all_devices_removed'));
|
||||||
onLogoutNow();
|
onLogoutNow();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'preact/hooks';
|
import { useMemo } from 'preact/hooks';
|
||||||
import { createInvite, deleteAllInvites, deleteInvalidInvites, deleteInvite, deleteUser, setUserStatus } from '@/lib/api/admin';
|
import { createInvite, deleteAllInvites, deleteInvalidInvites, deleteInvite, deleteUser, setUserStatus } from '@/lib/api/admin';
|
||||||
|
import { deriveLoginHash } from '@/lib/api/auth';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import type { AppConfirmState } from '@/components/AppGlobalOverlays';
|
import type { AppConfirmState } from '@/components/AppGlobalOverlays';
|
||||||
import type { AuthedFetch } from '@/lib/api/shared';
|
import type { AuthedFetch } from '@/lib/api/shared';
|
||||||
@@ -8,6 +9,8 @@ type Notify = (type: 'success' | 'error' | 'warning', text: string) => void;
|
|||||||
|
|
||||||
interface UseAdminActionsOptions {
|
interface UseAdminActionsOptions {
|
||||||
authedFetch: AuthedFetch;
|
authedFetch: AuthedFetch;
|
||||||
|
email: string;
|
||||||
|
defaultKdfIterations: number;
|
||||||
onNotify: Notify;
|
onNotify: Notify;
|
||||||
onSetConfirm: (next: AppConfirmState | null) => void;
|
onSetConfirm: (next: AppConfirmState | null) => void;
|
||||||
refetchUsers: () => Promise<unknown>;
|
refetchUsers: () => Promise<unknown>;
|
||||||
@@ -15,7 +18,24 @@ interface UseAdminActionsOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useAdminActions(options: UseAdminActionsOptions) {
|
export default function useAdminActions(options: UseAdminActionsOptions) {
|
||||||
const { authedFetch, onNotify, onSetConfirm, refetchUsers, refetchInvites } = options;
|
const {
|
||||||
|
authedFetch,
|
||||||
|
email,
|
||||||
|
defaultKdfIterations,
|
||||||
|
onNotify,
|
||||||
|
onSetConfirm,
|
||||||
|
refetchUsers,
|
||||||
|
refetchInvites,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
async function withMasterPasswordHash(masterPassword: string | undefined): Promise<string> {
|
||||||
|
const normalizedEmail = String(email || '').trim().toLowerCase();
|
||||||
|
const normalizedPassword = String(masterPassword || '');
|
||||||
|
if (!normalizedEmail) throw new Error(t('txt_profile_unavailable'));
|
||||||
|
if (!normalizedPassword.trim()) throw new Error(t('txt_master_password_is_required'));
|
||||||
|
const derived = await deriveLoginHash(normalizedEmail, normalizedPassword, defaultKdfIterations);
|
||||||
|
return derived.hash;
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -26,35 +46,61 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async createInvite(hours: number) {
|
async createInvite(hours: number) {
|
||||||
try {
|
onSetConfirm({
|
||||||
await createInvite(authedFetch, hours);
|
title: t('txt_create_timed_invite'),
|
||||||
await refetchInvites();
|
message: t('txt_enter_master_password_to_continue'),
|
||||||
onNotify('success', t('txt_invite_created'));
|
requireMasterPassword: true,
|
||||||
} catch (error) {
|
onConfirm: (masterPassword) => {
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_create_invite_failed'));
|
onSetConfirm(null);
|
||||||
}
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await createInvite(authedFetch, hours, hash);
|
||||||
|
await refetchInvites();
|
||||||
|
onNotify('success', t('txt_invite_created'));
|
||||||
|
} catch (error) {
|
||||||
|
onNotify('error', error instanceof Error ? error.message : t('txt_create_invite_failed'));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleUserStatus(userId: string, status: 'active' | 'banned') {
|
async toggleUserStatus(userId: string, status: 'active' | 'banned') {
|
||||||
try {
|
const nextStatus = status === 'active' ? 'banned' : 'active';
|
||||||
await setUserStatus(authedFetch, userId, status === 'active' ? 'banned' : 'active');
|
onSetConfirm({
|
||||||
await refetchUsers();
|
title: nextStatus === 'banned' ? t('txt_ban') : t('txt_unban'),
|
||||||
onNotify('success', t('txt_user_status_updated'));
|
message: t('txt_enter_master_password_to_continue'),
|
||||||
} catch (error) {
|
danger: nextStatus === 'banned',
|
||||||
onNotify('error', error instanceof Error ? error.message : t('txt_update_user_status_failed'));
|
requireMasterPassword: true,
|
||||||
}
|
onConfirm: (masterPassword) => {
|
||||||
|
onSetConfirm(null);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await setUserStatus(authedFetch, userId, nextStatus, hash);
|
||||||
|
await refetchUsers();
|
||||||
|
onNotify('success', t('txt_user_status_updated'));
|
||||||
|
} catch (error) {
|
||||||
|
onNotify('error', error instanceof Error ? error.message : t('txt_update_user_status_failed'));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteInvite(code: string) {
|
async deleteInvite(code: string) {
|
||||||
onSetConfirm({
|
onSetConfirm({
|
||||||
title: t('txt_delete_invite'),
|
title: t('txt_delete_invite'),
|
||||||
message: t('txt_delete_invite_confirm_message'),
|
message: `${t('txt_delete_invite_confirm_message')}\n${t('txt_enter_master_password_to_continue')}`,
|
||||||
danger: true,
|
danger: true,
|
||||||
onConfirm: () => {
|
requireMasterPassword: true,
|
||||||
|
onConfirm: (masterPassword) => {
|
||||||
onSetConfirm(null);
|
onSetConfirm(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await deleteInvite(authedFetch, code);
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await deleteInvite(authedFetch, code, hash);
|
||||||
await refetchInvites();
|
await refetchInvites();
|
||||||
onNotify('success', t('txt_invite_deleted'));
|
onNotify('success', t('txt_invite_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -68,13 +114,15 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
async deleteInvalidInvites() {
|
async deleteInvalidInvites() {
|
||||||
onSetConfirm({
|
onSetConfirm({
|
||||||
title: t('txt_delete_invalid_invites'),
|
title: t('txt_delete_invalid_invites'),
|
||||||
message: t('txt_delete_invalid_invites_confirm_message'),
|
message: `${t('txt_delete_invalid_invites_confirm_message')}\n${t('txt_enter_master_password_to_continue')}`,
|
||||||
danger: true,
|
danger: true,
|
||||||
onConfirm: () => {
|
requireMasterPassword: true,
|
||||||
|
onConfirm: (masterPassword) => {
|
||||||
onSetConfirm(null);
|
onSetConfirm(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await deleteInvalidInvites(authedFetch);
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await deleteInvalidInvites(authedFetch, hash);
|
||||||
await refetchInvites();
|
await refetchInvites();
|
||||||
onNotify('success', t('txt_invalid_invites_deleted'));
|
onNotify('success', t('txt_invalid_invites_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,13 +136,15 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
async deleteAllInvites() {
|
async deleteAllInvites() {
|
||||||
onSetConfirm({
|
onSetConfirm({
|
||||||
title: t('txt_delete_all_invites'),
|
title: t('txt_delete_all_invites'),
|
||||||
message: t('txt_delete_all_invite_codes_active_inactive'),
|
message: `${t('txt_delete_all_invite_codes_active_inactive')}\n${t('txt_enter_master_password_to_continue')}`,
|
||||||
danger: true,
|
danger: true,
|
||||||
onConfirm: () => {
|
requireMasterPassword: true,
|
||||||
|
onConfirm: (masterPassword) => {
|
||||||
onSetConfirm(null);
|
onSetConfirm(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAllInvites(authedFetch);
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await deleteAllInvites(authedFetch, hash);
|
||||||
await refetchInvites();
|
await refetchInvites();
|
||||||
onNotify('success', t('txt_all_invites_deleted'));
|
onNotify('success', t('txt_all_invites_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -108,13 +158,15 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
async deleteUser(userId: string) {
|
async deleteUser(userId: string) {
|
||||||
onSetConfirm({
|
onSetConfirm({
|
||||||
title: t('txt_delete_user'),
|
title: t('txt_delete_user'),
|
||||||
message: t('txt_delete_this_user_and_all_user_data'),
|
message: `${t('txt_delete_this_user_and_all_user_data')}\n${t('txt_enter_master_password_to_continue')}`,
|
||||||
danger: true,
|
danger: true,
|
||||||
onConfirm: () => {
|
requireMasterPassword: true,
|
||||||
|
onConfirm: (masterPassword) => {
|
||||||
onSetConfirm(null);
|
onSetConfirm(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await deleteUser(authedFetch, userId);
|
const hash = await withMasterPasswordHash(masterPassword);
|
||||||
|
await deleteUser(authedFetch, userId, hash);
|
||||||
await refetchUsers();
|
await refetchUsers();
|
||||||
onNotify('success', t('txt_user_deleted'));
|
onNotify('success', t('txt_user_deleted'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,6 +177,6 @@ export default function useAdminActions(options: UseAdminActionsOptions) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[authedFetch, onNotify, onSetConfirm, refetchInvites, refetchUsers]
|
[authedFetch, defaultKdfIterations, email, onNotify, onSetConfirm, refetchInvites, refetchUsers]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-12
@@ -15,45 +15,62 @@ export async function listAdminInvites(authedFetch: AuthedFetch): Promise<AdminI
|
|||||||
return body?.data || [];
|
return body?.data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInvite(authedFetch: AuthedFetch, hours: number): Promise<void> {
|
export async function createInvite(authedFetch: AuthedFetch, hours: number, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch('/api/admin/invites', {
|
const resp = await authedFetch('/api/admin/invites', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ expiresInHours: hours }),
|
body: JSON.stringify({ expiresInHours: hours, masterPasswordHash }),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Create invite failed');
|
if (!resp.ok) throw new Error('Create invite failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteInvite(authedFetch: AuthedFetch, code: string): Promise<void> {
|
export async function deleteInvite(authedFetch: AuthedFetch, code: string, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch(`/api/admin/invites/${encodeURIComponent(code)}`, { method: 'DELETE' });
|
const resp = await authedFetch(`/api/admin/invites/${encodeURIComponent(code)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ masterPasswordHash }),
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error('Delete invite failed');
|
if (!resp.ok) throw new Error('Delete invite failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteInvalidInvites(authedFetch: AuthedFetch): Promise<void> {
|
export async function deleteInvalidInvites(authedFetch: AuthedFetch, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch('/api/admin/invites?scope=invalid', { method: 'DELETE' });
|
const resp = await authedFetch('/api/admin/invites?scope=invalid', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ masterPasswordHash }),
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error('Delete invalid invites failed');
|
if (!resp.ok) throw new Error('Delete invalid invites failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAllInvites(authedFetch: AuthedFetch): Promise<void> {
|
export async function deleteAllInvites(authedFetch: AuthedFetch, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch('/api/admin/invites', { method: 'DELETE' });
|
const resp = await authedFetch('/api/admin/invites', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ masterPasswordHash }),
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error('Delete all invites failed');
|
if (!resp.ok) throw new Error('Delete all invites failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserStatus(
|
export async function setUserStatus(
|
||||||
authedFetch: AuthedFetch,
|
authedFetch: AuthedFetch,
|
||||||
userId: string,
|
userId: string,
|
||||||
status: 'active' | 'banned'
|
status: 'active' | 'banned',
|
||||||
|
masterPasswordHash: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const resp = await authedFetch(`/api/admin/users/${encodeURIComponent(userId)}/status`, {
|
const resp = await authedFetch(`/api/admin/users/${encodeURIComponent(userId)}/status`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ status }),
|
body: JSON.stringify({ status, masterPasswordHash }),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Update user status failed');
|
if (!resp.ok) throw new Error('Update user status failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUser(authedFetch: AuthedFetch, userId: string): Promise<void> {
|
export async function deleteUser(authedFetch: AuthedFetch, userId: string, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
const resp = await authedFetch(`/api/admin/users/${encodeURIComponent(userId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ masterPasswordHash }),
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error('Delete user failed');
|
if (!resp.ok) throw new Error('Delete user failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1140,8 +1140,12 @@ export async function updateAuthorizedDeviceName(
|
|||||||
if (!resp.ok) throw new Error(t('txt_update_device_note_failed'));
|
if (!resp.ok) throw new Error(t('txt_update_device_note_failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAllAuthorizedDevices(authedFetch: AuthedFetch): Promise<void> {
|
export async function deleteAllAuthorizedDevices(authedFetch: AuthedFetch, masterPasswordHash: string): Promise<void> {
|
||||||
const resp = await authedFetch('/api/devices', { method: 'DELETE' });
|
const resp = await authedFetch('/api/devices', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ masterPasswordHash }),
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error(t('txt_remove_all_devices_failed'));
|
if (!resp.ok) throw new Error(t('txt_remove_all_devices_failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user