From c8234862584ceeefda7c2ddf3fdc4479f16ad52d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 16 Oct 2026 21:41:47 +0000 Subject: [PATCH 1/2] fix(auth): password sign-up shows check-email, never expects a session (planning#2284) --- apps/blocks/e2e-live/constructive-graphql.ts | 7 +++-- .../standalone-session.test.ts | 15 +++------- .../console-runtime/standalone-session.ts | 24 +++++++++------- .../feature-packs/auth/auth-entry-panel.tsx | 2 +- packages/sheets/src/auth/auth-types.ts | 3 +- .../sheets/src/auth/hooks/use-register.ts | 28 ++++--------------- packages/sheets/src/auth/ui/register-form.tsx | 21 -------------- packages/sheets/src/auth/utils/schemas.ts | 1 - 8 files changed, 30 insertions(+), 71 deletions(-) diff --git a/apps/blocks/e2e-live/constructive-graphql.ts b/apps/blocks/e2e-live/constructive-graphql.ts index d6438e12..b4e62dc3 100644 --- a/apps/blocks/e2e-live/constructive-graphql.ts +++ b/apps/blocks/e2e-live/constructive-graphql.ts @@ -64,7 +64,7 @@ const SIGN_IN = /* GraphQL */ ` const SIGN_UP = /* GraphQL */ ` mutation ConsoleKitProofSignUp($input: SignUpInput!) { signUp(input: $input) { - result { id userId accessToken accessTokenExpiresAt } + clientMutationId } } `; @@ -183,9 +183,10 @@ export async function signUp( const data = await graphQL>( endpointUrl(tenant, 'auth'), SIGN_UP, - { input: { email: credentials.email, password: credentials.password, rememberMe: false } } + { input: { email: credentials.email, password: credentials.password } } ); - return sessionFrom(data, 'signUp'); + // sign_up never signs anyone in; the password just set does + return signIn(tenant, credentials); } export async function signOut(tenant: ProofTenant, token: string): Promise { diff --git a/apps/blocks/src/blocks/console-runtime/standalone-session.test.ts b/apps/blocks/src/blocks/console-runtime/standalone-session.test.ts index 66b14221..533add99 100644 --- a/apps/blocks/src/blocks/console-runtime/standalone-session.test.ts +++ b/apps/blocks/src/blocks/console-runtime/standalone-session.test.ts @@ -140,16 +140,7 @@ describe('database-scoped standalone session', () => { it('requests a distinct CSRF bootstrap for sign-up', async () => { const fetchImpl = vi.fn().mockResolvedValue(response({ - data: { - signUp: { - result: { - id: 'session-1', - userId: 'user-1', - accessToken: 'secret-token', - accessTokenExpiresAt: '2030-01-01T00:00:00.000Z' - } - } - } + data: { signUp: { clientMutationId: null } } })); const csrfTokenProvider = vi.fn().mockResolvedValue('signup-csrf-token'); const session = createDatabaseScopedStandaloneSession({ @@ -160,7 +151,9 @@ describe('database-scoped standalone session', () => { storage: { session: memoryStorage(), local: memoryStorage() } }); - await session.signUp({ email: 'person@example.com', password: 'password' }); + await expect(session.signUp({ email: 'person@example.com', password: 'password' })) + .resolves.toEqual({ status: 'check-email' }); + expect(session.getSnapshot().status).toBe('anonymous'); expect(csrfTokenProvider).toHaveBeenCalledWith(expect.objectContaining({ operation: 'signUp' diff --git a/apps/blocks/src/blocks/console-runtime/standalone-session.ts b/apps/blocks/src/blocks/console-runtime/standalone-session.ts index 545e418f..d4198f91 100644 --- a/apps/blocks/src/blocks/console-runtime/standalone-session.ts +++ b/apps/blocks/src/blocks/console-runtime/standalone-session.ts @@ -30,12 +30,7 @@ const SIGN_IN_MUTATION = /* GraphQL */ ` const SIGN_UP_MUTATION = /* GraphQL */ ` mutation ConsoleSignUp($input: SignUpInput!) { signUp(input: $input) { - result { - id - userId - accessToken - accessTokenExpiresAt - } + clientMutationId } } `; @@ -70,6 +65,10 @@ export type ConsoleAuthenticationOutcome = | Readonly<{ status: 'mfa-required'; challengeToken: string; + }> + | Readonly<{ + /** sign_up never signs anyone in; the tenant mails a sign-in link. */ + status: 'check-email'; }>; export type ConsoleAuthenticationOperation = 'signIn' | 'signUp'; @@ -577,8 +576,9 @@ export function createDatabaseScopedStandaloneSession( input: { email: credentials.email.trim(), password: credentials.password, - rememberMe: credentials.rememberMe === true, - credentialKind: 'bearer', + ...(field === 'signIn' + ? { rememberMe: credentials.rememberMe === true, credentialKind: 'bearer' } + : {}), ...(csrfToken ? { csrfToken } : {}) } }, @@ -593,8 +593,12 @@ export function createDatabaseScopedStandaloneSession( ) { throw supersededAuthenticationError(); } - const result = mutationRecord(payload, field); - if (field === 'signIn' && result?.mfaRequired === true) { + if (field === 'signUp') { + setAnonymous(); + return { status: 'check-email' }; + } + const result = mutationRecord(payload, 'signIn'); + if (result?.mfaRequired === true) { const challengeToken = nonEmptyString(result.mfaChallengeToken); if (!challengeToken) { throw new ConsoleSessionOperationError( diff --git a/apps/blocks/src/blocks/feature-packs/auth/auth-entry-panel.tsx b/apps/blocks/src/blocks/feature-packs/auth/auth-entry-panel.tsx index f04d3b28..ed69ae06 100644 --- a/apps/blocks/src/blocks/feature-packs/auth/auth-entry-panel.tsx +++ b/apps/blocks/src/blocks/feature-packs/auth/auth-entry-panel.tsx @@ -189,7 +189,7 @@ export function AuthEntryPanel({ password, rememberMe }); - onAuthenticated?.(); + setFeedback('Check your email for a link to confirm your address and sign in.'); } else if (mode === 'recover-password') { if (!actions?.recoverPassword) return; await actions.recoverPassword({ email: email.trim() }); diff --git a/packages/sheets/src/auth/auth-types.ts b/packages/sheets/src/auth/auth-types.ts index aa25c013..65f86396 100644 --- a/packages/sheets/src/auth/auth-types.ts +++ b/packages/sheets/src/auth/auth-types.ts @@ -43,14 +43,13 @@ export interface SignInMutationResult { export interface SignUpInput { email: string; password: string; - rememberMe?: boolean; } export type SignUpMutationVariables = MutationVariables; export interface SignUpMutationResult { signUp?: { - result?: AuthResultRecord | null; + clientMutationId?: string | null; } | null; } diff --git a/packages/sheets/src/auth/hooks/use-register.ts b/packages/sheets/src/auth/hooks/use-register.ts index 5f04faac..d16eb293 100644 --- a/packages/sheets/src/auth/hooks/use-register.ts +++ b/packages/sheets/src/auth/hooks/use-register.ts @@ -3,21 +3,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { RegisterFormData } from '../utils/schemas'; import { useSheetsContext } from '../../context/sheets-context'; import { useSheetsStoreApi } from '../../store/sheets-store'; -import { setStoredToken } from '../utils/token-store'; import { executeAuthMutation } from '../auth-execute'; -import { toAuthToken, type SignUpMutationResult, type SignUpMutationVariables } from '../auth-types'; +import { type SignUpMutationResult, type SignUpMutationVariables } from '../auth-types'; const SIGN_UP_MUTATION = /* GraphQL */ ` mutation SignUp($input: SignUpInput!) { signUp(input: $input) { - result { - id - userId - accessToken - accessTokenExpiresAt - isVerified - totpEnabled - } + clientMutationId } } `; @@ -30,29 +22,21 @@ export function useSheetsRegister() { return useMutation({ mutationKey: ['sheets', 'auth', 'signUp'], mutationFn: async (formData: RegisterFormData) => { - const result = await executeAuthMutation( + // sign_up never signs anyone in: the address is mailed a link that does + await executeAuthMutation( config, SIGN_UP_MUTATION, { input: { email: formData.email, password: formData.password, - rememberMe: formData.rememberMe ?? true, }, } satisfies SignUpMutationVariables, ); - const signUpResult = result.signUp?.result; - const token = signUpResult ? toAuthToken(signUpResult) : null; - - return { token, email: formData.email, identityKey: signUpResult?.userId ?? null }; + return { email: formData.email }; }, - onSuccess: ({ token, identityKey }) => { - if (token) { - const databaseId = config.databaseId || 'default'; - setStoredToken(databaseId, token.accessToken, token.expiresAt, identityKey); - storeApi.getState().setAuthenticated(token.accessToken, token.expiresAt, identityKey); - } + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sheets'] }); }, onError: () => { diff --git a/packages/sheets/src/auth/ui/register-form.tsx b/packages/sheets/src/auth/ui/register-form.tsx index b8f5467a..d8367727 100644 --- a/packages/sheets/src/auth/ui/register-form.tsx +++ b/packages/sheets/src/auth/ui/register-form.tsx @@ -5,7 +5,6 @@ import { parseGraphQLError } from '../utils/auth-errors'; import type { RegisterFormData } from '../utils/schemas'; import { registerSchema } from '../utils/schemas'; import { Button } from '@constructive-io/ui/button'; -import { Checkbox } from '@constructive-io/ui/checkbox'; import { AuthErrorAlert } from './auth-error-alert'; import { AuthLoadingButton } from './auth-loading-button'; @@ -27,7 +26,6 @@ export function SheetsRegisterForm({ onRegister, onShowLogin, defaultEmail = '' email: defaultEmail, password: '', confirmPassword: '', - rememberMe: true, }, onSubmit: async ({ value }) => { setError(null); @@ -89,25 +87,6 @@ export function SheetsRegisterForm({ onRegister, onShowLogin, defaultEmail = '' )} - - {(field) => ( -
- { - field.handleChange(checked === true); - }} - /> - -
- )} -
data.password === data.confirmPassword, { message: 'Passwords do not match', From b3cd735d993f8ae014aa6432c5104433fda8dd0d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 16 Oct 2026 22:07:58 +0000 Subject: [PATCH 2/2] fix(auth): sheets register shows check-email after sign-up --- apps/blocks/e2e-live/constructive-graphql.ts | 2 +- packages/sheets/src/auth/ui/register-form.tsx | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/blocks/e2e-live/constructive-graphql.ts b/apps/blocks/e2e-live/constructive-graphql.ts index b4e62dc3..5414672d 100644 --- a/apps/blocks/e2e-live/constructive-graphql.ts +++ b/apps/blocks/e2e-live/constructive-graphql.ts @@ -180,7 +180,7 @@ export async function signUp( tenant: ProofTenant, credentials: ProofCredentials ): Promise { - const data = await graphQL>( + await graphQL>( endpointUrl(tenant, 'auth'), SIGN_UP, { input: { email: credentials.email, password: credentials.password } } diff --git a/packages/sheets/src/auth/ui/register-form.tsx b/packages/sheets/src/auth/ui/register-form.tsx index d8367727..4cdd2f03 100644 --- a/packages/sheets/src/auth/ui/register-form.tsx +++ b/packages/sheets/src/auth/ui/register-form.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from 'react'; import { useForm } from '@tanstack/react-form'; +import { CheckCircleIcon } from 'lucide-react'; import { parseGraphQLError } from '../utils/auth-errors'; import type { RegisterFormData } from '../utils/schemas'; @@ -19,6 +20,7 @@ export interface SheetsRegisterFormProps { export function SheetsRegisterForm({ onRegister, onShowLogin, defaultEmail = '' }: SheetsRegisterFormProps) { const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); const passwordInputRef = useRef(null); const form = useForm({ @@ -34,6 +36,7 @@ export function SheetsRegisterForm({ onRegister, onShowLogin, defaultEmail = '' try { const validatedData = registerSchema.parse(value); await onRegister(validatedData); + setIsSuccess(true); } catch (err) { setError(parseGraphQLError(err).message); } finally { @@ -42,6 +45,24 @@ export function SheetsRegisterForm({ onRegister, onShowLogin, defaultEmail = '' }, }); + if (isSuccess) { + return ( +
+
+
+ +
+
+
+

Check your email

+

+ We've sent a link to confirm your address and sign in. +

+
+
+ ); + } + return (