mirror of
https://github.com/BreizhHardware/jellyseerr.git
synced 2026-01-18 16:47:33 +01:00
fix: remove email requirement for the user, and use the username if no email provided (#900)
* fix: remove email requirement for the user, and use the username if no email provided * fix: update translations * fix: remove useless console.log * test: fix user list test * fix: disallow Plex users from changing their email
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
const testUser = {
|
const testUser = {
|
||||||
displayName: 'Test User',
|
username: 'Test User',
|
||||||
emailAddress: 'test@seeerr.dev',
|
emailAddress: 'test@seeerr.dev',
|
||||||
password: 'test1234',
|
password: 'test1234',
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,7 @@ describe('User List', () => {
|
|||||||
|
|
||||||
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||||
|
|
||||||
cy.get('#displayName').type(testUser.displayName);
|
cy.get('#username').type(testUser.username);
|
||||||
cy.get('#email').type(testUser.emailAddress);
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
cy.get('#password').type(testUser.password);
|
cy.get('#password').type(testUser.password);
|
||||||
|
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
// with admin permission
|
// with admin permission
|
||||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||||
user = new User({
|
user = new User({
|
||||||
email: body.email,
|
email: body.email || account.User.Name,
|
||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
@@ -328,7 +328,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
}),
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,7 +374,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
if (account.User.PrimaryImageTag) {
|
if (account.User.PrimaryImageTag) {
|
||||||
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||||
} else {
|
} else {
|
||||||
user.avatar = gravatarUrl(user.email, {
|
user.avatar = gravatarUrl(user.email || account.User.Name, {
|
||||||
default: 'mm',
|
default: 'mm',
|
||||||
size: 200,
|
size: 200,
|
||||||
});
|
});
|
||||||
@@ -413,10 +416,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!body.email) {
|
|
||||||
throw new Error('add_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
user = new User({
|
user = new User({
|
||||||
email: body.email,
|
email: body.email,
|
||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
@@ -426,7 +425,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
|
: gravatarUrl(body.email || account.User.Name, {
|
||||||
|
default: 'mm',
|
||||||
|
size: 200,
|
||||||
|
}),
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
//initialize Jellyfin/Emby users with local login
|
//initialize Jellyfin/Emby users with local login
|
||||||
|
|||||||
@@ -41,7 +41,19 @@ router.get('/', async (req, res, next) => {
|
|||||||
break;
|
break;
|
||||||
case 'displayname':
|
case 'displayname':
|
||||||
query = query.orderBy(
|
query = query.orderBy(
|
||||||
"(CASE WHEN (user.username IS NULL OR user.username = '') THEN (CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN user.email ELSE LOWER(user.plexUsername) END) ELSE LOWER(user.username) END)",
|
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
|
||||||
|
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
|
||||||
|
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
|
||||||
|
user.email
|
||||||
|
ELSE
|
||||||
|
LOWER(user.jellyfinUsername)
|
||||||
|
END)
|
||||||
|
ELSE
|
||||||
|
LOWER(user.jellyfinUsername)
|
||||||
|
END)
|
||||||
|
ELSE
|
||||||
|
LOWER(user.username)
|
||||||
|
END`,
|
||||||
'ASC'
|
'ASC'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -90,12 +102,13 @@ router.post(
|
|||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
|
const email = body.email || body.username;
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
const existingUser = await userRepository
|
const existingUser = await userRepository
|
||||||
.createQueryBuilder('user')
|
.createQueryBuilder('user')
|
||||||
.where('user.email = :email', {
|
.where('user.email = :email', {
|
||||||
email: body.email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
})
|
})
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
@@ -108,7 +121,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||||
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
|
const avatar = gravatarUrl(email, { default: 'mm', size: 200 });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!passedExplicitPassword &&
|
!passedExplicitPassword &&
|
||||||
@@ -118,9 +131,9 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = new User({
|
const user = new User({
|
||||||
|
email,
|
||||||
avatar: body.avatar ?? avatar,
|
avatar: body.avatar ?? avatar,
|
||||||
username: body.username,
|
username: body.username,
|
||||||
email: body.email,
|
|
||||||
password: body.password,
|
password: body.password,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
plexToken: '',
|
plexToken: '',
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ userSettingsRoutes.post<
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.username = req.body.username;
|
user.username = req.body.username;
|
||||||
user.email = req.body.email ?? user.email;
|
if (user.jellyfinUsername) {
|
||||||
|
user.email = req.body.email || user.jellyfinUsername || user.email;
|
||||||
|
}
|
||||||
|
|
||||||
// Update quota values only if the user has the correct permissions
|
// Update quota values only if the user has the correct permissions
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -90,9 +90,11 @@ const UserDropdown = () => {
|
|||||||
<span className="truncate text-xl font-semibold text-gray-200">
|
<span className="truncate text-xl font-semibold text-gray-200">
|
||||||
{user?.displayName}
|
{user?.displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-sm text-gray-400">
|
{user?.displayName?.toLowerCase() !== user?.email && (
|
||||||
{user?.email}
|
<span className="truncate text-sm text-gray-400">
|
||||||
</span>
|
{user?.email}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user && <MiniQuotaDisplay userId={user?.id} />}
|
{user && <MiniQuotaDisplay userId={user?.id} />}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const { data: createdUsers } = await res.json();
|
const createdUsers = await res.json();
|
||||||
|
|
||||||
if (!createdUsers.length) {
|
if (!createdUsers.length) {
|
||||||
throw new Error('No users were imported from Jellyfin.');
|
throw new Error('No users were imported from Jellyfin.');
|
||||||
|
|||||||
@@ -68,14 +68,15 @@ const messages = defineMessages('components.UserList', {
|
|||||||
usercreatedfailedexisting:
|
usercreatedfailedexisting:
|
||||||
'The provided email address is already in use by another user.',
|
'The provided email address is already in use by another user.',
|
||||||
usercreatedsuccess: 'User created successfully!',
|
usercreatedsuccess: 'User created successfully!',
|
||||||
displayName: 'Display Name',
|
username: 'Username',
|
||||||
email: 'Email Address',
|
email: 'Email Address',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
passwordinfodescription:
|
passwordinfodescription:
|
||||||
'Configure an application URL and enable email notifications to allow automatic password generation.',
|
'Configure an application URL and enable email notifications to allow automatic password generation.',
|
||||||
autogeneratepassword: 'Automatically Generate Password',
|
autogeneratepassword: 'Automatically Generate Password',
|
||||||
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
autogeneratepasswordTip: 'Email a server-generated password to the user',
|
||||||
validationEmail: 'You must provide a valid email address',
|
validationUsername: 'You must provide an username',
|
||||||
|
validationEmail: 'Email required',
|
||||||
sortCreated: 'Join Date',
|
sortCreated: 'Join Date',
|
||||||
sortDisplayName: 'Display Name',
|
sortDisplayName: 'Display Name',
|
||||||
sortRequests: 'Request Count',
|
sortRequests: 'Request Count',
|
||||||
@@ -208,9 +209,10 @@ const UserList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateUserSchema = Yup.object().shape({
|
const CreateUserSchema = Yup.object().shape({
|
||||||
email: Yup.string()
|
username: Yup.string().required(
|
||||||
.required(intl.formatMessage(messages.validationEmail))
|
intl.formatMessage(messages.validationUsername)
|
||||||
.email(intl.formatMessage(messages.validationEmail)),
|
),
|
||||||
|
email: Yup.string().email(intl.formatMessage(messages.validationEmail)),
|
||||||
password: Yup.lazy((value) =>
|
password: Yup.lazy((value) =>
|
||||||
!value
|
!value
|
||||||
? Yup.string()
|
? Yup.string()
|
||||||
@@ -258,7 +260,7 @@ const UserList = () => {
|
|||||||
setDeleteModal({ isOpen: false, user: deleteModal.user })
|
setDeleteModal({ isOpen: false, user: deleteModal.user })
|
||||||
}
|
}
|
||||||
title={intl.formatMessage(messages.deleteuser)}
|
title={intl.formatMessage(messages.deleteuser)}
|
||||||
subTitle={deleteModal.user?.displayName}
|
subTitle={deleteModal.user?.username}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.deleteconfirm)}
|
{intl.formatMessage(messages.deleteconfirm)}
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -276,7 +278,7 @@ const UserList = () => {
|
|||||||
>
|
>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
displayName: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
genpassword: false,
|
genpassword: false,
|
||||||
@@ -290,7 +292,7 @@ const UserList = () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: values.displayName,
|
username: values.username,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
password: values.genpassword ? null : values.password,
|
password: values.genpassword ? null : values.password,
|
||||||
}),
|
}),
|
||||||
@@ -370,23 +372,24 @@ const UserList = () => {
|
|||||||
)}
|
)}
|
||||||
<Form className="section">
|
<Form className="section">
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="displayName" className="text-label">
|
<label htmlFor="username" className="text-label">
|
||||||
{intl.formatMessage(messages.displayName)}
|
{intl.formatMessage(messages.username)}
|
||||||
|
<span className="label-required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
<Field
|
<Field id="username" name="username" type="text" />
|
||||||
id="displayName"
|
|
||||||
name="displayName"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{errors.username &&
|
||||||
|
touched.username &&
|
||||||
|
typeof errors.username === 'string' && (
|
||||||
|
<div className="error">{errors.username}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="email" className="text-label">
|
<label htmlFor="email" className="text-label">
|
||||||
{intl.formatMessage(messages.email)}
|
{intl.formatMessage(messages.email)}
|
||||||
<span className="label-required">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field">
|
||||||
@@ -645,9 +648,16 @@ const UserList = () => {
|
|||||||
className="text-base font-bold leading-5 transition duration-300 hover:underline"
|
className="text-base font-bold leading-5 transition duration-300 hover:underline"
|
||||||
data-testid="user-list-username-link"
|
data-testid="user-list-username-link"
|
||||||
>
|
>
|
||||||
{user.displayName}
|
{user.username ||
|
||||||
|
user.jellyfinUsername ||
|
||||||
|
user.plexUsername ||
|
||||||
|
user.email}
|
||||||
</Link>
|
</Link>
|
||||||
{user.displayName.toLowerCase() !== user.email && (
|
{(
|
||||||
|
user.username ||
|
||||||
|
user.jellyfinUsername ||
|
||||||
|
user.plexUsername
|
||||||
|
)?.toLowerCase() !== user.email && (
|
||||||
<div className="text-sm leading-5 text-gray-300">
|
<div className="text-sm leading-5 text-gray-300">
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,9 +93,14 @@ const UserGeneralSettings = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const UserGeneralSettingsSchema = Yup.object().shape({
|
const UserGeneralSettingsSchema = Yup.object().shape({
|
||||||
email: Yup.string()
|
email:
|
||||||
.email(intl.formatMessage(messages.validationemailformat))
|
user?.id === 1
|
||||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
? Yup.string()
|
||||||
|
.email(intl.formatMessage(messages.validationemailformat))
|
||||||
|
.required(intl.formatMessage(messages.validationemailrequired))
|
||||||
|
: Yup.string().email(
|
||||||
|
intl.formatMessage(messages.validationemailformat)
|
||||||
|
),
|
||||||
discordId: Yup.string()
|
discordId: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
|
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||||
@@ -134,7 +139,7 @@ const UserGeneralSettings = () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
displayName: data?.username ?? '',
|
displayName: data?.username ?? '',
|
||||||
email: data?.email ?? '',
|
email: data?.email?.includes('@') ? data.email : '',
|
||||||
discordId: data?.discordId ?? '',
|
discordId: data?.discordId ?? '',
|
||||||
locale: data?.locale,
|
locale: data?.locale,
|
||||||
region: data?.region,
|
region: data?.region,
|
||||||
@@ -157,7 +162,8 @@ const UserGeneralSettings = () => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: values.displayName,
|
username: values.displayName,
|
||||||
email: values.email,
|
email:
|
||||||
|
values.email || user?.jellyfinUsername || user?.plexUsername,
|
||||||
discordId: values.discordId,
|
discordId: values.discordId,
|
||||||
locale: values.locale,
|
locale: values.locale,
|
||||||
region: values.region,
|
region: values.region,
|
||||||
@@ -264,7 +270,9 @@ const UserGeneralSettings = () => {
|
|||||||
name="displayName"
|
name="displayName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={
|
placeholder={
|
||||||
user?.plexUsername ? user.plexUsername : user?.email
|
user?.username ||
|
||||||
|
user?.jellyfinUsername ||
|
||||||
|
user?.plexUsername
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,6 +297,7 @@ const UserGeneralSettings = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="example@domain.com"
|
placeholder="example@domain.com"
|
||||||
|
disabled={user?.plexUsername}
|
||||||
className={
|
className={
|
||||||
user?.warnings.find((w) => w === 'userEmailRequired')
|
user?.warnings.find((w) => w === 'userEmailRequired')
|
||||||
? 'border-2 border-red-400 focus:border-blue-600'
|
? 'border-2 border-red-400 focus:border-blue-600'
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface User {
|
|||||||
id: number;
|
id: number;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
plexUsername?: string;
|
plexUsername?: string;
|
||||||
|
jellyfinUsername?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -1111,7 +1111,6 @@
|
|||||||
"components.UserList.creating": "Creating…",
|
"components.UserList.creating": "Creating…",
|
||||||
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
|
"components.UserList.deleteconfirm": "Are you sure you want to delete this user? All of their request data will be permanently removed.",
|
||||||
"components.UserList.deleteuser": "Delete User",
|
"components.UserList.deleteuser": "Delete User",
|
||||||
"components.UserList.displayName": "Display Name",
|
|
||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
@@ -1145,9 +1144,11 @@
|
|||||||
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
|
"components.UserList.userdeleteerror": "Something went wrong while deleting the user.",
|
||||||
"components.UserList.userfail": "Something went wrong while saving user permissions.",
|
"components.UserList.userfail": "Something went wrong while saving user permissions.",
|
||||||
"components.UserList.userlist": "User List",
|
"components.UserList.userlist": "User List",
|
||||||
|
"components.UserList.username": "Username",
|
||||||
"components.UserList.users": "Users",
|
"components.UserList.users": "Users",
|
||||||
"components.UserList.userssaved": "User permissions saved successfully!",
|
"components.UserList.userssaved": "User permissions saved successfully!",
|
||||||
"components.UserList.validationEmail": "You must provide a valid email address",
|
"components.UserList.validationEmail": "Email required",
|
||||||
|
"components.UserList.validationUsername": "You must provide an username",
|
||||||
"components.UserList.validationpasswordminchars": "Password is too short; should be a minimum of 8 characters",
|
"components.UserList.validationpasswordminchars": "Password is too short; should be a minimum of 8 characters",
|
||||||
"components.UserProfile.ProfileHeader.joindate": "Joined {joindate}",
|
"components.UserProfile.ProfileHeader.joindate": "Joined {joindate}",
|
||||||
"components.UserProfile.ProfileHeader.profile": "View Profile",
|
"components.UserProfile.ProfileHeader.profile": "View Profile",
|
||||||
|
|||||||
Reference in New Issue
Block a user