Explorar o código

feat: move security form to rsc

Nicolas Meienberger hai 1 ano
pai
achega
1b434e7355

+ 0 - 0
src/client/modules/Settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx → src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.test.tsx


+ 18 - 16
src/client/modules/Settings/components/ChangePasswordForm/ChangePasswordForm.tsx → src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.tsx

@@ -1,17 +1,16 @@
 import React from 'react';
 import React from 'react';
 import { Input } from '@/components/ui/Input';
 import { Input } from '@/components/ui/Input';
 import { Button } from '@/components/ui/Button';
 import { Button } from '@/components/ui/Button';
-import { trpc } from '@/utils/trpc';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { z } from 'zod';
 import { z } from 'zod';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
 import { toast } from 'react-hot-toast';
 import { toast } from 'react-hot-toast';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
-import type { MessageKey } from '@/server/utils/errors';
+import { useAction } from 'next-safe-action/hook';
+import { changePasswordAction } from '@/actions/settings/change-password';
 
 
 export const ChangePasswordForm = () => {
 export const ChangePasswordForm = () => {
-  const globalT = useTranslations();
   const t = useTranslations('settings.security');
   const t = useTranslations('settings.security');
 
 
   const schema = z
   const schema = z
@@ -29,16 +28,19 @@ export const ChangePasswordForm = () => {
         });
         });
       }
       }
     });
     });
+
   type FormValues = z.infer<typeof schema>;
   type FormValues = z.infer<typeof schema>;
 
 
   const router = useRouter();
   const router = useRouter();
-  const changePassword = trpc.auth.changePassword.useMutation({
-    onError: (e) => {
-      toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
-    },
-    onSuccess: () => {
-      toast.success(t('password-change-success'));
-      router.push('/');
+
+  const changePasswordMutation = useAction(changePasswordAction, {
+    onSuccess: (data) => {
+      if (!data.success) {
+        toast.error(data.failure.reason);
+      } else {
+        toast.success(t('password-change-success'));
+        router.push('/');
+      }
     },
     },
   });
   });
 
 
@@ -51,22 +53,22 @@ export const ChangePasswordForm = () => {
   });
   });
 
 
   const onSubmit = (values: FormValues) => {
   const onSubmit = (values: FormValues) => {
-    changePassword.mutate(values);
+    changePasswordMutation.execute(values);
   };
   };
 
 
   return (
   return (
     <form onSubmit={handleSubmit(onSubmit)} className="mb-4 w-100 ">
     <form onSubmit={handleSubmit(onSubmit)} className="mb-4 w-100 ">
-      <Input disabled={changePassword.isLoading} {...register('currentPassword')} error={errors.currentPassword?.message} type="password" placeholder={t('form.current-password')} />
-      <Input disabled={changePassword.isLoading} {...register('newPassword')} error={errors.newPassword?.message} className="mt-2" type="password" placeholder={t('form.new-password')} />
+      <Input disabled={changePasswordMutation.isExecuting} {...register('currentPassword')} error={errors.currentPassword?.message} type="password" placeholder={t('form.current-password')} />
+      <Input disabled={changePasswordMutation.isExecuting} {...register('newPassword')} error={errors.newPassword?.message} className="mt-2" type="password" placeholder={t('form.new-password')} />
       <Input
       <Input
-        disabled={changePassword.isLoading}
+        disabled={changePasswordMutation.isExecuting}
         {...register('newPasswordConfirm')}
         {...register('newPasswordConfirm')}
         error={errors.newPasswordConfirm?.message}
         error={errors.newPasswordConfirm?.message}
         className="mt-2"
         className="mt-2"
         type="password"
         type="password"
         placeholder={t('form.confirm-password')}
         placeholder={t('form.confirm-password')}
       />
       />
-      <Button disabled={changePassword.isLoading} className="mt-3" type="submit">
+      <Button disabled={changePasswordMutation.isExecuting} className="mt-3" type="submit">
         {t('form.change-password')}
         {t('form.change-password')}
       </Button>
       </Button>
     </form>
     </form>

+ 0 - 0
src/client/modules/Settings/components/ChangePasswordForm/index.ts → src/app/(dashboard)/settings/components/ChangePasswordForm/index.ts


+ 0 - 0
src/client/modules/Settings/components/OtpForm/OptForm.test.tsx → src/app/(dashboard)/settings/components/OtpForm/OptForm.test.tsx


+ 45 - 44
src/client/modules/Settings/components/OtpForm/OtpForm.tsx → src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
 import React from 'react';
-import { trpc } from '@/utils/trpc';
 import { Switch } from '@/components/ui/Switch';
 import { Switch } from '@/components/ui/Switch';
 import { Button } from '@/components/ui/Button';
 import { Button } from '@/components/ui/Button';
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
@@ -9,10 +8,13 @@ import { OtpInput } from '@/components/ui/OtpInput';
 import { toast } from 'react-hot-toast';
 import { toast } from 'react-hot-toast';
 import { useDisclosure } from '@/client/hooks/useDisclosure';
 import { useDisclosure } from '@/client/hooks/useDisclosure';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
-import type { MessageKey } from '@/server/utils/errors';
+import { useAction } from 'next-safe-action/hook';
+import { getTotpUriAction } from '@/actions/settings/get-totp-uri';
+import { setupTotpAction } from '@/actions/settings/setup-totp-action';
+import { disableTotpAction } from '@/actions/settings/disable-totp';
 
 
-export const OtpForm = () => {
-  const globalT = useTranslations();
+export const OtpForm = (props: { totpEnabled: boolean }) => {
+  const { totpEnabled } = props;
   const t = useTranslations('settings.security');
   const t = useTranslations('settings.security');
   const [password, setPassword] = React.useState('');
   const [password, setPassword] = React.useState('');
   const [key, setKey] = React.useState('');
   const [key, setKey] = React.useState('');
@@ -23,54 +25,53 @@ export const OtpForm = () => {
   const setupOtpDisclosure = useDisclosure();
   const setupOtpDisclosure = useDisclosure();
   const disableOtpDisclosure = useDisclosure();
   const disableOtpDisclosure = useDisclosure();
 
 
-  const ctx = trpc.useContext();
-  const me = trpc.auth.me.useQuery();
-
-  const getTotpUri = trpc.auth.getTotpUri.useMutation({
-    onMutate: () => {
+  const getTotpUriMutation = useAction(getTotpUriAction, {
+    onExecute: () => {
       setupOtpDisclosure.close();
       setupOtpDisclosure.close();
     },
     },
-    onError: (e) => {
-      setPassword('');
-      toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
-    },
     onSuccess: (data) => {
     onSuccess: (data) => {
-      setKey(data.key);
-      setUri(data.uri);
+      if (!data.success) {
+        setPassword('');
+        toast.error(data.failure.reason);
+      } else {
+        setKey(data.key);
+        setUri(data.uri);
+      }
     },
     },
   });
   });
 
 
-  const setupTotp = trpc.auth.setupTotp.useMutation({
-    onMutate: () => {},
-    onError: (e) => {
-      setTotpCode('');
-      toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
-    },
-    onSuccess: () => {
-      setTotpCode('');
-      setKey('');
-      setUri('');
-      toast.success(t('2fa-enable-success'));
-      ctx.auth.me.invalidate();
+  const setupTotpMutation = useAction(setupTotpAction, {
+    onSuccess: (data) => {
+      if (!data.success) {
+        setTotpCode('');
+        toast.error(data.failure.reason);
+      } else {
+        setTotpCode('');
+        setKey('');
+        setUri('');
+        toast.success(t('2fa-enable-success'));
+        // ctx.auth.me.invalidate();
+      }
     },
     },
   });
   });
 
 
-  const disableTotp = trpc.auth.disableTotp.useMutation({
-    onMutate: () => {
+  const disableTotpMutation = useAction(disableTotpAction, {
+    onExecute: () => {
       disableOtpDisclosure.close();
       disableOtpDisclosure.close();
     },
     },
-    onError: (e) => {
-      setPassword('');
-      toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
-    },
-    onSuccess: () => {
-      toast.success(t('2fa-disable-success'));
-      ctx.auth.me.invalidate();
+    onSuccess: (data) => {
+      if (!data.success) {
+        setPassword('');
+        toast.error(data.failure.reason);
+      } else {
+        toast.success(t('2fa-disable-success'));
+        //ctx.auth.me.invalidate();
+      }
     },
     },
   });
   });
 
 
   const renderSetupQr = () => {
   const renderSetupQr = () => {
-    if (!uri || me.data?.totpEnabled) return null;
+    if (!uri || totpEnabled) return null;
 
 
     return (
     return (
       <div className="mt-4">
       <div className="mt-4">
@@ -85,7 +86,7 @@ export const OtpForm = () => {
         <div className="mb-4">
         <div className="mb-4">
           <p className="text-muted">{t('enter-2fa-code')}</p>
           <p className="text-muted">{t('enter-2fa-code')}</p>
           <OtpInput value={totpCode} valueLength={6} onChange={(e) => setTotpCode(e)} />
           <OtpInput value={totpCode} valueLength={6} onChange={(e) => setTotpCode(e)} />
-          <Button disabled={totpCode.trim().length < 6} onClick={() => setupTotp.mutate({ totpCode })} className="mt-3 btn-success">
+          <Button disabled={totpCode.trim().length < 6} onClick={() => setupTotpMutation.execute({ totpCode })} className="mt-3 btn-success">
             {t('enable-2fa')}
             {t('enable-2fa')}
           </Button>
           </Button>
         </div>
         </div>
@@ -103,8 +104,8 @@ export const OtpForm = () => {
 
 
   return (
   return (
     <>
     <>
-      {!key && <Switch disabled={!me.isSuccess} onCheckedChange={handleTotp} checked={me.data?.totpEnabled} label={t('enable-2fa')} />}
-      {getTotpUri.isLoading && (
+      {!key && <Switch onCheckedChange={handleTotp} checked={totpEnabled} label={t('enable-2fa')} />}
+      {getTotpUriMutation.isExecuting && (
         <div className="progress w-50">
         <div className="progress w-50">
           <div className="progress-bar progress-bar-indeterminate bg-green" />
           <div className="progress-bar progress-bar-indeterminate bg-green" />
         </div>
         </div>
@@ -119,12 +120,12 @@ export const OtpForm = () => {
             <form
             <form
               onSubmit={(e) => {
               onSubmit={(e) => {
                 e.preventDefault();
                 e.preventDefault();
-                getTotpUri.mutate({ password });
+                getTotpUriMutation.execute({ password });
               }}
               }}
             >
             >
               <p className="text-muted">{t('password-needed-hint')}</p>
               <p className="text-muted">{t('password-needed-hint')}</p>
               <Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
               <Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
-              <Button loading={getTotpUri.isLoading} type="submit" className="btn-success mt-3">
+              <Button loading={getTotpUriMutation.isExecuting} type="submit" className="btn-success mt-3">
                 {t('enable-2fa')}
                 {t('enable-2fa')}
               </Button>
               </Button>
             </form>
             </form>
@@ -140,12 +141,12 @@ export const OtpForm = () => {
             <form
             <form
               onSubmit={(e) => {
               onSubmit={(e) => {
                 e.preventDefault();
                 e.preventDefault();
-                disableTotp.mutate({ password });
+                disableTotpMutation.execute({ password });
               }}
               }}
             >
             >
               <p className="text-muted">{t('password-needed-hint')}</p>
               <p className="text-muted">{t('password-needed-hint')}</p>
               <Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
               <Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
-              <Button loading={disableTotp.isLoading} type="submit" className="btn-danger mt-3">
+              <Button loading={disableTotpMutation.isExecuting} type="submit" className="btn-danger mt-3">
                 {t('disable-2fa')}
                 {t('disable-2fa')}
               </Button>
               </Button>
             </form>
             </form>

+ 0 - 0
src/client/modules/Settings/components/OtpForm/index.ts → src/app/(dashboard)/settings/components/OtpForm/index.ts


+ 0 - 0
src/client/modules/Settings/containers/SecurityContainer/SecurityContainer.test.tsx → src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.test.tsx


+ 8 - 4
src/client/modules/Settings/containers/SecurityContainer/SecurityContainer.tsx → src/app/(dashboard)/settings/components/SecurityContainer/SecurityContainer.tsx

@@ -1,11 +1,15 @@
+'use client';
+
 import React from 'react';
 import React from 'react';
 import { IconLock, IconKey } from '@tabler/icons-react';
 import { IconLock, IconKey } from '@tabler/icons-react';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
-import { OtpForm } from '../../components/OtpForm';
-import { ChangePasswordForm } from '../../components/ChangePasswordForm';
+import { OtpForm } from '../OtpForm';
+import { ChangePasswordForm } from '../ChangePasswordForm';
 
 
-export const SecurityContainer = () => {
+export const SecurityContainer = (props: { totpEnabled: boolean }) => {
+  const { totpEnabled } = props;
   const t = useTranslations('settings.security');
   const t = useTranslations('settings.security');
+
   return (
   return (
     <div className="card-body">
     <div className="card-body">
       <div className="d-flex">
       <div className="d-flex">
@@ -23,7 +27,7 @@ export const SecurityContainer = () => {
         <br />
         <br />
         {t('2fa-subtitle-2')}
         {t('2fa-subtitle-2')}
       </p>
       </p>
-      <OtpForm />
+      <OtpForm totpEnabled={totpEnabled} />
     </div>
     </div>
   );
   );
 };
 };

+ 0 - 0
src/client/modules/Settings/containers/SecurityContainer/index.ts → src/app/(dashboard)/settings/components/SecurityContainer/index.ts


+ 6 - 1
src/app/(dashboard)/settings/page.tsx

@@ -8,6 +8,8 @@ import { getCurrentLocale } from 'src/utils/getCurrentLocale';
 import { SettingsTabTriggers } from './components/SettingsTabTriggers';
 import { SettingsTabTriggers } from './components/SettingsTabTriggers';
 import { GeneralActions } from './components/GeneralActions';
 import { GeneralActions } from './components/GeneralActions';
 import { SettingsContainer } from './components/SettingsContainer';
 import { SettingsContainer } from './components/SettingsContainer';
+import { SecurityContainer } from './components/SecurityContainer';
+import { getUserFromCookie } from '@/server/common/session.helpers';
 
 
 export async function generateMetadata(): Promise<Metadata> {
 export async function generateMetadata(): Promise<Metadata> {
   const translator = await getTranslatorFromCookie();
   const translator = await getTranslatorFromCookie();
@@ -23,6 +25,7 @@ export default async function SettingsPage({ searchParams }: { searchParams: { t
   const version = await systemService.getVersion();
   const version = await systemService.getVersion();
   const settings = getSettings();
   const settings = getSettings();
   const locale = getCurrentLocale();
   const locale = getCurrentLocale();
+  const user = await getUserFromCookie();
 
 
   return (
   return (
     <div className="card d-flex">
     <div className="card d-flex">
@@ -34,7 +37,9 @@ export default async function SettingsPage({ searchParams }: { searchParams: { t
         <TabsContent value="settings">
         <TabsContent value="settings">
           <SettingsContainer initialValues={settings} currentLocale={locale} />
           <SettingsContainer initialValues={settings} currentLocale={locale} />
         </TabsContent>
         </TabsContent>
-        <TabsContent value="security">{/* <SecurityContainer /> */}</TabsContent>
+        <TabsContent value="security">
+          <SecurityContainer totpEnabled={Boolean(user?.totpEnabled)} />
+        </TabsContent>
       </Tabs>
       </Tabs>
     </div>
     </div>
   );
   );

+ 31 - 0
src/app/actions/settings/change-password.ts

@@ -0,0 +1,31 @@
+'use server';
+
+import { z } from 'zod';
+import { action } from '@/lib/safe-action';
+import { getUserFromCookie } from '@/server/common/session.helpers';
+import { AuthServiceClass } from '@/server/services/auth/auth.service';
+import { db } from '@/server/db';
+import { handleActionError } from '../utils/handle-action-error';
+
+const input = z.object({ currentPassword: z.string(), newPassword: z.string() });
+
+/**
+ * Given the current password and a new password, change the password of the current user.
+ */
+export const changePasswordAction = action(input, async ({ currentPassword, newPassword }) => {
+  try {
+    const user = await getUserFromCookie();
+
+    if (!user) {
+      throw new Error('User not found');
+    }
+
+    const authService = new AuthServiceClass(db);
+
+    await authService.changePassword({ userId: user.id, currentPassword, newPassword });
+
+    return { success: true };
+  } catch (e) {
+    return handleActionError(e);
+  }
+});

+ 30 - 0
src/app/actions/settings/disable-totp.ts

@@ -0,0 +1,30 @@
+'use server';
+
+import { z } from 'zod';
+import { action } from '@/lib/safe-action';
+import { getUserFromCookie } from '@/server/common/session.helpers';
+import { AuthServiceClass } from '@/server/services/auth/auth.service';
+import { db } from '@/server/db';
+import { handleActionError } from '../utils/handle-action-error';
+
+const input = z.object({ password: z.string() });
+
+/**
+ * Given a valid user password, disable TOTP for the user
+ */
+export const disableTotpAction = action(input, async ({ password }) => {
+  try {
+    const user = await getUserFromCookie();
+
+    if (!user) {
+      throw new Error('User not found');
+    }
+
+    const authService = new AuthServiceClass(db);
+    await authService.disableTotp({ userId: user.id, password });
+
+    return { success: true };
+  } catch (e) {
+    return handleActionError(e);
+  }
+});

+ 30 - 0
src/app/actions/settings/get-totp-uri.ts

@@ -0,0 +1,30 @@
+'use server';
+
+import { z } from 'zod';
+import { action } from '@/lib/safe-action';
+import { getUserFromCookie } from '@/server/common/session.helpers';
+import { AuthServiceClass } from '@/server/services/auth/auth.service';
+import { db } from '@/server/db';
+import { handleActionError } from '../utils/handle-action-error';
+
+const input = z.object({ password: z.string() });
+
+/**
+ * Given user's password, return the TOTP URI and key
+ */
+export const getTotpUriAction = action(input, async ({ password }) => {
+  try {
+    const user = await getUserFromCookie();
+
+    if (!user) {
+      throw new Error('User not found');
+    }
+
+    const authService = new AuthServiceClass(db);
+    const { key, uri } = await authService.getTotpUri({ userId: user.id, password });
+
+    return { success: true, key, uri };
+  } catch (e) {
+    return handleActionError(e);
+  }
+});

+ 30 - 0
src/app/actions/settings/setup-totp-action.ts

@@ -0,0 +1,30 @@
+'use server';
+
+import { z } from 'zod';
+import { action } from '@/lib/safe-action';
+import { getUserFromCookie } from '@/server/common/session.helpers';
+import { AuthServiceClass } from '@/server/services/auth/auth.service';
+import { db } from '@/server/db';
+import { handleActionError } from '../utils/handle-action-error';
+
+const input = z.object({ totpCode: z.string() });
+
+/**
+ * Given a valid user's TOTP code, activate TOTP for the user
+ */
+export const setupTotpAction = action(input, async ({ totpCode }) => {
+  try {
+    const user = await getUserFromCookie();
+
+    if (!user) {
+      throw new Error('User not found');
+    }
+
+    const authService = new AuthServiceClass(db);
+    await authService.setupTotp({ userId: user.id, totpCode });
+
+    return { success: true };
+  } catch (e) {
+    return handleActionError(e);
+  }
+});

+ 2 - 3
src/app/actions/settings/update-settings.ts

@@ -1,6 +1,5 @@
 'use server';
 'use server';
 
 
-import { z } from 'zod';
 import { action } from '@/lib/safe-action';
 import { action } from '@/lib/safe-action';
 import { getUserFromCookie } from '@/server/common/session.helpers';
 import { getUserFromCookie } from '@/server/common/session.helpers';
 import { settingsSchema } from '@runtipi/shared';
 import { settingsSchema } from '@runtipi/shared';
@@ -10,7 +9,7 @@ import { handleActionError } from '../utils/handle-action-error';
 /**
 /**
  * Given a settings object, update the settings.json file
  * Given a settings object, update the settings.json file
  */
  */
-export const updateSettingsAction = action(settingsSchema, async () => {
+export const updateSettingsAction = action(settingsSchema, async (settings) => {
   try {
   try {
     const user = await getUserFromCookie();
     const user = await getUserFromCookie();
 
 
@@ -18,7 +17,7 @@ export const updateSettingsAction = action(settingsSchema, async () => {
       throw new Error('Not authorized');
       throw new Error('Not authorized');
     }
     }
 
 
-    await setSettings(settingsSchema as z.infer<typeof settingsSchema>);
+    await setSettings(settings);
 
 
     return { success: true };
     return { success: true };
   } catch (e) {
   } catch (e) {

+ 1 - 1
src/server/core/TipiConfig/TipiConfig.ts

@@ -5,7 +5,7 @@ import nextConfig from 'next/config';
 import { readJsonFile } from '../../common/fs.helpers';
 import { readJsonFile } from '../../common/fs.helpers';
 import { Logger } from '../Logger';
 import { Logger } from '../Logger';
 
 
-type TipiSettingsType = z.infer<typeof settingsSchema>;
+type TipiSettingsType = z.input<typeof settingsSchema>;
 
 
 const formatErrors = (errors: { fieldErrors: Record<string, string[]> }) =>
 const formatErrors = (errors: { fieldErrors: Record<string, string[]> }) =>
   Object.entries(errors.fieldErrors)
   Object.entries(errors.fieldErrors)

+ 0 - 8
src/server/routers/auth/auth.router.ts

@@ -8,12 +8,4 @@ const AuthService = new AuthServiceClass(db);
 export const authRouter = router({
 export const authRouter = router({
   me: publicProcedure.query(async ({ ctx }) => AuthService.me(ctx.userId)),
   me: publicProcedure.query(async ({ ctx }) => AuthService.me(ctx.userId)),
   changeLocale: protectedProcedure.input(z.object({ locale: z.string() })).mutation(async ({ input, ctx }) => AuthService.changeLocale({ userId: Number(ctx.userId), locale: input.locale })),
   changeLocale: protectedProcedure.input(z.object({ locale: z.string() })).mutation(async ({ input, ctx }) => AuthService.changeLocale({ userId: Number(ctx.userId), locale: input.locale })),
-  // Password
-  changePassword: protectedProcedure
-    .input(z.object({ currentPassword: z.string(), newPassword: z.string() }))
-    .mutation(({ input, ctx }) => AuthService.changePassword({ userId: Number(ctx.userId), ...input })),
-  // Totp
-  getTotpUri: protectedProcedure.input(z.object({ password: z.string() })).mutation(({ input, ctx }) => AuthService.getTotpUri({ userId: Number(ctx.userId), password: input.password })),
-  setupTotp: protectedProcedure.input(z.object({ totpCode: z.string() })).mutation(({ input, ctx }) => AuthService.setupTotp({ userId: Number(ctx.userId), totpCode: input.totpCode })),
-  disableTotp: protectedProcedure.input(z.object({ password: z.string() })).mutation(({ input, ctx }) => AuthService.disableTotp({ userId: Number(ctx.userId), password: input.password })),
 });
 });