feat: Phase 3+4 — security hardening + polish + offline + Electron push
Phase 3 (security): - P3.1: Feature gate server-side enforcement (403 on disabled features) - P3.2: Unified auth error interceptor (401→logout) - P3.3: Store-level state isolation via StoreSnapshot contract (added message-list-tabs + task stores to snapshot/restore cycle) - P3.4: Push event bus extraction — email-store no longer imports calendar/contact/filter/file stores directly - P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter Phase 4 (polish): - P4.1: Offline write queue — pending operations in localStorage, auto-retry on reconnect, offline-queue-indicator banner - P4.2: Identity spoofing — fromOverrideEmail domain validation - P4.3: WebSocket push for Electron via main-process IPC bridge (ws package with Authorization headers)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useFocusTrap } from "@/hooks/use-focus-trap";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -569,6 +569,34 @@ export function EmailComposer({
|
||||
const [fromOverrideEnabled, setFromOverrideEnabled] = useState<boolean>(initialData?.fromOverrideEnabled ?? false);
|
||||
const [fromOverrideEmail, setFromOverrideEmail] = useState<string>(initialData?.fromOverrideEmail ?? '');
|
||||
const [fromOverrideName, setFromOverrideName] = useState<string>(initialData?.fromOverrideName ?? '');
|
||||
const [fromOverrideWarning, setFromOverrideWarning] = useState<string>('');
|
||||
|
||||
// Validate that from override domain matches at least one of the user's identities
|
||||
const ownIdentityDomains = useMemo(() => new Set(
|
||||
identities.map(i => i.email).filter(Boolean).map(email => {
|
||||
const atPos = email.indexOf('@');
|
||||
return atPos >= 0 ? email.slice(atPos + 1).toLowerCase() : '';
|
||||
}).filter(d => d.length > 0),
|
||||
), [identities]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fromOverrideEnabled || !fromOverrideEmail.trim()) {
|
||||
setFromOverrideWarning('');
|
||||
return;
|
||||
}
|
||||
const email = fromOverrideEmail.trim();
|
||||
const atPos = email.indexOf('@');
|
||||
if (atPos < 0) {
|
||||
setFromOverrideWarning('Invalid email address');
|
||||
return;
|
||||
}
|
||||
const domain = email.slice(atPos + 1).toLowerCase();
|
||||
if (!ownIdentityDomains.has(domain)) {
|
||||
setFromOverrideWarning(`This email's domain (${domain}) does not match any of your verified identities`);
|
||||
} else {
|
||||
setFromOverrideWarning('');
|
||||
}
|
||||
}, [fromOverrideEnabled, fromOverrideEmail, ownIdentityDomains]);
|
||||
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
|
||||
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
||||
const [showCloseDialog, setShowCloseDialog] = useState(false);
|
||||
@@ -2358,6 +2386,11 @@ export function EmailComposer({
|
||||
>
|
||||
{fromOverrideEnabled ? t('from_override.toggle_on') : t('from_override.toggle_off')}
|
||||
</Button>
|
||||
{fromOverrideWarning && (
|
||||
<span className="text-xs text-amber-600 dark:text-amber-400 ml-2" role="alert">
|
||||
{fromOverrideWarning}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user