From d5f817e734131cdacc229361d9498a095af57950 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Mon, 29 Jul 2024 21:27:31 +0200 Subject: [PATCH] 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 --- cypress/e2e/user/user-list.cy.ts | 4 +- server/routes/auth.ts | 18 ++++---- server/routes/user/index.ts | 21 +++++++-- server/routes/user/usersettings.ts | 4 +- src/components/Layout/UserDropdown/index.tsx | 8 ++-- .../UserList/JellyfinImportModal.tsx | 2 +- src/components/UserList/index.tsx | 46 +++++++++++-------- .../UserGeneralSettings/index.tsx | 21 ++++++--- src/hooks/useUser.ts | 1 + src/i18n/locale/en.json | 5 +- 10 files changed, 85 insertions(+), 45 deletions(-) diff --git a/cypress/e2e/user/user-list.cy.ts b/cypress/e2e/user/user-list.cy.ts index 503bd23f1..82117023b 100644 --- a/cypress/e2e/user/user-list.cy.ts +++ b/cypress/e2e/user/user-list.cy.ts @@ -1,5 +1,5 @@ const testUser = { - displayName: 'Test User', + username: 'Test User', emailAddress: 'test@seeerr.dev', password: 'test1234', }; @@ -32,7 +32,7 @@ describe('User List', () => { 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('#password').type(testUser.password); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index a473522fa..966dc2694 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -320,7 +320,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { // with admin permission settings.main.mediaServerType = MediaServerType.JELLYFIN; user = new User({ - email: body.email, + email: body.email || account.User.Name, jellyfinUsername: account.User.Name, jellyfinUserId: account.User.Id, jellyfinDeviceId: deviceId, @@ -328,7 +328,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { permissions: Permission.ADMIN, avatar: account.User.PrimaryImageTag ? `${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, }); @@ -371,7 +374,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { if (account.User.PrimaryImageTag) { user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; } else { - user.avatar = gravatarUrl(user.email, { + user.avatar = gravatarUrl(user.email || account.User.Name, { default: 'mm', size: 200, }); @@ -413,10 +416,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => { } ); - if (!body.email) { - throw new Error('add_email'); - } - user = new User({ email: body.email, jellyfinUsername: account.User.Name, @@ -426,7 +425,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { permissions: settings.main.defaultPermissions, avatar: account.User.PrimaryImageTag ? `${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, }); //initialize Jellyfin/Emby users with local login diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 22ebcaa80..016709c60 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -41,7 +41,19 @@ router.get('/', async (req, res, next) => { break; case 'displayname': 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' ); break; @@ -90,12 +102,13 @@ router.post( const settings = getSettings(); const body = req.body; + const email = body.email || body.username; const userRepository = getRepository(User); const existingUser = await userRepository .createQueryBuilder('user') .where('user.email = :email', { - email: body.email.toLowerCase(), + email: email.toLowerCase(), }) .getOne(); @@ -108,7 +121,7 @@ router.post( } 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 ( !passedExplicitPassword && @@ -118,9 +131,9 @@ router.post( } const user = new User({ + email, avatar: body.avatar ?? avatar, username: body.username, - email: body.email, password: body.password, permissions: settings.main.defaultPermissions, plexToken: '', diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 53eed9ef9..9669cb186 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -98,7 +98,9 @@ userSettingsRoutes.post< } 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 if ( diff --git a/src/components/Layout/UserDropdown/index.tsx b/src/components/Layout/UserDropdown/index.tsx index 4abe04e3b..8620f6068 100644 --- a/src/components/Layout/UserDropdown/index.tsx +++ b/src/components/Layout/UserDropdown/index.tsx @@ -90,9 +90,11 @@ const UserDropdown = () => { {user?.displayName} - - {user?.email} - + {user?.displayName?.toLowerCase() !== user?.email && ( + + {user?.email} + + )} {user && } diff --git a/src/components/UserList/JellyfinImportModal.tsx b/src/components/UserList/JellyfinImportModal.tsx index 3d1dc9f63..ed549ac5d 100644 --- a/src/components/UserList/JellyfinImportModal.tsx +++ b/src/components/UserList/JellyfinImportModal.tsx @@ -78,7 +78,7 @@ const JellyfinImportModal: React.FC = ({ }), }); if (!res.ok) throw new Error(); - const { data: createdUsers } = await res.json(); + const createdUsers = await res.json(); if (!createdUsers.length) { throw new Error('No users were imported from Jellyfin.'); diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index df1ce3e4b..66df469be 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -68,14 +68,15 @@ const messages = defineMessages('components.UserList', { usercreatedfailedexisting: 'The provided email address is already in use by another user.', usercreatedsuccess: 'User created successfully!', - displayName: 'Display Name', + username: 'Username', email: 'Email Address', password: 'Password', passwordinfodescription: 'Configure an application URL and enable email notifications to allow automatic password generation.', autogeneratepassword: 'Automatically Generate Password', 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', sortDisplayName: 'Display Name', sortRequests: 'Request Count', @@ -208,9 +209,10 @@ const UserList = () => { } const CreateUserSchema = Yup.object().shape({ - email: Yup.string() - .required(intl.formatMessage(messages.validationEmail)) - .email(intl.formatMessage(messages.validationEmail)), + username: Yup.string().required( + intl.formatMessage(messages.validationUsername) + ), + email: Yup.string().email(intl.formatMessage(messages.validationEmail)), password: Yup.lazy((value) => !value ? Yup.string() @@ -258,7 +260,7 @@ const UserList = () => { setDeleteModal({ isOpen: false, user: deleteModal.user }) } title={intl.formatMessage(messages.deleteuser)} - subTitle={deleteModal.user?.displayName} + subTitle={deleteModal.user?.username} > {intl.formatMessage(messages.deleteconfirm)} @@ -276,7 +278,7 @@ const UserList = () => { > { 'Content-Type': 'application/json', }, body: JSON.stringify({ - username: values.displayName, + username: values.username, email: values.email, password: values.genpassword ? null : values.password, }), @@ -370,23 +372,24 @@ const UserList = () => { )}
-
@@ -645,9 +648,16 @@ const UserList = () => { className="text-base font-bold leading-5 transition duration-300 hover:underline" data-testid="user-list-username-link" > - {user.displayName} + {user.username || + user.jellyfinUsername || + user.plexUsername || + user.email} - {user.displayName.toLowerCase() !== user.email && ( + {( + user.username || + user.jellyfinUsername || + user.plexUsername + )?.toLowerCase() !== user.email && (
{user.email}
diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 7b100a0d1..3bcf1a049 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -93,9 +93,14 @@ const UserGeneralSettings = () => { ); const UserGeneralSettingsSchema = Yup.object().shape({ - email: Yup.string() - .email(intl.formatMessage(messages.validationemailformat)) - .required(intl.formatMessage(messages.validationemailrequired)), + email: + user?.id === 1 + ? Yup.string() + .email(intl.formatMessage(messages.validationemailformat)) + .required(intl.formatMessage(messages.validationemailrequired)) + : Yup.string().email( + intl.formatMessage(messages.validationemailformat) + ), discordId: Yup.string() .nullable() .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), @@ -134,7 +139,7 @@ const UserGeneralSettings = () => { { }, body: JSON.stringify({ username: values.displayName, - email: values.email, + email: + values.email || user?.jellyfinUsername || user?.plexUsername, discordId: values.discordId, locale: values.locale, region: values.region, @@ -264,7 +270,9 @@ const UserGeneralSettings = () => { name="displayName" type="text" placeholder={ - user?.plexUsername ? user.plexUsername : user?.email + user?.username || + user?.jellyfinUsername || + user?.plexUsername } />
@@ -289,6 +297,7 @@ const UserGeneralSettings = () => { name="email" type="text" placeholder="example@domain.com" + disabled={user?.plexUsername} className={ user?.warnings.find((w) => w === 'userEmailRequired') ? 'border-2 border-red-400 focus:border-blue-600' diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index fa034437d..f1e830066 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -12,6 +12,7 @@ export interface User { id: number; warnings: string[]; plexUsername?: string; + jellyfinUsername?: string; username?: string; displayName: string; email: string; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 390a7e1bb..3dc25214b 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1111,7 +1111,6 @@ "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.deleteuser": "Delete User", - "components.UserList.displayName": "Display Name", "components.UserList.edituser": "Edit User Permissions", "components.UserList.email": "Email Address", "components.UserList.importedfromJellyfin": "{userCount} {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.userfail": "Something went wrong while saving user permissions.", "components.UserList.userlist": "User List", + "components.UserList.username": "Username", "components.UserList.users": "Users", "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.UserProfile.ProfileHeader.joindate": "Joined {joindate}", "components.UserProfile.ProfileHeader.profile": "View Profile",