mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(forms): keep password managers out of a place's contact fields and the admin user forms
1Password filled the signed-in user's login into a place's e-mail field, and offered the admin's own credentials for an account being created. The fields now carry each manager's opt-out; autocomplete=off alone is ignored.
This commit is contained in:
@@ -73,5 +73,19 @@ describe('PlaceContactFields (#2472)', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'Take over from the place details' }))
|
await user.click(screen.getByRole('button', { name: 'Take over from the place details' }))
|
||||||
expect(JSON.parse(onChange.mock.lastCall![1])).toEqual(looked)
|
expect(JSON.parse(onChange.mock.lastCall![1])).toEqual(looked)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('FE-PLANNER-CONTACT-005: the place\'s phone and e-mail keep password managers out', () => {
|
||||||
|
render(<Harness onChange={vi.fn()} />)
|
||||||
|
// A place's address is not the user's login; 1Password filled it in (5.0.0-pre.1 feedback).
|
||||||
|
for (const label of ['Phone', 'E-mail']) {
|
||||||
|
const field = screen.getByLabelText(label)
|
||||||
|
expect(field).toHaveAttribute('data-1p-ignore', 'true')
|
||||||
|
expect(field).toHaveAttribute('data-lpignore', 'true')
|
||||||
|
expect(field).toHaveAttribute('data-bwignore', 'true')
|
||||||
|
expect(field).toHaveAttribute('autocomplete', 'off')
|
||||||
|
}
|
||||||
|
// Still an e-mail field, so the keyboard and the browser's check stay.
|
||||||
|
expect(screen.getByLabelText('E-mail')).toHaveAttribute('type', 'email')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DialogSection, fs } from '../shared/DialogShell'
|
|||||||
import { AddRowButton, EditorField, GRID_2, INPUT } from '../shared/dialogParts'
|
import { AddRowButton, EditorField, GRID_2, INPUT } from '../shared/dialogParts'
|
||||||
import { cleanWeek, readWeek, weekdayNames, writeWeek } from './placeHours'
|
import { cleanWeek, readWeek, weekdayNames, writeWeek } from './placeHours'
|
||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
|
import { NO_AUTOFILL } from '../../utils/noAutofill'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Anchor for the "add by hand" jump from an empty search. */
|
/** Anchor for the "add by hand" jump from an empty search. */
|
||||||
@@ -80,14 +81,14 @@ export function PlaceContactFields({ id: anchorId, phone, email, openingHours, o
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Phone size={13} strokeWidth={2} className="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-content-faint" />
|
<Phone size={13} strokeWidth={2} className="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-content-faint" />
|
||||||
<input id={`${id}-phone`} type="tel" value={phone} onChange={e => onChange('phone', e.target.value)}
|
<input id={`${id}-phone`} type="tel" value={phone} onChange={e => onChange('phone', e.target.value)}
|
||||||
maxLength={50} placeholder="+49 …" className={`${INPUT} ps-8`} />
|
maxLength={50} placeholder="+49 …" className={`${INPUT} ps-8`} {...NO_AUTOFILL} />
|
||||||
</div>
|
</div>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
<EditorField label={t('places.formEmail')} htmlFor={`${id}-email`}>
|
<EditorField label={t('places.formEmail')} htmlFor={`${id}-email`}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Mail size={13} strokeWidth={2} className="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-content-faint" />
|
<Mail size={13} strokeWidth={2} className="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-content-faint" />
|
||||||
<input id={`${id}-email`} type="email" value={email} onChange={e => onChange('email', e.target.value)}
|
<input id={`${id}-email`} type="email" value={email} onChange={e => onChange('email', e.target.value)}
|
||||||
maxLength={254} placeholder="info@…" className={`${INPUT} ps-8`} />
|
maxLength={254} placeholder="info@…" className={`${INPUT} ps-8`} {...NO_AUTOFILL} />
|
||||||
</div>
|
</div>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { useAdmin } from '../../../pages/admin/useAdmin'
|
|||||||
import MSheet from '../../components/MSheet'
|
import MSheet from '../../components/MSheet'
|
||||||
import MSegmented from '../../components/MSegmented'
|
import MSegmented from '../../components/MSegmented'
|
||||||
import { MAdminButton, MAdminField, MAdminInput, MAdminSecretInput, MAdminSheetFrame } from './MAdminUi'
|
import { MAdminButton, MAdminField, MAdminInput, MAdminSecretInput, MAdminSheetFrame } from './MAdminUi'
|
||||||
|
import { NO_AUTOFILL } from '../../../utils/noAutofill'
|
||||||
|
|
||||||
interface MAdminSheetsProps {
|
interface MAdminSheetsProps {
|
||||||
admin: ReturnType<typeof useAdmin>
|
admin: ReturnType<typeof useAdmin>
|
||||||
@@ -78,6 +79,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
value={createForm.username}
|
value={createForm.username}
|
||||||
onChange={(e) => setCreateForm((f) => ({ ...f, username: e.target.value }))}
|
onChange={(e) => setCreateForm((f) => ({ ...f, username: e.target.value }))}
|
||||||
placeholder={t('settings.username')}
|
placeholder={t('settings.username')}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={`${t('common.email')} *`}>
|
<MAdminField label={`${t('common.email')} *`}>
|
||||||
@@ -86,6 +88,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
value={createForm.email}
|
value={createForm.email}
|
||||||
onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))}
|
||||||
placeholder={t('common.email')}
|
placeholder={t('common.email')}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={`${t('common.password')} *`}>
|
<MAdminField label={`${t('common.password')} *`}>
|
||||||
@@ -93,6 +96,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
value={createForm.password}
|
value={createForm.password}
|
||||||
onChange={(e) => setCreateForm((f) => ({ ...f, password: e.target.value }))}
|
onChange={(e) => setCreateForm((f) => ({ ...f, password: e.target.value }))}
|
||||||
placeholder={t('common.password')}
|
placeholder={t('common.password')}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={t('settings.role')}>
|
<MAdminField label={t('settings.role')}>
|
||||||
@@ -127,6 +131,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
type="text"
|
type="text"
|
||||||
value={editForm.username}
|
value={editForm.username}
|
||||||
onChange={(e) => setEditForm((f) => ({ ...f, username: e.target.value }))}
|
onChange={(e) => setEditForm((f) => ({ ...f, username: e.target.value }))}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={t('common.email')}>
|
<MAdminField label={t('common.email')}>
|
||||||
@@ -134,6 +139,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
type="email"
|
type="email"
|
||||||
value={editForm.email}
|
value={editForm.email}
|
||||||
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={t('admin.newPassword')} hint={t('admin.newPasswordHint')}>
|
<MAdminField label={t('admin.newPassword')} hint={t('admin.newPasswordHint')}>
|
||||||
@@ -141,6 +147,7 @@ export default function MAdminSheets({ admin, t }: MAdminSheetsProps) {
|
|||||||
value={editForm.password}
|
value={editForm.password}
|
||||||
onChange={(e) => setEditForm((f) => ({ ...f, password: e.target.value }))}
|
onChange={(e) => setEditForm((f) => ({ ...f, password: e.target.value }))}
|
||||||
placeholder={t('admin.newPasswordPlaceholder')}
|
placeholder={t('admin.newPasswordPlaceholder')}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</MAdminField>
|
</MAdminField>
|
||||||
<MAdminField label={t('settings.role')}>
|
<MAdminField label={t('settings.role')}>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER } from '../../components/Settin
|
|||||||
import type { TranslationFn } from '../../types';
|
import type { TranslationFn } from '../../types';
|
||||||
import type { useAdmin } from './useAdmin';
|
import type { useAdmin } from './useAdmin';
|
||||||
import PasswordChecklist from '../../components/shared/PasswordChecklist';
|
import PasswordChecklist from '../../components/shared/PasswordChecklist';
|
||||||
|
import { NO_AUTOFILL } from '../../utils/noAutofill';
|
||||||
|
|
||||||
interface AdminUserModalsProps {
|
interface AdminUserModalsProps {
|
||||||
admin: ReturnType<typeof useAdmin>;
|
admin: ReturnType<typeof useAdmin>;
|
||||||
@@ -66,6 +67,8 @@ function PasswordInput({
|
|||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type={shown ? 'text' : 'password'}
|
type={shown ? 'text' : 'password'}
|
||||||
|
// Another user's password: never the admin's own, and a manager may offer a fresh one.
|
||||||
|
autoComplete="new-password"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -166,6 +169,7 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea
|
|||||||
onChange={(e) => setCreateForm((f) => ({ ...f, username: e.target.value }))}
|
onChange={(e) => setCreateForm((f) => ({ ...f, username: e.target.value }))}
|
||||||
placeholder={t('settings.username')}
|
placeholder={t('settings.username')}
|
||||||
className={INPUT}
|
className={INPUT}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
<EditorField label={`${t('common.email')} *`} htmlFor={`${createId}-email`}>
|
<EditorField label={`${t('common.email')} *`} htmlFor={`${createId}-email`}>
|
||||||
@@ -176,6 +180,7 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea
|
|||||||
onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setCreateForm((f) => ({ ...f, email: e.target.value }))}
|
||||||
placeholder={t('common.email')}
|
placeholder={t('common.email')}
|
||||||
className={INPUT}
|
className={INPUT}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,6 +244,7 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea
|
|||||||
value={editForm.username}
|
value={editForm.username}
|
||||||
onChange={(e) => setEditForm((f) => ({ ...f, username: e.target.value }))}
|
onChange={(e) => setEditForm((f) => ({ ...f, username: e.target.value }))}
|
||||||
className={INPUT}
|
className={INPUT}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
<EditorField label={t('common.email')} htmlFor={`${editId}-email`}>
|
<EditorField label={t('common.email')} htmlFor={`${editId}-email`}>
|
||||||
@@ -248,6 +254,7 @@ export default function AdminUserModals({ admin, t }: AdminUserModalsProps): Rea
|
|||||||
value={editForm.email}
|
value={editForm.email}
|
||||||
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setEditForm((f) => ({ ...f, email: e.target.value }))}
|
||||||
className={INPUT}
|
className={INPUT}
|
||||||
|
{...NO_AUTOFILL}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* For a field that holds somebody else's details: a place's e-mail and phone,
|
||||||
|
* the account an admin is creating. Password managers read such a field as a
|
||||||
|
* login and offer, or simply fill in, the signed-in user's own credentials.
|
||||||
|
* `autocomplete="off"` alone does not stop them (1Password ignores it on
|
||||||
|
* purpose), so each one gets its own opt-out. Never use it on a login or on the
|
||||||
|
* user's own account fields, where autofill is wanted.
|
||||||
|
*/
|
||||||
|
export const NO_AUTOFILL = {
|
||||||
|
autoComplete: 'off',
|
||||||
|
'data-1p-ignore': 'true',
|
||||||
|
'data-lpignore': 'true',
|
||||||
|
'data-bwignore': 'true',
|
||||||
|
'data-form-type': 'other',
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user