a717f03898
* feat(mcp-oauth): let an account with no company connect and sign up from the OAuth popup Identity unlock for agent-first onboarding (#1814, shape B+). A person with no Accounted account can now connect from an MCP client, create the account inside the Connect popup and finish the OAuth dance. - authorize/token no longer require a company: consent renders a companyless variant and the key is minted with company_id NULL. - validateApiKey returns companyId string|null and binds an unbound key to the user's first company on the first validation after it exists. - MCP server: company-dependent tools and data resources answer with a structured NO_COMPANY_YET error; the company-independent tools still run; telemetry skips when there is no company scope. - /api/events fails closed instead of throwing for an unbound key. - authorize forces TOTP enrollment (not just verification) for password accounts with no factor, since the middleware skips enrollment for zero-company users; BankID-linked accounts stay exempt. - /login forwards next to /register; register, GoogleAuthButton and /auth/callback carry it back to the consent page (callback honours only /api/mcp-oauth/authorize, via safeReturnTo); /mfa/enroll hard-navigates to /api/* destinations like /mfa/verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * refactor(company): move getActiveCompanyId out of the next/headers module lib/auth/api-keys.ts needs the resolver for unbound-key binding, but lib/company/context.ts imports next/headers for the legacy company cookie and Turbopack refuses that import on some of api-keys' import paths (the preview build failed). The resolver and CompanyContextError now live in lib/company/active-company.ts; context.ts re-exports them so every caller and test mock is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(mcp-oauth): fail closed on a failed assurance lookup; enroll Back aborts instead of looping Review findings on #1855: requireAal2 let consent through at AAL1 when getAuthenticatorAssuranceLevel() returned nothing and a verified factor existed. Only a positive AAL2 answer passes now; a failed lookup and the inconsistent verified-factor-at-AAL1 case both step up to /mfa/verify. Back on /mfa/enroll with the consent page as returnTo went straight back into the redirect loop; it now aborts to the app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useLocale, useTranslations } from 'next-intl'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Loader2 } from 'lucide-react'
|
|
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
|
|
import { GoogleMark } from '@/components/ui/provider-marks'
|
|
|
|
|
|
/**
|
|
* "Continue with Google" for the login and register pages.
|
|
*
|
|
* Kicks off the Supabase OAuth redirect; the round-trip lands in
|
|
* /auth/callback (PKCE code exchange), which owns MFA routing, invite
|
|
* acceptance and silent-team creation for OAuth sign-ins and sign-ups alike.
|
|
* The flow=oauth marker lets the callback tag failures so the login page
|
|
* shows Google-specific copy instead of the email-confirmation framing.
|
|
*/
|
|
export function GoogleAuthButton({
|
|
onError,
|
|
compact = false,
|
|
next,
|
|
}: {
|
|
onError: (message: string) => void
|
|
/**
|
|
* Half-width alternative-method chip on the login panel: shows just the
|
|
* mark and "Google" (a brand name, never translated), with the full label
|
|
* kept as the accessible name.
|
|
*/
|
|
compact?: boolean
|
|
/**
|
|
* Post-auth destination, already passed through safeReturnTo by the caller.
|
|
* Forwarded to /auth/callback as `next` so an OAuth sign-in or sign-up that
|
|
* started from the MCP consent page (/login?next=/api/mcp-oauth/authorize…)
|
|
* resumes the consent flow instead of landing on the dashboard. '/' (the
|
|
* safeReturnTo fallback) means no destination and is not forwarded.
|
|
*/
|
|
next?: string
|
|
}) {
|
|
const [isRedirecting, setIsRedirecting] = useState(false)
|
|
const supabase = createClient()
|
|
const tAuth = useTranslations('auth')
|
|
const errorLocale = useLocale() as ErrorLocale
|
|
|
|
const handleClick = async () => {
|
|
setIsRedirecting(true)
|
|
try {
|
|
const callback = new URL('/auth/callback', window.location.origin)
|
|
callback.searchParams.set('flow', 'oauth')
|
|
if (next && next !== '/') callback.searchParams.set('next', next)
|
|
const { error } = await supabase.auth.signInWithOAuth({
|
|
provider: 'google',
|
|
options: {
|
|
redirectTo: callback.toString(),
|
|
},
|
|
})
|
|
if (error) {
|
|
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
|
|
setIsRedirecting(false)
|
|
}
|
|
// On success the browser navigates away; keep the spinner until then.
|
|
} catch (error) {
|
|
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
|
|
setIsRedirecting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className={compact ? 'h-10 w-full gap-2' : 'w-full h-11'}
|
|
onClick={handleClick}
|
|
disabled={isRedirecting}
|
|
aria-label={tAuth('continue_with_google')}
|
|
>
|
|
{isRedirecting ? (
|
|
<Loader2 className={compact ? 'h-4 w-4 animate-spin' : 'mr-2 h-4 w-4 animate-spin'} />
|
|
) : (
|
|
<span className={compact ? 'flex items-center' : 'mr-2 flex items-center'}>
|
|
<GoogleMark />
|
|
</span>
|
|
)}
|
|
{compact ? 'Google' : tAuth('continue_with_google')}
|
|
</Button>
|
|
)
|
|
}
|