From e5083ec1df301be1d36355da40aff2ff8954e22b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:12:33 +0200 Subject: [PATCH] fix: restore admin panel after Stalwart v0.16 REST API removal --- app/admin/layout.tsx | 105 ++++++++++++++------------ app/api/admin/auth/route.ts | Bin 5977 -> 8742 bytes components/layout/navigation-rail.tsx | 11 ++- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 305b47d4..41fe9e05 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -65,6 +65,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const router = useRouter(); const pathname = usePathname(); const [authenticated, setAuthenticated] = useState(null); + const [authError, setAuthError] = useState(null); const [isStalwartAdmin, setIsStalwartAdmin] = useState(false); const { appLogoLightUrl, appLogoDarkUrl, loginLogoLightUrl, loginLogoDarkUrl } = useConfig(); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); @@ -73,54 +74,59 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) : (appLogoLightUrl || appLogoDarkUrl || loginLogoLightUrl); useEffect(() => { - if (pathname !== '/admin/login') { - checkAuth(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pathname]); + if (pathname === '/admin/login') return; + let cancelled = false; - function getJmapHeaders(): Record { - return getActiveAccountSlotHeaders(); - } + async function checkAuth() { + try { + const jmapHeaders = getActiveAccountSlotHeaders(); + const res = await apiFetch('/api/admin/auth', { headers: jmapHeaders }); + const data = await res.json(); + if (cancelled) return; - async function checkAuth() { - try { - const jmapHeaders = getJmapHeaders(); - const res = await apiFetch('/api/admin/auth', { headers: jmapHeaders }); - const data = await res.json(); + const stalwartAdmin = data.stalwartAdmin === true; + setIsStalwartAdmin(stalwartAdmin); - const stalwartAdmin = data.stalwartAdmin === true; - setIsStalwartAdmin(stalwartAdmin); + // If neither password-based admin nor Stalwart admin, redirect away + if (!data.enabled && !stalwartAdmin) { + router.replace('/'); + return; + } - // If neither password-based admin nor Stalwart admin, redirect away - if (!data.enabled && !stalwartAdmin) { - router.replace('/'); - return; - } - - if (data.authenticated) { - setAuthenticated(true); - return; - } - - // If Stalwart admin but not yet authenticated, auto-login - if (stalwartAdmin) { - const loginRes = await apiFetch('/api/admin/auth', { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...jmapHeaders }, - body: JSON.stringify({ stalwartAuth: true }), - }); - if (loginRes.ok) { + if (data.authenticated) { setAuthenticated(true); return; } - } - router.replace('/admin/login'); - } catch { - router.replace('/admin/login'); + // If Stalwart admin but not yet authenticated, auto-login + if (stalwartAdmin) { + const loginRes = await apiFetch('/api/admin/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...jmapHeaders }, + body: JSON.stringify({ stalwartAuth: true }), + }); + if (cancelled) return; + if (loginRes.ok) { + setAuthenticated(true); + return; + } + const body = await loginRes.json().catch(() => ({})); + setAuthError(body?.error || `Admin auto-login failed (HTTP ${loginRes.status})`); + setAuthenticated(false); + return; + } + + router.replace('/admin/login'); + } catch (err) { + if (cancelled) return; + setAuthError(err instanceof Error ? err.message : 'Network error during admin check'); + setAuthenticated(false); + } } - } + + checkAuth(); + return () => { cancelled = true; }; + }, [pathname, router]); async function handleLogout() { await apiFetch('/api/admin/auth', { method: 'DELETE' }); @@ -132,14 +138,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) return <>{children}; } - if (authenticated === null) { - return ( -
-
Loading...
-
- ); - } - return (
{/* Slim webmail nav rail */} @@ -269,7 +267,18 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) {/* Main content */}
- {children} + {authError ? ( +
+

Admin authentication failed

+

{authError}

+
+ ) : authenticated === null ? ( +
+ Loading admin panel… +
+ ) : authenticated ? ( + children + ) : null}
diff --git a/app/api/admin/auth/route.ts b/app/api/admin/auth/route.ts index 3ba03af683674df424422e04320a35b182808735..4d94f59cac36252d1d910341d218c0d906a0bde4 100644 GIT binary patch literal 8742 zcmdT}ZEqXL5$YiX;x>wl>hMP6CD zp-EZHsg+|zud?L_TeiM=_`rpoE_d}HoxA*%sjBOuOuK|7ZnD~XxLl;Bw$DvfKQE>( z?~TDRXL}&Hu2Q&dYK#b49o8=`~e%sSAN%-ttwaK!=kuywoBv8no{I% zy(nR%E3Ld5EbEz^JItKT>yuY|kF8Yl#;N0(9bdg>d)(aBd$*N%dyw(#jh$whI`+Wc zJ@Rs6c~(rPw%l5*4>#7qd1tlBu1#4FOPj(PXR>N*U*%Wy#?2Cz^6)|L@#BXN=rIl1 zE^-s9XO_mxvV`1cXK_GIed|i`wN=GP#J5R|iF&t?0+o!7G~EdRC+~ zHd&T50zMojPrjnk&WjH=mD64ypPkd(L%a!wIZZ&n#TejK{?i znk{TOcM|uCY@RL(m)9LAl9K&-Q6O-HQyZ;ZRU<0M5=bhG<#eVRF7p%yvhd4gnfHU2 z`tRpvaS6p2X5=zguM(Cd(k_4Q4^)L7N~T<0!V*cVO2t&G%D?}IzHSul()G-ZXYi*; znwBQ7*#m|86dFVO+U9udpCojm@$jB3Z4EU!9Hp(6S=|*u5-?cnLzV01Z?MX5vt?P7 z)UH@tfkt`5#K_S&H{2)l$#H3Dgo|_L_H4itd|v~1(xSUP2-u#Az!k<)G7xav0_~B z;e#86DAAhZl#3hp3s|Rp%$C$1mTU>$)eK>qHYN zt|2=Mf^vIJr)KdjQWB}%1q6RE+0yoDR20Cj{2NYT09Dned^sP%r#DaA9Vu2*t@1HV zmibt6d1CAF?4Pclox>w=W_wv?eF}0Jp8MpX{RF>s`t%Bb?kf8&f+!gOls+PdNKL}O zby-Lvsg{EgTEs9u6(z!A4nhfQfWqPQc889hvMetpF}%Os>2A2RaTeP|2=GdkAS4{; zH<@b#WEJDsBKA;{By3KXDmTpyj9fcKAJL79L(_6tfov4Zpf2}=prUB)7e0J&!vMJD zQ^aU|70_q5FO;IIzZm`BSz<{6Z|j%j$4U) zaJtmnPj&^#mw)^ys}rraH)VG7hwzADJ2!KCb15E%uaMThR-qFc)nsG>?b<KHMq$J-N_Ju5FebTp>((Pudxr$LjMR?F)Ts{r&j7#Z5&1%tUc9 zt;PJPDBZsWQd=lDHacm{6C-$+_bD-RXg_L{O^T~dbD+`|l<=fzw?h0i5dq3fJkU0z zn|E3qTTb7;Aw}DMa@M#cD z{Ja#%!Gx4s-IoQvc0V)Jy&nC5#==~`OX#L&mW!I(Q4h=(_A{Ty4LC?1nO+iFYYYU(oQ21bED%?ATD+=l2 zsfS`qQ->^*$$^YUn9xXe)aBB~xPu8oRwp1R5OiwlSu(aRYwM=wZz6d$bgIGUo+)b^+J*lNy<4*4QuAvpN zXF%T$ItH`F7TLC@=p9ABhGJWY<@+9H(|Oz1iw1NJ_s}3>secl}o#bj#PeEw!T(G#JIx&jC#>>O`<#Ex{Yvma|S`1P3Os@UquzPTflL_Pj{#r;O->c zaCGhwMeuz+RNY?>V;;iu;{3okFlyLCIAr`2ZnItt9Qc?9Syd?L;clfva2^8e{TR_! z9w5VId<>9}N`$#^PS==?@{l>CN+(Qu_mdmw80g78+(wh(_V%KsCwlqrf(qPjH=dX3 z+ARhV3-)dbqREEFcrw9U8P>u@!Pt!+nd1wc<{UT_sLaX;S_hs)J!a z+R|?g8Scs*4Tis`<3zA9Pt|N1pCWsdqp?2mPQP|Vr41w>@*MqOsAFcnS?0w-RrB3% ztrx`1jtHP`#05>~-HYaYqScx4Uz7Vh z%{0%F_#jFObug5#KIs4hd|kTp!f#pk)JPWqu)`?{|}@r B`-K1i delta 1721 zcmaJ>L2nyX5LOBzWfMhC;@C;-wZ};l+ljjtmD03{(h$_BBqSxRLnR1-KGrYs()F&j z?>Ui)Oo|XEB-C^c2vHMsC(KqVyRJ=?JxD3@J(-pn`O_szWh z>heSD&V{3k?;ky=qSbW~OI;Rz+Lgv{yOs|PBR@4|z!YropzH|Y_?`e+ViFv$=-5nh zC`jh+vp@nXmK_f&fwSkhe3uJL)vfGMItNyzrzdqq5vqa*P+|fI$K^~&@I4N`P8B?- zDr#Ue4-U)BW4l}diJFkSeAeZA+=YM(zZ%$_5WFI=fP+oMoFWf6{q-7DRR6r84kRpH zUSGNT_Qvw{^_4^(KJ>3E{TZFQ(5DAtt@+gyi;1YA}Fk66A_J9aKDAkQjJo;8R7_lgzY z@uU`jr@PmBh?ER!a6oUo(S80I_5P$gwq?){(ApKUsE}4p%Gf@0B88TCaYrZm=FG7#PH|daCZaDxEmTiJO+7J_+BOpvf~lQp6ynPTx2TQ>^%M%n(R=&c2d372;`vEN4crYY%D@6tYURqd72nxco6sjnTw@< z-|S4@VXojh)u=bN?r}dntJWMDL~(W?ewxztrr?(_fgcW}yA1q#U>Y9|Ox_(_N@%d= zARW=Vx!M5Lj}5Ns)%qZs#a{=fo^Pht4eSd~U?MVxH^NE09Ua5>MtWQG#BajW_#4F^ zhfO-0!k30-le&^PDy8{6Tvs0o*D2Fh+9ht^!h1uLqxw+qls>VT>-zh=sE9loTBLTb z5FFsgkr(mD$jhN7nWlZjDHBbSy4C0mrEMOwc@&);Xd~}2*U^ZH$T+SI_v2f`FW{Zw z4doL#ZW!1f9>cy7laNx8B(9D0oTA|U!K9KHrmTNY<=%)(+4l$2_;+$zUCnxH8t$lt zpO3!To=7+rYfnUZ^m=R>KZ-3<{r9nT9EzVM?&G)Z%3M?R`q+P$H#SCg{0s#$(NjCqBe;v@2Df!=uC`K1ht=?}>Abk=4~# z0e>;`ab4CR^j}Vl;O*q*Q{1=!<1Hs<+AgTQ{0=6lCKWN!NcHLJ|Ft`g`>AW~CTi5m G@_zsb(*}wF diff --git a/components/layout/navigation-rail.tsx b/components/layout/navigation-rail.tsx index 595dc55a..4bdccb05 100644 --- a/components/layout/navigation-rail.tsx +++ b/components/layout/navigation-rail.tsx @@ -8,7 +8,6 @@ import { icons as lucideIcons, type LucideIcon } from "lucide-react"; import { useConfig } from "@/hooks/use-config"; import { useThemeStore } from "@/stores/theme-store"; import { usePathname, Link, useRouter } from "@/i18n/navigation"; -import NextLink from "next/link"; import { useTranslations } from "next-intl"; import { useCalendarStore } from "@/stores/calendar-store"; import { useEmailStore } from "@/stores/email-store"; @@ -336,9 +335,9 @@ export function NavigationRail({ ); })} - {/* Admin (Stalwart admins) */} + {/* Admin (Stalwart admins) — hard nav because /admin lives outside the [locale] tree */} {isStalwartAdmin && ( - {t("admin") || "Admin"} - + )} {/* Settings */} @@ -514,13 +513,13 @@ export function NavigationRail({ {/* Footer: Admin + Settings + Help + Storage Quota + Sign Out + Push Status */}
{isStalwartAdmin && ( - - + )}