Explorar el Código

add account update view incl delete flow

Aneurin Barker Snook hace 1 año
padre
commit
d900326cec
Se han modificado 3 ficheros con 177 adiciones y 2 borrados
  1. 2 2
      web/src/providers/session.ts
  2. 5 0
      web/src/routes.tsx
  3. 170 0
      web/src/views/AccountSettingsView.tsx

+ 2 - 2
web/src/providers/session.ts

@@ -36,9 +36,9 @@ export function SessionProvider({ children, value: { authStorage } }: ProviderPr
 
   async function heartbeat(token?: string) {
     try {
-      const res = await api.getAccount({ ...options, token })
+      const res = await api.getAccount({ ...options, token: token || options.token })
       setAccount(res.account)
-      setToken(token)
+      if (token) setToken(token)
       setLoggedIn(true)
     } catch (err) {
       const status = (err as api.RequestError).xhr?.status || 500

+ 5 - 0
web/src/routes.tsx

@@ -7,12 +7,17 @@ import LoginView from './views/LoginView'
 import { Outlet } from 'react-router-dom'
 import type { RouteObject } from 'react-router-dom'
 import CreateAccountView from './views/CreateAccountView'
+import AccountSettingsView from './views/AccountSettingsView'
 
 const coreRoutes: RouteObject[] = [
   {
     path: '',
     element: <HerdListView />,
   },
+  {
+    path: '/account/settings',
+    element: <AccountSettingsView />,
+  },
   {
     path: '/herd/:id',
     element: <HerdView />,

+ 170 - 0
web/src/views/AccountSettingsView.tsx

@@ -0,0 +1,170 @@
+import ButtonSet from '@/components/ButtonSet'
+import Chip from '@/components/Chip'
+import CreateButton from '@/components/button/CreateButton'
+import FormGroup from '@/components/form/FormGroup'
+import FormInput from '@/components/form/FormInput'
+import { Link } from 'react-router-dom'
+import LoadingIndicator from '@/components/LoadingIndicator'
+import Main from '@/components/Main'
+import Notice from '@/components/Notice'
+import Pagination from '@/components/Pagination'
+import ResetButton from '@/components/button/ResetButton'
+import Row from '@/components/Row'
+import SaveButton from '@/components/button/SaveButton'
+import SearchForm from '@/components/SearchForm'
+import api from '@/api'
+import { useForm } from 'react-hook-form'
+import { useNavigate } from 'react-router-dom'
+import { useRouteSearch } from '@/hooks'
+import { useCallback, useEffect, useState } from 'react'
+import { useConnection, useSession } from '@/hooks'
+import Placeholder from '@/components/Placeholder'
+import { CloudIcon, InboxIcon, TrashIcon } from '@heroicons/react/20/solid'
+import HideShowButton from '@/components/button/HideShowButton'
+import Button from '@/components/button/Button'
+import DeleteButton from '@/components/button/DeleteButton'
+
+interface AccountUpdateFormData extends Pick<api.Account, 'email' | 'password'> {}
+
+function useHerdCreateForm() {
+  const form = useForm<AccountUpdateFormData>({ mode: 'onBlur' })
+
+  const inputs = {
+    email: form.register('email', { validate: {
+      required: value => value.length >= 1 || 'Required',
+    }}),
+    password: form.register('password', { validate: value => {
+      if (!value) return
+      if (value.length < 8) return 'Must be at least 8 characters'
+    }}),
+  }
+
+  return { ...form, inputs }
+}
+
+export default function AccountSettingsView() {
+  const { account, ...session } = useSession()
+  const updateAccountForm = useHerdCreateForm()
+  const navigate = useNavigate()
+  const { options } = useConnection()
+
+  const [busy, setBusy] = useState(false)
+  const [error, setError] = useState<Error>()
+  const [passwordVisible, setPasswordVisible] = useState(false)
+
+  async function deleteAccount() {
+    if (busy || !account) return
+
+    try {
+      setBusy(true)
+      setError(undefined)
+      await api.deleteAccount(options)
+      session.logout()
+    } catch (err) {
+      setError(err as Error)
+    } finally {
+      setBusy(false)
+    }
+  }
+
+  function resetAccountUpdate() {
+    if (!account) return
+
+    updateAccountForm.reset({
+      email: account.email,
+      password: '',
+    })
+  }
+
+  async function updateAccount(data: AccountUpdateFormData) {
+    if (busy || !account) return
+
+    try {
+      setBusy(true)
+      setError(undefined)
+      await api.updateAccount(options, account._id, {
+        account: {
+          email: data.email || undefined,
+          password: data.password || undefined,
+        },
+      })
+      await session.heartbeat()
+      resetAccountUpdate()
+    } catch (err) {
+      setError(err as Error)
+    } finally {
+      setBusy(false)
+    }
+  }
+
+  useEffect(() => {
+    resetAccountUpdate()
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  return account && (
+    <Main>
+      <header>
+        <h1>Your account</h1>
+
+        <ButtonSet>
+          <Button className="fill" onClick={() => navigate('/')}>
+            <InboxIcon />
+            <span>Back to Herds</span>
+          </Button>
+        </ButtonSet>
+      </header>
+
+      <Notice error={error} />
+
+      <form onSubmit={updateAccountForm.handleSubmit(updateAccount)}>
+        <FormGroup name="Settings">
+          <p>You can update your login details below.</p>
+
+          <FormInput id="email" label="Email address">
+            <input id="email" type="text" {...updateAccountForm.inputs.email} />
+            <Chip className="mini" error={updateAccountForm.formState.errors.email} />
+          </FormInput>
+
+          <FormInput id="password" label="Password">
+            <Row className="hidden">
+              <input id="password" type={passwordVisible ? 'text' : 'password'} {...updateAccountForm.inputs.password} />
+              <ButtonSet>
+                <HideShowButton visible={passwordVisible} onClick={() => setPasswordVisible(!passwordVisible)} />
+              </ButtonSet>
+            </Row>
+            {updateAccountForm.formState.errors.password && <Chip className="mini" error={updateAccountForm.formState.errors.password} />}
+          </FormInput>
+
+          <ButtonSet>
+            <SaveButton type="submit" className="fill" />
+            <ResetButton onClick={resetAccountUpdate} />
+          </ButtonSet>
+        </FormGroup>
+      </form>
+
+      <FormGroup name="Danger zone">
+        <p>
+          If you would like to stop using Herda, you can delete your account and all your data.
+        </p>
+
+        <Notice className="warn">Account deletion is not reversible. Proceed with care.</Notice>
+
+        <ButtonSet>
+          <DeleteButton
+            className="fill"
+            confirm={(
+              <>
+                <TrashIcon />
+                <span>Yes, really delete my account!</span>
+              </>
+            )}
+            onClick={deleteAccount}
+          >
+            <span>Delete my account</span>
+          </DeleteButton>
+        </ButtonSet>
+      </FormGroup>
+    </Main>
+  )
+}