mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
Add isolated Pages demo mode with sample vault data
This commit is contained in:
+140
-14
@@ -54,7 +54,21 @@ import { APP_NOTIFY_EVENT, type AppNotifyDetail } from '@/lib/app-notify';
|
||||
import { dispatchBackupProgress, type BackupProgressDetail } from '@/lib/backup-restore-progress';
|
||||
import { decryptSends, decryptVaultCore } from '@/lib/vault-decrypt';
|
||||
import { decryptSendsInWorker, decryptVaultCoreInWorker } from '@/lib/vault-worker';
|
||||
import type { AppPhase, Cipher, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types';
|
||||
import {
|
||||
DEMO_CIPHERS,
|
||||
DEMO_ADMIN_INVITES,
|
||||
DEMO_ADMIN_USERS,
|
||||
DEMO_AUTHORIZED_DEVICES,
|
||||
DEMO_FOLDERS,
|
||||
DEMO_SENDS,
|
||||
createDemoBackupSettings,
|
||||
IS_DEMO_MODE,
|
||||
createDemoCompletedLogin,
|
||||
createDemoInitialBootstrapState,
|
||||
createDemoMainRoutesProps,
|
||||
} from '@/lib/demo';
|
||||
import type { AdminBackupSettings } from '@/lib/api/backup';
|
||||
import type { AdminInvite, AdminUser, AppPhase, AuthorizedDevice, Cipher, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types';
|
||||
import type { VaultCoreSnapshot } from '@/lib/vault-cache';
|
||||
|
||||
function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
|
||||
@@ -138,9 +152,15 @@ function readSessionTimeoutAction(): SessionTimeoutAction {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []);
|
||||
const initialBootstrap = useMemo(
|
||||
() => (IS_DEMO_MODE ? createDemoInitialBootstrapState() : readInitialAppBootstrapState()),
|
||||
[]
|
||||
);
|
||||
const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []);
|
||||
const initialProfileSnapshot = useMemo(() => loadProfileSnapshot(initialBootstrap.session?.email), [initialBootstrap]);
|
||||
const initialProfileSnapshot = useMemo(
|
||||
() => (IS_DEMO_MODE ? null : loadProfileSnapshot(initialBootstrap.session?.email)),
|
||||
[initialBootstrap]
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null);
|
||||
const [location, navigate] = useLocation();
|
||||
@@ -192,6 +212,10 @@ export default function App() {
|
||||
const [decryptedFolders, setDecryptedFolders] = useState<VaultFolder[]>([]);
|
||||
const [decryptedCiphers, setDecryptedCiphers] = useState<Cipher[]>([]);
|
||||
const [decryptedSends, setDecryptedSends] = useState<Send[]>([]);
|
||||
const [demoUsers, setDemoUsers] = useState<AdminUser[]>(() => DEMO_ADMIN_USERS.map((user) => ({ ...user })));
|
||||
const [demoInvites, setDemoInvites] = useState<AdminInvite[]>(() => DEMO_ADMIN_INVITES.map((invite) => ({ ...invite })));
|
||||
const [demoAuthorizedDevices, setDemoAuthorizedDevices] = useState<AuthorizedDevice[]>(() => DEMO_AUTHORIZED_DEVICES.map((device) => ({ ...device })));
|
||||
const [demoBackupSettings, setDemoBackupSettings] = useState<AdminBackupSettings>(() => createDemoBackupSettings());
|
||||
const [cachedVaultCore, setCachedVaultCore] = useState<VaultCoreSnapshot | null>(null);
|
||||
const [vaultInitialDecryptDone, setVaultInitialDecryptDone] = useState(false);
|
||||
const [vaultDecryptError, setVaultDecryptError] = useState('');
|
||||
@@ -294,6 +318,7 @@ export default function App() {
|
||||
}, [themePreference]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) return;
|
||||
saveProfileSnapshot(profile);
|
||||
}, [profile]);
|
||||
|
||||
@@ -374,6 +399,17 @@ export default function App() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) {
|
||||
setDefaultKdfIterations(initialBootstrap.defaultKdfIterations);
|
||||
setJwtWarning(null);
|
||||
setSession(null);
|
||||
setProfile(null);
|
||||
setPhase('login');
|
||||
setUnlockPreparing(false);
|
||||
if (location !== '/login') navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
const boot = await bootstrapAppSession(initialBootstrap);
|
||||
@@ -393,6 +429,7 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
if (phase !== 'locked' || !session) return;
|
||||
if (IS_DEMO_MODE) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const result = await hydrateLockedSession(session, profile);
|
||||
@@ -441,6 +478,15 @@ export default function App() {
|
||||
|
||||
async function handleLogin() {
|
||||
if (pendingAuthAction) return;
|
||||
if (IS_DEMO_MODE) {
|
||||
setPendingAuthAction('login');
|
||||
try {
|
||||
await finalizeLogin(createDemoCompletedLogin(loginValues.email), t('txt_login_success'));
|
||||
} finally {
|
||||
setPendingAuthAction(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!loginValues.email || !loginValues.password) {
|
||||
pushToast('error', t('txt_please_input_email_and_password'));
|
||||
return;
|
||||
@@ -513,6 +559,12 @@ export default function App() {
|
||||
|
||||
async function handleRegister() {
|
||||
if (pendingAuthAction) return;
|
||||
if (IS_DEMO_MODE) {
|
||||
pushToast('warning', t('txt_demo_readonly_message'));
|
||||
setPhase('login');
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
if (!registerValues.email || !registerValues.password) {
|
||||
pushToast('error', t('txt_please_input_email_and_password'));
|
||||
return;
|
||||
@@ -561,6 +613,10 @@ export default function App() {
|
||||
|
||||
async function handleTogglePasswordHint() {
|
||||
if (pendingAuthAction) return;
|
||||
if (IS_DEMO_MODE) {
|
||||
openPasswordHintDialog(t('txt_demo_master_password_hint'));
|
||||
return;
|
||||
}
|
||||
const email = loginValues.email.trim().toLowerCase();
|
||||
if (!email) return;
|
||||
|
||||
@@ -595,12 +651,21 @@ export default function App() {
|
||||
|
||||
function handleShowLockedPasswordHint() {
|
||||
if (pendingAuthAction) return;
|
||||
openPasswordHintDialog(profile?.masterPasswordHint ?? null);
|
||||
openPasswordHintDialog((IS_DEMO_MODE ? t('txt_demo_master_password_hint') : profile?.masterPasswordHint) ?? null);
|
||||
}
|
||||
|
||||
async function handleUnlock() {
|
||||
if (pendingAuthAction) return;
|
||||
if (!session?.email) return;
|
||||
if (IS_DEMO_MODE) {
|
||||
setPendingAuthAction('unlock');
|
||||
try {
|
||||
await finalizeLogin(createDemoCompletedLogin(session.email), t('txt_unlocked'));
|
||||
} finally {
|
||||
setPendingAuthAction(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!unlockPassword) {
|
||||
pushToast('error', t('txt_please_input_master_password'));
|
||||
return;
|
||||
@@ -652,7 +717,9 @@ export default function App() {
|
||||
}
|
||||
|
||||
function logoutNow() {
|
||||
void revokeCurrentSession(sessionRef.current);
|
||||
if (!IS_DEMO_MODE) {
|
||||
void revokeCurrentSession(sessionRef.current);
|
||||
}
|
||||
setConfirm(null);
|
||||
setSession(null);
|
||||
clearProfileSnapshot();
|
||||
@@ -758,6 +825,36 @@ export default function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_DEMO_MODE) return;
|
||||
if (phase !== 'app') {
|
||||
setDecryptedFolders([]);
|
||||
setDecryptedCiphers([]);
|
||||
setDecryptedSends([]);
|
||||
setDemoUsers(DEMO_ADMIN_USERS.map((user) => ({ ...user })));
|
||||
setDemoInvites(DEMO_ADMIN_INVITES.map((invite) => ({ ...invite })));
|
||||
setDemoAuthorizedDevices(DEMO_AUTHORIZED_DEVICES.map((device) => ({ ...device })));
|
||||
setDemoBackupSettings(createDemoBackupSettings());
|
||||
setVaultInitialDecryptDone(false);
|
||||
setSendsDecryptDone(false);
|
||||
return;
|
||||
}
|
||||
setDecryptedFolders(DEMO_FOLDERS.map((folder) => ({ ...folder })));
|
||||
setDecryptedCiphers(DEMO_CIPHERS.map((cipher) => ({ ...cipher })));
|
||||
setDecryptedSends(DEMO_SENDS.map((send) => ({ ...send })));
|
||||
setDemoUsers(DEMO_ADMIN_USERS.map((user) => ({ ...user })));
|
||||
setDemoInvites(DEMO_ADMIN_INVITES.map((invite) => ({ ...invite })));
|
||||
setDemoAuthorizedDevices(DEMO_AUTHORIZED_DEVICES.map((device) => ({ ...device })));
|
||||
setDemoBackupSettings(createDemoBackupSettings());
|
||||
setVaultDecryptError('');
|
||||
setVaultInitialDecryptDone(true);
|
||||
setSendsDecryptDone(true);
|
||||
}, [phase]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) {
|
||||
setCachedVaultCore(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
if (phase !== 'app' || !session?.symEncKey || !session?.symMacKey || !vaultCacheKey) {
|
||||
setCachedVaultCore(null);
|
||||
@@ -790,7 +887,7 @@ export default function App() {
|
||||
const vaultCoreQuery = useQuery({
|
||||
queryKey: ['vault-core', vaultCacheKey],
|
||||
queryFn: () => loadVaultCoreSyncSnapshot(authedFetch, vaultCacheKey),
|
||||
enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey && !!vaultCacheKey,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.symEncKey && !!session?.symMacKey && !!vaultCacheKey,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const encryptedVaultCore = vaultCoreQuery.data || cachedVaultCore;
|
||||
@@ -801,7 +898,7 @@ export default function App() {
|
||||
const sendsQuery = useQuery({
|
||||
queryKey: sendsQueryKey,
|
||||
queryFn: () => getSends(authedFetch),
|
||||
enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey && location === '/sends' && !encryptedSendsFromSync,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.symEncKey && !!session?.symMacKey && location === '/sends' && !encryptedSendsFromSync,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const encryptedSends = sendsQuery.data || encryptedSendsFromSync;
|
||||
@@ -818,7 +915,7 @@ export default function App() {
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['profile', vaultCacheKey || session?.email],
|
||||
queryFn: () => getProfile(authedFetch),
|
||||
enabled: phase === 'app' && !!session?.accessToken,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
useEffect(() => {
|
||||
@@ -830,31 +927,31 @@ export default function App() {
|
||||
const usersQuery = useQuery({
|
||||
queryKey: ['admin-users', vaultCacheKey],
|
||||
queryFn: () => listAdminUsers(authedFetch),
|
||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const invitesQuery = useQuery({
|
||||
queryKey: ['admin-invites', vaultCacheKey],
|
||||
queryFn: () => listAdminInvites(authedFetch),
|
||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const totpStatusQuery = useQuery({
|
||||
queryKey: ['totp-status', vaultCacheKey || session?.email],
|
||||
queryFn: () => getTotpStatus(authedFetch),
|
||||
enabled: phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const authorizedDevicesQuery = useQuery({
|
||||
queryKey: ['authorized-devices', vaultCacheKey || session?.email],
|
||||
queryFn: () => getAuthorizedDevices(authedFetch),
|
||||
enabled: phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
useQuery({
|
||||
queryKey: ['admin-backup-settings', vaultCacheKey],
|
||||
queryFn: () => backupActions.loadSettings(),
|
||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
@@ -864,6 +961,7 @@ export default function App() {
|
||||
}, [phase, vaultInitialDecryptDone, isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) return;
|
||||
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey) return;
|
||||
if (!vaultInitialDecryptDone) return;
|
||||
if (!isAdminProfile(profile)) return;
|
||||
@@ -879,6 +977,7 @@ export default function App() {
|
||||
}, [session?.accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) return;
|
||||
if (!session?.symEncKey || !session?.symMacKey) {
|
||||
setDecryptedFolders([]);
|
||||
setDecryptedCiphers([]);
|
||||
@@ -930,6 +1029,7 @@ export default function App() {
|
||||
}, [session?.symEncKey, session?.symMacKey, encryptedFolders, encryptedCiphers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) return;
|
||||
if (!session?.symEncKey || !session?.symMacKey) {
|
||||
setDecryptedSends([]);
|
||||
setSendsDecryptDone(false);
|
||||
@@ -998,6 +1098,7 @@ export default function App() {
|
||||
silentRefreshVaultRef.current = refreshVaultSilently;
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_DEMO_MODE) return;
|
||||
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey || !vaultInitialDecryptDone) return;
|
||||
|
||||
let disposed = false;
|
||||
@@ -1348,6 +1449,24 @@ export default function App() {
|
||||
onRestoreRemoteBackup: backupActions.restoreRemoteBackup,
|
||||
onRestoreRemoteBackupAllowingChecksumMismatch: backupActions.restoreRemoteBackupAllowingChecksumMismatch,
|
||||
};
|
||||
const effectiveMainRoutesProps = IS_DEMO_MODE
|
||||
? createDemoMainRoutesProps(mainRoutesProps, pushToast, {
|
||||
ciphers: decryptedCiphers,
|
||||
folders: decryptedFolders,
|
||||
sends: decryptedSends,
|
||||
users: demoUsers,
|
||||
invites: demoInvites,
|
||||
authorizedDevices: demoAuthorizedDevices,
|
||||
backupSettings: demoBackupSettings,
|
||||
setCiphers: setDecryptedCiphers,
|
||||
setFolders: setDecryptedFolders,
|
||||
setSends: setDecryptedSends,
|
||||
setUsers: setDemoUsers,
|
||||
setInvites: setDemoInvites,
|
||||
setAuthorizedDevices: setDemoAuthorizedDevices,
|
||||
setBackupSettings: setDemoBackupSettings,
|
||||
})
|
||||
: mainRoutesProps;
|
||||
|
||||
if (jwtWarning) {
|
||||
return <JwtWarningPage reason={jwtWarning.reason} minLength={jwtWarning.minLength} />;
|
||||
@@ -1394,6 +1513,9 @@ export default function App() {
|
||||
<AuthViews
|
||||
mode={phase}
|
||||
pendingAction={pendingAuthAction}
|
||||
relaxedLoginInput={IS_DEMO_MODE}
|
||||
authPlaceholder={IS_DEMO_MODE ? t('txt_demo_auth_placeholder') : undefined}
|
||||
unlockPlaceholder={IS_DEMO_MODE ? t('txt_demo_unlock_placeholder') : undefined}
|
||||
unlockReady={!!session?.email}
|
||||
unlockPreparing={unlockPreparing}
|
||||
loginValues={loginValues}
|
||||
@@ -1412,6 +1534,10 @@ export default function App() {
|
||||
navigate('/login');
|
||||
}}
|
||||
onGotoRegister={() => {
|
||||
if (IS_DEMO_MODE) {
|
||||
pushToast('warning', t('txt_demo_readonly_message'));
|
||||
return;
|
||||
}
|
||||
if (inviteCodeFromUrl) {
|
||||
setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl }));
|
||||
}
|
||||
@@ -1478,7 +1604,7 @@ export default function App() {
|
||||
onLogout={handleLogout}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
onToggleMobileSidebar={() => setMobileSidebarToggleKey((key) => key + 1)}
|
||||
mainRoutesProps={mainRoutesProps}
|
||||
mainRoutesProps={effectiveMainRoutesProps}
|
||||
/>
|
||||
|
||||
<AppGlobalOverlays
|
||||
|
||||
Reference in New Issue
Block a user