Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 2 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"testMatch": ["<rootDir>/src/**/*(*.)@(spec|test).[tj]s?(x)"],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1"
"^@components/(.*)$": "<rootDir>/src/components/$1",
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1"
}
}
50 changes: 47 additions & 3 deletions src/DirectusProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as React from 'react';

import { Directus, TypeMap } from '@directus/sdk';
import {
AuthStates,
DirectusAssetProps,
DirectusContextType,
DirectusContextTypeGeneric,
DirectusImageProps,
DirectusProviderProps,
} from '@/types';

import { Directus, TypeMap, UserType } from '@directus/sdk';

import { DirectusAsset } from '@components/DirectusAsset';
import { DirectusImage } from '@components/DirectusImage';

Expand All @@ -22,12 +24,18 @@ export const DirectusContext = React.createContext<DirectusContextTypeGeneric<an
export const DirectusProvider = <T extends TypeMap = TypeMap>({
apiUrl,
options,
autoLogin,
children,
}: DirectusProviderProps): JSX.Element => {
const [user, setUser] = React.useState<UserType | null>(null);
const [authState, setAuthState] = React.useState<AuthStates>('loading');

const directus = React.useMemo(() => new Directus<T>(apiUrl, options), [apiUrl, options]);

const value = React.useMemo<DirectusContextType<T>>(
() => ({
apiUrl: apiUrl,
directus: new Directus<T>(apiUrl, options),
directus: directus,
DirectusAsset: ({ asset, render, ...props }: DirectusAssetProps) => {
console.warn('Deprecated: Please import DirectusAsset directly from react-directus');
return <DirectusAsset asset={asset} render={render} {...props} />;
Expand All @@ -36,10 +44,46 @@ export const DirectusProvider = <T extends TypeMap = TypeMap>({
console.warn('Deprecated: Please import DirectusImage directly from react-directus');
return <DirectusImage asset={asset} render={render} {...props} />;
},
_directusUser: user,
_setDirecctusUser: setUser,
_authState: authState,
_setAuthState: setAuthState,
}),
[apiUrl, options]
[apiUrl, directus, user, authState]
);

React.useEffect(() => {
const checkAuth = async () => {
let newAuthState: AuthStates = 'unauthenticated';
try {
await directus.auth.refresh();
const token = await directus.auth.token;

if (token) {
const dUser = (await directus.users.me.read({
// * is a valid field, but typescript doesn't like it
// It's a wildcard, so it will return all fields
// This is the only way to get all fields
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fields: ['*'] as any,
})) as UserType;

if (dUser) {
newAuthState = 'authenticated';
setUser(dUser);
}
}
} catch (error) {
console.log('auth-error', error);
} finally {
setAuthState(newAuthState || 'unauthenticated');
}
};
if (autoLogin) {
checkAuth();
}
}, [directus, autoLogin]);

return <DirectusContext.Provider value={value}>{children}</DirectusContext.Provider>;
};

Expand Down
94 changes: 94 additions & 0 deletions src/hooks/useDirectusAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { DirectusAuthHook } from '../types';
import { DirectusContext } from '../DirectusProvider';
import React from 'react';
import { UserType } from '@directus/sdk';

/**
* A hook to access the Directus authentication state and methods.
* @example
* ```tsx
* import { useDirectusAuth } from 'react-directus';
*
* const Login = () => {
* const { login } = useDirectusAuth();
*
* const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
* e.preventDefault();
* const { email, password } = e.currentTarget.elements;
* login(email.value, password.value)
* .catch((err) => {
* console.error(err);
* });
* };
*
* return (
* <form onSubmit={handleSubmit}>
* <input type="email" name="email" />
* <input type="password" name="password" />
* <button type="submit">Login</button>
* </form>
* );
* };
*
* export default Login;
* ```
*/

export const useDirectusAuth = (): DirectusAuthHook => {
const directusContext = React.useContext(DirectusContext);

if (!directusContext) {
throw new Error('useDirectusAuth has to be used within the DirectusProvider');
}

const {
directus,
_authState: authState,
_setAuthState: setAuthState,
_directusUser: directusUser,
_setDirecctusUser: setDirectusUser,
} = directusContext;

const login = React.useCallback<DirectusAuthHook['login']>(
async (email: string, password: string) => {
await directus.auth.login({
email,
password,
});

const dUser = (await directus.users.me.read({
fields: ['*'],
})) as UserType;

if (dUser) {
setDirectusUser(dUser);
setAuthState('authenticated');
} else {
setDirectusUser(null);
setAuthState('unauthenticated');
}
},
[directus]
);

const logout = React.useCallback<DirectusAuthHook['logout']>(async () => {
try {
await directus.auth.logout();
} finally {
setAuthState('unauthenticated');
setDirectusUser(null);
}
}, [directus]);

const value = React.useMemo<DirectusAuthHook>(
() => ({
user: directusUser,
authState,
login,
logout,
}),
[directus, directusUser, authState]
);

return value;
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { DirectusProvider, useDirectus } from '@/DirectusProvider';
export { DirectusAsset } from '@components/DirectusAsset';
export { DirectusImage } from '@components/DirectusImage';
export { useDirectusAuth } from '@hooks/useDirectusAuth';
58 changes: 57 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { DirectusOptions, IDirectus, TypeMap } from '@directus/sdk';
import { DirectusOptions, IDirectus, TypeMap, UserType } from '@directus/sdk';
import { DirectusAsset } from '@components/DirectusAsset';
import { DirectusImage } from '@components/DirectusImage';

Expand Down Expand Up @@ -61,9 +61,16 @@ export interface DirectusProviderProps {
apiUrl: string;
/** A set of options to pass to the Directus client. */
options?: DirectusOptions;
/**
* If `true`, the provider will try to login the user automatically on mount.
* @default false
*/
autoLogin?: boolean;
children: React.ReactNode;
}

export type AuthStates = 'loading' | 'authenticated' | 'unauthenticated';

/**
* Shape of the main context.
*/
Expand All @@ -75,6 +82,55 @@ export interface DirectusContextType<T extends TypeMap> {
DirectusAsset: typeof DirectusAsset;
/** The context-aware `DirectusImage` component, with pre-filled props. */
DirectusImage: typeof DirectusImage;
/**
* Please use the data provided by the `useDirectusAuth` hook instead.
* @default 'loading'
* @internal
*/
_authState: AuthStates;
/**
* Please use the functions provided by the `useDirectusAuth` hook instead.
* @internal
*/
_setAuthState: React.Dispatch<React.SetStateAction<AuthStates>>;
/**
* Please use the data provided by the `useDirectusAuth` hook instead.
* @default null
* @internal
*/
_directusUser: UserType | null;
/**
* Please use the functions provided by the `useDirectusAuth` hook instead.
* @internal
*/
_setDirecctusUser: React.Dispatch<React.SetStateAction<UserType | null>>;
}

export type DirectusContextTypeGeneric<T extends TypeMap> = DirectusContextType<T> | null;

export interface DirectusAuthHook {
/**
* Login the user. If successful, the user will be stored in the context.
* Else, an error will be thrown.
* @param email - The user email.
* @param password - The user password.
* @throws {Error} - If the login fails.
*/
login: (email: string, password: string) => Promise<void>;
/**
* Logout the user. If successful, the user will be removed from the context.
* Else, an error will be thrown.
* @throws {Error} - If the logout fails.
*/
logout: () => Promise<void>;
/**
* Represents the current authentication state.
* @default 'loading'
*/
authState: AuthStates;
/**
* The current authenticated user.
* @default null
*/
user: UserType | null;
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"]
}
}
}