Skip to content

Commit

Permalink
Merge pull request #553 from sanger/selfRegistration
Browse files Browse the repository at this point in the history
Self registration as end user
  • Loading branch information
seenanair authored Feb 13, 2024
2 parents 2f8f6d9 + c4cf96c commit ba5bbc2
Show file tree
Hide file tree
Showing 10 changed files with 1,664 additions and 1,434 deletions.
59 changes: 54 additions & 5 deletions cypress/e2e/pages/login.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ describe('Login', () => {
graphql.mutation('Login', () => {
return HttpResponse.json({
data: {
user: null
login: {
user: null
}
}
});
})
Expand All @@ -29,7 +31,7 @@ describe('Login', () => {

cy.get("input[name='username']").type('jb1');
cy.get("input[name='password']").type('supersecret');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();
});

it('shows an error message', () => {
Expand All @@ -41,7 +43,7 @@ describe('Login', () => {
beforeEach(() => {
cy.get("input[name='username']").type('jb1');
cy.get("input[name='password']").type('supersecret');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();
});

it('shows a success message', () => {
Expand All @@ -56,7 +58,7 @@ describe('Login', () => {
context('When username is missing', () => {
beforeEach(() => {
cy.get("input[name='password']").type('supersecret');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();
});

it('does not submit the form', () => {
Expand All @@ -67,7 +69,7 @@ describe('Login', () => {
describe('When password is missing', () => {
beforeEach(() => {
cy.get("input[name='username']").type('jb1');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();
});

it('does not submit the form', () => {
Expand All @@ -76,3 +78,50 @@ describe('Login', () => {
});
});
});

describe('Self Registration', () => {
describe('When Registration succeed', () => {
before(() => {
cy.visitAsGuest('/login');
register();
});
it('shows a success message', () => {
cy.findByText('Successfully registered as End User!').should('be.visible');
});
it('redirects to the Dashboard', () => {
cy.location('pathname').should('eq', '/');
});
});

describe('When Registration fails', () => {
before(() => {
cy.visitAsGuest('/login');
cy.msw().then(({ worker, graphql }) => {
worker.use(
graphql.mutation('RegisterAsEndUser', () => {
return HttpResponse.json({
data: {
registerAsEndUser: {
user: null
}
}
});
})
);
});
register();
});
it('shows an error message', () => {
cy.findByText('LDAP check failed for userx').should('be.visible');
});
it('remains on the login page', () => {
cy.location('pathname').should('eq', '/login');
});
});
});

const register = () => {
cy.findByTestId('username').type('userx');
cy.findByTestId('password').type('myPassword123');
cy.findByTestId('register').click();
};
4 changes: 2 additions & 2 deletions cypress/e2e/pages/workProgress.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,11 @@ describe('Work Progress', () => {
cy.url().should('be.equal', 'http://localhost:3000/login');
});
});
context('On succesful login, it redirects to SGP page', () => {
context('On successful login, it redirects to SGP page', () => {
before(() => {
cy.get("input[name='username']").type('jb1');
cy.get("input[name='password']").type('supersecret');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();
});
it('goes to Login page', () => {
cy.url().should('be.equal', 'http://localhost:3000/sgp');
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/shared/authRoutes.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Authorized routes', () => {
it('should redirect to /admin/registration after logging in', () => {
cy.get("input[name='username']").type('jb1');
cy.get("input[name='password']").type('supersecret');
cy.get("button[type='submit']").click();
cy.findByTestId('signIn').click();

cy.location().should((location) => {
expect(location.pathname).to.eq('/admin/registration');
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons/LoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { ButtonProps } from './Button';
import PinkButton from './PinkButton';

interface LoginButtonProps extends ButtonProps {}
export interface LoginButtonProps extends ButtonProps {}

const LoginButton = (props: LoginButtonProps) => {
return (
Expand Down
26 changes: 26 additions & 0 deletions src/components/buttons/RegisterButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { LoginButtonProps } from './LoginButton';
import WhiteButton from './WhiteButton';

const RegisterButton = (props: LoginButtonProps) => {
return (
<WhiteButton {...props} type="submit" style={{ width: '100%' }} className="relative">
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<svg
className="h-5 w-5 text-sdb group-hover:text-sdb-400 transition ease-in-out duration-150"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clipRule="evenodd"
/>
</svg>
</span>
{props.children}
</WhiteButton>
);
};

export default RegisterButton;
7 changes: 7 additions & 0 deletions src/graphql/mutations/RegisterAsEndUser.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mutation RegisterAsEndUser($username: String!, $password: String!) {
registerAsEndUser(username: $username, password: $password) {
user {
...UserFields
}
}
}
23 changes: 22 additions & 1 deletion src/mocks/handlers/userHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
LoginMutationVariables,
LogoutMutation,
LogoutMutationVariables,
RegisterAsEndUserMutation,
RegisterAsEndUserMutationVariables,
SetUserRoleMutation,
SetUserRoleMutationVariables,
UserRole
Expand Down Expand Up @@ -88,7 +90,26 @@ const userHandlers = [
{ status: 404 }
);
}
})
}),

graphql.mutation<RegisterAsEndUserMutation, RegisterAsEndUserMutationVariables>(
'RegisterAsEndUser',
({ variables }) => {
const { username } = variables;
sessionStorage.setItem(CURRENT_USER_KEY, username);
return HttpResponse.json({
data: {
registerAsEndUser: {
user: {
__typename: 'User',
username,
role: UserRole.Enduser
}
}
}
});
}
)
];

export default userHandlers;
89 changes: 62 additions & 27 deletions src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { motion } from 'framer-motion';
import { extractServerErrors } from '../types/stan';
import { StanCoreContext } from '../lib/sdk';
import { ClientError } from 'graphql-request';
import RegisterButton from '../components/buttons/RegisterButton';

/**
* Schema used by Formik in the login form.
*/
const LoginSchema = Yup.object().shape({
username: Yup.string().required('Username is required'),
password: Yup.string().required('Password is required')
password: Yup.string().required('Password is required'),
isSelfRegistration: Yup.boolean()
});

const Login = (): JSX.Element => {
Expand All @@ -31,12 +33,13 @@ const Login = (): JSX.Element => {
}, [auth, location]);

const stanCore = useContext(StanCoreContext);
const [showLoginSuccess, setShowLoginSuccess] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | undefined>(undefined);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const formInitialValues = {
username: '',
password: ''
password: '',
isSelfRegistration: false
};

const submitCredentials = async (
Expand All @@ -46,24 +49,39 @@ const Login = (): JSX.Element => {
try {
setErrorMessage(null);

const { login } = await stanCore.Login({
username: credentials.username,
password: credentials.password
});

if (!login?.user?.username) {
setErrorMessage('Username or password is incorrect');
const { user } = credentials.isSelfRegistration
? await stanCore
.RegisterAsEndUser({
username: credentials.username,
password: credentials.password
})
.then((response) => {
return response.registerAsEndUser;
})
: await stanCore
.Login({
username: credentials.username,
password: credentials.password
})
.then((response) => {
return response.login;
});

if (!user) {
setErrorMessage(
credentials.isSelfRegistration
? `LDAP check failed for ${credentials.username}`
: 'Username or password is incorrect'
);
formikHelpers.setSubmitting(false);
return;
}

setShowLoginSuccess(true);
const userInfo = login.user;

setSuccessMessage(credentials.isSelfRegistration ? 'Successfully registered as End User!' : 'Login Successful!');
// Allow some time for the user to see the success message before redirecting
setTimeout(() => {
auth.setAuthState({
user: userInfo
user: user!
});
formikHelpers.setSubmitting(false);
}, 1500);
Expand Down Expand Up @@ -94,38 +112,42 @@ const Login = (): JSX.Element => {
<h2 className="mt-6 text-center text-3xl leading-9 font-extrabold text-white">Sign in to STAN</h2>
</div>

{showLoginSuccess && <Success message={'Login Successful!'} className="mt-8" />}
{successMessage && <Success message={successMessage} className="mt-8" />}

{location.state?.success && !showLoginSuccess && errorMessage == null && (
{location.state?.success && !successMessage && errorMessage == null && (
<Success message={location.state.success} className="mt-8" />
)}

{location.state?.warning && !showLoginSuccess && errorMessage == null && (
{location.state?.warning && !successMessage && errorMessage == null && (
<Warning className="mt-8" message={location.state.warning} />
)}

{errorMessage && <Warning className="mt-8" message={errorMessage} />}

<Formik
initialValues={formInitialValues}
onSubmit={(values, formikHelpers) => {
submitCredentials(values, formikHelpers);
}}
onSubmit={(values, formikHelpers) => submitCredentials(values, formikHelpers)}
validationSchema={LoginSchema}
validateOnChange={false}
validateOnBlur={false}
>
{(formik) => (
<Form className="mt-8">
<ErrorMessage name="username" />
<ErrorMessage name="password" />
<ErrorMessage
component="div"
name="username"
className="items-start justify-between border-l-4 border-orange-600 p-2 bg-orange-200 text-orange-800"
/>
<ErrorMessage
component="div"
name="password"
className="items-start justify-between border-l-4 border-orange-600 p-2 bg-orange-200 text-orange-800"
/>
<div className="rounded-md shadow-sm">
<div>
<Field
data-testid="username"
aria-label="Sanger username"
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5"
placeholder="Sanger username"
/>
Expand All @@ -134,17 +156,30 @@ const Login = (): JSX.Element => {

<div className="-mt-px">
<Field
data-testid="password"
aria-label="Password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5"
placeholder="Password"
/>
</div>

<div className="mt-6">
<LoginButton loading={formik.isSubmitting}>Sign In</LoginButton>
<LoginButton loading={formik.isSubmitting} data-testid="signIn" disabled={formik.isSubmitting}>
Sign In <span className=" ml-2"> (Existing User)</span>
</LoginButton>
</div>
<div className="m-3 text-white text-center">OR</div>
<div className="">
<RegisterButton
disabled={formik.isSubmitting}
data-testid="register"
loading={formik.isSubmitting}
onClick={() => formik.setFieldValue('isSelfRegistration', true)}
>
Register <span className=" ml-2"> (New User)</span>
</RegisterButton>
</div>
</Form>
)}
Expand Down
Loading

0 comments on commit ba5bbc2

Please sign in to comment.