Skip to content

Commit 554442b

Browse files
Fritsch-TechSakuk3gremo
authored
Add useDirectusAuth hook for state-based authentication (#364)
Co-authored-by: Sakuk <[email protected]> Co-authored-by: Marco Polichetti <[email protected]>
1 parent 4b41c78 commit 554442b

File tree

7 files changed

+244
-7
lines changed

7 files changed

+244
-7
lines changed

README.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ root.render(
6767
);
6868
```
6969

70-
## ⚙️ The hook `useDirectus`
70+
## ⚙️ Hooks
71+
72+
### `useDirectus`
7173

7274
After adding the provider, you can access the configured client anywhere in the app, using the `useDirectus` hook:
7375

@@ -93,6 +95,43 @@ export const TodoList = () => {
9395
};
9496
```
9597

98+
### `useDirectusAuth`
99+
100+
The `useDirectusAuth` hook provides a few methods for working with the [Directus Authentication API](https://docs.directus.io/reference/old-sdk.html#authentication):
101+
102+
- `login` - a function that accepts an email and password and returns a promise that resolves to the user object if the login is successful or rejects with an error otherwise
103+
- `logout` - a function that logs out the current user
104+
- `user` - the current user object
105+
- `authState` - the current authentication state, one of `loading` (the initial state), `logged-in` or `logged-out`
106+
107+
```jsx
108+
import { useDirectusAuth } from 'react-directus';
109+
110+
const Login = () => {
111+
const { login } = useDirectusAuth();
112+
113+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
114+
e.preventDefault();
115+
116+
const { email, password } = e.currentTarget.elements;
117+
login(email.value, password.value).catch(err => {
118+
console.error(err);
119+
});
120+
};
121+
122+
return (
123+
<form onSubmit={handleSubmit}>
124+
<input type='email' name='email' />
125+
<input type='password' name='password' />
126+
<button type='submit'>Login</button>
127+
</form>
128+
);
129+
};
130+
131+
export default Login;
132+
133+
```
134+
96135
## 🧩 Components (so far...)
97136

98137
This package contains a few components for working with Direcuts [files](https://docs.directus.io/reference/files/). They are all configured for using the `apiUrl` specified in the provider. Hopefully, more will come in the future 🤗.

jest.config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"testMatch": ["<rootDir>/src/**/*(*.)@(spec|test).[tj]s?(x)"],
66
"moduleNameMapper": {
77
"^@/(.*)$": "<rootDir>/src/$1",
8-
"^@components/(.*)$": "<rootDir>/src/components/$1"
8+
"^@components/(.*)$": "<rootDir>/src/components/$1",
9+
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1"
910
}
1011
}

src/DirectusProvider.tsx

+48-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as React from 'react';
22

3-
import { Directus, TypeMap } from '@directus/sdk';
43
import {
4+
AuthStates,
55
DirectusAssetProps,
66
DirectusContextType,
77
DirectusContextTypeGeneric,
88
DirectusImageProps,
99
DirectusProviderProps,
1010
} from '@/types';
1111

12+
import { Directus, TypeMap, UserType } from '@directus/sdk';
13+
1214
import { DirectusAsset } from '@components/DirectusAsset';
1315
import { DirectusImage } from '@components/DirectusImage';
1416

@@ -22,12 +24,18 @@ export const DirectusContext = React.createContext<DirectusContextTypeGeneric<an
2224
export const DirectusProvider = <T extends TypeMap = TypeMap>({
2325
apiUrl,
2426
options,
27+
autoLogin,
2528
children,
2629
}: DirectusProviderProps): JSX.Element => {
30+
const [user, setUser] = React.useState<UserType | null>(null);
31+
const [authState, setAuthState] = React.useState<AuthStates>('loading');
32+
33+
const directus = React.useMemo(() => new Directus<T>(apiUrl, options), [apiUrl, options]);
34+
2735
const value = React.useMemo<DirectusContextType<T>>(
2836
() => ({
29-
apiUrl: apiUrl,
30-
directus: new Directus<T>(apiUrl, options),
37+
apiUrl,
38+
directus,
3139
DirectusAsset: ({ asset, render, ...props }: DirectusAssetProps) => {
3240
console.warn('Deprecated: Please import DirectusAsset directly from react-directus');
3341
return <DirectusAsset asset={asset} render={render} {...props} />;
@@ -36,10 +44,46 @@ export const DirectusProvider = <T extends TypeMap = TypeMap>({
3644
console.warn('Deprecated: Please import DirectusImage directly from react-directus');
3745
return <DirectusImage asset={asset} render={render} {...props} />;
3846
},
47+
_directusUser: user,
48+
_setDirecctusUser: setUser,
49+
_authState: authState,
50+
_setAuthState: setAuthState,
3951
}),
40-
[apiUrl, options]
52+
[apiUrl, directus, user, authState]
4153
);
4254

55+
React.useEffect(() => {
56+
const checkAuth = async () => {
57+
let newAuthState: AuthStates = 'unauthenticated';
58+
try {
59+
await directus.auth.refresh();
60+
const token = await directus.auth.token;
61+
62+
if (token) {
63+
const dUser = (await directus.users.me.read({
64+
// * is a valid field, but typescript doesn't like it
65+
// It's a wildcard, so it will return all fields
66+
// This is the only way to get all fields
67+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
68+
fields: ['*'] as any,
69+
})) as UserType;
70+
71+
if (dUser) {
72+
newAuthState = 'authenticated';
73+
setUser(dUser);
74+
}
75+
}
76+
} catch (error) {
77+
console.log('auth-error', error);
78+
} finally {
79+
setAuthState(newAuthState || 'unauthenticated');
80+
}
81+
};
82+
if (autoLogin) {
83+
checkAuth();
84+
}
85+
}, [directus, autoLogin]);
86+
4387
return <DirectusContext.Provider value={value}>{children}</DirectusContext.Provider>;
4488
};
4589

src/hooks/useDirectusAuth.tsx

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { DirectusAuthHook } from '../types';
2+
import { DirectusContext } from '../DirectusProvider';
3+
import React from 'react';
4+
import { UserType } from '@directus/sdk';
5+
6+
/**
7+
* A hook to access the Directus authentication state and methods.
8+
*
9+
* @example
10+
* ```tsx
11+
* import { useDirectusAuth } from 'react-directus';
12+
*
13+
* const Login = () => {
14+
* const { login } = useDirectusAuth();
15+
*
16+
* const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
17+
* e.preventDefault();
18+
*
19+
* const { email, password } = e.currentTarget.elements;
20+
* login(email.value, password.value)
21+
* .catch((err) => {
22+
* console.error(err);
23+
* });
24+
* };
25+
*
26+
* return (
27+
* <form onSubmit={handleSubmit}>
28+
* <input type="email" name="email" />
29+
* <input type="password" name="password" />
30+
* <button type="submit">Login</button>
31+
* </form>
32+
* );
33+
* };
34+
*
35+
* export default Login;
36+
* ```
37+
*/
38+
export const useDirectusAuth = (): DirectusAuthHook => {
39+
const directusContext = React.useContext(DirectusContext);
40+
41+
if (!directusContext) {
42+
throw new Error('useDirectusAuth has to be used within the DirectusProvider');
43+
}
44+
45+
const {
46+
directus,
47+
_authState: authState,
48+
_setAuthState: setAuthState,
49+
_directusUser: directusUser,
50+
_setDirecctusUser: setDirectusUser,
51+
} = directusContext;
52+
53+
const login = React.useCallback<DirectusAuthHook['login']>(
54+
async (email: string, password: string) => {
55+
await directus.auth.login({
56+
email,
57+
password,
58+
});
59+
60+
const dUser = (await directus.users.me.read({
61+
fields: ['*'],
62+
})) as UserType;
63+
64+
if (dUser) {
65+
setDirectusUser(dUser);
66+
setAuthState('authenticated');
67+
} else {
68+
setDirectusUser(null);
69+
setAuthState('unauthenticated');
70+
}
71+
},
72+
[directus]
73+
);
74+
75+
const logout = React.useCallback<DirectusAuthHook['logout']>(async () => {
76+
try {
77+
await directus.auth.logout();
78+
} finally {
79+
setAuthState('unauthenticated');
80+
setDirectusUser(null);
81+
}
82+
}, [directus]);
83+
84+
const value = React.useMemo<DirectusAuthHook>(
85+
() => ({
86+
user: directusUser,
87+
authState,
88+
login,
89+
logout,
90+
}),
91+
[directus, directusUser, authState]
92+
);
93+
94+
return value;
95+
};

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { DirectusProvider, useDirectus } from '@/DirectusProvider';
22
export { DirectusAsset } from '@components/DirectusAsset';
33
export { DirectusImage } from '@components/DirectusImage';
4+
export { useDirectusAuth } from '@hooks/useDirectusAuth';

src/types.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { DirectusOptions, IDirectus, TypeMap } from '@directus/sdk';
2+
import { DirectusOptions, IDirectus, TypeMap, UserType } from '@directus/sdk';
33
import { DirectusAsset } from '@components/DirectusAsset';
44
import { DirectusImage } from '@components/DirectusImage';
55

@@ -103,9 +103,16 @@ export interface DirectusProviderProps {
103103
apiUrl: string;
104104
/** A set of options to pass to the Directus client. */
105105
options?: DirectusOptions;
106+
/**
107+
* If `true`, the provider will try to login the user automatically on mount.
108+
* @default false
109+
*/
110+
autoLogin?: boolean;
106111
children: React.ReactNode;
107112
}
108113

114+
export type AuthStates = 'loading' | 'authenticated' | 'unauthenticated';
115+
109116
/**
110117
* Shape of the main context.
111118
*/
@@ -117,6 +124,55 @@ export interface DirectusContextType<T extends TypeMap> {
117124
DirectusAsset: typeof DirectusAsset;
118125
/** The context-aware `DirectusImage` component, with pre-filled props. */
119126
DirectusImage: typeof DirectusImage;
127+
/**
128+
* Please use the data provided by the `useDirectusAuth` hook instead.
129+
* @default 'loading'
130+
* @internal
131+
*/
132+
_authState: AuthStates;
133+
/**
134+
* Please use the functions provided by the `useDirectusAuth` hook instead.
135+
* @internal
136+
*/
137+
_setAuthState: React.Dispatch<React.SetStateAction<AuthStates>>;
138+
/**
139+
* Please use the data provided by the `useDirectusAuth` hook instead.
140+
* @default null
141+
* @internal
142+
*/
143+
_directusUser: UserType | null;
144+
/**
145+
* Please use the functions provided by the `useDirectusAuth` hook instead.
146+
* @internal
147+
*/
148+
_setDirecctusUser: React.Dispatch<React.SetStateAction<UserType | null>>;
120149
}
121150

122151
export type DirectusContextTypeGeneric<T extends TypeMap> = DirectusContextType<T> | null;
152+
153+
export interface DirectusAuthHook {
154+
/**
155+
* Login the user. If successful, the user will be stored in the context.
156+
* Else, an error will be thrown.
157+
* @param email - The user email.
158+
* @param password - The user password.
159+
* @throws {Error} - If the login fails.
160+
*/
161+
login: (email: string, password: string) => Promise<void>;
162+
/**
163+
* Logout the user. If successful, the user will be removed from the context.
164+
* Else, an error will be thrown.
165+
* @throws {Error} - If the logout fails.
166+
*/
167+
logout: () => Promise<void>;
168+
/**
169+
* Represents the current authentication state.
170+
* @default 'loading'
171+
*/
172+
authState: AuthStates;
173+
/**
174+
* The current authenticated user.
175+
* @default null
176+
*/
177+
user: UserType | null;
178+
}

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"paths": {
1616
"@/*": ["*"],
1717
"@components/*": ["components/*"],
18+
"@hooks/*": ["hooks/*"]
1819
}
1920
}
2021
}

0 commit comments

Comments
 (0)