Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ const withSessionTasks = base
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk)
.setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key');

const withSessionTasksResetPassword = base
.clone()
.setId('withSessionTasksResetPassword')
.setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev')
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-reset-password').sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-reset-password').pk);

const withBillingJwtV2 = base
.clone()
.setId('withBillingJwtV2')
Expand Down Expand Up @@ -217,6 +224,7 @@ export const envs = {
withRestrictedMode,
withReverification,
withSessionTasks,
withSessionTasksResetPassword,
withSignInOrUpEmailLinksFlow,
withSignInOrUpFlow,
withSignInOrUpwithRestrictedModeFlow,
Expand Down
11 changes: 6 additions & 5 deletions integration/presets/longRunningApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const createLongRunningApps = () => {
{ id: 'next.appRouter.withSignInOrUpFlow', config: next.appRouter, env: envs.withSignInOrUpFlow },
{ id: 'next.appRouter.withSignInOrUpEmailLinksFlow', config: next.appRouter, env: envs.withSignInOrUpEmailLinksFlow },
{ id: 'next.appRouter.withSessionTasks', config: next.appRouter, env: envs.withSessionTasks },
{ id: 'next.appRouter.withSessionTasksResetPassword', config: next.appRouter, env: envs.withSessionTasksResetPassword },
{ id: 'next.appRouter.withLegalConsent', config: next.appRouter, env: envs.withLegalConsent },

/**
* Quickstart apps
*/
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },

/**
/**
* Billing apps
*/
{ id: 'withBillingJwtV2.next.appRouter', config: next.appRouter, env: envs.withBillingJwtV2 },
Expand All @@ -59,14 +60,14 @@ export const createLongRunningApps = () => {
{ id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks },
{ id: 'vue.vite', config: vue.vite, env: envs.withCustomRoles },

/**
/**
* Tanstack apps - basic flows
*/
{ id: 'tanstack.react-start', config: tanstack.reactStart, env: envs.withEmailCodes },

/**
* Various apps - basic flows
*/
*/
{ id: 'withBilling.astro.node', config: astro.node, env: envs.withBilling },
{ id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
{ id: 'astro.static.withCustomRoles', config: astro.static, env: envs.withCustomRoles },
Expand All @@ -78,7 +79,7 @@ export const createLongRunningApps = () => {

const apps = configs.map(longRunningApplication);

return {
return {
getByPattern: (patterns: Array<string | (typeof configs)[number]['id']>) => {
const res = new Set(patterns.map(pattern => apps.filter(app => idMatchesPattern(app.id, pattern))).flat());
if (!res.size) {
Expand Down
9 changes: 9 additions & 0 deletions integration/testUtils/organizationsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type FakeOrganization = Pick<Organization, 'slug' | 'name'>;
export type OrganizationService = {
deleteAll: () => Promise<void>;
createFakeOrganization: () => FakeOrganization;
createBapiOrganization: (fakeOrganization: FakeOrganization & { createdBy: string }) => Promise<Organization>;
};

export const createOrganizationsService = (clerkClient: ClerkClient) => {
Expand All @@ -19,6 +20,14 @@ export const createOrganizationsService = (clerkClient: ClerkClient) => {
const bulkDeletionPromises = organizations.data.map(({ id }) => clerkClient.organizations.deleteOrganization(id));
await Promise.all(bulkDeletionPromises);
},
createBapiOrganization: async (fakeOrganization: FakeOrganization & { createdBy: string }) => {
const organization = await clerkClient.organizations.createOrganization({
name: fakeOrganization.name,
slug: fakeOrganization.slug,
createdBy: fakeOrganization.createdBy,
});
return organization;
},
};

return self;
Expand Down
4 changes: 4 additions & 0 deletions integration/testUtils/usersService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type UserService = {
createFakeOrganization: (userId: string) => Promise<FakeOrganization>;
getUser: (opts: { id?: string; email?: string }) => Promise<User | undefined>;
createFakeAPIKey: (userId: string) => Promise<FakeAPIKey>;
passwordUntrusted: (userId: string) => Promise<void>;
};

/**
Expand Down Expand Up @@ -210,6 +211,9 @@ export const createUserService = (clerkClient: ClerkClient) => {
revoke: () => clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }),
} satisfies FakeAPIKey;
},
passwordUntrusted: async (userId: string) => {
await clerkClient.users.__experimental_passwordUntrusted(userId);
},
};

return self;
Expand Down
99 changes: 99 additions & 0 deletions integration/tests/session-tasks-sign-in-reset-password.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { test } from '@playwright/test';

import { hash } from '../models/helpers';
import { appConfigs } from '../presets';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasksResetPassword] })(
'session tasks after sign-in reset password flow @nextjs',
({ app }) => {
test.describe.configure({ mode: 'parallel' });

test.afterAll(async () => {
await app.teardown();
});

test('resolve both reset password and organization selection tasks after sign-in', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

const user = u.services.users.createFakeUser();
const createdUser = await u.services.users.createBapiUser(user);

await u.services.users.passwordUntrusted(createdUser.id);

// Performs sign-in
await u.po.signIn.goTo();
await u.po.signIn.setIdentifier(user.email);
await u.po.signIn.continue();
await u.po.signIn.setPassword(user.password);
await u.po.signIn.continue();

await u.page.getByRole('textbox', { name: 'code' }).click();
await u.page.keyboard.type('424242', { delay: 100 });

// Redirects back to tasks when accessing protected route by `auth.protect`
await u.page.goToRelative('/page-protected');

const newPassword = `${hash()}_testtest`;
await u.po.sessionTask.resolveResetPasswordTask({
newPassword: newPassword,
confirmPassword: newPassword,
});

await u.po.sessionTask.resolveForceOrganizationSelectionTask({
name: 'Test Organization',
});

// Navigates to after sign-in
await u.page.waitForAppUrl('/page-protected');

await u.page.signOut();
await u.page.context().clearCookies();

await user.deleteIfExists();
await u.services.organizations.deleteAll();
});

test('sign-in with email and resolve the reset password task', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
const user = u.services.users.createFakeUser();
const createdUser = await u.services.users.createBapiUser(user);

await u.services.users.passwordUntrusted(createdUser.id);
const fakeOrganization = u.services.organizations.createFakeOrganization();
await u.services.organizations.createBapiOrganization({
...fakeOrganization,
createdBy: createdUser.id,
});

// Performs sign-in
await u.po.signIn.goTo();
await u.po.signIn.setIdentifier(user.email);
await u.po.signIn.continue();
await u.po.signIn.setPassword(user.password);
await u.po.signIn.continue();

await u.page.getByRole('textbox', { name: 'code' }).fill('424242');

await u.po.expect.toBeSignedIn();

// Redirects back to tasks when accessing protected route by `auth.protect`
await u.page.goToRelative('/page-protected');

const newPassword = `${hash()}_testtest`;
await u.po.sessionTask.resolveResetPasswordTask({
newPassword: newPassword,
confirmPassword: newPassword,
});

// Navigates to after sign-in
await u.page.waitForAppUrl('/page-protected');

await u.page.signOut();
await u.page.context().clearCookies();

await user.deleteIfExists();
await u.services.organizations.deleteAll();
});
},
);
11 changes: 11 additions & 0 deletions packages/backend/src/api/endpoints/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,15 @@ export class UserAPI extends AbstractAPI {
path: joinPaths(basePath, userId, 'totp'),
});
}

public async __experimental_passwordUntrusted(userId: string) {
this.requireId(userId);
return this.request<User>({
method: 'POST',
path: joinPaths(basePath, userId, 'password_untrusted'),
bodyParams: {
revokeAllSessions: false,
},
});
}
}
25 changes: 24 additions & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@
{ "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" },
{ "path": "./dist/query-core-vendors*.js", "maxSize": "11KB" },
{ "path": "./dist/zxcvbn-ts-core*.js", "maxSize": "12KB" },
{ "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" }
{ "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
{ "path": "./dist/organizationprofile*.js", "maxSize": "10KB" },
{ "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" },
{ "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" },
{ "path": "./dist/signin*.js", "maxSize": "18KB" },
{ "path": "./dist/signup*.js", "maxSize": "9.5KB" },
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
{ "path": "./dist/waitlist*.js", "maxSize": "1.5KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.82KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/up-plans-page*.js", "maxSize": "1.0KB" },
{ "path": "./dist/op-plans-page*.js", "maxSize": "1.0KB" },
{ "path": "./dist/statement-page*.js", "maxSize": "1.0KB" },
{ "path": "./dist/payment-attempt-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/sessionTasks*.js", "maxSize": "3.0KB" }
]
}
3 changes: 2 additions & 1 deletion packages/clerk-js/src/test/fixture-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => {
Partial<UserJSON>,
'email_addresses' | 'phone_numbers' | 'external_accounts' | 'organization_memberships'
> & {
identifier?: string;
email_addresses?: Array<string | Partial<EmailAddressJSON>>;
phone_numbers?: Array<string | Partial<PhoneNumberJSON>>;
external_accounts?: Array<OAuthProvider | Partial<ExternalAccountJSON>>;
Expand All @@ -57,7 +58,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => {
first_name: 'FirstName',
last_name: 'LastName',
image_url: '',
identifier: 'email@test.com',
identifier: params.identifier || 'email@test.com',
user_id: '',
...params,
} as PublicUserDataJSON;
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,14 @@ export const arSA: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: undefined,
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/be-BY.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,14 @@ export const beBY: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: 'Вы ўжо з’яўляецеся членам гэтай арганізацыі.',
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/bg-BG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,14 @@ export const bgBG: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: 'Вие вече сте член на тази организация.',
captcha_invalid: undefined,
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/bn-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,14 @@ export const bnIN: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: '{{email}} ইতিমধ্যে সংগঠনের একজন সদস্য।',
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/ca-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,14 @@ export const caES: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: undefined,
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/cs-CZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,14 @@ export const csCZ: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: '{{email}} je již členem organizace.',
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/da-DK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,14 @@ export const daDK: LocalizationResource = {
actionText: undefined,
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: undefined,
captcha_invalid:
Expand Down
8 changes: 8 additions & 0 deletions packages/localizations/src/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,14 @@ export const deDE: LocalizationResource = {
actionText: 'Angemeldet als {{identifier}}',
},
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
title: undefined,
},
unstable__errors: {
already_a_member_in_organization: 'Sie sind bereits Mitglied in dieser Organisation.',
captcha_invalid:
Expand Down
Loading
Loading