@cantera/acc-sign-intemplate
A ready-made /sign-in page on acc-auth-routes: the scoped sign-in when signed out, and a live connection panel — account, token expiry, held scopes — once connected.
npx shadcn@latest add @cantera/acc-sign-inInstalled at /sign-in: the page and its loading skeleton. Signed out, it renders ScopedAutodeskSignIn; /sign-in?next=/your-page returns there after the callback. Signed in, it shows AccConnectionPanel.
Next: 1. Fill the keys acc-auth-routes added to .env.local (see its notes above). 2. Run next dev and open /sign-in.
Never set ACC_AUTH_DEMO=1 where real accounts exist. Reference: https://canteraui.vercel.app/components/acc-sign-in
This is the exact code the CLI installs into your project — you own it from there.
import { TokenError } from 'aec-auth'
import { cookies, headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { AccConnectionPanel } from '@/components/acc-connection-panel'
import { ScopedAutodeskSignIn } from '@/components/scoped-autodesk-sign-in'
import {
APS_PROVIDER_ID,
appOrigin,
getSessionToken,
openSession,
SESSION_COOKIE,
safeNext,
} from '@/lib/acc-auth'
import { apsProvider } from '@/lib/aps-oauth-preset'
import type { OAuthConnection } from '@/lib/oauth-types'
/** Render <AccSignIn nextPath="/your-page" /> from any server page; the
* default export is a ready-made /sign-in page. */
async function requestOrigin(): Promise<string> {
const headerList = await headers()
const host = headerList.get('x-forwarded-host') ?? headerList.get('host') ?? 'localhost:3000'
const proto = headerList.get('x-forwarded-proto') ?? 'http'
return appOrigin(`${proto}://${host}`)
}
export async function AccSignIn({
nextPath = '/sign-in',
headingLevel = 'h1',
}: {
nextPath?: string
/** Heading level for the block's title. Drop to h2 when embedding under one. */
headingLevel?: 'h1' | 'h2' | 'h3'
}) {
const Heading = headingLevel
const cookieStore = await cookies()
const session = await openSession(cookieStore.get(SESSION_COOKIE)?.value)
const signInHref = `/api/auth/${APS_PROVIDER_ID}?next=${encodeURIComponent(nextPath)}`
if (!session) {
return (
<ScopedAutodeskSignIn
nextPath={nextPath}
title="Sign in"
titleAs={headingLevel}
description="Choose the access to grant, then continue with Autodesk."
/>
)
}
const account = { name: session.name, email: session.email, avatarUrl: session.avatarUrl }
let connection: OAuthConnection
try {
const origin = await requestOrigin()
const token = await getSessionToken(origin, session)
connection = {
provider: apsProvider,
status: 'connected',
account,
scopes: token.scopes ? [...token.scopes] : session.scopes,
expiresAt: token.expiresAt,
}
} catch (error) {
connection = {
provider: apsProvider,
status:
error instanceof TokenError && error.code === 'consent_required' ? 'expired' : 'error',
account,
scopes: session.scopes,
error:
error instanceof TokenError && error.code === 'consent_required'
? 'Grant lost — reconnect to continue.'
: 'Could not refresh the token.',
}
}
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Heading className="font-heading font-medium text-2xl tracking-tight">
Autodesk connection
</Heading>
<AccConnectionPanel
connection={connection}
signOutHref={`/api/auth/signout?next=${encodeURIComponent(nextPath)}`}
signInHref={signInHref}
/>
</div>
)
}
export default async function SignInPage({
searchParams,
}: {
searchParams: Promise<{ next?: string | string[] }>
}) {
// A repeated ?next= arrives as an array; treat it as absent rather than
// guessing which destination was meant.
const { next } = await searchParams
const nextPath = safeNext(typeof next === 'string' ? next : undefined, '/sign-in')
if (nextPath !== '/sign-in') {
// ?next= means the same thing signed in or out: an already-signed-in
// visitor continues to the destination instead of stalling on the panel.
const cookieStore = await cookies()
const session = await openSession(cookieStore.get(SESSION_COOKIE)?.value)
if (session) redirect(nextPath)
}
return (
<main className="flex flex-1 items-center justify-center p-6">
<AccSignIn nextPath={nextPath} />
</main>
)
}
import { LoaderCircleIcon } from 'lucide-react'
/** Deliberately still: skeleton rows at the panel's own geometry, no shimmer;
* the one spinner carries the announcement. */
export default function SignInLoading() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="flex w-full max-w-sm flex-col gap-4">
<output className="flex items-center gap-2 text-muted-foreground text-sm">
{/* The spin lives on a wrapper: transform animations on the <svg>
itself skip the compositor in some engines. */}
<span aria-hidden className="grid size-3.5 shrink-0 animate-spin place-items-center">
<LoaderCircleIcon className="size-3.5" />
</span>
Checking your Autodesk connection
</output>
{/* Same box as Card: rounded-xl, ring-1, py-(--card-spacing) at 4. */}
<div aria-hidden className="rounded-xl bg-card py-4 ring-1 ring-foreground/10">
<div className="flex flex-col gap-3 px-4">
<div className="flex items-center gap-3">
<div className="size-5 shrink-0 rounded bg-muted" />
<div className="h-4 w-28 rounded bg-muted" />
<div className="ml-auto h-7 w-24 shrink-0 rounded-lg bg-muted" />
</div>
<div className="flex items-center gap-2">
<div className="size-7 shrink-0 rounded-full bg-muted" />
<div className="flex flex-col gap-1">
<div className="h-3.5 w-24 rounded bg-muted" />
<div className="h-3.5 w-40 rounded bg-muted" />
</div>
</div>
<div className="flex items-center gap-2">
<div className="h-6 w-24 shrink-0 rounded-md bg-muted" />
<div className="h-3 w-20 rounded bg-muted" />
</div>
</div>
</div>
</div>
</main>
)
}