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:
parent
35d60cc43b
commit
12d81a172f
5 changed files with 34 additions and 44 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
18
proxy.ts
18
proxy.ts
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue