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
84 changes: 84 additions & 0 deletions packages/shared/src/components/auth/RegistrationForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SettingsContext from '../../contexts/SettingsContext';
import { mockGraphQL } from '../../../__tests__/helpers/graphql';
import { GET_USERNAME_SUGGESTION } from '../../graphql/users';
import { AuthTriggers } from '../../lib/auth';
import * as betterAuthHook from '../../hooks/useIsBetterAuth';
import type { AuthOptionsProps } from './common';

const user = null;
Expand All @@ -24,6 +25,7 @@ beforeEach(() => {
jest.clearAllMocks();
nock.cleanAll();
jest.clearAllMocks();
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(false);
});

const onSuccessfulLogin = jest.fn();
Expand Down Expand Up @@ -129,6 +131,55 @@ const renderLogin = async (email: string) => {
});
};

const renderBetterAuthRegistration = async (
email = 'sshanzel@yahoo.com',
name = 'Lee Solevilla',
username = 'leesolevilla',
) => {
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(true);
renderComponent();
await waitForNock();

nock(process.env.NEXT_PUBLIC_API_URL as string)
.get('/a/auth/check-email')
.query({ email })
.reply(200, { result: false });

fireEvent.input(screen.getByPlaceholderText('Email'), {
target: { value: email },
});
fireEvent.click(await screen.findByTestId('email_signup_submit'));
await waitForNock();

let queryCalled = false;
mockGraphQL({
request: {
query: GET_USERNAME_SUGGESTION,
variables: { name },
},
result: () => {
queryCalled = true;
return { data: { generateUniqueUsername: username } };
},
});

await screen.findByTestId('registration_form');
const nameInput = screen.getByPlaceholderText('Name');
fireEvent.input(screen.getByPlaceholderText('Enter a username'), {
target: { value: username },
});
fireEvent.input(screen.getByPlaceholderText('Name'), {
target: { value: name },
});
simulateTextboxInput(nameInput as HTMLTextAreaElement, name);
fireEvent.input(screen.getByPlaceholderText('Create a password'), {
target: { value: '#123xAbc' },
});

await waitForNock();
await waitFor(() => expect(queryCalled).toBeTruthy());
};

// NOTE: Chris turned this off needs a good re-look at
// it('should post registration', async () => {
// const email = 'sshanzel@yahoo.com';
Expand Down Expand Up @@ -171,6 +222,39 @@ it('should show login if email exists', async () => {
expect(text).toBeInTheDocument();
});

it('should show a generic sign up error when Better Auth sign up is privacy-protected', async () => {
const email = 'sshanzel@yahoo.com';
await renderBetterAuthRegistration(email);

nock(process.env.NEXT_PUBLIC_API_URL as string)
.post('/a/auth/sign-up/email', {
name: 'Lee Solevilla',
email,
password: '#123xAbc',
})
.reply(200, { status: true });
nock(process.env.NEXT_PUBLIC_API_URL as string)
.post('/a/auth/sign-in/email', {
email,
password: '#123xAbc',
})
.reply(401, {
code: 'INVALID_CREDENTIALS',
message: 'Invalid credentials',
});

fireEvent.submit(await screen.findByTestId('registration_form'));
await waitForNock();

await waitFor(() => {
expect(
screen.getByText(
"We couldn't complete sign up. If you already have an account, try signing in instead.",
),
).toBeInTheDocument();
});
});

describe('testing username auto generation', () => {
it('should suggest a valid option', async () => {
const email = 'sshanzel@yahoo.com';
Expand Down
66 changes: 50 additions & 16 deletions packages/shared/src/hooks/useRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
submitKratosFlow,
} from '../lib/kratos';
import {
type BetterAuthResponse,
betterAuthSignIn,
betterAuthSignUp,
betterAuthSendSignupVerification,
Expand Down Expand Up @@ -70,6 +71,28 @@ interface UseRegistration {
type FormParams = Omit<RegistrationParameters, 'csrf_token'>;

const EMAIL_EXISTS_ERROR_ID = KRATOS_ERROR.EXISTING_USER;
const BETTER_AUTH_SIGNUP_FALLBACK_ERROR =
"We couldn't complete sign up. If you already have an account, try signing in instead.";

const isBetterAuthVerificationRequired = (
response: BetterAuthResponse,
): boolean => {
if (response.code === '403') {
return true;
}

const error = response.error?.toLowerCase();

if (!error) {
return false;
}

return (
error.includes('verify') ||
error.includes('verification') ||
error.includes('unverified')
);
};

const useRegistration = ({
key,
Expand Down Expand Up @@ -243,28 +266,39 @@ const useRegistration = ({
},
onSuccess: async (res, params) => {
if (res.error) {
if (res.error.toLowerCase().includes('already')) {
const signInRes = await betterAuthSignIn({
email: params.email,
password: params.password,
});
if (signInRes.error) {
onInvalidRegistration?.({
'traits.email': signInRes.error,
});
return;
}
await refetchBoot();
return;
}
onInvalidRegistration?.({
'traits.email': res.error,
});
return;
}

await betterAuthSendSignupVerification();
onInitializeVerification?.();
const signInRes = await betterAuthSignIn({
email: params.email,
password: params.password,
});

if (!signInRes.error) {
await refetchBoot();
return;
}

if (isBetterAuthVerificationRequired(signInRes)) {
await betterAuthSendSignupVerification();
onInitializeVerification?.();
return;
}

const normalizedSignInError = signInRes.error.toLowerCase();
const signupError =
normalizedSignInError.includes('invalid') ||
normalizedSignInError.includes('credentials') ||
normalizedSignInError.includes('password')
? BETTER_AUTH_SIGNUP_FALLBACK_ERROR
: signInRes.error;

onInvalidRegistration?.({
'traits.email': signupError,
});
},
});

Expand Down
7 changes: 5 additions & 2 deletions packages/shared/src/lib/betterAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ export type BetterAuthResponse = {
error?: string;
code?: string;
message?: string;
status?: boolean;
user?: {
id: string;
name: string;
email: string;
};
};

type BetterAuthResult<T = Record<string, unknown>> = T & { error?: string };
type BetterAuthResult<T = Record<string, unknown>> = T &
Pick<BetterAuthResponse, 'error' | 'code' | 'message' | 'status'>;

const betterAuthPost = async <T = Record<string, unknown>>(
path: string,
Expand All @@ -27,8 +29,9 @@ const betterAuthPost = async <T = Record<string, unknown>>(

if (!res.ok) {
try {
const data = await res.json();
const data = (await res.json()) as BetterAuthResult<T>;
return {
...data,
error: data?.message || data?.error || data?.code || fallbackError,
} as BetterAuthResult<T>;
} catch {
Expand Down
43 changes: 43 additions & 0 deletions packages/webapp/__tests__/AccountSecurityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { waitForNock } from '@dailydotdev/shared/__tests__/helpers/utilities';
import { AuthContextProvider } from '@dailydotdev/shared/src/contexts/AuthContext';
import { getNodeValue } from '@dailydotdev/shared/src/lib/auth';
import * as betterAuthHook from '@dailydotdev/shared/src/hooks/useIsBetterAuth';
import * as toastNotificationHook from '@dailydotdev/shared/src/hooks/useToastNotification';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { LazyModalElement } from '@dailydotdev/shared/src/components/modals/LazyModalElement';
import nock from 'nock';
Expand Down Expand Up @@ -52,6 +54,10 @@ beforeEach(() => {
jest.clearAllMocks();
nock.cleanAll();
matchMedia('1020');
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(false);
jest
.spyOn(toastNotificationHook, 'useToastNotification')
.mockReturnValue({ displayToast } as never);
});

const defaultLoggedUser: LoggedUser = {
Expand All @@ -65,6 +71,7 @@ const defaultLoggedUser: LoggedUser = {

const updateUser = jest.fn();
const refetchBoot = jest.fn();
const displayToast = jest.fn();

const waitAllRenderMocks = async () => {
await waitForNock();
Expand Down Expand Up @@ -233,6 +240,42 @@ it('should allow changing of email but require verification', async () => {
expect(sent).toBeInTheDocument();
});

it('should show generic change email confirmation for Better Auth', async () => {
jest.spyOn(betterAuthHook, 'useIsBetterAuth').mockReturnValue(true);
const email = 'sample@email.com';
nock(process.env.NEXT_PUBLIC_API_URL as string)
.get('/a/auth/list-accounts')
.reply(200, [{ providerId: 'credential' }]);
const changeEmailScope = nock(process.env.NEXT_PUBLIC_API_URL as string)
.post('/a/auth/change-email', { newEmail: email })
.reply(200, { status: true });

renderComponent();
await waitForNock();

fireEvent.click(await screen.findByText('Change email'));
fireEvent.input(screen.getByPlaceholderText('Email'), {
target: { value: email },
});

const sendCodeButton = await screen.findByText('Send code');
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(submitEvent, 'submitter', {
value: sendCodeButton,
});

await act(() => sendCodeButton.dispatchEvent(submitEvent));
await waitForNock();

expect(changeEmailScope.isDone()).toBeTruthy();
expect(displayToast).toHaveBeenCalledWith(
'If that email is available, we sent a verification code.',
);
});

it('should allow setting new password', async () => {
renderComponent();
await waitAllRenderMocks();
Expand Down
7 changes: 7 additions & 0 deletions packages/webapp/pages/settings/security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const seo: NextSeoProps = {
title: getTemplatedTitle('Manage account security'),
};

const BETTER_AUTH_CHANGE_EMAIL_MESSAGE =
'If that email is available, we sent a verification code.';

const AccountSecurityPage = (): ReactElement => {
const updatePasswordRef = useRef<HTMLFormElement>();
const { user, refetchBoot } = useAuthContext();
Expand Down Expand Up @@ -198,6 +201,10 @@ const AccountSecurityPage = (): ReactElement => {
const result = await betterAuthChangeEmail(email);
if (result.error) {
setHint(result.error);
return;
}
if (result.status) {
displayToast(BETTER_AUTH_CHANGE_EMAIL_MESSAGE);
}
return;
}
Expand Down