@ttoss/react-auth 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@ttoss/react-auth",
3
+ "version": "1.1.0",
4
+ "description": "ttoss authentication module for React apps.",
5
+ "license": "UNLICENSED",
6
+ "author": "ttoss",
7
+ "contributors": [
8
+ "Pedro Arantes <pedro@arantespp.com> (https://arantespp.com/contact)"
9
+ ],
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/esm/index.js",
12
+ "files": [
13
+ "dist",
14
+ "i18n",
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "prebuild": "ttoss-i18n --no-compile",
19
+ "build": "tsup",
20
+ "test": "jest"
21
+ },
22
+ "typings": "./dist/index.d.ts",
23
+ "dependencies": {
24
+ "@ttoss/cloud-auth": "^0.2.0",
25
+ "@ttoss/forms": "^0.11.4",
26
+ "@xstate/react": "^3.0.1",
27
+ "xstate": "^4.35.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@ttoss/react-i18n": "^1.17.2",
31
+ "@ttoss/react-notifications": "^1.18.3",
32
+ "@ttoss/ui": "^1.26.3",
33
+ "aws-amplify": "5.x.x",
34
+ "react": ">=16.8.0"
35
+ },
36
+ "devDependencies": {
37
+ "@ttoss/config": "^1.25.0",
38
+ "@ttoss/i18n-cli": "^0.2.0",
39
+ "@ttoss/react-i18n": "^1.17.2",
40
+ "@ttoss/react-notifications": "^1.19.0",
41
+ "@ttoss/test-utils": "^1.18.3",
42
+ "@ttoss/ui": "^1.27.0",
43
+ "aws-amplify": "^5.0.7"
44
+ },
45
+ "keywords": [
46
+ "React",
47
+ "authentication"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "gitHead": "01759d9c247bed2fe52ca580e87c0b21544cac49"
53
+ }
package/src/Auth.tsx ADDED
@@ -0,0 +1,199 @@
1
+ import * as React from 'react';
2
+ import { Auth as AmplifyAuth } from 'aws-amplify';
3
+ import { AuthConfirmSignUp } from './AuthConfirmSignUp';
4
+ import { AuthSignIn } from './AuthSignIn';
5
+ import { AuthSignUp } from './AuthSignUp';
6
+ import { LogoContextProps, LogoProvider } from './AuthCard';
7
+ import { assign, createMachine } from 'xstate';
8
+ import { useAuth } from './AuthProvider';
9
+ import { useMachine } from '@xstate/react';
10
+ import { useNotifications } from '@ttoss/react-notifications';
11
+ import type { OnConfirmSignUp, OnSignIn, OnSignUp } from './types';
12
+
13
+ type AuthState =
14
+ | {
15
+ value: 'signIn';
16
+ context: { email?: string };
17
+ }
18
+ | {
19
+ value: 'signUp';
20
+ context: Record<string, never>;
21
+ }
22
+ | {
23
+ value: 'signUpConfirm';
24
+ context: { email: string };
25
+ }
26
+ | {
27
+ value: 'signUpResendConfirmation';
28
+ context: { email: string };
29
+ };
30
+
31
+ type AuthEvent =
32
+ | { type: 'SIGN_UP' }
33
+ | { type: 'SIGN_UP_CONFIRM'; email: string }
34
+ | { type: 'SIGN_UP_CONFIRMED'; email: string }
35
+ | { type: 'SIGN_UP_RESEND_CONFIRMATION'; email: string }
36
+ | { type: 'RETURN_TO_SIGN_IN' };
37
+
38
+ type AuthContext = { email?: string };
39
+
40
+ const authMachine = createMachine<AuthContext, AuthEvent, AuthState>(
41
+ {
42
+ predictableActionArguments: true,
43
+ initial: 'signIn',
44
+ states: {
45
+ signIn: {
46
+ on: {
47
+ SIGN_UP: { target: 'signUp' },
48
+ SIGN_UP_RESEND_CONFIRMATION: {
49
+ actions: ['assignEmail'],
50
+ target: 'signUpConfirm',
51
+ },
52
+ },
53
+ },
54
+ signUp: {
55
+ on: {
56
+ SIGN_UP_CONFIRM: {
57
+ actions: ['assignEmail'],
58
+ target: 'signUpConfirm',
59
+ },
60
+ RETURN_TO_SIGN_IN: { target: 'signIn' },
61
+ },
62
+ },
63
+ signUpConfirm: {
64
+ on: {
65
+ SIGN_UP_CONFIRMED: {
66
+ actions: ['assignEmail'],
67
+ target: 'signIn',
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ {
74
+ actions: {
75
+ assignEmail: assign({
76
+ email: (_, event) => {
77
+ return (event as any).email;
78
+ },
79
+ }),
80
+ },
81
+ }
82
+ );
83
+
84
+ const AuthWithoutLogo = () => {
85
+ const { isAuthenticated } = useAuth();
86
+
87
+ const [state, send] = useMachine(authMachine);
88
+
89
+ const { setLoading } = useNotifications();
90
+
91
+ const onSignIn = React.useCallback<OnSignIn>(
92
+ async ({ email, password }) => {
93
+ try {
94
+ setLoading(true);
95
+ await AmplifyAuth.signIn(email, password);
96
+ // toast('Signed In');
97
+ } catch (error) {
98
+ switch ((error as any).code) {
99
+ case 'UserNotConfirmedException':
100
+ await AmplifyAuth.resendSignUp(email);
101
+ send({ type: 'SIGN_UP_RESEND_CONFIRMATION', email } as any);
102
+ break;
103
+ default:
104
+ // toast(JSON.stringify(error, null, 2));
105
+ }
106
+ } finally {
107
+ setLoading(false);
108
+ }
109
+ },
110
+ [send, setLoading]
111
+ );
112
+
113
+ const onSignUp = React.useCallback<OnSignUp>(
114
+ async ({ email, password }) => {
115
+ try {
116
+ setLoading(true);
117
+ await AmplifyAuth.signUp({
118
+ username: email,
119
+ password,
120
+ attributes: { email },
121
+ });
122
+ // toast('Signed Up');
123
+ send({ type: 'SIGN_UP_CONFIRM', email } as any);
124
+ } catch (error) {
125
+ // toast(JSON.stringify(error, null, 2));
126
+ } finally {
127
+ setLoading(false);
128
+ }
129
+ },
130
+ [send, setLoading]
131
+ );
132
+
133
+ const onConfirmSignUp = React.useCallback<OnConfirmSignUp>(
134
+ async ({ email, code }) => {
135
+ try {
136
+ setLoading(true);
137
+ await AmplifyAuth.confirmSignUp(email, code);
138
+ // toast('Confirmed Signed In');
139
+ send({ type: 'SIGN_UP_CONFIRMED', email } as any);
140
+ } catch (error) {
141
+ // toast(JSON.stringify(error, null, 2));
142
+ } finally {
143
+ setLoading(false);
144
+ }
145
+ },
146
+ [send, setLoading]
147
+ );
148
+
149
+ const onReturnToSignIn = React.useCallback(() => {
150
+ send({ type: 'RETURN_TO_SIGN_IN' });
151
+ }, [send]);
152
+
153
+ if (isAuthenticated) {
154
+ return null;
155
+ }
156
+
157
+ if (state.matches('signUp')) {
158
+ return (
159
+ <AuthSignUp onSignUp={onSignUp} onReturnToSignIn={onReturnToSignIn} />
160
+ );
161
+ }
162
+
163
+ if (state.matches('signUpConfirm')) {
164
+ return (
165
+ <AuthConfirmSignUp
166
+ onConfirmSignUp={onConfirmSignUp}
167
+ email={(state.context as any).email}
168
+ />
169
+ );
170
+ }
171
+
172
+ return (
173
+ <AuthSignIn
174
+ onSignIn={onSignIn}
175
+ onSignUp={() => {
176
+ return send('SIGN_UP');
177
+ }}
178
+ defaultValues={{ email: (state.context as any).email }}
179
+ />
180
+ );
181
+ };
182
+
183
+ const withLogo = <T extends Record<string, unknown>>(
184
+ Component: React.ComponentType<T>
185
+ ) => {
186
+ const WithLogo = ({ logo, ...componentProps }: T & LogoContextProps) => {
187
+ return (
188
+ <LogoProvider logo={logo}>
189
+ <Component {...(componentProps as T)} />
190
+ </LogoProvider>
191
+ );
192
+ };
193
+
194
+ WithLogo.displayName = 'WithLogo';
195
+
196
+ return WithLogo;
197
+ };
198
+
199
+ export const Auth = withLogo(AuthWithoutLogo);
@@ -0,0 +1,85 @@
1
+ import * as React from 'react';
2
+ import { Button, Card, Flex, Link, Text } from '@ttoss/ui';
3
+ import { useNotifications } from '@ttoss/react-notifications';
4
+
5
+ export type LogoContextProps = {
6
+ logo?: React.ReactNode;
7
+ children?: React.ReactNode;
8
+ };
9
+
10
+ const LogoContext = React.createContext<LogoContextProps>({});
11
+
12
+ export const LogoProvider = ({ children, ...values }: LogoContextProps) => {
13
+ return <LogoContext.Provider value={values}>{children}</LogoContext.Provider>;
14
+ };
15
+
16
+ type LinkProps = {
17
+ label: string;
18
+ onClick: () => void;
19
+ };
20
+
21
+ type AuthCardProps = {
22
+ children: React.ReactNode;
23
+ title: string;
24
+ buttonLabel: string;
25
+ links?: LinkProps[];
26
+ };
27
+
28
+ export const AuthCard = ({
29
+ children,
30
+ title,
31
+ buttonLabel,
32
+ links = [],
33
+ }: AuthCardProps) => {
34
+ const { logo } = React.useContext(LogoContext);
35
+
36
+ const { isLoading } = useNotifications();
37
+
38
+ return (
39
+ <Card sx={{ maxWidth: '564px' }}>
40
+ <Flex sx={{ flexDirection: 'column', gap: 3 }}>
41
+ {logo && (
42
+ <Flex sx={{ width: '100%', justifyContent: 'center' }}>{logo}</Flex>
43
+ )}
44
+ <Text
45
+ variant="title"
46
+ sx={{ alignSelf: 'center', marginY: 4, fontSize: 5 }}
47
+ >
48
+ {title}
49
+ </Text>
50
+ {children}
51
+ <Flex sx={{ justifyContent: 'space-between', marginTop: 3 }}>
52
+ <Button
53
+ type="submit"
54
+ aria-label="submit-login"
55
+ variant="cta"
56
+ disabled={isLoading}
57
+ sx={{ width: '100%' }}
58
+ >
59
+ {buttonLabel}
60
+ </Button>
61
+ </Flex>
62
+
63
+ <Flex
64
+ sx={{
65
+ justifyContent: 'space-between',
66
+ flexDirection: 'column',
67
+ gap: 3,
68
+ marginTop: 4,
69
+ color: 'text',
70
+ }}
71
+ >
72
+ {links.map((link) => {
73
+ return (
74
+ link && (
75
+ <Link key={link.label} onClick={link.onClick}>
76
+ {link.label}
77
+ </Link>
78
+ )
79
+ );
80
+ })}
81
+ </Flex>
82
+ </Flex>
83
+ </Card>
84
+ );
85
+ };
@@ -0,0 +1,70 @@
1
+ import { AuthCard } from './AuthCard';
2
+ import { Form, FormFieldInput, useForm, yup, yupResolver } from '@ttoss/forms';
3
+ import { useI18n } from '@ttoss/react-i18n';
4
+ import type { OnConfirmSignUp } from './types';
5
+
6
+ export const AuthConfirmSignUp = ({
7
+ email,
8
+ onConfirmSignUp,
9
+ }: {
10
+ email: string;
11
+ onConfirmSignUp: OnConfirmSignUp;
12
+ }) => {
13
+ const { intl } = useI18n();
14
+
15
+ const schema = yup
16
+ .object()
17
+ .shape({
18
+ code: yup
19
+ .string()
20
+ .required(
21
+ intl.formatMessage({
22
+ description: 'Required field.',
23
+ defaultMessage: 'Required field',
24
+ })
25
+ )
26
+ .max(
27
+ 6,
28
+ intl.formatMessage(
29
+ {
30
+ description: 'Minimum {value} characters.',
31
+ defaultMessage: 'Minimum {value} characters',
32
+ },
33
+ { value: 6 }
34
+ )
35
+ ),
36
+ })
37
+ .required();
38
+
39
+ const formMethods = useForm<yup.TypeOf<typeof schema>>({
40
+ resolver: yupResolver(schema),
41
+ });
42
+
43
+ return (
44
+ <Form
45
+ {...formMethods}
46
+ onSubmit={({ code }) => {
47
+ return onConfirmSignUp({ code, email });
48
+ }}
49
+ >
50
+ <AuthCard
51
+ buttonLabel={intl.formatMessage({
52
+ description: 'Confirm',
53
+ defaultMessage: 'Confirm',
54
+ })}
55
+ title={intl.formatMessage({
56
+ description: 'Confirmation',
57
+ defaultMessage: 'Confirmation',
58
+ })}
59
+ >
60
+ <FormFieldInput
61
+ name="code"
62
+ label={intl.formatMessage({
63
+ description: 'Sign up confirmation code',
64
+ defaultMessage: 'Code',
65
+ })}
66
+ />
67
+ </AuthCard>
68
+ </Form>
69
+ );
70
+ };
@@ -0,0 +1,21 @@
1
+ import { Flex, FlexProps } from '@ttoss/ui';
2
+
3
+ export type AuthContainerProps = FlexProps & { backgroundImageUrl?: string };
4
+
5
+ export const AuthContainer = ({ sx, ...props }: AuthContainerProps) => {
6
+ return (
7
+ <Flex
8
+ {...props}
9
+ sx={{
10
+ height: '100vh',
11
+ justifyContent: 'center',
12
+ alignItems: 'center',
13
+ margin: 0,
14
+ backgroundPosition: 'center',
15
+ backgroundRepeat: 'no-repeat',
16
+ backgroundSize: 'cover',
17
+ ...sx,
18
+ }}
19
+ />
20
+ );
21
+ };
@@ -0,0 +1,84 @@
1
+ import * as React from 'react';
2
+ import { Auth, Hub } from 'aws-amplify';
3
+
4
+ type User = {
5
+ id: string;
6
+ email: string;
7
+ emailVerified: string;
8
+ } | null;
9
+
10
+ type Tokens = {
11
+ idToken: string;
12
+ accessToken: string;
13
+ refreshToken: string;
14
+ } | null;
15
+
16
+ const signOut = () => {
17
+ return Auth.signOut();
18
+ };
19
+
20
+ const AuthContext = React.createContext<{
21
+ signOut: () => Promise<any>;
22
+ isAuthenticated: boolean;
23
+ user: User;
24
+ tokens: Tokens;
25
+ }>({
26
+ signOut,
27
+ isAuthenticated: false,
28
+ user: null,
29
+ tokens: null,
30
+ });
31
+
32
+ const AuthProvider = ({ children }: { children: React.ReactNode }) => {
33
+ const [user, setUser] = React.useState<User>(null);
34
+
35
+ const [tokens, setTokens] = React.useState<Tokens>(null);
36
+
37
+ React.useEffect(() => {
38
+ const updateUser = () => {
39
+ Auth.currentAuthenticatedUser()
40
+ .then(({ attributes, signInUserSession }) => {
41
+ setUser({
42
+ id: attributes.sub,
43
+ email: attributes.email,
44
+ emailVerified: attributes['email_verified'],
45
+ });
46
+
47
+ setTokens({
48
+ idToken: signInUserSession.idToken.jwtToken,
49
+ accessToken: signInUserSession.accessToken.jwtToken,
50
+ refreshToken: signInUserSession.refreshToken.token,
51
+ });
52
+ })
53
+ .catch(() => {
54
+ setUser(null);
55
+ setTokens(null);
56
+ });
57
+ };
58
+
59
+ const updateUserListener = Hub.listen('auth', updateUser);
60
+
61
+ /**
62
+ * Check manually the first time.
63
+ */
64
+ updateUser();
65
+
66
+ return () => {
67
+ updateUserListener();
68
+ };
69
+ }, []);
70
+
71
+ const isAuthenticated = !!user;
72
+
73
+ return (
74
+ <AuthContext.Provider value={{ signOut, isAuthenticated, user, tokens }}>
75
+ {children}
76
+ </AuthContext.Provider>
77
+ );
78
+ };
79
+
80
+ export const useAuth = () => {
81
+ return React.useContext(AuthContext);
82
+ };
83
+
84
+ export default AuthProvider;
@@ -0,0 +1,111 @@
1
+ import { AuthCard } from './AuthCard';
2
+ import { Form, FormFieldInput, useForm, yup, yupResolver } from '@ttoss/forms';
3
+ import { PASSWORD_MINIMUM_LENGTH } from '@ttoss/cloud-auth';
4
+ import { useI18n } from '@ttoss/react-i18n';
5
+ import type { OnSignIn, OnSignInInput } from './types';
6
+
7
+ export type AuthSignInProps = {
8
+ onSignIn: OnSignIn;
9
+ onSignUp: () => void;
10
+ defaultValues?: Partial<OnSignInInput>;
11
+ urlLogo?: string;
12
+ };
13
+
14
+ export const AuthSignIn = ({
15
+ onSignIn,
16
+ onSignUp,
17
+ defaultValues,
18
+ }: AuthSignInProps) => {
19
+ const { intl } = useI18n();
20
+
21
+ const schema = yup.object().shape({
22
+ email: yup
23
+ .string()
24
+ .required(
25
+ intl.formatMessage({
26
+ description: 'Email is a required field.',
27
+ defaultMessage: 'Email field is required',
28
+ })
29
+ )
30
+ .email(
31
+ intl.formatMessage({
32
+ description: 'Invalid email.',
33
+ defaultMessage: 'Invalid email',
34
+ })
35
+ ),
36
+ password: yup
37
+ .string()
38
+ .required(
39
+ intl.formatMessage({
40
+ description: 'Password is required.',
41
+ defaultMessage: 'Password field is required',
42
+ })
43
+ )
44
+ .min(
45
+ PASSWORD_MINIMUM_LENGTH,
46
+ intl.formatMessage(
47
+ {
48
+ description: 'Password must be at least {value} characters long.',
49
+ defaultMessage: 'Password requires {value} characters',
50
+ },
51
+ { value: PASSWORD_MINIMUM_LENGTH }
52
+ )
53
+ )
54
+ .trim(),
55
+ });
56
+
57
+ const formMethods = useForm<OnSignInInput>({
58
+ defaultValues,
59
+ resolver: yupResolver(schema),
60
+ });
61
+
62
+ const onSubmitForm = (data: OnSignInInput) => {
63
+ return onSignIn(data);
64
+ };
65
+
66
+ return (
67
+ <Form {...formMethods} onSubmit={onSubmitForm}>
68
+ <AuthCard
69
+ title={intl.formatMessage({
70
+ description: 'Sign in title.',
71
+ defaultMessage: 'Login',
72
+ })}
73
+ buttonLabel={intl.formatMessage({
74
+ description: 'Button label.',
75
+ defaultMessage: 'Login',
76
+ })}
77
+ links={[
78
+ {
79
+ onClick: onSignUp,
80
+ label: intl.formatMessage({
81
+ description: 'Link to retrieve password.',
82
+ defaultMessage: 'Do you forgot your password?',
83
+ }),
84
+ },
85
+ {
86
+ onClick: onSignUp,
87
+ label: intl.formatMessage({
88
+ description: 'Link to sign up.',
89
+ defaultMessage: "Don't have an account? Sign up",
90
+ }),
91
+ },
92
+ ]}
93
+ >
94
+ <FormFieldInput
95
+ name="email"
96
+ label={intl.formatMessage({
97
+ description: 'Email label.',
98
+ defaultMessage: 'Email',
99
+ })}
100
+ />
101
+ <FormFieldInput
102
+ name="password"
103
+ label={intl.formatMessage({
104
+ description: 'Password label.',
105
+ defaultMessage: 'Password',
106
+ })}
107
+ />
108
+ </AuthCard>
109
+ </Form>
110
+ );
111
+ };