Skip to content

Commit 0139ad8

Browse files
authored
Merge pull request #52 from DevCalebR/codex/admin-fix-support-shortcut-readonly-mismatches
[codex] Fix admin support shortcut routing
2 parents ffc369e + 6abb5a0 commit 0139ad8

13 files changed

Lines changed: 283 additions & 39 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { ADMIN_CUSTOMER_BUSINESS_COOKIE } from '@/lib/admin-customer-context';
4+
import { resolveSafeAdminCustomerAppPath } from '@/lib/admin-customer-paths';
5+
import { getAdminSession } from '@/lib/admin';
6+
import { db } from '@/lib/db';
7+
8+
export async function GET(request: Request, { params }: { params: { businessId: string } }) {
9+
const adminSession = await getAdminSession();
10+
const requestUrl = new URL(request.url);
11+
12+
if (!adminSession?.userId) {
13+
return NextResponse.redirect(new URL('/sign-in', requestUrl));
14+
}
15+
16+
if (!adminSession.isAdmin) {
17+
return NextResponse.redirect(new URL('/app', requestUrl));
18+
}
19+
20+
const business = await db.business.findUnique({
21+
where: { id: params.businessId },
22+
select: { id: true },
23+
});
24+
25+
if (!business) {
26+
return NextResponse.redirect(new URL('/admin?error=Business%20not%20found.', requestUrl));
27+
}
28+
29+
const nextPath = resolveSafeAdminCustomerAppPath(requestUrl.searchParams.get('path'));
30+
const response = NextResponse.redirect(new URL(nextPath, requestUrl));
31+
32+
response.cookies.set(ADMIN_CUSTOMER_BUSINESS_COOKIE, business.id, {
33+
httpOnly: true,
34+
sameSite: 'lax',
35+
secure: process.env.NODE_ENV === 'production',
36+
path: '/app',
37+
});
38+
39+
return response;
40+
}

app/admin/[businessId]/page.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
sendBusinessTestSmsAction,
1414
setBusinessProvisioningStatusAction,
1515
} from '@/app/admin/actions';
16+
import { buildAdminCustomerOpenHref } from '@/lib/admin-customer-paths';
1617
import {
1718
buildAdminOnboardingConfidence,
1819
canDeleteTestBusiness,
@@ -180,9 +181,9 @@ function getOperatorEventDetails(value: unknown) {
180181

181182
function buildOperatorEventRelatedHref(businessId: string, relatedEntityType: string | null, relatedEntityId: string | null) {
182183
if (!relatedEntityType || !relatedEntityId) return null;
183-
if (relatedEntityType === 'lead') return `/admin/${businessId}/workspace#recent-leads`;
184+
if (relatedEntityType === 'lead') return buildAdminCustomerOpenHref(businessId, `/app/leads/${relatedEntityId}`);
184185
if (relatedEntityType === 'message') return `/admin/${businessId}/workspace#recent-activity`;
185-
if (relatedEntityType === 'call') return `/admin/${businessId}/workspace#call-flow-snapshot`;
186+
if (relatedEntityType === 'call') return buildAdminCustomerOpenHref(businessId, '/app/call-flow');
186187
return null;
187188
}
188189

@@ -495,12 +496,15 @@ export default async function AdminBusinessDetailPage({
495496
</p>
496497
</div>
497498
<div className="flex flex-wrap gap-2">
498-
<Link className={buttonVariants({ variant: 'default' })} href={`/admin/${business.id}/workspace`}>
499+
<Link className={buttonVariants({ variant: 'default' })} href={buildAdminCustomerOpenHref(business.id, '/app')}>
499500
Open customer workspace
500501
</Link>
501-
<Link className={buttonVariants({ variant: 'outline' })} href={`/admin/${business.id}/workspace#recent-leads`}>
502+
<Link className={buttonVariants({ variant: 'outline' })} href={buildAdminCustomerOpenHref(business.id, '/app/leads?view=attention')}>
502503
Open customer leads
503504
</Link>
505+
<Link className={buttonVariants({ variant: 'outline' })} href={`/admin/${business.id}/workspace`}>
506+
View support workspace snapshot
507+
</Link>
504508
<Link className={buttonVariants({ variant: 'outline' })} href="/admin">
505509
Back to board
506510
</Link>
@@ -670,22 +674,25 @@ export default async function AdminBusinessDetailPage({
670674
<Card className="bg-card/90">
671675
<CardHeader>
672676
<CardTitle>Support mode shortcuts</CardTitle>
673-
<CardDescription>Safe customer-side entry points without impersonation or tenant bleed.</CardDescription>
677+
<CardDescription>Open the real customer pages for this business, or choose the snapshot view when you only need read-only context.</CardDescription>
674678
</CardHeader>
675679
<CardContent className="space-y-4">
676680
<div className="grid gap-2">
677-
<Link className={buttonVariants({ variant: 'default', size: 'sm' })} href={`/admin/${business.id}/workspace`}>
681+
<Link className={buttonVariants({ variant: 'default', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app')}>
678682
Open customer workspace
679683
</Link>
680-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${business.id}/workspace#recent-leads`}>
684+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/leads?view=attention')}>
681685
Open customer leads
682686
</Link>
683-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${business.id}/workspace#settings-snapshot`}>
687+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/settings')}>
684688
Open customer settings
685689
</Link>
686-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${business.id}/workspace#call-flow-snapshot`}>
690+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/call-flow')}>
687691
Open customer call flow
688692
</Link>
693+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href={`/admin/${business.id}/workspace`}>
694+
View support workspace snapshot
695+
</Link>
689696
</div>
690697

691698
<form action={sendBusinessTestSmsAction} className="rounded-xl border bg-background/80 p-4">
@@ -711,7 +718,7 @@ export default async function AdminBusinessDetailPage({
711718
</form>
712719

713720
<div className="rounded-xl border bg-background/80 p-4 text-sm text-muted-foreground">
714-
Support mode stays read-only. Use it to inspect leads, settings, and call flow quickly without weakening business isolation.
721+
Support workspace snapshots stay read-only. The buttons above open the real customer pages in an admin-scoped customer mode so you can act without impersonation.
715722
</div>
716723
</CardContent>
717724
</Card>

app/admin/[businessId]/workspace/page.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from 'next/link';
22
import { notFound } from 'next/navigation';
33

4+
import { buildAdminCustomerOpenHref } from '@/lib/admin-customer-paths';
45
import { buildAdminNextStep } from '@/lib/admin-dashboard';
56
import { requireAdmin } from '@/lib/admin';
67
import { getAdminOwnerState } from '@/lib/admin-provisioning';
@@ -79,7 +80,7 @@ export default async function AdminBusinessWorkspacePage({ params }: { params: {
7980
<div>
8081
<h1 className="text-3xl font-semibold tracking-tight">{business.name} support mode workspace</h1>
8182
<p className="text-sm text-muted-foreground">
82-
Read-only customer context so the founder can inspect leads, settings, and call flow without impersonation.
83+
Read-only customer snapshot for fast inspection. Use the customer-mode buttons below when you need the real editable customer pages.
8384
</p>
8485
</div>
8586
<div className="flex flex-wrap gap-2">
@@ -96,7 +97,7 @@ export default async function AdminBusinessWorkspacePage({ params }: { params: {
9697
<Card className="border-primary/20 bg-primary/5">
9798
<CardHeader>
9899
<CardTitle>Support snapshot</CardTitle>
99-
<CardDescription>Immediate health signal plus quick jumps into the customer context that matter most.</CardDescription>
100+
<CardDescription>Immediate health signal plus clear separation between real customer pages and read-only snapshot sections.</CardDescription>
100101
</CardHeader>
101102
<CardContent className="space-y-4">
102103
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
@@ -122,17 +123,32 @@ export default async function AdminBusinessWorkspacePage({ params }: { params: {
122123
</div>
123124

124125
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
125-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href="#recent-leads">
126+
<Link className={buttonVariants({ variant: 'default', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app')}>
127+
Open customer workspace
128+
</Link>
129+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/leads?view=attention')}>
126130
Open customer leads
127131
</Link>
128-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href="#settings-snapshot">
132+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/settings')}>
129133
Open customer settings
130134
</Link>
131-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href="#call-flow-snapshot">
135+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app/call-flow')}>
132136
Open customer call flow
133137
</Link>
138+
</div>
139+
140+
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
141+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href="#recent-leads">
142+
View leads snapshot
143+
</Link>
144+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href="#settings-snapshot">
145+
View settings snapshot
146+
</Link>
147+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href="#call-flow-snapshot">
148+
View call flow snapshot
149+
</Link>
134150
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href="#recent-activity">
135-
Open recent activity
151+
View recent activity snapshot
136152
</Link>
137153
</div>
138154
</CardContent>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { ADMIN_CUSTOMER_BUSINESS_COOKIE } from '@/lib/admin-customer-context';
4+
import { getAdminSession } from '@/lib/admin';
5+
6+
export async function GET(request: Request) {
7+
const adminSession = await getAdminSession();
8+
const requestUrl = new URL(request.url);
9+
10+
if (!adminSession?.userId) {
11+
return NextResponse.redirect(new URL('/sign-in', requestUrl));
12+
}
13+
14+
if (!adminSession.isAdmin) {
15+
return NextResponse.redirect(new URL('/app', requestUrl));
16+
}
17+
18+
const businessId = requestUrl.searchParams.get('businessId')?.trim();
19+
const redirectPath = businessId ? `/admin/${businessId}` : '/admin';
20+
const response = NextResponse.redirect(new URL(redirectPath, requestUrl));
21+
22+
response.cookies.set(ADMIN_CUSTOMER_BUSINESS_COOKIE, '', {
23+
httpOnly: true,
24+
sameSite: 'lax',
25+
secure: process.env.NODE_ENV === 'production',
26+
path: '/app',
27+
maxAge: 0,
28+
});
29+
30+
return response;
31+
}

app/admin/page.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
restoreBusinessAction,
1111
sendBusinessTestSmsAction,
1212
} from '@/app/admin/actions';
13+
import { buildAdminCustomerOpenHref } from '@/lib/admin-customer-paths';
1314
import {
1415
adminBoardFilterOptions,
1516
buildAdminBusinessPickerLabel,
@@ -566,12 +567,24 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
566567
<Link className={buttonVariants({ size: 'sm' })} href={`/admin/${selectedBusinessRow.business.id}`}>
567568
Open business
568569
</Link>
569-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${selectedBusinessRow.business.id}/workspace`}>
570-
Open workspace
570+
<Link
571+
className={buttonVariants({ variant: 'outline', size: 'sm' })}
572+
href={buildAdminCustomerOpenHref(selectedBusinessRow.business.id, '/app')}
573+
>
574+
Open customer workspace
575+
</Link>
576+
<Link
577+
className={buttonVariants({ variant: 'outline', size: 'sm' })}
578+
href={buildAdminCustomerOpenHref(selectedBusinessRow.business.id, '/app/settings')}
579+
>
580+
Open customer settings
571581
</Link>
572582
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href={`/admin/${selectedBusinessRow.business.id}#advanced`}>
573583
Open full advanced controls
574584
</Link>
585+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href={`/admin/${selectedBusinessRow.business.id}/workspace`}>
586+
View support workspace snapshot
587+
</Link>
575588
</div>
576589
</div>
577590

@@ -723,12 +736,18 @@ export default async function AdminPage({ searchParams }: { searchParams?: Recor
723736
<Link className={buttonVariants({ size: 'sm' })} href={`/admin/${business.id}`}>
724737
Open business
725738
</Link>
726-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${business.id}/workspace`}>
727-
Open workspace
739+
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={buildAdminCustomerOpenHref(business.id, '/app')}>
740+
Open customer workspace
728741
</Link>
729-
<Link className={buttonVariants({ variant: 'outline', size: 'sm' })} href={`/admin/${business.id}/workspace#recent-leads`}>
742+
<Link
743+
className={buttonVariants({ variant: 'outline', size: 'sm' })}
744+
href={buildAdminCustomerOpenHref(business.id, '/app/leads?view=attention')}
745+
>
730746
Open customer leads
731747
</Link>
748+
<Link className={buttonVariants({ variant: 'ghost', size: 'sm' })} href={`/admin/${business.id}/workspace`}>
749+
View support workspace snapshot
750+
</Link>
732751
{!isBusinessArchived(business) ? (
733752
<form action={provisionBusinessAction}>
734753
<input type="hidden" name="businessId" value={business.id} />

app/app/layout.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { auth } from '@clerk/nextjs/server';
22
import { redirect } from 'next/navigation';
3+
import Link from 'next/link';
34

45
import { AppNav } from '@/components/app-nav';
6+
import { getAdminCustomerActingContext } from '@/lib/admin-customer-context';
7+
import { buildAdminCustomerExitHref } from '@/lib/admin-customer-paths';
58
import { db } from '@/lib/db';
69
import { getPortfolioDemoBusiness, isPortfolioDemoMode } from '@/lib/portfolio-demo';
710
import { getCustomerSystemStatus } from '@/lib/system-status';
@@ -25,25 +28,48 @@ export default async function AppLayout({ children }: { children: React.ReactNod
2528
);
2629
}
2730

31+
const adminCustomerContext = await getAdminCustomerActingContext();
2832
const { userId } = await auth();
2933
if (!userId) {
3034
redirect('/sign-in');
3135
}
3236

33-
const business = await db.business.findUnique({ where: { ownerClerkId: userId } });
37+
const business = adminCustomerContext
38+
? adminCustomerContext.business
39+
: await db.business.findUnique({ where: { ownerClerkId: userId } });
3440
const successfulLeadCount = business
3541
? await db.lead.count({ where: { businessId: business.id, OR: [{ ownerNotifiedAt: { not: null } }, { notifiedAt: { not: null } }] } })
3642
: 0;
3743
const systemStatus = business ? getCustomerSystemStatus(business, successfulLeadCount) : null;
3844

3945
return (
40-
<div className="min-h-screen">
41-
<AppNav
42-
business={business}
43-
systemStatusLabel={systemStatus?.label ?? 'Not live yet'}
44-
systemStatusVariant={systemStatus?.badgeVariant ?? 'outline'}
45-
/>
46-
<main className="container py-8">{children}</main>
47-
</div>
48-
);
46+
<div className="min-h-screen">
47+
<AppNav
48+
business={business}
49+
systemStatusLabel={systemStatus?.label ?? 'Not live yet'}
50+
systemStatusVariant={systemStatus?.badgeVariant ?? 'outline'}
51+
/>
52+
{adminCustomerContext && business ? (
53+
<div className="border-b bg-primary/5">
54+
<div className="container flex flex-col gap-3 py-3 text-sm lg:flex-row lg:items-center lg:justify-between">
55+
<div>
56+
<p className="font-medium">Admin customer mode</p>
57+
<p className="text-muted-foreground">
58+
You are using the real customer pages for <span className="font-medium text-foreground">{business.name}</span>.
59+
</p>
60+
</div>
61+
<div className="flex flex-wrap gap-2">
62+
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={`/admin/${business.id}`}>
63+
Back to operator controls
64+
</Link>
65+
<Link className="rounded-md border px-3 py-2 text-sm font-medium transition-colors hover:bg-background" href={buildAdminCustomerExitHref(business.id)}>
66+
Exit customer mode
67+
</Link>
68+
</div>
69+
</div>
70+
</div>
71+
) : null}
72+
<main className="container py-8">{children}</main>
73+
</div>
74+
);
4975
}

app/app/settings/actions.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use server';
22

3-
import { auth, currentUser } from '@clerk/nextjs/server';
3+
import { currentUser } from '@clerk/nextjs/server';
44
import { revalidatePath } from 'next/cache';
55
import { redirect } from 'next/navigation';
66

77
import { requireAdmin } from '@/lib/admin';
88
import { logAuditEvent } from '@/lib/audit-log';
9-
import { getBusinessForOwnerClerkId } from '@/lib/business-access';
9+
import { requireBusiness } from '@/lib/auth';
1010
import { db } from '@/lib/db';
1111
import { maskPhoneForAudit, normalizePhoneNumber, normalizePhoneNumberToE164 } from '@/lib/phone';
1212
import { getTwilioBusinessClient } from '@/lib/twilio-client';
@@ -16,11 +16,7 @@ import { syncTwilioIncomingPhoneNumberWebhooks } from '@/lib/twilio';
1616
import { businessSettingsSchema, businessTwilioAdminOverrideSchema, buyNumberSchema } from '@/lib/validators';
1717

1818
async function getBusinessForOwner() {
19-
const { userId } = await auth();
20-
if (!userId) redirect('/sign-in');
21-
const business = await getBusinessForOwnerClerkId(userId);
22-
if (!business) redirect('/app/onboarding');
23-
return business;
19+
return requireBusiness();
2420
}
2521

2622
async function saveBusinessTwilioNumber(businessId: string, params: { phoneNumber: string | null; phoneNumberSid: string; syncedAt: Date }) {

0 commit comments

Comments
 (0)