Skip to content

Commit

Permalink
feat: add db login
Browse files Browse the repository at this point in the history
  • Loading branch information
jrea committed May 4, 2023
1 parent 04ae56e commit 399e910
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 20 deletions.
90 changes: 90 additions & 0 deletions packages/react/src/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { useMutation } from '@tanstack/react-query';
import Cookies from 'js-cookie';

import { Attribute } from '../lib/SimpleForm/types';
import { useNileConfig } from '../context';
import SimpleForm from '../lib/SimpleForm';
import { AttributeType } from '../lib/SimpleForm/types';

import { Props, AllowedAny } from './types';

export default function LoginForm(props: Props) {
const { workspace, database, basePath, allowClientCookies } = useNileConfig();

const { attributes, onSuccess, onError, beforeMutate } = props;
const fetchPath = `${basePath}/workspaces/${workspace}/databases/${database}/users/login`;

const handleMutate =
typeof beforeMutate === 'function'
? beforeMutate
: (data: AllowedAny): AllowedAny => data;

const mutation = useMutation(
async (data: { email: string; password: string }) => {
const _data = handleMutate(data);
const res = await fetch(fetchPath, {
method: 'POST',
body: JSON.stringify(_data),
headers: {
'content-type': 'application/json',
},
}).catch((e) => e);
if (res.ok === false) {
throw new Error(res.status);
}
try {
return await res.json();
} catch (e) {
return e;
}
},
{
onSuccess: (token, data) => {
if (token) {
if (allowClientCookies) {
Cookies.set('token', token.token, {
'max-age': token.maxAge,
});
}

onSuccess && onSuccess(token, data);
}
},
onError: (error, data) => {
onError && onError(error as Error, data);
},
}
);

const completeAttributes = React.useMemo(() => {
const mainAttributes: Attribute[] = [
{
name: 'email',
label: 'Email',
type: AttributeType.Text,
defaultValue: '',
required: true,
},
{
name: 'password',
label: 'Password',
type: AttributeType.Password,
defaultValue: '',
required: true,
},
];
if (attributes && attributes.length > 0) {
return mainAttributes.concat(attributes);
}
return mainAttributes;
}, [attributes]);

return (
<SimpleForm
mutation={mutation}
buttonText="Log in"
attributes={completeAttributes}
/>
);
}
1 change: 1 addition & 0 deletions packages/react/src/LoginForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LoginForm';
18 changes: 18 additions & 0 deletions packages/react/src/LoginForm/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Token } from '@theniledev/js';

import { Attribute } from '../lib/SimpleForm/types';

type LoginSuccess = (
token: Token,
LoginInfo: { email: string; password: string }
) => void;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AllowedAny = any;

export interface Props {
beforeMutate?: (data: AllowedAny) => AllowedAny;
onSuccess: LoginSuccess;
onError?: (error: Error, data: AllowedAny) => void;
attributes?: Attribute[];
}
17 changes: 11 additions & 6 deletions packages/react/src/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,26 @@ export default function SignUpForm(props: Props) {
'content-type': 'application/json',
},
}).catch((e) => e);

if (res.ok === false) {
throw new Error(res.status);
}

try {
if (res) {
if (allowClientCookies) {
Cookies.set('token', res.token.token, {
'max-age': res.token.maxAge,
});
}
return await res.json();
}
} catch (e) {
return e;
}
},
{
onSuccess: (_, data) => {
onSuccess: (res, data) => {
if (allowClientCookies) {
Cookies.set('token', res.token.token, {
'max-age': res.token.maxAge,
});
}
onSuccess && onSuccess(data);
},
onError: (error, data) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,12 @@ import React from 'react';
import { Meta, Story } from '@storybook/react';
import Stack from '@mui/joy/Stack';

import LoginForm from '../src/components/LoginForm';
import GoogleLoginButton from '../src/GoogleLoginButton';
import { NileProvider } from '../src/context';
import LoginForm from '../../src/components/LoginForm';
import GoogleLoginButton from '../../src/GoogleLoginButton';
import { NileProvider } from '../../src/context';

const meta: Meta = {
component: LoginForm,
argTypes: {
children: {
control: {
type: 'text',
},
},
},
parameters: {
controls: { expanded: false },
},
};

export default meta;
Expand Down
24 changes: 24 additions & 0 deletions packages/react/stories/LoginForm/UserLoginForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import Stack from '@mui/joy/Stack';

import LoginForm from '../../src/LoginForm';
import { NileProvider } from '../../src/context';

const meta: Meta = {
component: LoginForm,
};

export default meta;

const Template: Story<null> = () => (
<NileProvider database="database" workspace="workspace">
<Stack sx={{ maxWidth: '20rem', margin: '0 auto' }} spacing={2}>
<LoginForm onSuccess={() => alert('success!')} />
</Stack>
</NileProvider>
);

// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
// https://storybook.js.org/docs/react/workflows/unit-testing
export const Default = Template.bind({});
34 changes: 34 additions & 0 deletions packages/react/test/LoginForm/LoginForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { NileProvider } from '@theniledev/react';
import Cookies from 'js-cookie';
import '../matchMedia.mock';

import LoginForm from '../../src/LoginForm/LoginForm';
import { token } from '../fetch.mock';

jest.mock('js-cookie');

describe('LoginForm', () => {
it('sets a js cookie by default', async () => {
const spy = jest.spyOn(Cookies, 'set');
const onSuccess = jest.fn();
global.fetch = token;
render(
<NileProvider workspace="workspace" database="database">
<LoginForm onSuccess={onSuccess} />
</NileProvider>
);
const password = screen.getByPlaceholderText('Password');
fireEvent.change(password, { target: { value: 'supersecret' } });

const email = screen.getByPlaceholderText('Email');
fireEvent.change(email, { target: { value: '[email protected]' } });

const button = screen.getByRole('button', { name: 'Log in' });
fireEvent.click(button);

await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1));
expect(spy).toHaveBeenCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions packages/react/test/SignUpForm/SignUpForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import '../matchMedia.mock';
import SignUpForm from '../../src/SignUpForm/SignUpForm';
import { token } from '../fetch.mock';

jest.mock('js-cookie');

describe('SignUpForm', () => {
it('sets a js cookie by default', async () => {
const spy = jest.spyOn(Cookies, 'set');
Expand Down
2 changes: 1 addition & 1 deletion packages/react/test/fetch.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FakeResponse {
});
}
json = async () => {
return this.payload;
return JSON.parse(this.payload);
};
}

Expand Down

1 comment on commit 399e910

@vercel
Copy link

@vercel vercel bot commented on 399e910 May 4, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.