Skip to content

Commit 92440ff

Browse files
committed
fix(academy): address PR review — isHosted regression, stuck isExecuting, revoked cert 500, certificate SSR
- Restore env-var-based isHosted check (was hardcoded true, breaking self-hosted deployments) - Fix isExecuting stuck at true when mock run fails validation — set isMockRunningRef immediately and reset both flags on early exit - Fix revoked/expired certificate causing 500 — any existing record (not just active) now returns 409 instead of falling through to INSERT - Convert certificate verification page from client component to server component — direct DB fetch, notFound() on missing cert, generateMetadata for SEO/social previews
1 parent 4e72334 commit 92440ff

File tree

4 files changed

+108
-85
lines changed

4 files changed

+108
-85
lines changed
Lines changed: 84 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
1-
'use client'
2-
3-
import type React from 'react'
4-
import { use } from 'react'
1+
import { db } from '@sim/db'
2+
import { academyCertificate } from '@sim/db/schema'
3+
import { eq } from 'drizzle-orm'
54
import { CheckCircle2, GraduationCap } from 'lucide-react'
6-
import { useAcademyCertificate } from '@/hooks/queries/academy'
5+
import type { Metadata } from 'next'
6+
import { notFound } from 'next/navigation'
7+
import type { AcademyCertificate } from '@/lib/academy/types'
8+
9+
interface CertificatePageProps {
10+
params: Promise<{ certificateNumber: string }>
11+
}
12+
13+
export async function generateMetadata({ params }: CertificatePageProps): Promise<Metadata> {
14+
const { certificateNumber } = await params
15+
const certificate = await fetchCertificate(certificateNumber)
16+
if (!certificate) return { title: 'Certificate Not Found' }
17+
return {
18+
title: `${certificate.metadata?.courseTitle} — Certificate`,
19+
description: `Verified certificate of completion awarded to ${certificate.metadata?.recipientName}.`,
20+
}
21+
}
22+
23+
async function fetchCertificate(certificateNumber: string): Promise<AcademyCertificate | null> {
24+
const [row] = await db
25+
.select()
26+
.from(academyCertificate)
27+
.where(eq(academyCertificate.certificateNumber, certificateNumber))
28+
.limit(1)
29+
return (row as unknown as AcademyCertificate) ?? null
30+
}
731

832
const DATE_FORMAT: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }
933
function formatDate(date: string | Date) {
@@ -19,90 +43,75 @@ function MetaRow({ label, children }: { label: string; children: React.ReactNode
1943
)
2044
}
2145

22-
interface CertificatePageProps {
23-
params: Promise<{ certificateNumber: string }>
24-
}
46+
export default async function CertificatePage({ params }: CertificatePageProps) {
47+
const { certificateNumber } = await params
48+
const certificate = await fetchCertificate(certificateNumber)
2549

26-
export default function CertificatePage({ params }: CertificatePageProps) {
27-
const { certificateNumber } = use(params)
28-
const { data: certificate, isLoading, error } = useAcademyCertificate(certificateNumber)
50+
if (!certificate) notFound()
2951

3052
return (
3153
<main className='flex flex-1 items-center justify-center px-6 py-20'>
32-
{isLoading ? (
33-
<div className='h-5 w-5 animate-spin rounded-full border-2 border-[#ECECEC] border-t-transparent' />
34-
) : error || !certificate ? (
35-
<div className='text-center'>
36-
<p className='mb-3 text-[#999] text-[16px]'>Certificate not found.</p>
37-
<p className='text-[#555] text-[14px]'>
38-
Certificate number{' '}
39-
<span className='font-[430] text-[#ECECEC]'>{certificateNumber}</span> is invalid or has
40-
been revoked.
41-
</p>
42-
</div>
43-
) : (
44-
<div className='w-full max-w-2xl'>
45-
<div className='rounded-[12px] border border-[#3A4A3A] bg-[#1C2A1C] p-10 text-center'>
46-
<div className='mb-6 flex justify-center'>
47-
<div className='flex h-16 w-16 items-center justify-center rounded-full border-2 border-[#4CAF50]/40 bg-[#4CAF50]/10'>
48-
<GraduationCap className='h-8 w-8 text-[#4CAF50]' />
49-
</div>
54+
<div className='w-full max-w-2xl'>
55+
<div className='rounded-[12px] border border-[#3A4A3A] bg-[#1C2A1C] p-10 text-center'>
56+
<div className='mb-6 flex justify-center'>
57+
<div className='flex h-16 w-16 items-center justify-center rounded-full border-2 border-[#4CAF50]/40 bg-[#4CAF50]/10'>
58+
<GraduationCap className='h-8 w-8 text-[#4CAF50]' />
5059
</div>
60+
</div>
5161

52-
<div className='mb-2 text-[#4CAF50]/70 text-[13px] uppercase tracking-[0.12em]'>
53-
Certificate of Completion
54-
</div>
62+
<div className='mb-2 text-[#4CAF50]/70 text-[13px] uppercase tracking-[0.12em]'>
63+
Certificate of Completion
64+
</div>
5565

56-
<h1 className='mb-1 font-[430] text-[#ECECEC] text-[28px] leading-[120%]'>
57-
{certificate.metadata?.courseTitle}
58-
</h1>
66+
<h1 className='mb-1 font-[430] text-[#ECECEC] text-[28px] leading-[120%]'>
67+
{certificate.metadata?.courseTitle}
68+
</h1>
5969

60-
{certificate.metadata?.recipientName && (
61-
<p className='mb-6 text-[#999] text-[16px]'>
62-
Awarded to{' '}
63-
<span className='text-[#ECECEC]'>{certificate.metadata.recipientName}</span>
64-
</p>
65-
)}
70+
{certificate.metadata?.recipientName && (
71+
<p className='mb-6 text-[#999] text-[16px]'>
72+
Awarded to{' '}
73+
<span className='text-[#ECECEC]'>{certificate.metadata.recipientName}</span>
74+
</p>
75+
)}
6676

67-
<div className='flex items-center justify-center gap-2 text-[#4CAF50]'>
68-
<CheckCircle2 className='h-4 w-4' />
69-
<span className='font-[430] text-[14px]'>Verified</span>
70-
</div>
77+
<div className='flex items-center justify-center gap-2 text-[#4CAF50]'>
78+
<CheckCircle2 className='h-4 w-4' />
79+
<span className='font-[430] text-[14px]'>Verified</span>
7180
</div>
81+
</div>
7282

73-
<div className='mt-6 divide-y divide-[#2A2A2A] rounded-[8px] border border-[#2A2A2A] bg-[#222]'>
74-
<MetaRow label='Certificate number'>
75-
<span className='font-mono text-[#ECECEC] text-[13px]'>
76-
{certificate.certificateNumber}
83+
<div className='mt-6 divide-y divide-[#2A2A2A] rounded-[8px] border border-[#2A2A2A] bg-[#222]'>
84+
<MetaRow label='Certificate number'>
85+
<span className='font-mono text-[#ECECEC] text-[13px]'>
86+
{certificate.certificateNumber}
87+
</span>
88+
</MetaRow>
89+
<MetaRow label='Issued'>
90+
<span className='text-[#ECECEC] text-[13px]'>{formatDate(certificate.issuedAt)}</span>
91+
</MetaRow>
92+
<MetaRow label='Status'>
93+
<span
94+
className={`text-[13px] capitalize ${
95+
certificate.status === 'active' ? 'text-[#4CAF50]' : 'text-[#f44336]'
96+
}`}
97+
>
98+
{certificate.status}
99+
</span>
100+
</MetaRow>
101+
{certificate.expiresAt && (
102+
<MetaRow label='Expires'>
103+
<span className='text-[#ECECEC] text-[13px]'>
104+
{formatDate(certificate.expiresAt)}
77105
</span>
78106
</MetaRow>
79-
<MetaRow label='Issued'>
80-
<span className='text-[#ECECEC] text-[13px]'>{formatDate(certificate.issuedAt)}</span>
81-
</MetaRow>
82-
<MetaRow label='Status'>
83-
<span
84-
className={`text-[13px] capitalize ${
85-
certificate.status === 'active' ? 'text-[#4CAF50]' : 'text-[#f44336]'
86-
}`}
87-
>
88-
{certificate.status}
89-
</span>
90-
</MetaRow>
91-
{certificate.expiresAt && (
92-
<MetaRow label='Expires'>
93-
<span className='text-[#ECECEC] text-[13px]'>
94-
{formatDate(certificate.expiresAt)}
95-
</span>
96-
</MetaRow>
97-
)}
98-
</div>
99-
100-
<p className='mt-5 text-center text-[#555] text-[13px]'>
101-
This certificate was issued by Sim AI, Inc. and verifies the holder has completed the{' '}
102-
{certificate.metadata?.courseTitle} program.
103-
</p>
107+
)}
104108
</div>
105-
)}
109+
110+
<p className='mt-5 text-center text-[#555] text-[13px]'>
111+
This certificate was issued by Sim AI, Inc. and verifies the holder has completed the{' '}
112+
{certificate.metadata?.courseTitle} program.
113+
</p>
114+
</div>
106115
</main>
107116
)
108117
}

apps/sim/app/academy/components/sandbox-canvas-provider.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,18 +278,24 @@ export function SandboxCanvasProvider({
278278

279279
const handleMockRun = useCallback(async () => {
280280
if (isMockRunningRef.current) return
281+
isMockRunningRef.current = true
281282

283+
const { setActiveBlocks, setIsExecuting } = useExecutionStore.getState()
282284
const { blocks, edges } = readCurrentCanvasState(workflowId)
283285
const result = validateExercise(blocks, edges, exerciseConfig.validationRules)
284286
setValidationResult(result)
285-
if (!result.passed) return
287+
if (!result.passed) {
288+
isMockRunningRef.current = false
289+
setIsExecuting(workflowId, false)
290+
return
291+
}
286292

287293
const plan = buildMockExecutionPlan(blocks, edges, exerciseConfig.mockOutputs ?? {})
288-
if (plan.length === 0) return
289-
290-
isMockRunningRef.current = true
291-
292-
const { setActiveBlocks, setIsExecuting } = useExecutionStore.getState()
294+
if (plan.length === 0) {
295+
isMockRunningRef.current = false
296+
setIsExecuting(workflowId, false)
297+
return
298+
}
293299
const { addConsole, clearWorkflowConsole } = useTerminalConsoleStore.getState()
294300
const workflowBlocks = useWorkflowStore.getState().blocks
295301

apps/sim/app/api/academy/certificates/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,14 @@ export async function POST(req: NextRequest) {
8787
.then((rows) => rows[0] ?? null),
8888
])
8989

90-
if (existing?.status === 'active') {
91-
return NextResponse.json({ certificate: existing })
90+
if (existing) {
91+
if (existing.status === 'active') {
92+
return NextResponse.json({ certificate: existing })
93+
}
94+
return NextResponse.json(
95+
{ error: 'A certificate for this course already exists but is not active.' },
96+
{ status: 409 }
97+
)
9298
}
9399

94100
const certificateNumber = generateCertificateNumber()

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Environment utility functions for consistent environment detection across the application
33
*/
4-
import { env, isFalsy, isTruthy } from './env'
4+
import { env, getEnv, isFalsy, isTruthy } from './env'
55

66
/**
77
* Is the application running in production mode
@@ -21,7 +21,9 @@ export const isTest = env.NODE_ENV === 'test'
2121
/**
2222
* Is this the hosted version of the application
2323
*/
24-
export const isHosted = true
24+
export const isHosted =
25+
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
26+
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
2527

2628
/**
2729
* Is billing enforcement enabled

0 commit comments

Comments
 (0)