diff --git a/app/api/auth/login/route.web.ts b/app/api/auth/login/route.web.ts index 6f49ee3..9e0b9c9 100644 --- a/app/api/auth/login/route.web.ts +++ b/app/api/auth/login/route.web.ts @@ -37,9 +37,19 @@ export async function GET(request: Request) { const requestUrl = new URL(request.url) const returnTo = requestUrl.searchParams.get("returnTo") ?? "/mail/inbox" const intent = requestUrl.searchParams.get("intent") + const bridge = requestUrl.searchParams.get("bridge") === "1" + // Embedded mode: the browser drives Authentik's flow executor (same origin) and authenticates + // the session in place, then navigates to the authorize URL. We just hand back the URL + the + // executor base, set the PKCE/state cookies, and never force a prompt. + const embedded = requestUrl.searchParams.get("embedded") === "1" + const promptParam = requestUrl.searchParams.get("prompt") const prompt = - requestUrl.searchParams.get("prompt") ?? - (intent === "add_account" ? "login select_account" : "select_account") + promptParam ?? + (bridge || embedded + ? null + : intent === "add_account" + ? "login select_account" + : "select_account") const jar = await cookies() const existingUser = platformUserFromToken( @@ -54,12 +64,16 @@ export async function GET(request: Request) { state, code_challenge: challenge, code_challenge_method: "S256", - prompt, }) + if (prompt) { + params.set("prompt", prompt) + } - const response = NextResponse.redirect( - `${cfg.authorizationEndpoint}?${params.toString()}` - ) + const authorizeUrl = `${cfg.authorizationEndpoint}?${params.toString()}` + + const response = embedded + ? NextResponse.json(buildEmbeddedContext(authorizeUrl)) + : NextResponse.redirect(authorizeUrl) const cookieOpts = oauthCookieOptions() response.cookies.set(PKCE_COOKIE, verifier, cookieOpts) response.cookies.set(STATE_COOKIE, state, cookieOpts) @@ -73,3 +87,31 @@ export async function GET(request: Request) { return response } + +/** + * Build the same-origin flow executor base + `next` query from a public authorize URL. + * The browser drives the flow at `${executorBase}/${slug}/?query=${flowQuery}`, then navigates + * to the returned `to` (the authorize URL) to obtain the code with its authenticated session. + */ +function buildEmbeddedContext(authorizeUrl: string): { + authorizeUrl: string + flowQuery: string + executorBase: string +} { + let executorBase = "/auth/api/v3/flows/executor" + let next = authorizeUrl + try { + const parsed = new URL(authorizeUrl) + const idx = parsed.pathname.indexOf("/application/") + const prefix = idx >= 0 ? parsed.pathname.slice(0, idx) : "/auth" + executorBase = `${prefix}/api/v3/flows/executor` + next = `${parsed.pathname}${parsed.search}` + } catch { + // keep defaults + } + return { + authorizeUrl, + flowQuery: `next=${encodeURIComponent(next)}`, + executorBase, + } +} diff --git a/app/api/auth/session/route.web.ts b/app/api/auth/session/route.web.ts index 9b9275f..cb23103 100644 --- a/app/api/auth/session/route.web.ts +++ b/app/api/auth/session/route.web.ts @@ -10,6 +10,7 @@ import { isAccessTokenValid, isIdTokenJwtValid, resolveBearerToken, + resolveSessionExpiresAt, } from "@/lib/auth/session" export async function GET() { @@ -23,7 +24,7 @@ export async function GET() { } if (isAccessTokenValid(accessToken, expiresAtRaw)) { - const expiresAt = Number(expiresAtRaw) + const expiresAt = resolveSessionExpiresAt(accessToken, expiresAtRaw) const user = platformUserFromToken(accessToken!) return NextResponse.json({ authenticated: true, @@ -47,7 +48,7 @@ export async function GET() { bearer = resolveBearerToken(tokens) } catch { if (accessToken && isIdTokenJwtValid(accessToken)) { - const expiresAt = Number(expiresAtRaw) || computeExpiresAt(3600) + const expiresAt = resolveSessionExpiresAt(accessToken, expiresAtRaw) const user = platformUserFromToken(accessToken) return NextResponse.json({ authenticated: true, diff --git a/components/admin/settings/sections/file-policies-section.tsx b/components/admin/settings/sections/file-policies-section.tsx index 69e6f89..ac8d863 100644 --- a/components/admin/settings/sections/file-policies-section.tsx +++ b/components/admin/settings/sections/file-policies-section.tsx @@ -61,10 +61,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ max_upload_mib: Number(e.target.value) || 1 }) @@ -76,10 +77,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ @@ -93,10 +95,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ retention_trash_days: Number(e.target.value) || 1 }) diff --git a/components/admin/settings/sections/quotas-section.tsx b/components/admin/settings/sections/quotas-section.tsx index 12e1d8b..4e7c85d 100644 --- a/components/admin/settings/sections/quotas-section.tsx +++ b/components/admin/settings/sections/quotas-section.tsx @@ -16,6 +16,7 @@ import { InputGroupInput, InputGroupText, } from "@/components/ui/input-group" +import { cn } from "@/lib/utils" export function QuotasSection() { const storageQuotas = useOrgSettingsStore((s) => s.storageQuotas) @@ -60,7 +61,6 @@ export function QuotasSection() { min={50} max={100} fallback={90} - className="max-w-xs" value={storageQuotas.warn_threshold_pct} onChange={(v) => setStorageQuotas({ warn_threshold_pct: v })} /> @@ -95,7 +95,6 @@ export function QuotasSection() { min={50} max={100} fallback={80} - className="max-w-xs" value={usageQuotas.llm_cost_warn_threshold_pct} onChange={(v) => setUsageQuotas({ llm_cost_warn_threshold_pct: v })} /> @@ -179,13 +178,14 @@ function SettingsNumberField({ }) { return ( - + onChange(Number(e.target.value) || fallback)} /> diff --git a/components/agenda/agenda-sidebar.tsx b/components/agenda/agenda-sidebar.tsx index 1397a79..770725b 100644 --- a/components/agenda/agenda-sidebar.tsx +++ b/components/agenda/agenda-sidebar.tsx @@ -35,6 +35,7 @@ import { calendarColor } from "@/lib/agenda/agenda-events" import { useAgendaSettingsStore, useAgendaUIStore } from "@/lib/agenda/agenda-store" import type { AgendaCalendar } from "@/lib/agenda/agenda-types" import { useMergedAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars" +import { usePersistHydrated } from "@/hooks/use-persist-hydrated" import { useIsMobile } from "@/hooks/use-mobile" import { cn } from "@/lib/utils" @@ -48,9 +49,11 @@ export function AgendaSidebar({ onCreateEvent: () => void }) { const isMobile = useIsMobile() + const settingsHydrated = usePersistHydrated(useAgendaSettingsStore) const sidebarCollapsed = useAgendaUIStore((s) => s.sidebarCollapsed) const setSidebarCollapsed = useAgendaUIStore((s) => s.setSidebarCollapsed) - const hiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds) + const storedHiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds) + const hiddenIds = settingsHydrated ? storedHiddenIds : [] const weekStart = useAgendaSettingsStore((s) => s.weekStart) const calendarViews = useAgendaSettingsStore((s) => s.calendarViews) const activeCalendarViewId = useAgendaSettingsStore((s) => s.activeCalendarViewId) @@ -144,7 +147,7 @@ export function AgendaSidebar({ - {calendarViews.length > 0 ? ( + {settingsHydrated && calendarViews.length > 0 ? ( <>