diff --git a/.dockerignore b/.dockerignore index 060be0c1f..da9e55a3a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,7 @@ lite-image !.env.dev.example scripts/ !scripts/lite/ +!scripts/desktop-build-flag.mjs !scripts/build-plugin-sandbox.mjs !scripts/build-pst-worker.mjs !scripts/build-store-worker.mjs diff --git a/app/(main)/[locale]/layout.tsx b/app/(main)/[locale]/layout.tsx index 992bef2ec..b672dd854 100644 --- a/app/(main)/[locale]/layout.tsx +++ b/app/(main)/[locale]/layout.tsx @@ -1,4 +1,5 @@ import { notFound } from "next/navigation"; +import { AppLockGate } from "@/components/app-lock/app-lock-gate"; import { IntlProvider } from "@/components/providers/intl-provider"; import { ThemeProvider } from "@/components/providers/theme-provider"; import { CalendarAlertProvider } from "@/components/providers/calendar-alert-provider"; @@ -27,7 +28,9 @@ import { PluginOAuthCallbackListener } from "@/components/providers/plugin-oauth import { PWAInstallPrompt } from "@/components/pwa-install-prompt"; import { AppUpdatePrompt } from "@/components/app-update-prompt"; import { OfflineHost } from "@/components/offline/offline-host"; +import { DeviceWipeHost } from "@/components/offline/device-wipe-host"; import { NotificationActionsHost } from "@/components/providers/notification-actions-host"; +import { DesktopOsHost } from "@/components/desktop/desktop-os-host"; import { DevicePolicyHost } from "@/components/device/device-policy-host"; import { PushNotificationPrompt } from "@/components/push-notification-prompt"; import { AccountArchiveRestorePrompt } from "@/components/account-archive-prompt"; @@ -37,6 +40,8 @@ import { FreeBusyAutoShare } from "@/components/calendar/free-busy-auto-share"; import { MainWindowOnly } from "@/components/popout/main-window-only"; import { PopoutHost } from "@/components/popout/popout-host"; import { WindowSyncHost } from "@/components/popout/window-sync-host"; +import { DesktopHost } from "@/components/desktop/desktop-host"; +import { WindowMenuBar } from "@/components/layout/window-menu-bar"; import { IS_LITE } from "@/lib/lite"; import { setRequestLocale } from "next-intl/server"; import { locales } from "@/i18n/routing"; @@ -75,6 +80,7 @@ export default async function LocaleLayout({ return ( + @@ -85,10 +91,13 @@ export default async function LocaleLayout({ + {!IS_LITE && } {!IS_LITE && } + {!IS_LITE && } + {!IS_LITE && } {children} @@ -128,6 +137,9 @@ export default async function LocaleLayout({ + + {/* Beside the gate, not under it: a stolen device at the lock screen still has to hear a wipe. */} + {!IS_LITE && } ); } diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index 4b1f3f439..70b3a7741 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { freshLoginRequired } from "@/lib/app-lock/sign-out"; import { Suspense, useState, useEffect, useRef, useCallback, useId, useMemo } from "react"; import { IS_DESKTOP_BUILD } from "@/lib/desktop/flag"; import { forgetDesktopServer, getDesktopBridge } from "@/lib/desktop/bridge"; @@ -14,6 +15,8 @@ import { IconButton } from "@/components/ui/icon-button"; import { Input } from "@/components/ui/input"; import { useAuthStore, consumeSignedOut, SESSION_EXPIRED_DRAFTS_UNSAVED_KEY } from "@/stores/auth-store"; import { consumeLost } from "@/lib/offline/lost"; +import { consumeWipeNote, onRefusedLoginsChanged, refusedWaits, type RefusedWait, type WipeNote } from "@/lib/devices/wipe-state"; +import { wipeNoteText } from "@/components/offline/device-wipe-host"; import { useAccountStore } from "@/stores/account-store"; import { useThemeStore } from "@/stores/theme-store"; import { usePolicyStore } from "@/stores/policy-store"; @@ -198,6 +201,7 @@ function LoginPageContent() { const [desktopWaiting, setDesktopWaiting] = useState(false); const tSessionLimits = useTranslations("session_limits"); const tOffline = useTranslations("offline"); + const tOfflineWipe = useTranslations("offlineWipe"); const tTheme = useTranslations("settings.appearance.theme"); const params = useParams(); const searchParams = useSearchParams(); @@ -302,8 +306,16 @@ function LoginPageContent() { const [sessionEnd, setSessionEnd] = useState(null); // Offline outbox messages a sign-out deleted unsent (#523), named once. const [outboxLost, setOutboxLost] = useState([]); + // Unsent mail a wipe took with it: how many, never which. + const [wipeLost, setWipeLost] = useState(null); + // Unsent mail that waits for a new password: how many and until when, never which. + const [refusedWaiting, setRefusedWaiting] = useState([]); useEffect(() => { setOutboxLost(consumeLost()); + setWipeLost(consumeWipeNote()); + const read = () => setRefusedWaiting(refusedWaits()); + read(); + return onRefusedLoginsChanged(read); }, []); const [expiredDraftsUnsaved, setExpiredDraftsUnsaved] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -667,6 +679,7 @@ function LoginPageContent() { locale: params.locale, server_id: selectedServer?.id, remember: persistProviderSession, + ...(freshLoginRequired() ? { fresh_login: true } : {}), ...(isMobileHandoff ? { mobile_redirect_uri: mobileRedirectUri, @@ -920,6 +933,7 @@ function LoginPageContent() { if (isAddAccountMode) { authUrl.searchParams.set("prompt", "select_account"); } + if (freshLoginRequired()) authUrl.searchParams.set("prompt", "login"); window.location.href = authUrl.toString(); }; @@ -1018,6 +1032,7 @@ function LoginPageContent() { if (isAddAccountMode) { authUrl.searchParams.set("prompt", "select_account"); } + if (freshLoginRequired()) authUrl.searchParams.set("prompt", "login"); window.location.href = authUrl.toString(); }; @@ -1251,7 +1266,7 @@ function LoginPageContent() { return (
{/* Theme toggle, hidden while the theme policy locks the mode (#1280) */} -