feat: add demo data for emails, files, filters, identities, mailboxes, vacation responses, and JMAP client interface

- Created demo emails with various states (inbox, sent, drafts, trash, etc.) in `emails.ts`.
- Added demo file nodes representing directories and files in `files.ts`.
- Implemented demo Sieve capabilities and scripts in `filters.ts`.
- Defined demo identities for users in `identities.ts`.
- Established demo mailboxes with permissions and counts in `mailboxes.ts`.
- Created a demo vacation response in `vacation.ts`.
- Introduced a comprehensive JMAP client interface in `client-interface.ts` to standardize interactions with the JMAP API.
This commit is contained in:
Linus Rath
2026-03-21 01:38:42 +01:00
parent a8be40579e
commit 2547c10060
53 changed files with 4036 additions and 110 deletions
+102 -2
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { JMAPClient } from '@/lib/jmap/client';
import type { IJMAPClient } from '@/lib/jmap/client-interface';
import { useIdentityStore } from './identity-store';
import { useContactStore } from './contact-store';
import { useVacationStore } from './vacation-store';
@@ -21,7 +22,7 @@ interface AuthState {
error: string | null;
serverUrl: string | null;
username: string | null;
client: JMAPClient | null;
client: IJMAPClient | null;
identities: Identity[];
primaryIdentity: Identity | null;
authMode: 'basic' | 'oauth';
@@ -30,9 +31,11 @@ interface AuthState {
tokenExpiresAt: number | null;
connectionLost: boolean;
activeAccountId: string | null;
isDemoMode: boolean;
login: (serverUrl: string, username: string, password: string, totp?: string, rememberMe?: boolean) => Promise<boolean>;
loginWithOAuth: (serverUrl: string, code: string, codeVerifier: string, redirectUri: string) => Promise<boolean>;
loginDemo: () => Promise<boolean>;
refreshAccessToken: () => Promise<string | null>;
logout: () => Promise<void>;
logoutAll: () => void;
@@ -140,7 +143,7 @@ function markSessionExpired(): void {
saveRedirectAfterLogin();
}
function initializeFeatureStores(client: JMAPClient): void {
function initializeFeatureStores(client: IJMAPClient): void {
if (client.supportsContacts()) {
const contactStore = useContactStore.getState();
contactStore.setSupportsSync(true);
@@ -242,6 +245,7 @@ export const useAuthStore = create<AuthState>()(
tokenExpiresAt: null,
connectionLost: false,
activeAccountId: null,
isDemoMode: false,
login: async (serverUrl, username, password, totp, rememberMe) => {
const effectivePassword = totp ? `${password}$${totp}` : password;
@@ -355,6 +359,68 @@ export const useAuthStore = create<AuthState>()(
}
},
loginDemo: async () => {
set({ isLoading: true, error: null });
try {
// Clear all store data before re-initializing with fresh demo data
clearAllStores();
const { DemoJMAPClient } = await import('@/lib/demo/demo-client');
const client = new DemoJMAPClient();
await client.connect();
const username = client.getUsername();
const { identities, primaryIdentity } = loadIdentities(await client.getIdentities(), username);
initializeFeatureStores(client);
// Register a demo account entry so the account-switcher shows
// proper avatar/name instead of a "?" placeholder.
const accountStore = useAccountStore.getState();
const demoAccountId = accountStore.addAccount({
label: primaryIdentity?.name || 'Demo User',
serverUrl: 'https://demo.example.com',
username,
authMode: 'basic',
rememberMe: false,
displayName: primaryIdentity?.name || 'Demo User',
email: primaryIdentity?.email || username,
lastLoginAt: Date.now(),
isConnected: true,
hasError: false,
isDefault: true,
});
accountStore.setActiveAccount(demoAccountId);
set({
isAuthenticated: true,
isLoading: false,
serverUrl: 'demo.example.com',
username,
client,
identities,
primaryIdentity,
authMode: 'basic',
rememberMe: false,
accessToken: null,
tokenExpiresAt: null,
connectionLost: false,
error: null,
activeAccountId: demoAccountId,
isDemoMode: true,
});
return true;
} catch (error) {
debug.error('Demo login error:', error);
set({
isLoading: false,
error: 'generic',
isAuthenticated: false,
client: null,
});
return false;
}
},
loginWithOAuth: async (serverUrl, code, codeVerifier, redirectUri) => {
set({ isLoading: true, error: null });
@@ -512,12 +578,39 @@ export const useAuthStore = create<AuthState>()(
logout: async () => {
const state = get();
const wasDemoMode = state.isDemoMode;
const wasOAuth = state.authMode === 'oauth';
const accountId = state.activeAccountId;
const accountStore = useAccountStore.getState();
const account = accountId ? accountStore.getAccountById(accountId) : null;
const slot = account?.cookieSlot ?? 0;
// Demo mode: simple cleanup, no network calls
if (wasDemoMode) {
set({ client: null });
state.client?.disconnect();
set({
isAuthenticated: false,
serverUrl: null,
username: null,
client: null,
identities: [],
primaryIdentity: null,
authMode: 'basic',
rememberMe: false,
accessToken: null,
tokenExpiresAt: null,
connectionLost: false,
error: null,
activeAccountId: null,
isDemoMode: false,
});
localStorage.removeItem('auth-storage');
clearAllStores();
redirectToLogin();
return;
}
clearRefreshTimer(accountId ?? undefined);
// Null out the client BEFORE disconnecting so the page doesn't fire
@@ -902,6 +995,13 @@ export const useAuthStore = create<AuthState>()(
const accountStore = useAccountStore.getState();
const accounts = accountStore.accounts;
// If the only account is the demo account, re-initialize demo mode
// instead of trying to restore a server session (which doesn't exist).
if (accounts.length === 1 && accounts[0].serverUrl === 'https://demo.example.com') {
await get().loginDemo();
return;
}
// Multi-account restoration: restore all registered accounts
if (accounts.length > 0) {
// Null out client so the page doesn't fire data-loading effects