Skip to content

Commit 2d42f44

Browse files
madster456N2D4
andauthored
Now allows user to update primary_email_auth_enabled to false via API (#697)
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Allows `primary_email_auth_enabled` to be set to `false` via API, fixing previous issue, with tests verifying behavior. > > - **Behavior**: > - Allows `primary_email_auth_enabled` to be set to `false` in `crud.tsx` by using nullish coalescing operator. > - Updates `usedForAuth` field in `contactChannel` when `primary_email_auth_enabled` changes without email change. > - **Tests**: > - Adds tests in `users.test.ts` to verify disabling and re-enabling `primary_email_auth_enabled`. > - Tests cover both specific user updates and current user updates via `/me` endpoint. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for b4b5354. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN --> --------- Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
1 parent d100033 commit 2d42f44

2 files changed

Lines changed: 145 additions & 2 deletions

File tree

apps/backend/src/app/api/latest/users/crud.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
722722
const passwordAuth = oldUser.authMethods.find((m) => m.passwordAuthMethod)?.passwordAuthMethod;
723723
const passkeyAuth = oldUser.authMethods.find((m) => m.passkeyAuthMethod)?.passkeyAuthMethod;
724724

725-
const primaryEmailAuthEnabled = data.primary_email_auth_enabled || !!primaryEmailContactChannel?.usedForAuth;
725+
const primaryEmailAuthEnabled = data.primary_email_auth_enabled ?? !!primaryEmailContactChannel?.usedForAuth;
726726
const primaryEmailVerified = data.primary_email_verified || !!primaryEmailContactChannel?.isVerified;
727727
await checkAuthData(tx, {
728728
tenancyId: auth.tenancy.id,
@@ -755,7 +755,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
755755
tenancyId_projectUserId_type_isPrimary: {
756756
tenancyId: auth.tenancy.id,
757757
projectUserId: params.user_id,
758-
type: 'EMAIL',
758+
type: 'EMAIL' as const,
759759
isPrimary: "TRUE",
760760
},
761761
},
@@ -794,6 +794,24 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
794794
});
795795
}
796796

797+
// if primary_email_auth_enabled is being updated without changing the email
798+
// - update the primary email contact channel's usedForAuth field
799+
if (data.primary_email_auth_enabled !== undefined && data.primary_email === undefined) {
800+
await tx.contactChannel.update({
801+
where: {
802+
tenancyId_projectUserId_type_isPrimary: {
803+
tenancyId: auth.tenancy.id,
804+
projectUserId: params.user_id,
805+
type: 'EMAIL',
806+
isPrimary: "TRUE",
807+
},
808+
},
809+
data: {
810+
usedForAuth: primaryEmailAuthEnabled ? BooleanTrue.TRUE : null,
811+
},
812+
});
813+
}
814+
797815
// if otp_auth_enabled is true
798816
// - create a new otp auth method if it doesn't exist
799817
// if otp_auth_enabled is false

apps/e2e/tests/backend/endpoints/api/v1/users.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,131 @@ describe("with server access", () => {
22642264
}
22652265
`);
22662266
});
2267+
2268+
it("should be able to properly disable primary_email_auth_enabled", async ({ expect }) => {
2269+
// Test for the fix where primary_email_auth_enabled couldn't be disabled properly
2270+
// due to an OR operator issue that always kept it true
2271+
2272+
// Create a user with email auth enabled
2273+
const response1 = await niceBackendFetch("/api/v1/users", {
2274+
accessType: "server",
2275+
method: "POST",
2276+
body: {
2277+
primary_email: backendContext.value.mailbox.emailAddress,
2278+
primary_email_auth_enabled: true,
2279+
display_name: "Test User",
2280+
},
2281+
});
2282+
expect(response1.status).toEqual(201);
2283+
expect(response1.body.primary_email_auth_enabled).toEqual(true);
2284+
const userId = response1.body.id;
2285+
2286+
// Update the user to disable email auth
2287+
const response2 = await niceBackendFetch("/api/v1/users/" + userId, {
2288+
accessType: "server",
2289+
method: "PATCH",
2290+
body: {
2291+
primary_email_auth_enabled: false,
2292+
},
2293+
});
2294+
expect(response2).toMatchInlineSnapshot(`
2295+
NiceResponse {
2296+
"status": 200,
2297+
"body": {
2298+
"auth_with_email": false,
2299+
"client_metadata": null,
2300+
"client_read_only_metadata": null,
2301+
"display_name": "Test User",
2302+
"has_password": false,
2303+
"id": "<stripped UUID>",
2304+
"is_anonymous": false,
2305+
"last_active_at_millis": <stripped field 'last_active_at_millis'>,
2306+
"oauth_providers": [],
2307+
"otp_auth_enabled": false,
2308+
"passkey_auth_enabled": false,
2309+
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
2310+
"primary_email_auth_enabled": false,
2311+
"primary_email_verified": false,
2312+
"profile_image_url": null,
2313+
"requires_totp_mfa": false,
2314+
"selected_team": null,
2315+
"selected_team_id": null,
2316+
"server_metadata": null,
2317+
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
2318+
},
2319+
"headers": Headers { <some fields may have been hidden> },
2320+
}
2321+
`);
2322+
2323+
// Verify the change persisted by reading the user again
2324+
const response3 = await niceBackendFetch("/api/v1/users/" + userId, {
2325+
accessType: "server",
2326+
});
2327+
expect(response3.status).toEqual(200);
2328+
expect(response3.body.primary_email_auth_enabled).toEqual(false);
2329+
expect(response3.body.auth_with_email).toEqual(false);
2330+
2331+
// Test that we can re-enable it
2332+
const response4 = await niceBackendFetch("/api/v1/users/" + userId, {
2333+
accessType: "server",
2334+
method: "PATCH",
2335+
body: {
2336+
primary_email_auth_enabled: true,
2337+
},
2338+
});
2339+
expect(response4.status).toEqual(200);
2340+
expect(response4.body.primary_email_auth_enabled).toEqual(true);
2341+
expect(response4.body.auth_with_email).toEqual(false); // Still false because no password/otp is set
2342+
2343+
// Verify re-enabling persisted
2344+
const response5 = await niceBackendFetch("/api/v1/users/" + userId, {
2345+
accessType: "server",
2346+
});
2347+
expect(response5.status).toEqual(200);
2348+
expect(response5.body.primary_email_auth_enabled).toEqual(true);
2349+
});
2350+
2351+
it("should be able to disable primary_email_auth_enabled on current user", async ({ expect }) => {
2352+
// Test the same functionality when updating the current user via /me endpoint
2353+
await Auth.Otp.signIn();
2354+
2355+
// First verify the user has email auth enabled (from OTP sign in)
2356+
const initialResponse = await niceBackendFetch("/api/v1/users/me", {
2357+
accessType: "server",
2358+
});
2359+
expect(initialResponse.status).toEqual(200);
2360+
expect(initialResponse.body.primary_email_auth_enabled).toEqual(true);
2361+
2362+
// Disable email auth on current user
2363+
const response1 = await niceBackendFetch("/api/v1/users/me", {
2364+
accessType: "server",
2365+
method: "PATCH",
2366+
body: {
2367+
primary_email_auth_enabled: false,
2368+
},
2369+
});
2370+
expect(response1.status).toEqual(200);
2371+
expect(response1.body.primary_email_auth_enabled).toEqual(false);
2372+
expect(response1.body.auth_with_email).toEqual(true); // May still be true due to existing auth methods
2373+
2374+
// Verify the change persisted by reading the user again
2375+
const response2 = await niceBackendFetch("/api/v1/users/me", {
2376+
accessType: "server",
2377+
});
2378+
expect(response2.status).toEqual(200);
2379+
expect(response2.body.primary_email_auth_enabled).toEqual(false);
2380+
2381+
// Re-enable email auth
2382+
const response3 = await niceBackendFetch("/api/v1/users/me", {
2383+
accessType: "server",
2384+
method: "PATCH",
2385+
body: {
2386+
primary_email_auth_enabled: true,
2387+
},
2388+
});
2389+
expect(response3.status).toEqual(200);
2390+
expect(response3.body.primary_email_auth_enabled).toEqual(true);
2391+
});
22672392
});
22682393

22692394
it.todo("creating a new user with an OAuth provider ID that does not exist should fail");

0 commit comments

Comments
 (0)