From e13055736a7fde1d41aa1a2e37f9bb8c45d501a0 Mon Sep 17 00:00:00 2001
From: Philip Molares <philip.molares@udo.edu>
Date: Sun, 21 May 2023 21:59:46 +0200
Subject: [PATCH] feat(frontend): handle username in lowercase

When handling usernames for login and registering with local or permissions, this makes sure that the username is always in lowercase.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
---
 .../permission-add-entry-field.tsx            |  4 +--
 .../login-page/auth/fields/username-field.tsx |  9 +++++-
 .../components/login-page/auth/via-ldap.tsx   |  5 ++--
 .../components/login-page/auth/via-local.tsx  |  5 ++--
 .../use-lowercase-on-input-change.spec.tsx    | 28 +++++++++++++++++++
 .../common/use-lowercase-on-input-change.ts   | 20 +++++++++++++
 .../hooks/common/use-on-input-change.spec.tsx | 27 ++++++++++++++++++
 .../src/hooks/common/use-on-input-change.ts   |  9 ++----
 frontend/src/pages/register.tsx               |  3 +-
 9 files changed, 95 insertions(+), 15 deletions(-)
 create mode 100644 frontend/src/hooks/common/use-lowercase-on-input-change.spec.tsx
 create mode 100644 frontend/src/hooks/common/use-lowercase-on-input-change.ts
 create mode 100644 frontend/src/hooks/common/use-on-input-change.spec.tsx

diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx
index 5e7f1a02c..b24483108 100644
--- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx
+++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-add-entry-field.tsx
@@ -3,7 +3,7 @@
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { useOnInputChange } from '../../../../../../hooks/common/use-on-input-change'
+import { useLowercaseOnInputChange } from '../../../../../../hooks/common/use-lowercase-on-input-change'
 import { UiIcon } from '../../../../../common/icons/ui-icon'
 import type { PermissionDisabledProps } from './permission-disabled.prop'
 import React, { useCallback, useState } from 'react'
@@ -31,7 +31,7 @@ export const PermissionAddEntryField: React.FC<PermissionAddEntryFieldProps & Pe
   const { t } = useTranslation()
 
   const [newEntryIdentifier, setNewEntryIdentifier] = useState('')
-  const onChange = useOnInputChange(setNewEntryIdentifier)
+  const onChange = useLowercaseOnInputChange(setNewEntryIdentifier)
 
   const onSubmit = useCallback(() => {
     onAddEntry(newEntryIdentifier)
diff --git a/frontend/src/components/login-page/auth/fields/username-field.tsx b/frontend/src/components/login-page/auth/fields/username-field.tsx
index fb33c1c85..def9a1472 100644
--- a/frontend/src/components/login-page/auth/fields/username-field.tsx
+++ b/frontend/src/components/login-page/auth/fields/username-field.tsx
@@ -8,19 +8,26 @@ import React from 'react'
 import { Form } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 
+export interface UsernameFieldProps extends AuthFieldProps {
+  value: string
+}
+
+//TODO: This should be replaced with the common username component. See https://github.com/hedgedoc/hedgedoc/issues/4128
 /**
  * Renders an input field for a username.
  *
  * @param onChange Hook that is called when the input is changed.
  * @param invalid True indicates that the username is invalid, false otherwise.
+ * @param value the username value
  */
-export const UsernameField: React.FC<AuthFieldProps> = ({ onChange, invalid }) => {
+export const UsernameField: React.FC<UsernameFieldProps> = ({ onChange, invalid, value }) => {
   const { t } = useTranslation()
 
   return (
     <Form.Group>
       <Form.Control
         isInvalid={invalid}
+        value={value}
         type='text'
         size='sm'
         placeholder={t('login.auth.username') ?? undefined}
diff --git a/frontend/src/components/login-page/auth/via-ldap.tsx b/frontend/src/components/login-page/auth/via-ldap.tsx
index a73656f4c..3b28d99e5 100644
--- a/frontend/src/components/login-page/auth/via-ldap.tsx
+++ b/frontend/src/components/login-page/auth/via-ldap.tsx
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 import { doLdapLogin } from '../../../api/auth/ldap'
+import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
 import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
 import { PasswordField } from './fields/password-field'
 import { UsernameField } from './fields/username-field'
@@ -38,7 +39,7 @@ export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) =>
     [username, password, identifier]
   )
 
-  const onUsernameChange = useOnInputChange(setUsername)
+  const onUsernameChange = useLowercaseOnInputChange(setUsername)
   const onPasswordChange = useOnInputChange(setPassword)
 
   return (
@@ -48,7 +49,7 @@ export const ViaLdap: React.FC<ViaLdapProps> = ({ providerName, identifier }) =>
           <Trans i18nKey='login.signInVia' values={{ service: providerName }} />
         </Card.Title>
         <Form onSubmit={onLoginSubmit}>
-          <UsernameField onChange={onUsernameChange} invalid={!!error} />
+          <UsernameField onChange={onUsernameChange} invalid={!!error} value={username} />
           <PasswordField onChange={onPasswordChange} invalid={!!error} />
           <Alert className='small' show={!!error} variant='danger'>
             <Trans i18nKey={error} />
diff --git a/frontend/src/components/login-page/auth/via-local.tsx b/frontend/src/components/login-page/auth/via-local.tsx
index 0cc8c8dfc..214fa2ea4 100644
--- a/frontend/src/components/login-page/auth/via-local.tsx
+++ b/frontend/src/components/login-page/auth/via-local.tsx
@@ -5,6 +5,7 @@
  */
 import { doLocalLogin } from '../../../api/auth/local'
 import { ErrorToI18nKeyMapper } from '../../../api/common/error-to-i18n-key-mapper'
+import { useLowercaseOnInputChange } from '../../../hooks/common/use-lowercase-on-input-change'
 import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
 import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
 import { ShowIf } from '../../common/show-if/show-if'
@@ -44,7 +45,7 @@ export const ViaLocal: React.FC = () => {
     [username, password]
   )
 
-  const onUsernameChange = useOnInputChange(setUsername)
+  const onUsernameChange = useLowercaseOnInputChange(setUsername)
   const onPasswordChange = useOnInputChange(setPassword)
 
   return (
@@ -54,7 +55,7 @@ export const ViaLocal: React.FC = () => {
           <Trans i18nKey='login.signInVia' values={{ service: t('login.auth.username') }} />
         </Card.Title>
         <Form onSubmit={onLoginSubmit} className={'d-flex gap-3 flex-column'}>
-          <UsernameField onChange={onUsernameChange} invalid={!!error} />
+          <UsernameField onChange={onUsernameChange} invalid={!!error} value={username} />
           <PasswordField onChange={onPasswordChange} invalid={!!error} />
           <Alert className='small' show={!!error} variant='danger'>
             <Trans i18nKey={error} />
diff --git a/frontend/src/hooks/common/use-lowercase-on-input-change.spec.tsx b/frontend/src/hooks/common/use-lowercase-on-input-change.spec.tsx
new file mode 100644
index 000000000..c11a509b7
--- /dev/null
+++ b/frontend/src/hooks/common/use-lowercase-on-input-change.spec.tsx
@@ -0,0 +1,28 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { useLowercaseOnInputChange } from './use-lowercase-on-input-change'
+import { fireEvent, render, screen } from '@testing-library/react'
+import React from 'react'
+
+describe('useOnInputChange', () => {
+  it('executes the setter', async () => {
+    const callback = jest.fn()
+    const testValue = 'TEST VALUE'
+
+    const Test: React.FC = () => {
+      const onChange = useLowercaseOnInputChange(callback)
+      return <input data-testid={'input'} type={'text'} onChange={onChange} />
+    }
+
+    render(<Test></Test>)
+
+    const element: HTMLInputElement = await screen.findByTestId('input')
+
+    fireEvent.change(element, { target: { value: testValue } })
+
+    expect(callback).toBeCalledWith('test value')
+  })
+})
diff --git a/frontend/src/hooks/common/use-lowercase-on-input-change.ts b/frontend/src/hooks/common/use-lowercase-on-input-change.ts
new file mode 100644
index 000000000..a741d48e0
--- /dev/null
+++ b/frontend/src/hooks/common/use-lowercase-on-input-change.ts
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { useOnInputChange } from './use-on-input-change'
+import type { ChangeEvent } from 'react'
+import { useCallback } from 'react'
+
+/**
+ * Takes an input change event and sends the lower case event value to a state setter.
+ *
+ * @param setter The setter method for the state.
+ * @return Hook that can be used as callback for onChange.
+ */
+export const useLowercaseOnInputChange = (
+  setter: (value: string) => void
+): ((event: ChangeEvent<HTMLInputElement>) => void) => {
+  return useOnInputChange(useCallback((value) => setter(value.toLowerCase()), [setter]))
+}
diff --git a/frontend/src/hooks/common/use-on-input-change.spec.tsx b/frontend/src/hooks/common/use-on-input-change.spec.tsx
new file mode 100644
index 000000000..e2c1160db
--- /dev/null
+++ b/frontend/src/hooks/common/use-on-input-change.spec.tsx
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { useOnInputChange } from './use-on-input-change'
+import { fireEvent, render, screen } from '@testing-library/react'
+import React from 'react'
+
+describe('useOnInputChange', () => {
+  it('executes the setter', async () => {
+    const callback = jest.fn()
+    const testValue = 'testValue'
+
+    const Test: React.FC = () => {
+      const onChange = useOnInputChange(callback)
+      return <input data-testid={'input'} type={'text'} onChange={onChange} />
+    }
+
+    render(<Test></Test>)
+
+    const element: HTMLInputElement = await screen.findByTestId('input')
+    fireEvent.change(element, { target: { value: testValue } })
+
+    expect(callback).toBeCalledWith(testValue)
+  })
+})
diff --git a/frontend/src/hooks/common/use-on-input-change.ts b/frontend/src/hooks/common/use-on-input-change.ts
index 707ff461b..58e3766d1 100644
--- a/frontend/src/hooks/common/use-on-input-change.ts
+++ b/frontend/src/hooks/common/use-on-input-change.ts
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
  *
  * SPDX-License-Identifier: AGPL-3.0-only
  */
@@ -13,10 +13,5 @@ import { useCallback } from 'react'
  * @return Hook that can be used as callback for onChange.
  */
 export const useOnInputChange = (setter: (value: string) => void): ((event: ChangeEvent<HTMLInputElement>) => void) => {
-  return useCallback(
-    (event) => {
-      setter(event.target.value)
-    },
-    [setter]
-  )
+  return useCallback((event) => setter(event.target.value), [setter])
 }
diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx
index 89e1d8538..2d58f1562 100644
--- a/frontend/src/pages/register.tsx
+++ b/frontend/src/pages/register.tsx
@@ -17,6 +17,7 @@ import { useUiNotifications } from '../components/notifications/ui-notification-
 import { RegisterError } from '../components/register-page/register-error'
 import { RegisterInfos } from '../components/register-page/register-infos'
 import { useApplicationState } from '../hooks/common/use-application-state'
+import { useLowercaseOnInputChange } from '../hooks/common/use-lowercase-on-input-change'
 import { useOnInputChange } from '../hooks/common/use-on-input-change'
 import type { NextPage } from 'next'
 import { useRouter } from 'next/router'
@@ -62,7 +63,7 @@ export const RegisterPage: NextPage = () => {
     return error?.backendErrorName === 'PasswordTooWeakError'
   }, [error])
 
-  const onUsernameChange = useOnInputChange(setUsername)
+  const onUsernameChange = useLowercaseOnInputChange(setUsername)
   const onDisplayNameChange = useOnInputChange(setDisplayName)
   const onPasswordChange = useOnInputChange(setPassword)
   const onPasswordAgainChange = useOnInputChange(setPasswordAgain)