diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
index 528c68331..2ed2f7181 100644
--- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
@@ -19,11 +19,12 @@ interface LoginProps {
}>;
}
-async function LoginContent({ searchParams }: { searchParams: { callbackUrl?: string } }) {
+async function LoginContent({ searchParams }: { searchParams: Promise<{ callbackUrl?: string }> }) {
+ const resolvedParams = await searchParams;
const session = await auth();
if (session) {
- redirect(sanitizeRedirectionUrl(searchParams.callbackUrl));
+ redirect(sanitizeRedirectionUrl(resolvedParams.callbackUrl));
}
const t = await getScopedI18n("user.page.login");
@@ -52,7 +53,7 @@ async function LoginContent({ searchParams }: { searchParams: { callbackUrl?: st
providers={env.AUTH_PROVIDERS}
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
isOidcAutoLoginEnabled={env.AUTH_OIDC_AUTO_LOGIN}
- callbackUrl={searchParams.callbackUrl ?? "/"}
+ callbackUrl={resolvedParams.callbackUrl ?? "/"}
/>
@@ -60,9 +61,7 @@ async function LoginContent({ searchParams }: { searchParams: { callbackUrl?: st
);
}
-export default async function Login(props: LoginProps) {
- const searchParams = await props.searchParams;
-
+export default function Login(props: LoginProps) {
return (
}
>
-
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index afb557ffb..148b87031 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -1,7 +1,7 @@
import type { Metadata, Viewport } from "next";
import { Suspense } from "react";
import { Inter } from "next/font/google";
-import { ColorSchemeScript } from "@mantine/core";
+import { ColorSchemeScript, Loader } from "@mantine/core";
import "@gfazioli/mantine-onboarding-tour/styles.css";
import "@homarr/notifications/styles.css";
@@ -70,11 +70,13 @@ export default async function Layout(props: {
-
+
+
+
-
+ }>
{props.children}
diff --git a/packages/api/src/integration-data-cache.ts b/packages/api/src/integration-data-cache.ts
index c8c090a1e..344746de9 100644
--- a/packages/api/src/integration-data-cache.ts
+++ b/packages/api/src/integration-data-cache.ts
@@ -39,6 +39,11 @@ export const getCachedIntegrationData = async (
}
}
+ // Set pending synchronously BEFORE creating the fetch promise to prevent
+ // concurrent stale-revalidation from spawning duplicate upstream calls.
+ const placeholder = existing ?? { data: undefined as T, timestamp: 0 };
+ if (!existing) cache.set(key, placeholder);
+
const pending = fetcher()
.then((data) => {
cache.set(key, { data, timestamp: Date.now() });
@@ -67,13 +72,9 @@ export const getCachedIntegrationData = async (
throw error;
});
- if (existing) {
- existing.pending = pending;
- return existing.data;
- }
+ placeholder.pending = pending;
- cache.set(key, { data: undefined as T, timestamp: 0, pending });
- return pending;
+ return existing ? existing.data : pending;
};
export const invalidateIntegrationDataCache = (integrationId: string) => {
diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts
index aef26559e..7570a8ee0 100644
--- a/packages/api/src/router/group.ts
+++ b/packages/api/src/router/group.ts
@@ -232,6 +232,14 @@ export const groupRouter = createTRPCRouter({
name: input.name,
})
.where(eq(groups.id, input.id));
+
+ const members = await ctx.db.query.groupMembers.findMany({
+ columns: { userId: true },
+ where: eq(groupMembers.groupId, input.id),
+ });
+ for (const member of members) {
+ invalidateUserCache(member.userId);
+ }
}),
savePartialSettings: permissionRequiredProcedure
.requiresPermission("admin")
@@ -325,6 +333,7 @@ export const groupRouter = createTRPCRouter({
ownerId: input.userId,
})
.where(eq(groups.id, input.groupId));
+ invalidateUserCache(input.userId);
}),
deleteGroup: permissionRequiredProcedure
.requiresPermission("admin")
diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts
index e93519723..7a46a398a 100644
--- a/packages/api/src/router/user.ts
+++ b/packages/api/src/router/user.ts
@@ -553,6 +553,7 @@ export const userRouter = createTRPCRouter({
password: hashedPassword,
})
.where(eq(users.id, input.userId));
+ invalidateUserCache(input.userId);
}),
changeHomeBoards: protectedProcedure
.input(convertIntersectionToZodObject(userChangeHomeBoardsSchema.and(z.object({ userId: z.string() }))))
diff --git a/packages/next-cache-handler/src/redis-handler.ts b/packages/next-cache-handler/src/redis-handler.ts
index c1aa39afd..30415e153 100644
--- a/packages/next-cache-handler/src/redis-handler.ts
+++ b/packages/next-cache-handler/src/redis-handler.ts
@@ -113,12 +113,9 @@ export class RedisCacheHandler implements CacheHandler {
});
const ttlMs = entry.expire * 1000;
+ if (ttlMs <= 0) return; // Dynamic entries (expire: 0) should not be persisted
await this.redis.connect().catch(() => {});
- if (ttlMs > 0) {
- await this.redis.set(this.entryKey(cacheKey), serialized, "PX", ttlMs);
- } else {
- await this.redis.set(this.entryKey(cacheKey), serialized);
- }
+ await this.redis.set(this.entryKey(cacheKey), serialized, "PX", ttlMs);
} catch {
// Fail open
} finally {