This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// GET /auth/callback: the provider sends the browser back here (MIB I3's path). The code is
|
||||
// exchanged on the server, the tokens are sealed into the session row, and the browser receives
|
||||
// one sealed cookie whose plaintext is a random handle. A failure of any step ends at the sign-in
|
||||
// page with a code for the person to read, never at a 500 (src/server/auth-response.ts).
|
||||
import { authRoutes } from "../../../src/server/app"
|
||||
import { authResponse } from "../../../src/server/auth-response"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
return authResponse(request, async () => (await authRoutes()).callback(request))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// GET /auth/dev-login?email=…&return_to=…: a sign-in for a developer with no identity provider.
|
||||
// @conusai-kit/auth refuses to build it in a production deployment, and refuses again at boot if
|
||||
// AUTH_DEV_LOGIN is set there, so this route cannot become a way in (spec §7).
|
||||
import { createDevLogin, loadAuthConfig } from "@conusai-kit/auth"
|
||||
import { authRoutes, webRuntime } from "../../../src/server/app"
|
||||
import { safeReturnTo, signInErrorHref } from "../../../src/server/auth-errors"
|
||||
import { authResponse, redirectTo } from "../../../src/server/auth-response"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const config = loadAuthConfig()
|
||||
if (!config.devLogin) return new Response("dev login is not enabled for this deployment", { status: 404 })
|
||||
return authResponse(request, async () => {
|
||||
const runtime = webRuntime()
|
||||
const data = runtime.authData ?? runtime.data
|
||||
if (!data) throw new Error("The selected profile has no auth data source")
|
||||
const { sessions } = await authRoutes()
|
||||
const response = await createDevLogin({ config, data, sessions }).signIn(request)
|
||||
// An address the adapter would not take is a failed sign-in like any other, not a bare 400.
|
||||
if (response.status === 400) return redirectTo(signInErrorHref("failed"))
|
||||
// The adapter always lands on `/`; the person was going somewhere (the invitation they were sent).
|
||||
const target = safeReturnTo(new URL(request.url).searchParams.get("return_to"))
|
||||
if (response.status !== 302 || target === "/") return response
|
||||
const headers = new Headers(response.headers)
|
||||
headers.set("location", target)
|
||||
return new Response(null, { status: 302, headers })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The sign-in page (spec §7): where a visitor lands when they need to be signed in, and where a
|
||||
// failed sign-in ends (src/server/auth-response.ts). It says what went wrong in the person's
|
||||
// language -- from a fixed set of codes, never from the provider's text -- and offers the way in the
|
||||
// deployment has: its identity provider, or, in development with none, any e-mail address.
|
||||
|
||||
import { loadAuthConfig } from "@conusai-kit/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
import type { ReactElement } from "react"
|
||||
import { AUTH_ERROR_MESSAGES, parseAuthError, safeReturnTo } from "../../../src/server/auth-errors"
|
||||
import { betterAuthFormLabels, MIN_PASSWORD_LENGTH } from "../../../src/server/better-auth" // kit:convex-better-auth
|
||||
import { pageContext } from "../../../src/server/page"
|
||||
import { BetterAuthForm } from "../../../src/ui/better-auth-form" // kit:convex-better-auth
|
||||
import { StateView } from "../../../src/ui/state-view"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function SignIn({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ error?: string; return_to?: string }>
|
||||
}): Promise<ReactElement> {
|
||||
const { error, return_to: returnToParam } = await searchParams
|
||||
const { i18n, signedIn } = await pageContext()
|
||||
const returnTo = safeReturnTo(returnToParam)
|
||||
// Somebody who is already in has nothing to do here, unless they were sent back with an error.
|
||||
const code = parseAuthError(error)
|
||||
if (signedIn && !code) redirect(returnTo)
|
||||
|
||||
const config = loadAuthConfig()
|
||||
const next = returnTo === "/" ? "" : `?return_to=${encodeURIComponent(returnTo)}`
|
||||
const betterAuth = config.provider === "convex-better-auth" // kit:convex-better-auth
|
||||
return (
|
||||
<StateView title={i18n.message("signin.title")}>
|
||||
{code ? (
|
||||
<p className="app-error" role="alert">
|
||||
{i18n.message(AUTH_ERROR_MESSAGES[code])}
|
||||
</p>
|
||||
) : (
|
||||
<p>{i18n.message("signin.intro")}</p>
|
||||
)}
|
||||
{config.oidc ? (
|
||||
<p>
|
||||
<a href={`/auth/sign-in/start${next}`}>{i18n.message("signin.continue")}</a>
|
||||
</p>
|
||||
) : null}
|
||||
{!config.oidc && config.devLogin ? (
|
||||
<form method="get" action="/auth/dev-login">
|
||||
<h2>{i18n.message("signin.devTitle")}</h2>
|
||||
<p>{i18n.message("signin.devHelp")}</p>
|
||||
{returnTo === "/" ? null : <input type="hidden" name="return_to" value={returnTo} />}
|
||||
<label className="kit-field" htmlFor="dev-email">
|
||||
{i18n.message("app.email")}
|
||||
<input id="dev-email" name="email" type="email" required autoComplete="email" />
|
||||
</label>
|
||||
<button type="submit">{i18n.message("signin.devSubmit")}</button>
|
||||
</form>
|
||||
) : null}
|
||||
{/* kit:convex-better-auth:begin */}
|
||||
{betterAuth ? (
|
||||
<>
|
||||
<BetterAuthForm
|
||||
mode="sign-in"
|
||||
returnTo={returnTo}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
labels={betterAuthFormLabels(i18n, "sign-in")}
|
||||
/>
|
||||
<p>
|
||||
<a href={`/auth/sign-up${next}`}>{i18n.message("signin.account.toSignUp")}</a>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
{/* kit:convex-better-auth:end */}
|
||||
{!config.oidc && !config.devLogin && config.provider === "oidc" ? (
|
||||
<p role="status">{i18n.message("signin.notConfigured")}</p>
|
||||
) : null}
|
||||
</StateView>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// GET /auth/sign-in/start: the redirect to the identity provider (spec §7). The transaction cookie it
|
||||
// sets carries the state, the nonce and the PKCE verifier, sealed; the browser gets no token. The
|
||||
// page people see before it is /auth/sign-in. A deployment with no provider has nothing to redirect
|
||||
// to, and sends the visitor back to that page, which says so.
|
||||
import { loadAuthConfig } from "@conusai-kit/auth"
|
||||
import { authRoutes } from "../../../../src/server/app"
|
||||
import { authResponse, redirectTo } from "../../../../src/server/auth-response"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
if (!loadAuthConfig().oidc) return redirectTo("/auth/sign-in")
|
||||
return authResponse(request, async () => (await authRoutes()).signIn(request))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// POST /auth/sign-out: revokes the session row and redirects to the provider's end-session
|
||||
// endpoint, which sends the browser to /auth/signed-out.
|
||||
//
|
||||
// POST, with the fetch-metadata half of csrf.ts's check: revoking a session is a state change, so
|
||||
// it must not hang on a GET, which a prefetch, a crawler or an <img> fires unprompted, and a
|
||||
// cross-site form POST is refused the same way the API refuses one. The synchronizer token itself
|
||||
// cannot be checked here -- a plain HTML form cannot send a header -- so the metadata a browser
|
||||
// always sends is the check.
|
||||
import { authRoutes } from "../../../src/server/app"
|
||||
import { authResponse } from "../../../src/server/auth-response"
|
||||
import { betterAuthSignOut } from "../../../src/server/better-auth-session" // kit:convex-better-auth
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
export const runtime = "nodejs"
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
return new Response("signing out is a POST", { status: 405, headers: { allow: "POST" } })
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
const site = request.headers.get("sec-fetch-site")
|
||||
if (site !== null && site !== "same-origin" && site !== "none") {
|
||||
return new Response("a cross-site sign-out is refused", { status: 403 })
|
||||
}
|
||||
const betterAuth = await betterAuthSignOut(request) // kit:convex-better-auth
|
||||
if (betterAuth) return betterAuth // kit:convex-better-auth
|
||||
return authResponse(request, async () => (await authRoutes()).signOut(request))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The sign-up page of the Convex Better Auth adapter (AUTH_PROVIDER=convex-better-auth): a new
|
||||
// account with an e-mail address and a password. An OIDC deployment has its identity provider's own
|
||||
// registration, so this page is not found there.
|
||||
|
||||
import { loadAuthConfig } from "@conusai-kit/auth"
|
||||
import { notFound, redirect } from "next/navigation"
|
||||
import type { ReactElement } from "react"
|
||||
import { safeReturnTo } from "../../../src/server/auth-errors"
|
||||
import { betterAuthFormLabels, MIN_PASSWORD_LENGTH } from "../../../src/server/better-auth"
|
||||
import { pageContext } from "../../../src/server/page"
|
||||
import { BetterAuthForm } from "../../../src/ui/better-auth-form"
|
||||
import { StateView } from "../../../src/ui/state-view"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function SignUp({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ return_to?: string }>
|
||||
}): Promise<ReactElement> {
|
||||
if (loadAuthConfig().provider !== "convex-better-auth") notFound()
|
||||
const { return_to: returnToParam } = await searchParams
|
||||
const { i18n, signedIn } = await pageContext()
|
||||
const returnTo = safeReturnTo(returnToParam)
|
||||
if (signedIn) redirect(returnTo)
|
||||
const next = returnTo === "/" ? "" : `?return_to=${encodeURIComponent(returnTo)}`
|
||||
return (
|
||||
<StateView title={i18n.message("signin.account.signUpTitle")}>
|
||||
<p>{i18n.message("signin.account.signUpIntro")}</p>
|
||||
<BetterAuthForm
|
||||
mode="sign-up"
|
||||
returnTo={returnTo}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
labels={betterAuthFormLabels(i18n, "sign-up")}
|
||||
/>
|
||||
<p>
|
||||
<a href={`/auth/sign-in${next}`}>{i18n.message("signin.account.toSignIn")}</a>
|
||||
</p>
|
||||
</StateView>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Where the identity provider sends the browser after an RP-initiated sign-out (MIB I3's path).
|
||||
|
||||
import type { ReactElement } from "react"
|
||||
import { pageContext } from "../../../src/server/page"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function SignedOut(): Promise<ReactElement> {
|
||||
const { i18n } = await pageContext()
|
||||
return (
|
||||
<main>
|
||||
<h1>{i18n.message("app.signedOut")}</h1>
|
||||
<p>{i18n.message("app.signedOutBody")}</p>
|
||||
<a href="/auth/sign-in">{i18n.message("app.signIn")}</a>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user