diff --git a/services/webmail/messages/en.json b/services/webmail/messages/en.json index 39083f4..bc7f22e 100644 --- a/services/webmail/messages/en.json +++ b/services/webmail/messages/en.json @@ -533,5 +533,13 @@ "zoom": "Zoom", "dragToPosition": "Drag to position", "notAnImage": "That file could not be read as an image." + }, + "accounts": { + "switchAccount": "Switch account", + "addAccount": "Add another account", + "signOutOne": "Sign out of {email}", + "signOutAll": "Sign out of all accounts", + "active": "Active", + "settings": "Account settings" } } diff --git a/services/webmail/messages/es.json b/services/webmail/messages/es.json index a68caa3..5156653 100644 --- a/services/webmail/messages/es.json +++ b/services/webmail/messages/es.json @@ -533,5 +533,13 @@ "zoom": "Zoom", "dragToPosition": "Arrastre para posicionar", "notAnImage": "No se pudo leer ese archivo como imagen." + }, + "accounts": { + "switchAccount": "Cambiar de cuenta", + "addAccount": "Añadir otra cuenta", + "signOutOne": "Cerrar sesión de {email}", + "signOutAll": "Cerrar sesión en todas las cuentas", + "active": "Activa", + "settings": "Ajustes de la cuenta" } } diff --git a/services/webmail/messages/pt-BR.json b/services/webmail/messages/pt-BR.json index 3930a8c..3ae4f05 100644 --- a/services/webmail/messages/pt-BR.json +++ b/services/webmail/messages/pt-BR.json @@ -533,5 +533,13 @@ "zoom": "Zoom", "dragToPosition": "Arraste para posicionar", "notAnImage": "Não foi possível ler esse arquivo como imagem." + }, + "accounts": { + "switchAccount": "Trocar de conta", + "addAccount": "Adicionar outra conta", + "signOutOne": "Sair de {email}", + "signOutAll": "Sair de todas as contas", + "active": "Ativa", + "settings": "Configurações da conta" } } diff --git a/services/webmail/src/app/(user)/user/layout.tsx b/services/webmail/src/app/(user)/user/layout.tsx index 87e8cad..8e59258 100644 --- a/services/webmail/src/app/(user)/user/layout.tsx +++ b/services/webmail/src/app/(user)/user/layout.tsx @@ -27,7 +27,7 @@ import { LanguageSwitcher } from "../../../i18n/LanguageSwitcher"; import { useAccount, isAdminRole } from "../../../lib/useAccount"; import { useFolders } from "../../../lib/useFolders"; import { folderBadge, customFolders } from "../../../lib/mailCounts"; -import { Avatar } from "../../../components/Avatar"; +import { AccountSwitcher } from "../../../components/AccountSwitcher"; export default function UserWebmailLayout({ children }: { children: React.ReactNode }) { const t = useTranslations(); @@ -274,36 +274,7 @@ export default function UserWebmailLayout({ children }: { children: React.ReactN )} - setMobileOpen(false)} - className="flex items-center gap-2.5 rounded-xl bg-dark-panel p-2.5 shadow-edge transition-colors hover:bg-dark-card" - > - - - - {account?.email ?? t("common.loading")} - - {/* Reports what is actually configured. The old fixed "2FA - active" reassured accounts that had no second factor at all. */} - - {account?.mfa_enrolled ? ( - <> - - {t("settings.mfaEnabled")} - - ) : ( - <> - - {t("settings.mfaDisabled")} - - )} - - - + setMobileOpen(false)} />
diff --git a/services/webmail/src/components/AccountSwitcher.tsx b/services/webmail/src/components/AccountSwitcher.tsx new file mode 100644 index 0000000..fc32fcd --- /dev/null +++ b/services/webmail/src/components/AccountSwitcher.tsx @@ -0,0 +1,134 @@ +"use client"; + +import React, { useCallback, useEffect, useRef, useState } from "react"; +import Link from "next/link"; +import { Check, ChevronsUpDown, LogOut, Plus, Settings } from "lucide-react"; +import { useTranslations } from "../i18n/provider"; +import { Avatar } from "./Avatar"; + +interface SignedInAccount { + id: string; + email: string; + active: boolean; +} + +/** + * The webmail accounts signed in on this browser, and the way to move between + * them without signing out. The auth service keeps one session per account; + * switching only changes which one is active. + */ +export function AccountSwitcher({ onNavigate }: { onNavigate?: () => void }) { + const t = useTranslations(); + const [accounts, setAccounts] = useState([]); + const [open, setOpen] = useState(false); + const root = useRef(null); + + const load = useCallback(() => { + fetch("/api/v1/auth/accounts") + .then((res) => (res.ok ? res.json() : { accounts: [] })) + .then((data) => setAccounts(data.accounts ?? [])) + .catch(() => undefined); + }, []); + + useEffect(load, [load]); + + useEffect(() => { + if (!open) return; + const close = (event: MouseEvent | KeyboardEvent) => { + if (event instanceof KeyboardEvent) { + if (event.key === "Escape") setOpen(false); + } else if (root.current && !root.current.contains(event.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", close); + document.addEventListener("keydown", close); + return () => { + document.removeEventListener("mousedown", close); + document.removeEventListener("keydown", close); + }; + }, [open]); + + const active = accounts.find((a) => a.active); + + // A reload rather than a client-side update: every screen holds the previous + // account's mail in state, and none of it should survive the switch. + const switchTo = async (id: string) => { + const res = await fetch("/api/v1/auth/switch", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + if (res.ok) window.location.href = "/user/mail/inbox"; + }; + + const signOut = async (all: boolean) => { + const res = await fetch("/api/v1/auth/logout", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ all }), + }).catch(() => null); + const data = res?.ok ? await res.json().catch(() => ({})) : {}; + window.location.href = data.remaining?.length ? "/user/mail/inbox" : "/user/login"; + }; + + const item = + "flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-[13px] transition-colors hover:bg-white/[0.07]"; + + return ( +
+ + + {open && ( +
+ {accounts.map((account) => ( + + ))} + + + {t("accounts.addAccount")} + +
+ + + {t("accounts.settings")} + + + {accounts.length > 1 && ( + + )} +
+ )} +
+ ); +}