refactor: migrate client auth queries to trpc procedures
This commit is contained in:
parent
2e13666d80
commit
f6a6b85b60
20 changed files with 135 additions and 530 deletions
|
@ -4,10 +4,10 @@ import React, { useEffect } from 'react';
|
|||
import clsx from 'clsx';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import semver from 'semver';
|
||||
import { useRefreshTokenQuery } from '../../generated/graphql';
|
||||
import { Header } from '../ui/Header';
|
||||
import styles from './Layout.module.scss';
|
||||
import { useSystemStore } from '../../state/systemStore';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
|
||||
interface IProps {
|
||||
loading?: boolean;
|
||||
|
@ -18,17 +18,20 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const Layout: React.FC<IProps> = ({ children, breadcrumbs, title, actions }) => {
|
||||
const { data } = useRefreshTokenQuery({ fetchPolicy: 'network-only' });
|
||||
const refreshToken = trpc.auth.refreshToken.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data?.token) localStorage.setItem('token', data.token);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refreshToken.mutate();
|
||||
}, []);
|
||||
|
||||
const { version } = useSystemStore();
|
||||
const defaultVersion = '0.0.0';
|
||||
const isLatest = semver.gte(version?.current || defaultVersion, version?.latest || defaultVersion);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.refreshToken?.token) {
|
||||
localStorage.setItem('token', data.refreshToken.token);
|
||||
}
|
||||
}, [data?.refreshToken?.token]);
|
||||
|
||||
const renderBreadcrumbs = () => {
|
||||
if (!breadcrumbs) {
|
||||
return null;
|
||||
|
|
|
@ -4,7 +4,6 @@ import React, { useEffect } from 'react';
|
|||
import clsx from 'clsx';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import semver from 'semver';
|
||||
import { useRefreshTokenQuery } from '../../generated/graphql';
|
||||
import { Header } from '../ui/Header';
|
||||
import styles from './Layout.module.scss';
|
||||
import { ErrorPage } from '../ui/ErrorPage';
|
||||
|
@ -22,18 +21,21 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const Layout: React.FC<IProps> = ({ children, breadcrumbs, title, actions, loading, error, loadingComponent, data }) => {
|
||||
const { data: dataRefreshToken } = useRefreshTokenQuery({ fetchPolicy: 'network-only' });
|
||||
const refreshToken = trpc.auth.refreshToken.useMutation({
|
||||
onSuccess: (d) => {
|
||||
if (d?.token) localStorage.setItem('token', d.token);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refreshToken.mutate();
|
||||
}, []);
|
||||
|
||||
const { data: dataVersion } = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
|
||||
|
||||
const defaultVersion = '0.0.0';
|
||||
const isLatest = semver.gte(dataVersion?.current || defaultVersion, dataVersion?.latest || defaultVersion);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataRefreshToken?.refreshToken?.token) {
|
||||
localStorage.setItem('token', dataRefreshToken.refreshToken.token);
|
||||
}
|
||||
}, [dataRefreshToken?.refreshToken?.token]);
|
||||
|
||||
const renderBreadcrumbs = () => {
|
||||
if (!breadcrumbs) {
|
||||
return null;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { getTRPCMock } from '../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../mocks/server';
|
||||
import { AuthProvider } from './AuthProvider';
|
||||
|
||||
|
@ -11,13 +11,12 @@ describe('Test: AuthProvider', () => {
|
|||
<div>Should not render</div>
|
||||
</AuthProvider>,
|
||||
);
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], type: 'query', response: null }));
|
||||
await waitFor(() => expect(screen.getByText('Login')).toBeInTheDocument());
|
||||
expect(screen.queryByText('Should not render')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children if user is logged in', async () => {
|
||||
server.use(graphql.query('Me', (req, res, ctx) => res(ctx.data({ me: { id: '1' } }))));
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<div>Should render</div>
|
||||
|
@ -28,7 +27,8 @@ describe('Test: AuthProvider', () => {
|
|||
});
|
||||
|
||||
it('should render register form if app is not configured', async () => {
|
||||
server.use(graphql.query('Configured', (req, res, ctx) => res(ctx.data({ isConfigured: false }))));
|
||||
server.use(getTRPCMock({ path: ['auth', 'me'], type: 'query', response: null }));
|
||||
server.use(getTRPCMock({ path: ['auth', 'isConfigured'], type: 'query', response: false }));
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
|
@ -36,7 +36,7 @@ describe('Test: AuthProvider', () => {
|
|||
</AuthProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Register')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText('Register your account')).toBeInTheDocument());
|
||||
expect(screen.queryByText('Should not render')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useConfiguredQuery, useMeQuery } from '../../../generated/graphql';
|
||||
import { LoginContainer } from '../../../modules/Auth/containers/LoginContainer';
|
||||
import { RegisterContainer } from '../../../modules/Auth/containers/RegisterContainer';
|
||||
import { trpc } from '../../../utils/trpc';
|
||||
import { StatusScreen } from '../../StatusScreen';
|
||||
|
||||
interface IProps {
|
||||
|
@ -9,19 +9,19 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const AuthProvider: React.FC<IProps> = ({ children }) => {
|
||||
const user = useMeQuery();
|
||||
const isConfigured = useConfiguredQuery();
|
||||
const loading = user.loading || isConfigured.loading;
|
||||
const me = trpc.auth.me.useQuery();
|
||||
const isConfigured = trpc.auth.isConfigured.useQuery();
|
||||
const loading = me.isLoading || isConfigured.isLoading;
|
||||
|
||||
if (loading && !user.data?.me) {
|
||||
if (loading) {
|
||||
return <StatusScreen title="" subtitle="" />;
|
||||
}
|
||||
|
||||
if (user.data?.me) {
|
||||
if (me.data) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!isConfigured?.data?.isConfigured) {
|
||||
if (!isConfigured?.data) {
|
||||
return <RegisterContainer />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,8 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, renderHook, screen } from '../../../../../tests/test-utils';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { useUIStore } from '../../../state/uiStore';
|
||||
import { Header } from './Header';
|
||||
|
||||
const logoutFn = jest.fn();
|
||||
const reloadFn = jest.fn();
|
||||
|
||||
jest.mock('../../../generated/graphql', () => ({
|
||||
useLogoutMutation: () => [logoutFn],
|
||||
}));
|
||||
|
||||
jest.mock('next/router', () => {
|
||||
const actualRouter = jest.requireActual('next-router-mock');
|
||||
|
||||
return {
|
||||
...actualRouter,
|
||||
reload: () => reloadFn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Header', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(<Header />);
|
||||
|
@ -63,19 +47,14 @@ describe('Header', () => {
|
|||
expect(result.current.darkMode).toBe(false);
|
||||
});
|
||||
|
||||
it('Should call the logout mutation on logout', () => {
|
||||
it('Should remove the token from local storage on logout', async () => {
|
||||
localStorage.setItem('token', 'token');
|
||||
const { container } = render(<Header />);
|
||||
const logoutButton = container.querySelector('[data-tip="Log out"]');
|
||||
fireEvent.click(logoutButton as Element);
|
||||
|
||||
expect(logoutFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should reload the page with next/router on logout', () => {
|
||||
const { container } = render(<Header />);
|
||||
const logoutButton = container.querySelector('[data-tip="Log out"]');
|
||||
fireEvent.click(logoutButton as Element);
|
||||
|
||||
expect(reloadFn).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem('token')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,12 +2,11 @@ import React from 'react';
|
|||
import { IconBrandGithub, IconHeart, IconLogout, IconMoon, IconSun } from '@tabler/icons';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import router from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import { getUrl } from '../../../core/helpers/url-helpers';
|
||||
import { useUIStore } from '../../../state/uiStore';
|
||||
import { NavBar } from '../NavBar';
|
||||
import { useLogoutMutation } from '../../../generated/graphql';
|
||||
import { trpc } from '../../../utils/trpc';
|
||||
|
||||
interface IProps {
|
||||
isUpdateAvailable?: boolean;
|
||||
|
@ -15,13 +14,13 @@ interface IProps {
|
|||
|
||||
export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
|
||||
const { setDarkMode } = useUIStore();
|
||||
const [logout] = useLogoutMutation();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
localStorage.removeItem('token');
|
||||
router.reload();
|
||||
};
|
||||
const utils = trpc.useContext();
|
||||
const logout = trpc.auth.logout.useMutation({
|
||||
onSuccess: () => {
|
||||
localStorage.removeItem('token');
|
||||
utils.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<header className="navbar navbar-expand-md navbar-dark navbar-overlap d-print-none">
|
||||
|
@ -67,7 +66,7 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable }) => {
|
|||
<div onClick={() => setDarkMode(false)} aria-hidden="true" className="nav-link px-0 hide-theme-light cursor-pointer" data-tip="Light mode">
|
||||
<IconSun data-testid="icon-sun" size={24} />
|
||||
</div>
|
||||
<div onClick={handleLogout} tabIndex={0} onKeyPress={handleLogout} role="button" className="nav-link px-0 cursor-pointer" data-tip="Log out">
|
||||
<div onClick={() => logout.mutate()} tabIndex={0} onKeyPress={() => logout.mutate()} role="button" className="nav-link px-0 cursor-pointer" data-tip="Log out">
|
||||
<IconLogout size={24} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -131,9 +131,6 @@ export type ListAppsResonse = {
|
|||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
installApp: App;
|
||||
login: TokenResponse;
|
||||
logout: Scalars['Boolean'];
|
||||
register: TokenResponse;
|
||||
startApp: App;
|
||||
stopApp: App;
|
||||
uninstallApp: App;
|
||||
|
@ -145,14 +142,6 @@ export type MutationInstallAppArgs = {
|
|||
input: AppInputType;
|
||||
};
|
||||
|
||||
export type MutationLoginArgs = {
|
||||
input: UsernamePasswordInput;
|
||||
};
|
||||
|
||||
export type MutationRegisterArgs = {
|
||||
input: UsernamePasswordInput;
|
||||
};
|
||||
|
||||
export type MutationStartAppArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
@ -177,21 +166,13 @@ export type Query = {
|
|||
__typename?: 'Query';
|
||||
getApp: App;
|
||||
installedApps: Array<App>;
|
||||
isConfigured: Scalars['Boolean'];
|
||||
listAppsInfo: ListAppsResonse;
|
||||
me?: Maybe<User>;
|
||||
refreshToken?: Maybe<TokenResponse>;
|
||||
};
|
||||
|
||||
export type QueryGetAppArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TokenResponse = {
|
||||
__typename?: 'TokenResponse';
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
__typename?: 'UpdateInfo';
|
||||
current: Scalars['Float'];
|
||||
|
@ -199,41 +180,12 @@ export type UpdateInfo = {
|
|||
latest: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['ID'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UsernamePasswordInput = {
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type InstallAppMutationVariables = Exact<{
|
||||
input: AppInputType;
|
||||
}>;
|
||||
|
||||
export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
|
||||
|
||||
export type LoginMutationVariables = Exact<{
|
||||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
|
||||
|
||||
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type LogoutMutation = { __typename?: 'Mutation'; logout: boolean };
|
||||
|
||||
export type RegisterMutationVariables = Exact<{
|
||||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
|
||||
|
||||
export type StartAppMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
}>;
|
||||
|
@ -326,10 +278,6 @@ export type InstalledAppsQuery = {
|
|||
}>;
|
||||
};
|
||||
|
||||
export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
|
||||
|
||||
export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type ListAppsQuery = {
|
||||
|
@ -353,14 +301,6 @@ export type ListAppsQuery = {
|
|||
};
|
||||
};
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
|
||||
|
||||
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
|
||||
|
||||
export const InstallAppDocument = gql`
|
||||
mutation InstallApp($input: AppInputType!) {
|
||||
installApp(input: $input) {
|
||||
|
@ -396,102 +336,6 @@ export function useInstallAppMutation(baseOptions?: Apollo.MutationHookOptions<I
|
|||
export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutation>;
|
||||
export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
|
||||
export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
|
||||
export const LoginDocument = gql`
|
||||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useLoginMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useLoginMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useLoginMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [loginMutation, { data, loading, error }] = useLoginMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
|
||||
}
|
||||
export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
|
||||
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
|
||||
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
|
||||
export const LogoutDocument = gql`
|
||||
mutation Logout {
|
||||
logout
|
||||
}
|
||||
`;
|
||||
export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useLogoutMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useLogoutMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useLogoutMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [logoutMutation, { data, loading, error }] = useLogoutMutation({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useLogoutMutation(baseOptions?: Apollo.MutationHookOptions<LogoutMutation, LogoutMutationVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<LogoutMutation, LogoutMutationVariables>(LogoutDocument, options);
|
||||
}
|
||||
export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
|
||||
export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
|
||||
export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
|
||||
export const RegisterDocument = gql`
|
||||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useRegisterMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useRegisterMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useRegisterMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [registerMutation, { data, loading, error }] = useRegisterMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRegisterMutation(baseOptions?: Apollo.MutationHookOptions<RegisterMutation, RegisterMutationVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<RegisterMutation, RegisterMutationVariables>(RegisterDocument, options);
|
||||
}
|
||||
export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
|
||||
export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
|
||||
export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
|
||||
export const StartAppDocument = gql`
|
||||
mutation StartApp($id: String!) {
|
||||
startApp(id: $id) {
|
||||
|
@ -789,38 +633,6 @@ export function useInstalledAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
|
|||
export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQuery>;
|
||||
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
|
||||
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
|
||||
export const ConfiguredDocument = gql`
|
||||
query Configured {
|
||||
isConfigured
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useConfiguredQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useConfiguredQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useConfiguredQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useConfiguredQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
|
||||
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
|
||||
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
|
||||
export const ListAppsDocument = gql`
|
||||
query ListApps {
|
||||
listAppsInfo {
|
||||
|
@ -867,71 +679,3 @@ export function useListAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<L
|
|||
export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
|
||||
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
|
||||
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
|
||||
export const MeDocument = gql`
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMeQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMeQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
||||
export const RefreshTokenDocument = gql`
|
||||
query RefreshToken {
|
||||
refreshToken {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRefreshTokenQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRefreshTokenQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRefreshTokenQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useRefreshTokenQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRefreshTokenQuery(baseOptions?: Apollo.QueryHookOptions<RefreshTokenQuery, RefreshTokenQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
|
||||
}
|
||||
export function useRefreshTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RefreshTokenQuery, RefreshTokenQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
|
||||
}
|
||||
export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery>;
|
||||
export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
|
||||
export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
mutation Logout {
|
||||
logout
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
query Configured {
|
||||
isConfigured
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
query Me {
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
query RefreshToken {
|
||||
refreshToken {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,75 +1,8 @@
|
|||
import { graphql } from 'msw';
|
||||
import { ConfiguredQuery, LoginMutation, LogoutMutationResult, MeQuery, RefreshTokenQuery, RegisterMutation, RegisterMutationVariables, UsernamePasswordInput } from '../generated/graphql';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { getTRPCMock } from './getTrpcMock';
|
||||
import appHandlers from './handlers/appHandlers';
|
||||
|
||||
const graphqlHandlers = [
|
||||
// Handles a "Login" mutation
|
||||
graphql.mutation('Login', (req, res, ctx) => {
|
||||
const { username } = req.variables as UsernamePasswordInput;
|
||||
sessionStorage.setItem('is-authenticated', username);
|
||||
|
||||
const result: LoginMutation = {
|
||||
login: { token: 'token' },
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
// Handles a "Logout" mutation
|
||||
graphql.mutation('Logout', (_req, res, ctx) => {
|
||||
sessionStorage.removeItem('is-authenticated');
|
||||
|
||||
const result: LogoutMutationResult['data'] = {
|
||||
logout: true,
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
// Handles me query
|
||||
graphql.query('Me', (_req, res, ctx) => {
|
||||
const isAuthenticated = sessionStorage.getItem('is-authenticated');
|
||||
if (!isAuthenticated) {
|
||||
return res(ctx.errors([{ message: 'Not authenticated' }]));
|
||||
}
|
||||
const result: MeQuery = {
|
||||
me: { id: '1' },
|
||||
};
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
graphql.query('RefreshToken', (_req, res, ctx) => {
|
||||
const result: RefreshTokenQuery = {
|
||||
refreshToken: { token: 'token' },
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
graphql.mutation('Register', (req, res, ctx) => {
|
||||
const {
|
||||
input: { username },
|
||||
} = req.variables as RegisterMutationVariables;
|
||||
|
||||
const result: RegisterMutation = {
|
||||
register: { token: 'token' },
|
||||
};
|
||||
|
||||
if (username === 'error@error.com') {
|
||||
return res(ctx.errors([{ message: 'Username is already taken' }]));
|
||||
}
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
appHandlers.listApps,
|
||||
appHandlers.getApp,
|
||||
appHandlers.installedApps,
|
||||
appHandlers.installApp,
|
||||
graphql.query('Configured', (_req, res, ctx) => {
|
||||
const result: ConfiguredQuery = {
|
||||
isConfigured: true,
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
];
|
||||
const graphqlHandlers = [appHandlers.listApps, appHandlers.getApp, appHandlers.installedApps, appHandlers.installApp];
|
||||
|
||||
export const handlers = [
|
||||
getTRPCMock({
|
||||
|
@ -94,5 +27,38 @@ export const handlers = [
|
|||
type: 'query',
|
||||
response: { cpu: { load: 0.1 }, disk: { available: 1, total: 2, used: 1 }, memory: { available: 1, total: 2, used: 1 } },
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'login'],
|
||||
type: 'mutation',
|
||||
response: { token: 'token' },
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'logout'],
|
||||
type: 'mutation',
|
||||
response: true,
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'refreshToken'],
|
||||
type: 'mutation',
|
||||
response: { token: 'token' },
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'register'],
|
||||
type: 'mutation',
|
||||
response: { token: 'token' },
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'me'],
|
||||
type: 'query',
|
||||
response: {
|
||||
id: faker.datatype.number(),
|
||||
username: faker.internet.userName(),
|
||||
},
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['auth', 'isConfigured'],
|
||||
type: 'query',
|
||||
response: true,
|
||||
}),
|
||||
...graphqlHandlers,
|
||||
];
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import { useMeQuery } from '../../../../generated/graphql';
|
||||
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { LoginContainer } from './LoginContainer';
|
||||
|
@ -40,21 +39,12 @@ describe('Test: LoginContainer', () => {
|
|||
expect(loginButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it('should call login mutation on submit', async () => {
|
||||
it('should add token in localStorage on submit', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
const password = faker.internet.password();
|
||||
const token = faker.datatype.uuid();
|
||||
|
||||
renderHook(() => useMeQuery());
|
||||
const loginFn = jest.fn();
|
||||
const fakeInstallHandler = graphql.mutation('Login', (req, res, ctx) => {
|
||||
loginFn(req.variables.input);
|
||||
sessionStorage.setItem('is-authenticated', email);
|
||||
return res(ctx.data({ login: { token } }));
|
||||
});
|
||||
|
||||
server.use(fakeInstallHandler);
|
||||
server.use(getTRPCMock({ path: ['auth', 'login'], type: 'mutation', response: { token } }));
|
||||
render(<LoginContainer />);
|
||||
|
||||
// Act
|
||||
|
@ -67,16 +57,15 @@ describe('Test: LoginContainer', () => {
|
|||
fireEvent.click(loginButton);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(loginFn).toHaveBeenCalledWith({ username: email, password }));
|
||||
expect(localStorage.getItem('token')).toEqual(token);
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem('token')).toEqual(token);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error message if login fails', async () => {
|
||||
// Arrange
|
||||
renderHook(() => useMeQuery());
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const fakeInstallHandler = graphql.mutation('Login', (req, res, ctx) => res(ctx.errors([{ message: 'my big error' }])));
|
||||
server.use(fakeInstallHandler);
|
||||
server.use(getTRPCMockError({ path: ['auth', 'login'], type: 'mutation', status: 500, message: 'my big error' }));
|
||||
render(<LoginContainer />);
|
||||
|
||||
// Act
|
||||
|
|
|
@ -1,51 +1,32 @@
|
|||
import { useApolloClient } from '@apollo/client';
|
||||
import React, { useState } from 'react';
|
||||
import { useLoginMutation } from '../../../../generated/graphql';
|
||||
import React from 'react';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { AuthFormLayout } from '../../components/AuthFormLayout';
|
||||
import { LoginForm } from '../../components/LoginForm';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
export const LoginContainer: React.FC = () => {
|
||||
const client = useApolloClient();
|
||||
const [login] = useLoginMutation({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addToast } = useToastStore();
|
||||
const utils = trpc.useContext();
|
||||
const login = trpc.auth.login.useMutation({
|
||||
onError: (e) => {
|
||||
localStorage.removeItem('token');
|
||||
addToast({ title: 'Login error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
localStorage.setItem('token', data.token);
|
||||
utils.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
localStorage.removeItem('token');
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (values: FormValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await login({ variables: { input: { username: values.email, password: values.password } } });
|
||||
|
||||
if (data?.login?.token) {
|
||||
localStorage.setItem('token', data.login.token);
|
||||
}
|
||||
|
||||
await client.refetchQueries({ include: ['Me'] });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const handlerSubmit = (values: FormValues) => {
|
||||
login.mutate({ username: values.email, password: values.password });
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthFormLayout>
|
||||
<LoginForm onSubmit={handleLogin} loading={loading} />
|
||||
<LoginForm onSubmit={handlerSubmit} loading={login.isLoading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import { useMeQuery } from '../../../../generated/graphql';
|
||||
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { RegisterContainer } from './RegisterContainer';
|
||||
|
@ -14,20 +13,13 @@ describe('Test: RegisterContainer', () => {
|
|||
expect(screen.getByText('Register')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call register mutation on submit', async () => {
|
||||
it('should add token in localStorage on submit', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
const password = faker.internet.password();
|
||||
const token = faker.datatype.uuid();
|
||||
|
||||
renderHook(() => useMeQuery());
|
||||
const registerFn = jest.fn();
|
||||
const fakeRegisterHandler = graphql.mutation('Register', (req, res, ctx) => {
|
||||
registerFn(req.variables.input);
|
||||
sessionStorage.setItem('is-authenticated', email);
|
||||
return res(ctx.data({ register: { token } }));
|
||||
});
|
||||
server.use(fakeRegisterHandler);
|
||||
server.use(getTRPCMock({ path: ['auth', 'register'], type: 'mutation', response: { token }, delay: 100 }));
|
||||
render(<RegisterContainer />);
|
||||
|
||||
// Act
|
||||
|
@ -42,8 +34,7 @@ describe('Test: RegisterContainer', () => {
|
|||
fireEvent.click(registerButton);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(registerFn).toHaveBeenCalledWith({ username: email, password }));
|
||||
expect(localStorage.getItem('token')).toEqual(token);
|
||||
await waitFor(() => expect(localStorage.getItem('token')).toEqual(token));
|
||||
});
|
||||
|
||||
it('should show toast if register mutation fails', async () => {
|
||||
|
@ -51,10 +42,8 @@ describe('Test: RegisterContainer', () => {
|
|||
const email = faker.internet.email();
|
||||
const password = faker.internet.password();
|
||||
|
||||
renderHook(() => useMeQuery());
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const fakeRegisterHandler = graphql.mutation('Register', (req, res, ctx) => res(ctx.errors([{ message: 'my big error' }])));
|
||||
server.use(fakeRegisterHandler);
|
||||
server.use(getTRPCMockError({ path: ['auth', 'register'], type: 'mutation', status: 500, message: 'my big error' }));
|
||||
render(<RegisterContainer />);
|
||||
|
||||
// Act
|
||||
|
|
|
@ -1,48 +1,32 @@
|
|||
import router from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useRegisterMutation } from '../../../../generated/graphql';
|
||||
import React from 'react';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { AuthFormLayout } from '../../components/AuthFormLayout';
|
||||
import { RegisterForm } from '../../components/RegisterForm';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
export const RegisterContainer: React.FC = () => {
|
||||
const { addToast } = useToastStore();
|
||||
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const utils = trpc.useContext();
|
||||
const register = trpc.auth.register.useMutation({
|
||||
onError: (e) => {
|
||||
localStorage.removeItem('token');
|
||||
addToast({ title: 'Register error', description: e.message, status: 'error' });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
localStorage.setItem('token', data.token);
|
||||
utils.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await register({ variables: { input: { username: values.email, password: values.password } } });
|
||||
|
||||
if (data?.register?.token) {
|
||||
localStorage.setItem('token', data.register.token);
|
||||
router.reload();
|
||||
} else {
|
||||
setLoading(false);
|
||||
handleError(new Error('Something went wrong'));
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
handleError(error);
|
||||
}
|
||||
const handlerSubmit = (value: FormValues) => {
|
||||
register.mutate({ username: value.email, password: value.password });
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthFormLayout>
|
||||
<RegisterForm onSubmit={handleRegister} loading={loading} />
|
||||
<RegisterForm onSubmit={handlerSubmit} loading={register.isLoading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { httpBatchLink } from '@trpc/client';
|
||||
import { httpBatchLink, loggerLink } from '@trpc/client';
|
||||
import { createTRPCNext } from '@trpc/next';
|
||||
import superjson from 'superjson';
|
||||
import type { AppRouter } from '../../server/routers/_app';
|
||||
|
@ -22,9 +22,9 @@ export const trpc = createTRPCNext<AppRouter>({
|
|||
*/
|
||||
transformer: superjson,
|
||||
links: [
|
||||
// loggerLink({
|
||||
// enabled: (opts) => process.env.NODE_ENV === 'development' || (opts.direction === 'down' && opts.result instanceof Error),
|
||||
// }),
|
||||
loggerLink({
|
||||
enabled: (opts) => process.env.NODE_ENV === 'development' || (opts.direction === 'down' && opts.result instanceof Error),
|
||||
}),
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers() {
|
||||
|
|
|
@ -15,14 +15,9 @@ import { SystemStatus, useSystemStore } from '../client/state/systemStore';
|
|||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { setDarkMode } = useUIStore();
|
||||
const status = trpc.system.status.useQuery(undefined, { refetchInterval: 5000 });
|
||||
const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
|
||||
|
||||
const { setStatus, setVersion } = useSystemStore();
|
||||
|
||||
useEffect(() => {
|
||||
setStatus(status.data?.status || SystemStatus.RUNNING);
|
||||
}, [status.data?.status, setStatus]);
|
||||
trpc.system.status.useQuery(undefined, { refetchInterval: 50000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
|
||||
const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
|
||||
|
||||
useEffect(() => {
|
||||
if (version.data) {
|
||||
|
|
Loading…
Reference in a new issue