mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Add isolated Pages demo mode with sample vault data
This commit is contained in:
+4
-1
@@ -9,11 +9,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wrangler dev -c wrangler.toml",
|
"dev": "wrangler dev -c wrangler.toml",
|
||||||
"dev:kv": "wrangler dev -c wrangler.kv.toml",
|
"dev:kv": "wrangler dev -c wrangler.kv.toml",
|
||||||
|
"dev:demo": "vite --config webapp/vite.config.ts --mode demo --host 127.0.0.1 --port 5174",
|
||||||
"build": "vite build --config webapp/vite.config.ts",
|
"build": "vite build --config webapp/vite.config.ts",
|
||||||
|
"build:demo": "vite build --config webapp/vite.config.ts --mode demo && node scripts/pages-spa-redirects.cjs",
|
||||||
"i18n": "node scripts/i18n-validate.cjs",
|
"i18n": "node scripts/i18n-validate.cjs",
|
||||||
"i18n:validate": "node scripts/i18n-validate.cjs",
|
"i18n:validate": "node scripts/i18n-validate.cjs",
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"deploy:kv": "wrangler deploy -c wrangler.kv.toml"
|
"deploy:kv": "wrangler deploy -c wrangler.kv.toml",
|
||||||
|
"deploy:demo": "npm run build:demo && wrangler pages deploy dist --project-name nw-demo"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const distDir = path.resolve(__dirname, '..', 'dist');
|
||||||
|
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, '_redirects'), '/* /index.html 200\n');
|
||||||
+139
-13
@@ -54,7 +54,21 @@ import { APP_NOTIFY_EVENT, type AppNotifyDetail } from '@/lib/app-notify';
|
|||||||
import { dispatchBackupProgress, type BackupProgressDetail } from '@/lib/backup-restore-progress';
|
import { dispatchBackupProgress, type BackupProgressDetail } from '@/lib/backup-restore-progress';
|
||||||
import { decryptSends, decryptVaultCore } from '@/lib/vault-decrypt';
|
import { decryptSends, decryptVaultCore } from '@/lib/vault-decrypt';
|
||||||
import { decryptSendsInWorker, decryptVaultCoreInWorker } from '@/lib/vault-worker';
|
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';
|
import type { VaultCoreSnapshot } from '@/lib/vault-cache';
|
||||||
|
|
||||||
function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
|
function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
|
||||||
@@ -138,9 +152,15 @@ function readSessionTimeoutAction(): SessionTimeoutAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []);
|
const initialBootstrap = useMemo(
|
||||||
|
() => (IS_DEMO_MODE ? createDemoInitialBootstrapState() : readInitialAppBootstrapState()),
|
||||||
|
[]
|
||||||
|
);
|
||||||
const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []);
|
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 queryClient = useQueryClient();
|
||||||
const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null);
|
const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null);
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
@@ -192,6 +212,10 @@ export default function App() {
|
|||||||
const [decryptedFolders, setDecryptedFolders] = useState<VaultFolder[]>([]);
|
const [decryptedFolders, setDecryptedFolders] = useState<VaultFolder[]>([]);
|
||||||
const [decryptedCiphers, setDecryptedCiphers] = useState<Cipher[]>([]);
|
const [decryptedCiphers, setDecryptedCiphers] = useState<Cipher[]>([]);
|
||||||
const [decryptedSends, setDecryptedSends] = useState<Send[]>([]);
|
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 [cachedVaultCore, setCachedVaultCore] = useState<VaultCoreSnapshot | null>(null);
|
||||||
const [vaultInitialDecryptDone, setVaultInitialDecryptDone] = useState(false);
|
const [vaultInitialDecryptDone, setVaultInitialDecryptDone] = useState(false);
|
||||||
const [vaultDecryptError, setVaultDecryptError] = useState('');
|
const [vaultDecryptError, setVaultDecryptError] = useState('');
|
||||||
@@ -294,6 +318,7 @@ export default function App() {
|
|||||||
}, [themePreference]);
|
}, [themePreference]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
saveProfileSnapshot(profile);
|
saveProfileSnapshot(profile);
|
||||||
}, [profile]);
|
}, [profile]);
|
||||||
|
|
||||||
@@ -374,6 +399,17 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
let mounted = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
const boot = await bootstrapAppSession(initialBootstrap);
|
const boot = await bootstrapAppSession(initialBootstrap);
|
||||||
@@ -393,6 +429,7 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phase !== 'locked' || !session) return;
|
if (phase !== 'locked' || !session) return;
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const result = await hydrateLockedSession(session, profile);
|
const result = await hydrateLockedSession(session, profile);
|
||||||
@@ -441,6 +478,15 @@ export default function App() {
|
|||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
if (pendingAuthAction) return;
|
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) {
|
if (!loginValues.email || !loginValues.password) {
|
||||||
pushToast('error', t('txt_please_input_email_and_password'));
|
pushToast('error', t('txt_please_input_email_and_password'));
|
||||||
return;
|
return;
|
||||||
@@ -513,6 +559,12 @@ export default function App() {
|
|||||||
|
|
||||||
async function handleRegister() {
|
async function handleRegister() {
|
||||||
if (pendingAuthAction) return;
|
if (pendingAuthAction) return;
|
||||||
|
if (IS_DEMO_MODE) {
|
||||||
|
pushToast('warning', t('txt_demo_readonly_message'));
|
||||||
|
setPhase('login');
|
||||||
|
navigate('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!registerValues.email || !registerValues.password) {
|
if (!registerValues.email || !registerValues.password) {
|
||||||
pushToast('error', t('txt_please_input_email_and_password'));
|
pushToast('error', t('txt_please_input_email_and_password'));
|
||||||
return;
|
return;
|
||||||
@@ -561,6 +613,10 @@ export default function App() {
|
|||||||
|
|
||||||
async function handleTogglePasswordHint() {
|
async function handleTogglePasswordHint() {
|
||||||
if (pendingAuthAction) return;
|
if (pendingAuthAction) return;
|
||||||
|
if (IS_DEMO_MODE) {
|
||||||
|
openPasswordHintDialog(t('txt_demo_master_password_hint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const email = loginValues.email.trim().toLowerCase();
|
const email = loginValues.email.trim().toLowerCase();
|
||||||
if (!email) return;
|
if (!email) return;
|
||||||
|
|
||||||
@@ -595,12 +651,21 @@ export default function App() {
|
|||||||
|
|
||||||
function handleShowLockedPasswordHint() {
|
function handleShowLockedPasswordHint() {
|
||||||
if (pendingAuthAction) return;
|
if (pendingAuthAction) return;
|
||||||
openPasswordHintDialog(profile?.masterPasswordHint ?? null);
|
openPasswordHintDialog((IS_DEMO_MODE ? t('txt_demo_master_password_hint') : profile?.masterPasswordHint) ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUnlock() {
|
async function handleUnlock() {
|
||||||
if (pendingAuthAction) return;
|
if (pendingAuthAction) return;
|
||||||
if (!session?.email) 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) {
|
if (!unlockPassword) {
|
||||||
pushToast('error', t('txt_please_input_master_password'));
|
pushToast('error', t('txt_please_input_master_password'));
|
||||||
return;
|
return;
|
||||||
@@ -652,7 +717,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logoutNow() {
|
function logoutNow() {
|
||||||
|
if (!IS_DEMO_MODE) {
|
||||||
void revokeCurrentSession(sessionRef.current);
|
void revokeCurrentSession(sessionRef.current);
|
||||||
|
}
|
||||||
setConfirm(null);
|
setConfirm(null);
|
||||||
setSession(null);
|
setSession(null);
|
||||||
clearProfileSnapshot();
|
clearProfileSnapshot();
|
||||||
@@ -758,6 +825,36 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
let cancelled = false;
|
||||||
if (phase !== 'app' || !session?.symEncKey || !session?.symMacKey || !vaultCacheKey) {
|
if (phase !== 'app' || !session?.symEncKey || !session?.symMacKey || !vaultCacheKey) {
|
||||||
setCachedVaultCore(null);
|
setCachedVaultCore(null);
|
||||||
@@ -790,7 +887,7 @@ export default function App() {
|
|||||||
const vaultCoreQuery = useQuery({
|
const vaultCoreQuery = useQuery({
|
||||||
queryKey: ['vault-core', vaultCacheKey],
|
queryKey: ['vault-core', vaultCacheKey],
|
||||||
queryFn: () => loadVaultCoreSyncSnapshot(authedFetch, 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,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const encryptedVaultCore = vaultCoreQuery.data || cachedVaultCore;
|
const encryptedVaultCore = vaultCoreQuery.data || cachedVaultCore;
|
||||||
@@ -801,7 +898,7 @@ export default function App() {
|
|||||||
const sendsQuery = useQuery({
|
const sendsQuery = useQuery({
|
||||||
queryKey: sendsQueryKey,
|
queryKey: sendsQueryKey,
|
||||||
queryFn: () => getSends(authedFetch),
|
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,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const encryptedSends = sendsQuery.data || encryptedSendsFromSync;
|
const encryptedSends = sendsQuery.data || encryptedSendsFromSync;
|
||||||
@@ -818,7 +915,7 @@ export default function App() {
|
|||||||
const profileQuery = useQuery({
|
const profileQuery = useQuery({
|
||||||
queryKey: ['profile', vaultCacheKey || session?.email],
|
queryKey: ['profile', vaultCacheKey || session?.email],
|
||||||
queryFn: () => getProfile(authedFetch),
|
queryFn: () => getProfile(authedFetch),
|
||||||
enabled: phase === 'app' && !!session?.accessToken,
|
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -830,31 +927,31 @@ export default function App() {
|
|||||||
const usersQuery = useQuery({
|
const usersQuery = useQuery({
|
||||||
queryKey: ['admin-users', vaultCacheKey],
|
queryKey: ['admin-users', vaultCacheKey],
|
||||||
queryFn: () => listAdminUsers(authedFetch),
|
queryFn: () => listAdminUsers(authedFetch),
|
||||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const invitesQuery = useQuery({
|
const invitesQuery = useQuery({
|
||||||
queryKey: ['admin-invites', vaultCacheKey],
|
queryKey: ['admin-invites', vaultCacheKey],
|
||||||
queryFn: () => listAdminInvites(authedFetch),
|
queryFn: () => listAdminInvites(authedFetch),
|
||||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const totpStatusQuery = useQuery({
|
const totpStatusQuery = useQuery({
|
||||||
queryKey: ['totp-status', vaultCacheKey || session?.email],
|
queryKey: ['totp-status', vaultCacheKey || session?.email],
|
||||||
queryFn: () => getTotpStatus(authedFetch),
|
queryFn: () => getTotpStatus(authedFetch),
|
||||||
enabled: phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
const authorizedDevicesQuery = useQuery({
|
const authorizedDevicesQuery = useQuery({
|
||||||
queryKey: ['authorized-devices', vaultCacheKey || session?.email],
|
queryKey: ['authorized-devices', vaultCacheKey || session?.email],
|
||||||
queryFn: () => getAuthorizedDevices(authedFetch),
|
queryFn: () => getAuthorizedDevices(authedFetch),
|
||||||
enabled: phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
enabled: !IS_DEMO_MODE && phase === 'app' && !!session?.accessToken && vaultInitialDecryptDone,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ['admin-backup-settings', vaultCacheKey],
|
queryKey: ['admin-backup-settings', vaultCacheKey],
|
||||||
queryFn: () => backupActions.loadSettings(),
|
queryFn: () => backupActions.loadSettings(),
|
||||||
enabled: phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
enabled: !IS_DEMO_MODE && phase === 'app' && isAdmin && vaultInitialDecryptDone,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -864,6 +961,7 @@ export default function App() {
|
|||||||
}, [phase, vaultInitialDecryptDone, isAdmin]);
|
}, [phase, vaultInitialDecryptDone, isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey) return;
|
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey) return;
|
||||||
if (!vaultInitialDecryptDone) return;
|
if (!vaultInitialDecryptDone) return;
|
||||||
if (!isAdminProfile(profile)) return;
|
if (!isAdminProfile(profile)) return;
|
||||||
@@ -879,6 +977,7 @@ export default function App() {
|
|||||||
}, [session?.accessToken]);
|
}, [session?.accessToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
if (!session?.symEncKey || !session?.symMacKey) {
|
if (!session?.symEncKey || !session?.symMacKey) {
|
||||||
setDecryptedFolders([]);
|
setDecryptedFolders([]);
|
||||||
setDecryptedCiphers([]);
|
setDecryptedCiphers([]);
|
||||||
@@ -930,6 +1029,7 @@ export default function App() {
|
|||||||
}, [session?.symEncKey, session?.symMacKey, encryptedFolders, encryptedCiphers]);
|
}, [session?.symEncKey, session?.symMacKey, encryptedFolders, encryptedCiphers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
if (!session?.symEncKey || !session?.symMacKey) {
|
if (!session?.symEncKey || !session?.symMacKey) {
|
||||||
setDecryptedSends([]);
|
setDecryptedSends([]);
|
||||||
setSendsDecryptDone(false);
|
setSendsDecryptDone(false);
|
||||||
@@ -998,6 +1098,7 @@ export default function App() {
|
|||||||
silentRefreshVaultRef.current = refreshVaultSilently;
|
silentRefreshVaultRef.current = refreshVaultSilently;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (IS_DEMO_MODE) return;
|
||||||
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey || !vaultInitialDecryptDone) return;
|
if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey || !vaultInitialDecryptDone) return;
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
@@ -1348,6 +1449,24 @@ export default function App() {
|
|||||||
onRestoreRemoteBackup: backupActions.restoreRemoteBackup,
|
onRestoreRemoteBackup: backupActions.restoreRemoteBackup,
|
||||||
onRestoreRemoteBackupAllowingChecksumMismatch: backupActions.restoreRemoteBackupAllowingChecksumMismatch,
|
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) {
|
if (jwtWarning) {
|
||||||
return <JwtWarningPage reason={jwtWarning.reason} minLength={jwtWarning.minLength} />;
|
return <JwtWarningPage reason={jwtWarning.reason} minLength={jwtWarning.minLength} />;
|
||||||
@@ -1394,6 +1513,9 @@ export default function App() {
|
|||||||
<AuthViews
|
<AuthViews
|
||||||
mode={phase}
|
mode={phase}
|
||||||
pendingAction={pendingAuthAction}
|
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}
|
unlockReady={!!session?.email}
|
||||||
unlockPreparing={unlockPreparing}
|
unlockPreparing={unlockPreparing}
|
||||||
loginValues={loginValues}
|
loginValues={loginValues}
|
||||||
@@ -1412,6 +1534,10 @@ export default function App() {
|
|||||||
navigate('/login');
|
navigate('/login');
|
||||||
}}
|
}}
|
||||||
onGotoRegister={() => {
|
onGotoRegister={() => {
|
||||||
|
if (IS_DEMO_MODE) {
|
||||||
|
pushToast('warning', t('txt_demo_readonly_message'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (inviteCodeFromUrl) {
|
if (inviteCodeFromUrl) {
|
||||||
setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl }));
|
setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl }));
|
||||||
}
|
}
|
||||||
@@ -1478,7 +1604,7 @@ export default function App() {
|
|||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
onToggleTheme={handleToggleTheme}
|
onToggleTheme={handleToggleTheme}
|
||||||
onToggleMobileSidebar={() => setMobileSidebarToggleKey((key) => key + 1)}
|
onToggleMobileSidebar={() => setMobileSidebarToggleKey((key) => key + 1)}
|
||||||
mainRoutesProps={mainRoutesProps}
|
mainRoutesProps={effectiveMainRoutesProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppGlobalOverlays
|
<AppGlobalOverlays
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ interface RegisterValues {
|
|||||||
|
|
||||||
interface AuthViewsProps {
|
interface AuthViewsProps {
|
||||||
mode: 'login' | 'register' | 'locked';
|
mode: 'login' | 'register' | 'locked';
|
||||||
|
relaxedLoginInput?: boolean;
|
||||||
|
authPlaceholder?: string;
|
||||||
|
unlockPlaceholder?: string;
|
||||||
pendingAction: 'login' | 'register' | 'unlock' | null;
|
pendingAction: 'login' | 'register' | 'unlock' | null;
|
||||||
unlockReady: boolean;
|
unlockReady: boolean;
|
||||||
unlockPreparing: boolean;
|
unlockPreparing: boolean;
|
||||||
@@ -46,6 +49,7 @@ function PasswordField(props: {
|
|||||||
onInput: (v: string) => void;
|
onInput: (v: string) => void;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
return (
|
return (
|
||||||
@@ -59,6 +63,7 @@ function PasswordField(props: {
|
|||||||
onInput={(e) => props.onInput((e.currentTarget as HTMLInputElement).value)}
|
onInput={(e) => props.onInput((e.currentTarget as HTMLInputElement).value)}
|
||||||
autoFocus={props.autoFocus}
|
autoFocus={props.autoFocus}
|
||||||
autoComplete={props.autoComplete}
|
autoComplete={props.autoComplete}
|
||||||
|
placeholder={props.placeholder}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="eye-btn" onClick={() => setShow((v) => !v)}>
|
<button type="button" className="eye-btn" onClick={() => setShow((v) => !v)}>
|
||||||
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
@@ -90,6 +95,7 @@ export default function AuthViews(props: AuthViewsProps) {
|
|||||||
value={props.unlockPassword}
|
value={props.unlockPassword}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
placeholder={props.unlockPlaceholder}
|
||||||
onInput={props.onChangeUnlock}
|
onInput={props.onChangeUnlock}
|
||||||
/>
|
/>
|
||||||
<div className="auth-support-row">
|
<div className="auth-support-row">
|
||||||
@@ -217,9 +223,10 @@ export default function AuthViews(props: AuthViewsProps) {
|
|||||||
<span>{t('txt_email')}</span>
|
<span>{t('txt_email')}</span>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
type="email"
|
type={props.relaxedLoginInput ? 'text' : 'email'}
|
||||||
value={props.loginValues.email}
|
value={props.loginValues.email}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
|
placeholder={props.authPlaceholder}
|
||||||
onInput={(e) => props.onChangeLogin({ ...props.loginValues, email: (e.currentTarget as HTMLInputElement).value })}
|
onInput={(e) => props.onChangeLogin({ ...props.loginValues, email: (e.currentTarget as HTMLInputElement).value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -227,6 +234,7 @@ export default function AuthViews(props: AuthViewsProps) {
|
|||||||
label={t('txt_master_password')}
|
label={t('txt_master_password')}
|
||||||
value={props.loginValues.password}
|
value={props.loginValues.password}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
placeholder={props.authPlaceholder}
|
||||||
onInput={(v) => props.onChangeLogin({ ...props.loginValues, password: v })}
|
onInput={(v) => props.onChangeLogin({ ...props.loginValues, password: v })}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toBufferSource } from '@/lib/crypto';
|
|||||||
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
|
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
|
||||||
import NotFoundPage from '@/components/NotFoundPage';
|
import NotFoundPage from '@/components/NotFoundPage';
|
||||||
import StandalonePageFrame from '@/components/StandalonePageFrame';
|
import StandalonePageFrame from '@/components/StandalonePageFrame';
|
||||||
|
import { getDemoPublicSend, IS_DEMO_MODE } from '@/lib/demo';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
|
|
||||||
interface PublicSendPageProps {
|
interface PublicSendPageProps {
|
||||||
@@ -108,6 +109,17 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
setNotFound(false);
|
setNotFound(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
if (IS_DEMO_MODE) {
|
||||||
|
const demoSend = getDemoPublicSend(props.accessId);
|
||||||
|
if (!demoSend) {
|
||||||
|
setNotFound(true);
|
||||||
|
setSendData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSendData(demoSend);
|
||||||
|
setNeedPassword(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!hasUsableSendKey(props.keyPart)) {
|
if (!hasUsableSendKey(props.keyPart)) {
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
setSendData(null);
|
setSendData(null);
|
||||||
@@ -153,6 +165,11 @@ export default function PublicSendPage(props: PublicSendPageProps) {
|
|||||||
setDownloadPercent(null);
|
setDownloadPercent(null);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
|
if (IS_DEMO_MODE) {
|
||||||
|
const bytes = new TextEncoder().encode('NodeWarden demo file Send.\nThis download is generated locally in demo mode.\n');
|
||||||
|
downloadBytesAsFile(bytes, sendData.decFileName || sendData.file?.fileName || 'nodewarden-demo-send.txt', 'application/octet-stream');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = await accessPublicSendFile(sendData.id, sendData.file.id, props.keyPart, password || undefined);
|
const url = await accessPublicSendFile(sendData.id, sendData.file.id, props.keyPart, password || undefined);
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) throw new Error(t('txt_download_failed'));
|
if (!resp.ok) throw new Error(t('txt_download_failed'));
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils';
|
import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils';
|
||||||
|
|
||||||
const ICON_LOAD_ROOT_MARGIN = '180px 0px';
|
const ICON_LOAD_ROOT_MARGIN = '180px 0px';
|
||||||
|
const SHOULD_LOAD_DEMO_BRAND_ICONS = __NODEWARDEN_DEMO__;
|
||||||
|
|
||||||
interface WebsiteIconProps {
|
interface WebsiteIconProps {
|
||||||
cipher: Cipher;
|
cipher: Cipher;
|
||||||
@@ -24,6 +25,21 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
|
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
|
||||||
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
|
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
|
||||||
const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : ''));
|
const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : ''));
|
||||||
|
const [demoIconUrl, setDemoIconUrl] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!SHOULD_LOAD_DEMO_BRAND_ICONS || !host) {
|
||||||
|
setDemoIconUrl('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let disposed = false;
|
||||||
|
void import('@/lib/demo-brand-icons').then(({ demoBrandIconUrl }) => {
|
||||||
|
if (!disposed) setDemoIconUrl(demoBrandIconUrl(host));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [host]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
@@ -72,6 +88,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
}, [host, shouldLoad, status]);
|
}, [host, shouldLoad, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (demoIconUrl) return;
|
||||||
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
|
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
void preloadWebsiteIcon(host, src).then((nextStatus) => {
|
void preloadWebsiteIcon(host, src).then((nextStatus) => {
|
||||||
@@ -82,7 +99,21 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [host, src, shouldLoad, status]);
|
}, [demoIconUrl, host, src, shouldLoad, status]);
|
||||||
|
|
||||||
|
if (demoIconUrl) {
|
||||||
|
return (
|
||||||
|
<span className="list-icon-stack" ref={nodeRef}>
|
||||||
|
<img
|
||||||
|
className="list-icon loaded"
|
||||||
|
src={demoIconUrl}
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!host || status === 'error') {
|
if (!host || status === 'error') {
|
||||||
return <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>;
|
return <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>;
|
||||||
@@ -103,3 +134,4 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
|
|||||||
|
import type { AppMainRoutesProps } from '@/components/AppMainRoutes';
|
||||||
|
import type { CompletedLogin, InitialAppBootstrapState } from '@/lib/app-auth';
|
||||||
|
import type { AdminBackupSettings } from '@/lib/api/backup';
|
||||||
|
import type { AdminInvite, AdminUser, AuthorizedDevice, Cipher, Folder, Send } from '@/lib/types';
|
||||||
|
|
||||||
|
export const IS_DEMO_MODE = false;
|
||||||
|
|
||||||
|
export const DEMO_CIPHERS: Cipher[] = [];
|
||||||
|
export const DEMO_ADMIN_INVITES: AdminInvite[] = [];
|
||||||
|
export const DEMO_ADMIN_USERS: AdminUser[] = [];
|
||||||
|
export const DEMO_AUTHORIZED_DEVICES: AuthorizedDevice[] = [];
|
||||||
|
export const DEMO_FOLDERS: Folder[] = [];
|
||||||
|
export const DEMO_SENDS: Send[] = [];
|
||||||
|
|
||||||
|
export function createDemoBackupSettings(): AdminBackupSettings {
|
||||||
|
return { destinations: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDemoInitialBootstrapState(): InitialAppBootstrapState {
|
||||||
|
return {
|
||||||
|
defaultKdfIterations: 600000,
|
||||||
|
jwtWarning: null,
|
||||||
|
session: null,
|
||||||
|
phase: 'login',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDemoCompletedLogin(): CompletedLogin {
|
||||||
|
throw new Error('Demo mode is not available in this build.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDemoMainRoutesProps(base: AppMainRoutesProps): AppMainRoutesProps {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDemoPublicSend(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function demoBrandIconUrl(_host: string): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,14 @@ const en: Record<string, string> = {
|
|||||||
"backup_strategy_under_construction": "Under construction.",
|
"backup_strategy_under_construction": "Under construction.",
|
||||||
"import_export_title": "Import & Export",
|
"import_export_title": "Import & Export",
|
||||||
"import_export_under_construction": "Under construction.",
|
"import_export_under_construction": "Under construction.",
|
||||||
|
"txt_demo_admin_refreshed": "Demo admin data refreshed.",
|
||||||
|
"txt_demo_auth_placeholder": "Demo: enter anything, or leave it empty",
|
||||||
|
"txt_demo_data_reset": "Demo data reset to defaults.",
|
||||||
|
"txt_demo_devices_refreshed": "Demo devices refreshed.",
|
||||||
|
"txt_demo_download_prepared": "Demo download prepared.",
|
||||||
|
"txt_demo_master_password_hint": "In demo mode, any input unlocks the vault.",
|
||||||
|
"txt_demo_readonly_message": "Demo mode is read-only for this action. No changes were saved.",
|
||||||
|
"txt_demo_unlock_placeholder": "Demo: any password works, even empty",
|
||||||
"txt_backup_export": "Export Backup",
|
"txt_backup_export": "Export Backup",
|
||||||
"txt_backup_import": "Restore",
|
"txt_backup_import": "Restore",
|
||||||
"txt_backup_include_attachments": "Include attachments",
|
"txt_backup_include_attachments": "Include attachments",
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ const es: Record<string, string> = {
|
|||||||
"backup_strategy_under_construction": "En construcción.",
|
"backup_strategy_under_construction": "En construcción.",
|
||||||
"import_export_title": "Importar y exportar",
|
"import_export_title": "Importar y exportar",
|
||||||
"import_export_under_construction": "En construcción.",
|
"import_export_under_construction": "En construcción.",
|
||||||
|
"txt_demo_admin_refreshed": "Datos de administración de la demo actualizados.",
|
||||||
|
"txt_demo_auth_placeholder": "Demo: escribe cualquier cosa, o déjalo vacío",
|
||||||
|
"txt_demo_data_reset": "Los datos de la demo volvieron a sus valores predeterminados.",
|
||||||
|
"txt_demo_devices_refreshed": "Dispositivos de la demo actualizados.",
|
||||||
|
"txt_demo_download_prepared": "Descarga de la demo preparada.",
|
||||||
|
"txt_demo_master_password_hint": "En modo demo, cualquier entrada desbloquea la bóveda.",
|
||||||
|
"txt_demo_readonly_message": "En modo demo, esta acción es de solo lectura. No se guardaron cambios.",
|
||||||
|
"txt_demo_unlock_placeholder": "Demo: cualquier contraseña funciona, incluso vacío",
|
||||||
"txt_backup_export": "Exportar copia de seguridad",
|
"txt_backup_export": "Exportar copia de seguridad",
|
||||||
"txt_backup_import": "Restaurar",
|
"txt_backup_import": "Restaurar",
|
||||||
"txt_backup_include_attachments": "Incluir archivos adjuntos",
|
"txt_backup_include_attachments": "Incluir archivos adjuntos",
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ const ru: Record<string, string> = {
|
|||||||
"backup_strategy_under_construction": "В стадии строительства.",
|
"backup_strategy_under_construction": "В стадии строительства.",
|
||||||
"import_export_title": "Импорт и экспорт",
|
"import_export_title": "Импорт и экспорт",
|
||||||
"import_export_under_construction": "В стадии строительства.",
|
"import_export_under_construction": "В стадии строительства.",
|
||||||
|
"txt_demo_admin_refreshed": "Демо-данные администратора обновлены.",
|
||||||
|
"txt_demo_auth_placeholder": "Демо: введите что угодно или оставьте пустым",
|
||||||
|
"txt_demo_data_reset": "Демо-данные сброшены к значениям по умолчанию.",
|
||||||
|
"txt_demo_devices_refreshed": "Демо-устройства обновлены.",
|
||||||
|
"txt_demo_download_prepared": "Демо-загрузка подготовлена.",
|
||||||
|
"txt_demo_master_password_hint": "В демо-режиме любое значение разблокирует хранилище.",
|
||||||
|
"txt_demo_readonly_message": "В демо-режиме это действие только для чтения. Изменения не сохранены.",
|
||||||
|
"txt_demo_unlock_placeholder": "Демо: подойдет любой пароль, даже пустой",
|
||||||
"txt_backup_export": "Экспортировать резервную копию",
|
"txt_backup_export": "Экспортировать резервную копию",
|
||||||
"txt_backup_import": "Восстановить",
|
"txt_backup_import": "Восстановить",
|
||||||
"txt_backup_include_attachments": "Включить вложения",
|
"txt_backup_include_attachments": "Включить вложения",
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ const zhCN: Record<string, string> = {
|
|||||||
"backup_strategy_under_construction": "正在搭建中",
|
"backup_strategy_under_construction": "正在搭建中",
|
||||||
"import_export_title": "导入导出",
|
"import_export_title": "导入导出",
|
||||||
"import_export_under_construction": "正在搭建中",
|
"import_export_under_construction": "正在搭建中",
|
||||||
|
"txt_demo_admin_refreshed": "Demo 管理数据已刷新。",
|
||||||
|
"txt_demo_auth_placeholder": "Demo:随便输入,也可以留空",
|
||||||
|
"txt_demo_data_reset": "Demo 数据已恢复为默认状态。",
|
||||||
|
"txt_demo_devices_refreshed": "Demo 设备已刷新。",
|
||||||
|
"txt_demo_download_prepared": "Demo 下载已准备好。",
|
||||||
|
"txt_demo_master_password_hint": "Demo 模式下,任意输入都可以解锁保险库。",
|
||||||
|
"txt_demo_readonly_message": "Demo 模式下此操作为只读,未保存任何更改。",
|
||||||
|
"txt_demo_unlock_placeholder": "Demo:任意密码都可解锁,留空也可以",
|
||||||
"txt_backup_export": "导出备份",
|
"txt_backup_export": "导出备份",
|
||||||
"txt_backup_import": "还原",
|
"txt_backup_import": "还原",
|
||||||
"txt_backup_include_attachments": "包含附件",
|
"txt_backup_include_attachments": "包含附件",
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ const zhTW: Record<string, string> = {
|
|||||||
"backup_strategy_under_construction": "正在搭建中",
|
"backup_strategy_under_construction": "正在搭建中",
|
||||||
"import_export_title": "導入導出",
|
"import_export_title": "導入導出",
|
||||||
"import_export_under_construction": "正在搭建中",
|
"import_export_under_construction": "正在搭建中",
|
||||||
|
"txt_demo_admin_refreshed": "Demo 管理數據已刷新。",
|
||||||
|
"txt_demo_auth_placeholder": "Demo:隨便輸入,也可以留空",
|
||||||
|
"txt_demo_data_reset": "Demo 數據已恢復為默認狀態。",
|
||||||
|
"txt_demo_devices_refreshed": "Demo 設備已刷新。",
|
||||||
|
"txt_demo_download_prepared": "Demo 下載已準備好。",
|
||||||
|
"txt_demo_master_password_hint": "Demo 模式下,任意輸入都可以解鎖保險庫。",
|
||||||
|
"txt_demo_readonly_message": "Demo 模式下此操作為只讀,未保存任何更改。",
|
||||||
|
"txt_demo_unlock_placeholder": "Demo:任意密碼都可解鎖,留空也可以",
|
||||||
"txt_backup_export": "導出備份",
|
"txt_backup_export": "導出備份",
|
||||||
"txt_backup_import": "還原",
|
"txt_backup_import": "還原",
|
||||||
"txt_backup_include_attachments": "包含附件",
|
"txt_backup_include_attachments": "包含附件",
|
||||||
|
|||||||
Vendored
+2
@@ -24,3 +24,5 @@ interface BarcodeDetectorConstructor {
|
|||||||
interface Window {
|
interface Window {
|
||||||
BarcodeDetector?: BarcodeDetectorConstructor;
|
BarcodeDetector?: BarcodeDetectorConstructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const __NODEWARDEN_DEMO__: boolean;
|
||||||
|
|||||||
+13
-1
@@ -5,11 +5,22 @@ import { defineConfig } from 'vite';
|
|||||||
|
|
||||||
const rootDir = fileURLToPath(new URL('.', import.meta.url));
|
const rootDir = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
|
const isDemo = mode === 'demo';
|
||||||
|
|
||||||
|
return {
|
||||||
root: rootDir,
|
root: rootDir,
|
||||||
plugins: [preact()],
|
plugins: [preact()],
|
||||||
|
define: {
|
||||||
|
__NODEWARDEN_DEMO__: JSON.stringify(isDemo),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@/lib/demo': path.resolve(rootDir, isDemo ? 'src/lib/demo.ts' : 'src/lib/demo.empty.ts'),
|
||||||
|
'@/lib/demo-brand-icons': path.resolve(
|
||||||
|
rootDir,
|
||||||
|
isDemo ? 'src/lib/demo-brand-icons.ts' : 'src/lib/demo.empty.ts'
|
||||||
|
),
|
||||||
'@': path.resolve(rootDir, 'src'),
|
'@': path.resolve(rootDir, 'src'),
|
||||||
'@shared': path.resolve(rootDir, '../shared'),
|
'@shared': path.resolve(rootDir, '../shared'),
|
||||||
},
|
},
|
||||||
@@ -83,4 +94,5 @@ export default defineConfig({
|
|||||||
'/.well-known': 'http://127.0.0.1:8787',
|
'/.well-known': 'http://127.0.0.1:8787',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user