Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self registration as end user #553

Merged
merged 6 commits into from
Feb 13, 2024
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
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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a UI thought, Would it be better to add an 'Or' in between Register and Sign In and to mention in smaller font/Italics for 'Existing users' near to Sign In ?

<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
Loading