feat(ST-903): load active product in layout, replace cookie with DB lookup in solo

- layout.tsx: fetch active_product_id, resolve product, clear stale ref server-side
- NavBar: add activeProduct prop (rendering changes in ST-904)
- solo/page.tsx: redirect via user.active_product_id instead of lastProductId cookie
- proxy.ts: remove lastProductId cookie logic
- lib/cookies.ts: deleted (no longer used)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 19:04:02 +02:00
parent 35d60cc43b
commit 12d81a172f
5 changed files with 34 additions and 44 deletions

View file

@ -3,6 +3,7 @@ import { cookies } from 'next/headers'
import { getIronSession } from 'iron-session'
import { SessionData, sessionOptions } from '@/lib/session'
import { prisma } from '@/lib/prisma'
import { productAccessFilter } from '@/lib/product-access'
import { NavBar } from '@/components/shared/nav-bar'
import { MinWidthBanner } from '@/components/shared/min-width-banner'
import { StatusBar } from '@/components/shared/status-bar'
@ -18,7 +19,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
const [user, userRoles] = await Promise.all([
prisma.user.findUnique({
where: { id: session.userId },
select: { username: true, email: true },
select: { username: true, email: true, active_product_id: true },
}),
prisma.userRole.findMany({
where: { user_id: session.userId },
@ -31,6 +32,23 @@ export default async function AppLayout({ children }: { children: React.ReactNod
redirect('/login')
}
// Resolve active product — clear stale reference if archived or inaccessible
let activeProduct: { id: string; name: string } | null = null
if (user.active_product_id) {
const product = await prisma.product.findFirst({
where: { id: user.active_product_id, archived: false, ...productAccessFilter(session.userId) },
select: { id: true, name: true },
})
if (product) {
activeProduct = product
} else {
await prisma.user.update({
where: { id: session.userId },
data: { active_product_id: null },
})
}
}
return (
<div className="h-screen bg-background flex flex-col overflow-hidden">
<a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md focus:text-sm">
@ -42,6 +60,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
userId={session.userId}
username={user.username}
email={user.email}
activeProduct={activeProduct}
/>
<MinWidthBanner />
<main id="main-content" className="flex-1 flex flex-col overflow-y-auto min-h-0">

View file

@ -1,7 +1,6 @@
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getLastProductCookie } from '@/lib/cookies'
import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access'
import { productAccessFilter } from '@/lib/product-access'
import { prisma } from '@/lib/prisma'
import { ProductPicker } from '@/components/solo/product-picker'
@ -9,11 +8,17 @@ export default async function SoloPage() {
const session = await getSession()
if (!session.userId) redirect('/login')
const lastProductId = await getLastProductCookie()
const user = await prisma.user.findUnique({
where: { id: session.userId },
select: { active_product_id: true },
})
if (lastProductId) {
const product = await getAccessibleProduct(lastProductId, session.userId)
if (product && !product.archived) redirect(`/products/${lastProductId}/solo`)
if (user?.active_product_id) {
const product = await prisma.product.findFirst({
where: { id: user.active_product_id, archived: false, ...productAccessFilter(session.userId) },
select: { id: true },
})
if (product) redirect(`/products/${user.active_product_id}/solo`)
}
const products = await prisma.product.findMany({

View file

@ -15,9 +15,10 @@ interface NavBarProps {
userId: string
username: string
email: string | null
activeProduct: { id: string; name: string } | null
}
export function NavBar({ isDemo, roles, userId, username, email }: NavBarProps) {
export function NavBar({ isDemo, roles, userId, username, email, activeProduct: _activeProduct }: NavBarProps) {
const pathname = usePathname()
const currentProduct = useProductStore(s => s.currentProduct)

View file

@ -1,19 +0,0 @@
import { cookies } from 'next/headers'
const LAST_PRODUCT_COOKIE = 'lastProductId'
const THIRTY_DAYS_SECONDS = 60 * 60 * 24 * 30
export async function setLastProductCookie(productId: string) {
const store = await cookies()
store.set(LAST_PRODUCT_COOKIE, productId, {
httpOnly: true,
sameSite: 'lax',
maxAge: THIRTY_DAYS_SECONDS,
path: '/',
})
}
export async function getLastProductCookie(): Promise<string | null> {
const store = await cookies()
return store.get(LAST_PRODUCT_COOKIE)?.value ?? null
}

View file

@ -5,9 +5,6 @@ import { sessionOptions } from '@/lib/session'
const protectedRoutes = ['/dashboard', '/products', '/todos', '/settings', '/solo']
const authRoutes = ['/login', '/register']
const SOLO_ROUTE = /^\/products\/([^/]+)\/solo$/
const THIRTY_DAYS_SECONDS = 60 * 60 * 24 * 30
export function proxy(request: NextRequest) {
const path = request.nextUrl.pathname
const isProtected = protectedRoutes.some(r => path.startsWith(r))
@ -24,20 +21,7 @@ export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
const response = NextResponse.next()
// Remember last visited product for /solo redirect
const soloMatch = path.match(SOLO_ROUTE)
if (soloMatch) {
response.cookies.set('lastProductId', soloMatch[1], {
httpOnly: true,
sameSite: 'lax',
maxAge: THIRTY_DAYS_SECONDS,
path: '/',
})
}
return response
return NextResponse.next()
}
export const config = {