mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 22:27:10 +01:00
* fix(ui): make self-service profile edits report their outcome
When a non-admin user saved their own profile (e.g. changing their
password via EnableUserEditing), the data provider followed the user
update with a call to the admin-only PUT /api/user/{id}/library
endpoint, which always failed with 403. The save error handler then
crashed reading error.body.errors on the plain-text response, so the
user got no notification at all - while the profile change had in fact
already been applied. This made password changes look like they were
silently ignored, and follow-up attempts failed with 'password does not
match' since the current password had already changed. Present since
the multi-library support introduced in v0.58.0 (#4181).
Only call the user-library association endpoint when the logged-in user
is an admin (the server manages assignments for self-edits), and make
the save error handler tolerate error bodies without field errors,
notifying a generic error instead of crashing.
* fix(ui): tolerate nullish rejection values in user save handler
Address review feedback: use optional chaining on the error itself in
the UserEdit save handler, so a nullish rejection value also results in
the generic error notification instead of a TypeError.
184 lines
5.1 KiB
JavaScript
184 lines
5.1 KiB
JavaScript
import React, { useCallback } from 'react'
|
|
import { makeStyles } from '@material-ui/core/styles'
|
|
import {
|
|
TextInput,
|
|
BooleanInput,
|
|
DateField,
|
|
PasswordInput,
|
|
Edit,
|
|
required,
|
|
email,
|
|
SimpleForm,
|
|
useTranslate,
|
|
Toolbar,
|
|
SaveButton,
|
|
useMutation,
|
|
useNotify,
|
|
useRedirect,
|
|
useRefresh,
|
|
FormDataConsumer,
|
|
usePermissions,
|
|
useRecordContext,
|
|
} from 'react-admin'
|
|
import { Typography } from '@material-ui/core'
|
|
import { Title } from '../common'
|
|
import DeleteUserButton from './DeleteUserButton'
|
|
import { LibrarySelectionField } from './LibrarySelectionField.jsx'
|
|
import { validateUserForm } from './userValidation'
|
|
|
|
const useStyles = makeStyles({
|
|
toolbar: {
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
},
|
|
})
|
|
|
|
const UserTitle = ({ record }) => {
|
|
const translate = useTranslate()
|
|
const resourceName = translate('resources.user.name', { smart_count: 1 })
|
|
return <Title subTitle={`${resourceName} ${record ? record.name : ''}`} />
|
|
}
|
|
|
|
const UserToolbar = ({ showDelete, ...props }) => (
|
|
<Toolbar {...props} classes={useStyles()}>
|
|
<SaveButton disabled={props.pristine} />
|
|
{showDelete && <DeleteUserButton />}
|
|
</Toolbar>
|
|
)
|
|
|
|
const CurrentPasswordInput = ({ formData, isMyself, ...rest }) => {
|
|
const { permissions } = usePermissions()
|
|
return formData.changePassword && (isMyself || permissions !== 'admin') ? (
|
|
<PasswordInput className="ra-input" source="currentPassword" {...rest} />
|
|
) : null
|
|
}
|
|
|
|
const NewPasswordInput = ({ formData, ...rest }) => {
|
|
const translate = useTranslate()
|
|
return formData.changePassword ? (
|
|
<PasswordInput
|
|
source="password"
|
|
className="ra-input"
|
|
label={translate('resources.user.fields.newPassword')}
|
|
{...rest}
|
|
/>
|
|
) : null
|
|
}
|
|
|
|
const UserEdit = (props) => {
|
|
const { permissions } = props
|
|
const translate = useTranslate()
|
|
const [mutate] = useMutation()
|
|
const notify = useNotify()
|
|
const redirect = useRedirect()
|
|
const refresh = useRefresh()
|
|
|
|
const isMyself = props.id === localStorage.getItem('userId')
|
|
const getNameHelperText = () =>
|
|
isMyself && {
|
|
helperText: translate('resources.user.helperTexts.name'),
|
|
}
|
|
const canDelete = permissions === 'admin' && !isMyself
|
|
|
|
const save = useCallback(
|
|
async (values) => {
|
|
try {
|
|
await mutate(
|
|
{
|
|
type: 'update',
|
|
resource: 'user',
|
|
payload: { id: values.id, data: values },
|
|
},
|
|
{ returnPromise: true },
|
|
)
|
|
notify('resources.user.notifications.updated', 'info', {
|
|
smart_count: 1,
|
|
})
|
|
permissions === 'admin' ? redirect('/user') : refresh()
|
|
} catch (error) {
|
|
if (error?.body?.errors) {
|
|
return error.body.errors
|
|
}
|
|
notify('ra.page.error', 'warning')
|
|
}
|
|
},
|
|
[mutate, notify, permissions, redirect, refresh],
|
|
)
|
|
|
|
// Custom validation function
|
|
const validateForm = (values) => {
|
|
return validateUserForm(values, translate)
|
|
}
|
|
|
|
return (
|
|
<Edit title={<UserTitle />} undoable={false} {...props}>
|
|
<SimpleForm
|
|
variant={'outlined'}
|
|
toolbar={<UserToolbar showDelete={canDelete} />}
|
|
save={save}
|
|
validate={validateForm}
|
|
>
|
|
{permissions === 'admin' && (
|
|
<TextInput
|
|
spellCheck={false}
|
|
source="userName"
|
|
validate={[required()]}
|
|
/>
|
|
)}
|
|
<TextInput
|
|
source="name"
|
|
validate={[required()]}
|
|
{...getNameHelperText()}
|
|
/>
|
|
<TextInput spellCheck={false} source="email" validate={[email()]} />
|
|
<BooleanInput source="changePassword" />
|
|
<FormDataConsumer>
|
|
{(formDataProps) => (
|
|
<CurrentPasswordInput
|
|
spellCheck={false}
|
|
isMyself={isMyself}
|
|
{...formDataProps}
|
|
/>
|
|
)}
|
|
</FormDataConsumer>
|
|
<FormDataConsumer>
|
|
{(formDataProps) => (
|
|
<NewPasswordInput spellCheck={false} {...formDataProps} />
|
|
)}
|
|
</FormDataConsumer>
|
|
|
|
{permissions === 'admin' && (
|
|
<BooleanInput source="isAdmin" initialValue={false} />
|
|
)}
|
|
|
|
{/* Conditional Library Selection for Admin Users Only */}
|
|
{permissions === 'admin' && (
|
|
<FormDataConsumer>
|
|
{({ formData }) => (
|
|
<>
|
|
{!formData.isAdmin && <LibrarySelectionField />}
|
|
|
|
{formData.isAdmin && (
|
|
<Typography
|
|
variant="body2"
|
|
color="textSecondary"
|
|
style={{ marginTop: 16, marginBottom: 16 }}
|
|
>
|
|
{translate('resources.user.message.adminAutoLibraries')}
|
|
</Typography>
|
|
)}
|
|
</>
|
|
)}
|
|
</FormDataConsumer>
|
|
)}
|
|
|
|
<DateField variant="body1" source="lastLoginAt" showTime />
|
|
<DateField variant="body1" source="lastAccessAt" showTime />
|
|
<DateField variant="body1" source="updatedAt" showTime />
|
|
<DateField variant="body1" source="createdAt" showTime />
|
|
</SimpleForm>
|
|
</Edit>
|
|
)
|
|
}
|
|
|
|
export default UserEdit
|