From 1ee753407f3facb59c056538f8f4b1f4e45e6ba1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 15 Jul 2024 12:44:17 +0300 Subject: [PATCH 1/2] feat(astro): Support `auth().redirectToSignIn()` at page level --- .../templates/astro-node/src/middleware.ts | 5 ++- .../astro-node/src/pages/organization.astro | 6 +++ integration/tests/astro/redirect.test.ts | 36 +++++++++++++++++ packages/astro/env.d.ts | 4 +- packages/astro/src/server/clerk-middleware.ts | 40 ++++++++++++++++--- packages/astro/src/server/get-auth.ts | 23 ++--------- 6 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 integration/tests/astro/redirect.test.ts diff --git a/integration/templates/astro-node/src/middleware.ts b/integration/templates/astro-node/src/middleware.ts index a1d53cf05ce..6984169db1b 100644 --- a/integration/templates/astro-node/src/middleware.ts +++ b/integration/templates/astro-node/src/middleware.ts @@ -8,7 +8,7 @@ const unautorized = () => /** * 3. Support handler */ -const isProtectedPage = createRouteMatcher(['/user(.*)', '/discover(.*)', /^\/organization/]); +const isProtectedPage = createRouteMatcher(['/user(.*)', '/discover(.*)']); const isProtectedApiRoute = createRouteMatcher(['/api/protected(.*)']); @@ -27,6 +27,9 @@ export const onRequest = clerkMiddleware((auth, context, next) => { } if (!auth().orgId && requestURL.pathname !== '/discover' && requestURL.pathname === '/organization') { + if (!auth().userId) { + return next(); + } const searchParams = new URLSearchParams({ redirectUrl: requestURL.href, }); diff --git a/integration/templates/astro-node/src/pages/organization.astro b/integration/templates/astro-node/src/pages/organization.astro index b6bf7544de5..4e6c9a06097 100644 --- a/integration/templates/astro-node/src/pages/organization.astro +++ b/integration/templates/astro-node/src/pages/organization.astro @@ -5,6 +5,12 @@ import { } from "@clerk/astro/components"; import Layout from "../layouts/Layout.astro"; import Streaming from "../layouts/Streaming.astro"; + +const { userId, redirectToSignIn } = Astro.locals.auth() + +if(!userId) { + return redirectToSignIn() +} --- diff --git a/integration/tests/astro/redirect.test.ts b/integration/tests/astro/redirect.test.ts new file mode 100644 index 00000000000..d7212cc1a53 --- /dev/null +++ b/integration/tests/astro/redirect.test.ts @@ -0,0 +1,36 @@ +import { test } from '@playwright/test'; + +import type { FakeUser } from '../../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; + +testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('redirectToSignIn for @astro', ({ app }) => { + test.describe.configure({ mode: 'parallel' }); + let fakeUser: FakeUser; + + test.beforeAll(async () => { + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('redirects to sign-in when unauthenticated (middleware)', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToStart(); + await u.page.getByRole('link', { name: 'User', exact: true }).click(); + await u.page.waitForURL(`${app.serverUrl}/sign-in?redirect_url=${encodeURIComponent(`${app.serverUrl}/user`)}`); + }); + + test('redirects to sign-in when unauthenticated (page)', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToStart(); + await u.page.getByRole('link', { name: 'Organization', exact: true }).click(); + await u.page.waitForURL( + `${app.serverUrl}/sign-in?redirect_url=${encodeURIComponent(`${app.serverUrl}/organization`)}`, + ); + }); +}); diff --git a/packages/astro/env.d.ts b/packages/astro/env.d.ts index 5f29733441a..c3d2cba37e6 100644 --- a/packages/astro/env.d.ts +++ b/packages/astro/env.d.ts @@ -4,7 +4,9 @@ declare namespace App { authStatus: string; authMessage: string | null; authReason: string | null; - auth: () => import('@clerk/astro/server').GetAuthReturn; + auth: () => import('@clerk/astro/server').GetAuthReturn & { + redirectToSignIn: import('@clerk/backend/internal').RedirectFun; + }; currentUser: () => Promise; } } diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index c7b9224ebed..4f3cf9c4579 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -1,5 +1,11 @@ import type { ClerkClient } from '@clerk/backend'; -import type { AuthenticateRequestOptions, AuthObject, ClerkRequest, RequestState } from '@clerk/backend/internal'; +import type { + AuthenticateRequestOptions, + AuthObject, + ClerkRequest, + RedirectFun, + RequestState, +} from '@clerk/backend/internal'; import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; import { handleValueOrFn, isDevelopmentFromSecretKey, isHttpOrHttps } from '@clerk/shared'; import { eventMethodCalled } from '@clerk/shared/telemetry'; @@ -87,7 +93,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { redirectToSignIn }); - decorateAstroLocal(context.request, context, requestState); + decorateAstroLocal(clerkRequest, context, requestState); /** * ALS is crucial for guaranteeing SSR in UI frameworks like React. @@ -218,14 +224,38 @@ Check if signInUrl is missing from your configuration or if it is not an absolut PUBLIC_CLERK_SIGN_IN_URL='SOME_URL' PUBLIC_CLERK_IS_SATELLITE='true'`; -function decorateAstroLocal(req: Request, context: APIContext, requestState: RequestState) { +function decorateAstroLocal(clerkRequest: ClerkRequest, context: APIContext, requestState: RequestState) { const { reason, message, status, token } = requestState; context.locals.authToken = token; context.locals.authStatus = status; context.locals.authMessage = message; context.locals.authReason = reason; - context.locals.auth = () => getAuth(req, context.locals); - context.locals.currentUser = createCurrentUser(req, context); + context.locals.auth = () => { + const authObject = getAuth(clerkRequest, context.locals); + + const clerkUrl = clerkRequest.clerkUrl; + + const redirectToSignIn: RedirectFun = (opts = {}) => { + const devBrowserToken = + clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) || + clerkRequest.cookies.get(constants.Cookies.DevBrowser); + + return createRedirect({ + redirectAdapter, + devBrowserToken: devBrowserToken, + baseUrl: clerkUrl.toString(), + publishableKey: getSafeEnv(context).pk!, + signInUrl: requestState.signInUrl, + signUpUrl: requestState.signUpUrl, + }).redirectToSignIn({ + returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl.toString(), + }); + }; + + return Object.assign(authObject, { redirectToSignIn }); + }; + + context.locals.currentUser = createCurrentUser(clerkRequest, context); } /** diff --git a/packages/astro/src/server/get-auth.ts b/packages/astro/src/server/get-auth.ts index 49d66936fe5..e269b6ba185 100644 --- a/packages/astro/src/server/get-auth.ts +++ b/packages/astro/src/server/get-auth.ts @@ -1,29 +1,14 @@ -import { - type AuthObject, - AuthStatus, - type SignedInAuthObject, - signedInAuthObject, - type SignedOutAuthObject, - signedOutAuthObject, -} from '@clerk/backend/internal'; +import { type AuthObject, AuthStatus, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { APIContext } from 'astro'; import { getSafeEnv } from './get-safe-env'; import { getAuthKeyFromRequest } from './utils'; -type AuthObjectWithoutResources = Omit; - -export type GetAuthReturn = - | AuthObjectWithoutResources - | AuthObjectWithoutResources; +export type GetAuthReturn = AuthObject; export const createGetAuth = ({ noAuthStatusMessage }: { noAuthStatusMessage: string }) => { - return ( - req: Request, - locals: APIContext['locals'], - opts?: { secretKey?: string }, - ): AuthObjectWithoutResources | AuthObjectWithoutResources => { + return (req: Request, locals: APIContext['locals'], opts?: { secretKey?: string }): GetAuthReturn => { // When the auth status is set, we trust that the middleware has already run // Then, we don't have to re-verify the JWT here, // we can just strip out the claims manually. @@ -62,5 +47,5 @@ const authAuthHeaderMissing = (helperName = 'auth') => `; export const getAuth = createGetAuth({ - noAuthStatusMessage: authAuthHeaderMissing('getAuth'), + noAuthStatusMessage: authAuthHeaderMissing(), }); From b6633c5664f287a42855670db20e6959b187ebd4 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 15 Jul 2024 15:01:50 +0300 Subject: [PATCH 2/2] chore(clerk-js): Add changeset --- .changeset/hot-pets-mix.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/hot-pets-mix.md diff --git a/.changeset/hot-pets-mix.md b/.changeset/hot-pets-mix.md new file mode 100644 index 00000000000..1719dd46f6a --- /dev/null +++ b/.changeset/hot-pets-mix.md @@ -0,0 +1,6 @@ +--- +"@clerk/astro": patch +--- + +Support `Astro.locals.auth().redirectToSignIn()` +This allows for redirectingToSignIn at the page level