浏览代码

refactor: migrate client auth queries to trpc procedures

Nicolas Meienberger 2 年之前
父节点
当前提交
f6a6b85b60
共有 20 个文件被更改,包括 135 次插入530 次删除
  1. 11 8
      packages/dashboard/src/client/components/Layout/Layout.tsx
  2. 10 8
      packages/dashboard/src/client/components/Layout/LayoutV2.tsx
  3. 5 5
      packages/dashboard/src/client/components/hoc/AuthProvider/AuthProvider.test.tsx
  4. 7 7
      packages/dashboard/src/client/components/hoc/AuthProvider/AuthProvider.tsx
  5. 6 27
      packages/dashboard/src/client/components/ui/Header/Header.test.tsx
  6. 9 10
      packages/dashboard/src/client/components/ui/Header/Header.tsx
  7. 0 256
      packages/dashboard/src/client/generated/graphql.tsx
  8. 0 5
      packages/dashboard/src/client/graphql/mutations/login.graphql
  9. 0 3
      packages/dashboard/src/client/graphql/mutations/logout.graphql
  10. 0 5
      packages/dashboard/src/client/graphql/mutations/register.graphql
  11. 0 3
      packages/dashboard/src/client/graphql/queries/isConfigured.graphql
  12. 0 5
      packages/dashboard/src/client/graphql/queries/me.graphql
  13. 0 5
      packages/dashboard/src/client/graphql/queries/refreshToken.graphql
  14. 35 69
      packages/dashboard/src/client/mocks/handlers.ts
  15. 7 18
      packages/dashboard/src/client/modules/Auth/containers/LoginContainer/LoginContainer.test.tsx
  16. 16 35
      packages/dashboard/src/client/modules/Auth/containers/LoginContainer/LoginContainer.tsx
  17. 5 16
      packages/dashboard/src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.test.tsx
  18. 18 34
      packages/dashboard/src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx
  19. 4 4
      packages/dashboard/src/client/utils/trpc.ts
  20. 2 7
      packages/dashboard/src/pages/_app.tsx

+ 11 - 8
packages/dashboard/src/client/components/Layout/Layout.tsx

@@ -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;

+ 10 - 8
packages/dashboard/src/client/components/Layout/LayoutV2.tsx

@@ -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;

+ 5 - 5
packages/dashboard/src/client/components/hoc/AuthProvider/AuthProvider.test.tsx

@@ -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();
   });
 });

+ 7 - 7
packages/dashboard/src/client/components/hoc/AuthProvider/AuthProvider.tsx

@@ -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 />;
   }
 

+ 6 - 27
packages/dashboard/src/client/components/ui/Header/Header.test.tsx

@@ -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', () => {
-    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', () => {
+  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(reloadFn).toHaveBeenCalledTimes(1);
+    await waitFor(() => {
+      expect(localStorage.getItem('token')).toBeNull();
+    });
   });
 });

+ 9 - 10
packages/dashboard/src/client/components/ui/Header/Header.tsx

@@ -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>

+ 0 - 256
packages/dashboard/src/client/generated/graphql.tsx

@@ -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>;

+ 0 - 5
packages/dashboard/src/client/graphql/mutations/login.graphql

@@ -1,5 +0,0 @@
-mutation Login($input: UsernamePasswordInput!) {
-  login(input: $input) {
-    token
-  }
-}

+ 0 - 3
packages/dashboard/src/client/graphql/mutations/logout.graphql

@@ -1,3 +0,0 @@
-mutation Logout {
-  logout
-}

+ 0 - 5
packages/dashboard/src/client/graphql/mutations/register.graphql

@@ -1,5 +0,0 @@
-mutation Register($input: UsernamePasswordInput!) {
-  register(input: $input) {
-    token
-  }
-}

+ 0 - 3
packages/dashboard/src/client/graphql/queries/isConfigured.graphql

@@ -1,3 +0,0 @@
-query Configured {
-  isConfigured
-}

+ 0 - 5
packages/dashboard/src/client/graphql/queries/me.graphql

@@ -1,5 +0,0 @@
-query Me {
-  me {
-    id
-  }
-}

+ 0 - 5
packages/dashboard/src/client/graphql/queries/refreshToken.graphql

@@ -1,5 +0,0 @@
-query RefreshToken {
-  refreshToken {
-    token
-  }
-}

+ 35 - 69
packages/dashboard/src/client/mocks/handlers.ts

@@ -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,
 ];

+ 7 - 18
packages/dashboard/src/client/modules/Auth/containers/LoginContainer/LoginContainer.test.tsx

@@ -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

+ 16 - 35
packages/dashboard/src/client/modules/Auth/containers/LoginContainer/LoginContainer.tsx

@@ -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>
   );
 };

+ 5 - 16
packages/dashboard/src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.test.tsx

@@ -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

+ 18 - 34
packages/dashboard/src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx

@@ -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 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 } } });
+  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();
+    },
+  });
 
-      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>
   );
 };

+ 4 - 4
packages/dashboard/src/client/utils/trpc.ts

@@ -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() {

+ 2 - 7
packages/dashboard/src/pages/_app.tsx

@@ -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) {