- Add server-side `<admin>/layout.tsx` that redirects non-super-admins to `/[portSlug]/dashboard`. Closes the gap where any authed user could guess the URL and reach Users / Roles / Audit Log / Backup. - `withAuth` super-admin branch now 404s when the requested portId does not match a real port row, preventing a compromised super-admin session from operating against a fabricated portId. - Portal JWTs now carry `aud: 'portal'` + `iss: 'pn-crm'` claims and `verifyPortalToken` requires both, so a portal token can no longer be replayed against the CRM session path or vice versa. In-flight tokens (≤24h) will be invalidated once on deploy. - `saved-views/[id]` PATCH and DELETE now do an explicit ownership check before the service call, returning 403 instead of relying on the service's internal userId filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
37 lines
948 B
TypeScript
37 lines
948 B
TypeScript
import { redirect } from 'next/navigation';
|
|
import { headers } from 'next/headers';
|
|
import { eq } from 'drizzle-orm';
|
|
|
|
import { auth } from '@/lib/auth';
|
|
import { db } from '@/lib/db';
|
|
import { userProfiles } from '@/lib/db/schema/users';
|
|
|
|
/**
|
|
* Guard: only super-admins (isSuperAdmin === true in user_profiles) may access
|
|
* any page under /[portSlug]/admin. Everyone else is redirected to their dashboard.
|
|
*/
|
|
export default async function AdminLayout({
|
|
children,
|
|
params,
|
|
}: {
|
|
children: React.ReactNode;
|
|
params: Promise<{ portSlug: string }>;
|
|
}) {
|
|
const { portSlug } = await params;
|
|
const session = await auth.api.getSession({ headers: await headers() });
|
|
|
|
if (!session?.user) {
|
|
redirect('/login');
|
|
}
|
|
|
|
const profile = await db.query.userProfiles.findFirst({
|
|
where: eq(userProfiles.userId, session.user.id),
|
|
});
|
|
|
|
if (!profile?.isSuperAdmin) {
|
|
redirect(`/${portSlug}/dashboard`);
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|