Skip to content

Commit

Permalink
Merge branch 'main' into renovate/react-router-dev-minor
Browse files Browse the repository at this point in the history
  • Loading branch information
LekoArts authored Jan 27, 2025
2 parents 1d76175 + 796adec commit 8bb1c67
Show file tree
Hide file tree
Showing 17 changed files with 214 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .changeset/fuzzy-rockets-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Improve error messages when `clerkMiddleware` is missing by suggesting the correct path to place the `middleware.ts` file.
5 changes: 5 additions & 0 deletions .changeset/thirty-badgers-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Navigate to specific instance instead of /last-active for configuration after keyless.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@testing-library/user-event": "^14.5.2",
"@types/cross-spawn": "^6.0.3",
"@types/jest": "^29.3.1",
"@types/node": "^22.10.7",
"@types/node": "^22.10.8",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@vitest/coverage-v8": "3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.7KB" }
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }
]
}
105 changes: 77 additions & 28 deletions packages/clerk-js/src/ui/components/KeylessPrompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,60 @@ const buttonIdentifierPrefix = `--clerk-keyless-prompt`;
const buttonIdentifier = `${buttonIdentifierPrefix}-button`;
const contentIdentifier = `${buttonIdentifierPrefix}-content`;

const baseElementStyles = css`
box-sizing: border-box;
padding: 0;
margin: 0;
background: none;
border: none;
line-height: 1.5;
font-family:
-apple-system,
BlinkMacSystemFont,
avenir next,
avenir,
segoe ui,
helvetica neue,
helvetica,
Cantarell,
Ubuntu,
roboto,
noto,
arial,
sans-serif;
text-decoration: none;
`;

/**
* If we cannot reconstruct the url properly, then simply fallback to Clerk Dashboard
*/
function withLastActiveFallback(cb: () => string): string {
try {
return cb();
} catch {
return 'https://dashboard.clerk.com/last-active';
}
}

function handleDashboardUrlParsing(url: string) {
// make sure this is a valid url
const __url = new URL(url);
const regex = /^https?:\/\/(.*?)\/apps\/app_(.+?)\/instances\/ins_(.+?)(?:\/.*)?$/;

const match = __url.href.match(regex);

if (!match) {
throw new Error('invalid value dashboard url structure');
}

// Extracting base domain, app ID with prefix, and instanceId with prefix
return {
baseDomain: `https://${match[1]}`,
appId: `app_${match[2]}`,
instanceId: `ins_${match[3]}`,
};
}

const _KeylessPrompt = (_props: KeylessPromptProps) => {
const { isSignedIn } = useUser();
const [isExpanded, setIsExpanded] = useState(false);
Expand All @@ -38,8 +92,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
const appName = environment.displayConfig.applicationName;

const isForcedExpanded = claimed || success || isExpanded;

const urlToDashboard = useMemo(() => {
const claimUrlToDashboard = useMemo(() => {
if (claimed) {
return _props.copyKeysUrl;
}
Expand All @@ -50,29 +103,23 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
return url.href;
}, [claimed, _props.copyKeysUrl, _props.claimUrl]);

const baseElementStyles = css`
box-sizing: border-box;
padding: 0;
margin: 0;
background: none;
border: none;
line-height: 1.5;
font-family:
-apple-system,
BlinkMacSystemFont,
avenir next,
avenir,
segoe ui,
helvetica neue,
helvetica,
Cantarell,
Ubuntu,
roboto,
noto,
arial,
sans-serif;
text-decoration: none;
`;
const instanceUrlToDashboard = useMemo(() => {
return withLastActiveFallback(() => {
const redirectUrlParts = handleDashboardUrlParsing(_props.copyKeysUrl);
const url = new URL(
`${redirectUrlParts.baseDomain}/apps/${redirectUrlParts.appId}/instances/${redirectUrlParts.instanceId}/user-authentication/email-phone-username`,
);
return url.href;
});
}, [_props.copyKeysUrl]);

const getKeysUrlFromLastActive = useMemo(() => {
return withLastActiveFallback(() => {
const redirectUrlParts = handleDashboardUrlParsing(_props.copyKeysUrl);
const url = new URL(`${redirectUrlParts.baseDomain}/last-active?path=api-keys`);
return url.href;
});
}, [_props.copyKeysUrl]);

const mainCTAStyles = css`
${baseElementStyles};
Expand Down Expand Up @@ -375,7 +422,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
<Link
isExternal
aria-label='Go to Dashboard to configure settings'
href='https://dashboard.clerk.com/last-active?path=user-authentication/email-phone-username'
href={instanceUrlToDashboard}
sx={t => ({
color: t.colors.$whiteAlpha600,
textDecoration: 'underline solid',
Expand Down Expand Up @@ -413,6 +460,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
}}
css={css`
${mainCTAStyles};
&:hover {
background: #4b4b4b;
transition: all 120ms ease-in-out;
Expand All @@ -431,7 +479,7 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
})}
>
<a
href={urlToDashboard}
href={claimUrlToDashboard}
target='_blank'
rel='noopener noreferrer'
css={css`
Expand Down Expand Up @@ -481,14 +529,15 @@ const _KeylessPrompt = (_props: KeylessPromptProps) => {
/>

<a
href='https://dashboard.clerk.com/last-active?path=api-keys'
href={getKeysUrlFromLastActive}
target='_blank'
rel='noopener noreferrer'
css={css`
${baseElementStyles};
color: #ffffff9e;
font-size: 0.75rem;
transition: color 120ms ease-out;
:hover {
color: #ffffffcf;
text-decoration: none;
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^18.19.72",
"@types/node": "^18.19.73",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"autoprefixer": "^10.4.20",
Expand Down
10 changes: 1 addition & 9 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getDynamicAuthData } from '../../server/buildClerkProps';
import type { NextClerkProviderProps } from '../../types';
import { canUseKeyless } from '../../utils/feature-flags';
import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
import { onlyTry } from '../../utils/only-try';
import { isNext13 } from '../../utils/sdk-versions';
import { ClientClerkProvider } from '../client/ClerkProvider';
import { deleteKeylessAction } from '../keyless-actions';
Expand All @@ -23,15 +24,6 @@ const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader()
return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || '';
});

/** Discards errors thrown by attempted code */
const onlyTry = (cb: () => unknown) => {
try {
cb();
} catch {
// ignore
}
};

export async function ClerkProvider(
props: Without<NextClerkProviderProps, '__unstable_invokeMiddlewareOnAuthStateChange'>,
) {
Expand Down
22 changes: 19 additions & 3 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { constants, createClerkRequest, createRedirect, type RedirectFun } from
import { notFound, redirect } from 'next/navigation';

import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants';
import { createGetAuth } from '../../server/createGetAuth';
import { createAsyncGetAuth } from '../../server/createGetAuth';
import { authAuthHeaderMissing } from '../../server/errors';
import { getAuthKeyFromRequest, getHeader } from '../../server/headers-utils';
import type { AuthProtect } from '../../server/protect';
import { createProtect } from '../../server/protect';
import { decryptClerkRequestData } from '../../server/utils';
import { isNextWithUnstableServerActions } from '../../utils/sdk-versions';
import { buildRequestLike } from './utils';

/**
Expand All @@ -25,8 +26,10 @@ type Auth = AuthObject & {
*/
redirectToSignIn: RedirectFun<ReturnType<typeof redirect>>;
};

export interface AuthFn {
(): Promise<Auth>;

/**
* `auth` includes a single property, the `protect()` method, which you can use in two ways:
* - to check if a user is authenticated (signed in)
Expand Down Expand Up @@ -60,9 +63,22 @@ export const auth: AuthFn = async () => {
require('server-only');

const request = await buildRequestLike();
const authObject = createGetAuth({

const stepsBasedOnSrcDirectory = async () => {
if (isNextWithUnstableServerActions) {
return [];
}

try {
const isSrcAppDir = await import('../../server/keyless-node.js').then(m => m.hasSrcAppDir());
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.ts`];
} catch {
return [];
}
};
const authObject = await createAsyncGetAuth({
debugLoggerName: 'auth()',
noAuthStatusMessage: authAuthHeaderMissing(),
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory()),
})(request);

const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl');
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/app-router/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function buildRequestLike(): Promise<NextRequest> {
}

throw new Error(
`Clerk: auth() and currentUser() are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`,
`Clerk: auth(), currentUser() and clerkClient(), are only supported in App Router (/app directory).\nIf you're using /pages, try getAuth() instead.\nOriginal error: ${e}`,
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/server/__tests__/createGetAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import hmacSHA1 from 'crypto-js/hmac-sha1';
import { NextRequest } from 'next/server';
import { describe, expect, it } from 'vitest';

import { createGetAuth, getAuth } from '../createGetAuth';
import { createSyncGetAuth, getAuth } from '../createGetAuth';

const mockSecretKey = 'sk_test_mock';

Expand All @@ -16,7 +16,7 @@ const mockTokenSignature = hmacSHA1(mockToken, 'sk_test_mock').toString();

describe('createGetAuth(opts)', () => {
it('returns a getAuth function', () => {
expect(createGetAuth({ debugLoggerName: 'test', noAuthStatusMessage: 'test' })).toBeInstanceOf(Function);
expect(createSyncGetAuth({ debugLoggerName: 'test', noAuthStatusMessage: 'test' })).toBeInstanceOf(Function);
});
});

Expand Down
46 changes: 40 additions & 6 deletions packages/nextjs/src/server/createGetAuth.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
import type { AuthObject } from '@clerk/backend';
import { constants } from '@clerk/backend/internal';
import { isTruthy } from '@clerk/shared/underscore';

import { withLogger } from '../utils/debugLogger';
import { isNextWithUnstableServerActions } from '../utils/sdk-versions';
import { getAuthDataFromRequest } from './data/getAuthDataFromRequest';
import { getAuthAuthHeaderMissing } from './errors';
import { getHeader } from './headers-utils';
import { detectClerkMiddleware, getHeader } from './headers-utils';
import type { RequestLike } from './types';
import { assertAuthStatus } from './utils';

export const createGetAuth = ({
export const createAsyncGetAuth = ({
debugLoggerName,
noAuthStatusMessage,
}: {
debugLoggerName: string;
noAuthStatusMessage: string;
}) =>
withLogger(debugLoggerName, logger => {
return async (req: RequestLike, opts?: { secretKey?: string }) => {
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
logger.enable();
}

if (!detectClerkMiddleware(req)) {
// Keep the same behaviour for versions that may have issues with bundling `node:fs`
if (isNextWithUnstableServerActions) {
assertAuthStatus(req, noAuthStatusMessage);
}

const missConfiguredMiddlewareLocation = await import('./keyless-node.js')
.then(m => m.suggestMiddlewareLocation())
.catch(() => undefined);

if (missConfiguredMiddlewareLocation) {
throw new Error(missConfiguredMiddlewareLocation);
}

// still throw there is no suggested move location
assertAuthStatus(req, noAuthStatusMessage);
}

return getAuthDataFromRequest(req, { ...opts, logger });
};
});

export const createSyncGetAuth = ({
debugLoggerName,
noAuthStatusMessage,
}: {
debugLoggerName: string;
noAuthStatusMessage: string;
}) =>
withLogger(debugLoggerName, logger => {
return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => {
return (req: RequestLike, opts?: { secretKey?: string }) => {
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
logger.enable();
}

assertAuthStatus(req, noAuthStatusMessage);

return getAuthDataFromRequest(req, { ...opts, logger });
};
});
Expand Down Expand Up @@ -107,7 +141,7 @@ export const createGetAuth = ({
* }
* ```
*/
export const getAuth = createGetAuth({
export const getAuth = createSyncGetAuth({
debugLoggerName: 'getAuth()',
noAuthStatusMessage: getAuthAuthHeaderMissing(),
});
4 changes: 2 additions & 2 deletions packages/nextjs/src/server/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ Check if signInUrl is missing from your configuration or if it is not an absolut

export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth');

export const authAuthHeaderMissing = (helperName = 'auth') =>
export const authAuthHeaderMissing = (helperName = 'auth', prefixSteps?: string[]) =>
`Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following:
- clerkMiddleware() is used in your Next.js Middleware.
- ${prefixSteps ? [...prefixSteps, ''].join('\n- ') : ' '}clerkMiddleware() is used in your Next.js Middleware.
- Your Middleware matcher is configured to match this route or page.
- If you are using the src directory, make sure the Middleware file is inside of it.
Expand Down
Loading

0 comments on commit 8bb1c67

Please sign in to comment.