feat: rewrite auth (#364)
* feat: wip rewrite auth * feat: restructure folders and fix stories and tests * feat: remove auth provider and fix tests
This commit is contained in:
@ -1,29 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { getUserIdFromToken } from '@/auth/services/AuthService';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { isAuthenticatingState } from '@/auth/states/isAuthenticatingState';
|
||||
import { useGetCurrentUserQuery } from '@/users/services';
|
||||
|
||||
type OwnProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: OwnProps) {
|
||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||
|
||||
const userIdFromToken = getUserIdFromToken();
|
||||
|
||||
const { data } = useGetCurrentUserQuery(userIdFromToken);
|
||||
const user = data?.users?.[0];
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setCurrentUser(user);
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [user, setCurrentUser, setIsAuthenticating]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
19
front/src/providers/apollo/ApolloProvider.tsx
Normal file
19
front/src/providers/apollo/ApolloProvider.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
|
||||
import { apolloClient } from './apollo-client';
|
||||
import { mockClient } from './mock-client';
|
||||
|
||||
export const ApolloProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [isMockMode] = useRecoilState(isMockModeState);
|
||||
|
||||
return (
|
||||
<ApolloProviderBase client={isMockMode ? mockClient : apolloClient}>
|
||||
{children}
|
||||
</ApolloProviderBase>
|
||||
);
|
||||
};
|
||||
36
front/src/providers/apollo/apollo-client.ts
Normal file
36
front/src/providers/apollo/apollo-client.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { tokenService } from '@/auth/services/TokenService';
|
||||
import { CommentThreadTarget } from '~/generated/graphql';
|
||||
|
||||
import { ApolloFactory } from './apollo.factory';
|
||||
|
||||
const apollo = new ApolloFactory({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
CommentThread: {
|
||||
fields: {
|
||||
commentThreadTargets: {
|
||||
merge(
|
||||
existing: CommentThreadTarget[] = [],
|
||||
incoming: CommentThreadTarget[],
|
||||
) {
|
||||
return [...incoming];
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
onUnauthenticatedError() {
|
||||
tokenService.removeTokenPair();
|
||||
},
|
||||
});
|
||||
|
||||
export const apolloClient = apollo.getClient();
|
||||
167
front/src/providers/apollo/apollo.factory.ts
Normal file
167
front/src/providers/apollo/apollo.factory.ts
Normal file
@ -0,0 +1,167 @@
|
||||
/* eslint-disable no-loop-func */
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloClientOptions,
|
||||
ApolloLink,
|
||||
createHttpLink,
|
||||
ServerError,
|
||||
ServerParseError,
|
||||
} from '@apollo/client';
|
||||
import { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import { Observable } from '@apollo/client/utilities';
|
||||
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
import { tokenService } from '@/auth/services/TokenService';
|
||||
|
||||
import { assertNotNull } from '../../modules/utils/assert';
|
||||
import { promiseToObservable } from '../../modules/utils/promise-to-observable';
|
||||
|
||||
import { ApolloManager } from './interfaces/apollo-manager.interface';
|
||||
import { loggerLink } from './logger';
|
||||
|
||||
const logger = loggerLink(() => 'Twenty');
|
||||
|
||||
let isRefreshing = false;
|
||||
let pendingRequests: (() => void)[] = [];
|
||||
|
||||
const resolvePendingRequests = () => {
|
||||
pendingRequests.map((callback) => callback());
|
||||
pendingRequests = [];
|
||||
};
|
||||
|
||||
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
||||
onError?: (err: GraphQLErrors | undefined) => void;
|
||||
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||
onUnauthenticatedError?: () => void;
|
||||
}
|
||||
|
||||
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
private client: ApolloClient<TCacheShape>;
|
||||
|
||||
constructor(opts: Options<TCacheShape>) {
|
||||
const {
|
||||
uri,
|
||||
onError: onErrorCb,
|
||||
onNetworkError,
|
||||
onUnauthenticatedError,
|
||||
...options
|
||||
} = opts;
|
||||
|
||||
const buildApolloLink = (): ApolloLink => {
|
||||
const httpLink = createHttpLink({
|
||||
uri,
|
||||
});
|
||||
|
||||
const authLink = setContext(async (_, { headers }) => {
|
||||
const credentials = tokenService.getTokenPair();
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: credentials?.accessToken
|
||||
? `Bearer ${credentials?.accessToken}`
|
||||
: '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const retryLink = new RetryLink({
|
||||
delay: {
|
||||
initial: 100,
|
||||
},
|
||||
attempts: {
|
||||
max: 2,
|
||||
retryIf: (error) => !!error,
|
||||
},
|
||||
});
|
||||
|
||||
const errorLink = onError(
|
||||
({ graphQLErrors, networkError, forward, operation }) => {
|
||||
if (graphQLErrors) {
|
||||
onErrorCb?.(graphQLErrors);
|
||||
|
||||
for (const graphQLError of graphQLErrors) {
|
||||
switch (graphQLError?.extensions?.code) {
|
||||
case 'UNAUTHENTICATED': {
|
||||
// error code is set to UNAUTHENTICATED
|
||||
// when AuthenticationError thrown in resolver
|
||||
let forward$: Observable<boolean>;
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
forward$ = promiseToObservable(
|
||||
renewToken(uri)
|
||||
.then(() => {
|
||||
resolvePendingRequests();
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
pendingRequests = [];
|
||||
onUnauthenticatedError?.();
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
}),
|
||||
).filter((value) => Boolean(value));
|
||||
} else {
|
||||
// Will only emit once the Promise is resolved
|
||||
forward$ = promiseToObservable(
|
||||
new Promise<boolean>((resolve) => {
|
||||
pendingRequests.push(() => resolve(true));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return forward$.flatMap(() => forward(operation));
|
||||
}
|
||||
default:
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(
|
||||
`[GraphQL error]: Message: ${
|
||||
graphQLError.message
|
||||
}, Location: ${
|
||||
graphQLError.locations
|
||||
? JSON.stringify(graphQLError.locations)
|
||||
: graphQLError.locations
|
||||
}, Path: ${graphQLError.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`[Network error]: ${networkError}`);
|
||||
}
|
||||
onNetworkError?.(networkError);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return ApolloLink.from(
|
||||
[
|
||||
errorLink,
|
||||
authLink,
|
||||
// Only show logger in dev mode
|
||||
process.env.NODE_ENV !== 'production' ? logger : null,
|
||||
retryLink,
|
||||
httpLink,
|
||||
].filter(assertNotNull),
|
||||
);
|
||||
};
|
||||
|
||||
this.client = new ApolloClient({
|
||||
...options,
|
||||
link: buildApolloLink(),
|
||||
});
|
||||
}
|
||||
|
||||
getClient() {
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
|
||||
export interface ApolloManager<TCacheShape> {
|
||||
getClient(): ApolloClient<TCacheShape>;
|
||||
}
|
||||
45
front/src/providers/apollo/logger/format-title.ts
Normal file
45
front/src/providers/apollo/logger/format-title.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { OperationType } from './operation-type';
|
||||
|
||||
const operationTypeColors = {
|
||||
query: '#03A9F4',
|
||||
mutation: '#61A600',
|
||||
subscription: '#61A600',
|
||||
error: '#F51818',
|
||||
default: '#61A600',
|
||||
};
|
||||
|
||||
const getOperationColor = (operationType: OperationType) => {
|
||||
return operationTypeColors[operationType] ?? operationTypeColors.default;
|
||||
};
|
||||
|
||||
const formatTitle = (
|
||||
operationType: OperationType,
|
||||
schemaName: string,
|
||||
queryName: string,
|
||||
time: string | number,
|
||||
) => {
|
||||
const headerCss = [
|
||||
'color: gray; font-weight: lighter', // title
|
||||
`color: ${getOperationColor(operationType)}; font-weight: bold;`, // operationType
|
||||
'color: gray; font-weight: lighter;', // schemaName
|
||||
'color: black; font-weight: bold;', // queryName
|
||||
];
|
||||
|
||||
const parts = [
|
||||
'%c apollo',
|
||||
`%c${operationType}`,
|
||||
`%c${schemaName}::%c${queryName}`,
|
||||
];
|
||||
|
||||
if (operationType !== OperationType.Subscription) {
|
||||
parts.push(`%c(in ${time} ms)`);
|
||||
headerCss.push('color: gray; font-weight: lighter;'); // time
|
||||
} else {
|
||||
parts.push(`%c(@ ${time})`);
|
||||
headerCss.push('color: gray; font-weight: lighter;'); // time
|
||||
}
|
||||
|
||||
return [parts.join(' '), ...headerCss];
|
||||
};
|
||||
|
||||
export default formatTitle;
|
||||
102
front/src/providers/apollo/logger/index.ts
Normal file
102
front/src/providers/apollo/logger/index.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { ApolloLink, gql, Operation } from '@apollo/client';
|
||||
|
||||
import formatTitle from './format-title';
|
||||
|
||||
const getGroup = (collapsed: boolean) =>
|
||||
collapsed
|
||||
? console.groupCollapsed.bind(console)
|
||||
: console.group.bind(console);
|
||||
|
||||
const parseQuery = (queryString: string) => {
|
||||
const queryObj = gql`
|
||||
${queryString}
|
||||
`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { name } = queryObj.definitions[0] as any;
|
||||
return [name ? name.value : 'Generic', queryString.trim()];
|
||||
};
|
||||
|
||||
export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
|
||||
new ApolloLink((operation, forward) => {
|
||||
const schemaName = getSchemaName(operation);
|
||||
operation.setContext({ start: Date.now() });
|
||||
|
||||
const { variables } = operation;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const operationType = (operation.query.definitions[0] as any).operation;
|
||||
const headers = operation.getContext().headers;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const [queryName, query] = parseQuery(operation.query.loc!.source.body);
|
||||
|
||||
if (operationType === 'subscription') {
|
||||
const date = new Date().toLocaleTimeString();
|
||||
|
||||
const titleArgs = formatTitle(operationType, schemaName, queryName, date);
|
||||
|
||||
console.groupCollapsed(...titleArgs);
|
||||
|
||||
if (variables && Object.keys(variables).length !== 0) {
|
||||
console.log('VARIABLES', variables);
|
||||
}
|
||||
|
||||
console.log('QUERY', query);
|
||||
|
||||
console.groupEnd();
|
||||
|
||||
return forward(operation);
|
||||
}
|
||||
|
||||
return forward(operation).map((result) => {
|
||||
const time = Date.now() - operation.getContext().start;
|
||||
const errors = result.errors ?? result.data?.[queryName]?.errors;
|
||||
const hasError = Boolean(errors);
|
||||
|
||||
try {
|
||||
const titleArgs = formatTitle(
|
||||
operationType,
|
||||
schemaName,
|
||||
queryName,
|
||||
time,
|
||||
);
|
||||
|
||||
getGroup(!hasError)(...titleArgs);
|
||||
|
||||
if (errors) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
errors.forEach((err: any) => {
|
||||
console.log(
|
||||
`%c${err.message}`,
|
||||
'color: #F51818; font-weight: lighter',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('HEADERS: ', headers);
|
||||
|
||||
if (variables && Object.keys(variables).length !== 0) {
|
||||
console.log('VARIABLES', variables);
|
||||
}
|
||||
|
||||
console.log('QUERY', query);
|
||||
|
||||
if (result.data) {
|
||||
console.log('RESULT', result.data);
|
||||
}
|
||||
if (errors) {
|
||||
console.log('ERRORS', errors);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} catch {
|
||||
// this may happen if console group is not supported
|
||||
console.log(
|
||||
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
|
||||
);
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
6
front/src/providers/apollo/logger/operation-type.ts
Normal file
6
front/src/providers/apollo/logger/operation-type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum OperationType {
|
||||
Query = 'query',
|
||||
Mutation = 'mutation',
|
||||
Subscription = 'subscription',
|
||||
Error = 'error',
|
||||
}
|
||||
36
front/src/providers/apollo/mock-client.ts
Normal file
36
front/src/providers/apollo/mock-client.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
createHttpLink,
|
||||
from,
|
||||
InMemoryCache,
|
||||
} from '@apollo/client';
|
||||
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
const apiLink = createHttpLink({
|
||||
uri: `${process.env.REACT_APP_API_URL}`,
|
||||
});
|
||||
|
||||
const mockLink = new ApolloLink((operation, forward) => {
|
||||
return forward(operation).map((response) => {
|
||||
if (operation.operationName === 'GetCompanies') {
|
||||
return { data: { companies: mockedCompaniesData } };
|
||||
}
|
||||
if (operation.operationName === 'Verify') {
|
||||
return { data: { user: [mockedUsersData[0]], tokens: {} } };
|
||||
}
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
export const mockClient = new ApolloClient({
|
||||
link: from([mockLink, apiLink]),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user