diff --git a/server/entity/User.ts b/server/entity/User.ts index 0b05efb22..078fe15ee 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -6,6 +6,7 @@ import { UpdateDateColumn, OneToMany, RelationCount, + AfterLoad, } from 'typeorm'; import { Permission, hasPermission } from '../lib/permissions'; import { MediaRequest } from './MediaRequest'; @@ -25,14 +26,19 @@ export class User { static readonly filteredFields: string[] = ['plexToken', 'password']; + public displayName: string; + @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; - @Column() - public username: string; + @Column({ nullable: true }) + public plexUsername: string; + + @Column({ nullable: true }) + public username?: string; @Column({ nullable: true, select: false }) public password?: string; @@ -125,4 +131,9 @@ export class User { }); } } + + @AfterLoad() + public setDisplayName(): void { + this.displayName = this.username || this.plexUsername; + } } diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index de6ed7ba4..798f6525f 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -104,7 +104,7 @@ class DiscordAgent fields.push( { name: 'Requested By', - value: payload.notifyUser.username ?? '', + value: payload.notifyUser.displayName ?? '', inline: true, }, { @@ -126,7 +126,7 @@ class DiscordAgent fields.push( { name: 'Requested By', - value: payload.notifyUser.username ?? '', + value: payload.notifyUser.displayName ?? '', inline: true, }, { @@ -148,7 +148,7 @@ class DiscordAgent fields.push( { name: 'Requested By', - value: payload.notifyUser.username ?? '', + value: payload.notifyUser.displayName ?? '', inline: true, }, { @@ -170,7 +170,7 @@ class DiscordAgent fields.push( { name: 'Requested By', - value: payload.notifyUser.username ?? '', + value: payload.notifyUser.displayName ?? '', inline: true, }, { diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index a74a4c18b..ccb428020 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -60,7 +60,7 @@ class EmailAgent mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.notifyUser.username, + requestedBy: payload.notifyUser.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, @@ -106,7 +106,7 @@ class EmailAgent mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.notifyUser.username, + requestedBy: payload.notifyUser.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, @@ -144,7 +144,7 @@ class EmailAgent mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.notifyUser.username, + requestedBy: payload.notifyUser.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, @@ -181,7 +181,7 @@ class EmailAgent mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.notifyUser.username, + requestedBy: payload.notifyUser.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, @@ -218,7 +218,7 @@ class EmailAgent mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), - requestedBy: payload.notifyUser.username, + requestedBy: payload.notifyUser.displayName, actionUrl: applicationUrl ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` : undefined, diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index 158d01c71..5b7713b06 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -48,42 +48,42 @@ class PushoverAgent const title = payload.subject; const plot = payload.message; - const user = payload.notifyUser.username; + const username = payload.notifyUser.displayName; switch (type) { case Notification.MEDIA_PENDING: messageTitle = 'New Request'; message += `${title}\n\n`; message += `${plot}\n\n`; - message += `Requested By\n${user}\n\n`; + message += `Requested By\n${username}\n\n`; message += `Status\nPending Approval\n`; break; case Notification.MEDIA_APPROVED: messageTitle = 'Request Approved'; message += `${title}\n\n`; message += `${plot}\n\n`; - message += `Requested By\n${user}\n\n`; + message += `Requested By\n${username}\n\n`; message += `Status\nProcessing Request\n`; break; case Notification.MEDIA_AVAILABLE: messageTitle = 'Now available!'; message += `${title}\n\n`; message += `${plot}\n\n`; - message += `Requested By\n${user}\n\n`; + message += `Requested By\n${username}\n\n`; message += `Status\nAvailable\n`; break; case Notification.MEDIA_DECLINED: messageTitle = 'Request Declined'; message += `${title}\n\n`; message += `${plot}\n\n`; - message += `Requested By\n${user}\n\n`; + message += `Requested By\n${username}\n\n`; message += `Status\nDeclined\n`; break; case Notification.TEST_NOTIFICATION: messageTitle = 'Test Notification'; message += `${title}\n\n`; message += `${plot}\n\n`; - message += `Requested By\n${user}\n`; + message += `Requested By\n${username}\n`; break; } diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index df338884f..f6ca6856b 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -69,7 +69,7 @@ class SlackAgent fields.push( { type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, + text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, }, { type: 'mrkdwn', @@ -85,7 +85,7 @@ class SlackAgent fields.push( { type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, + text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, }, { type: 'mrkdwn', @@ -101,7 +101,7 @@ class SlackAgent fields.push( { type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, + text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, }, { type: 'mrkdwn', @@ -117,7 +117,7 @@ class SlackAgent fields.push( { type: 'mrkdwn', - text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, + text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`, }, { type: 'mrkdwn', diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 62de93ec0..a2b09c1cd 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -51,7 +51,7 @@ class TelegramAgent const title = this.escapeText(payload.subject); const plot = this.escapeText(payload.message); - const user = this.escapeText(payload.notifyUser.username); + const user = this.escapeText(payload.notifyUser.displayName); /* eslint-disable no-useless-escape */ switch (type) { diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index d0e502e8e..796593da1 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -16,7 +16,7 @@ const KeyMap: Record = { subject: 'subject', message: 'message', image: 'image', - notifyuser_username: 'notifyUser.username', + notifyuser_username: 'notifyUser.displayName', notifyuser_email: 'notifyUser.email', notifyuser_avatar: 'notifyUser.avatar', media_tmdbid: 'media.tmdbId', diff --git a/server/migration/1611508672722-AddDisplayNameToUser.ts b/server/migration/1611508672722-AddDisplayNameToUser.ts new file mode 100644 index 000000000..cacea0597 --- /dev/null +++ b/server/migration/1611508672722-AddDisplayNameToUser.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDisplayNameToUser1611508672722 implements MigrationInterface { + name = 'AddDisplayNameToUser1611508672722'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "username" FROM "user"` + ); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + await queryRunner.query( + `CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername" FROM "user"` + ); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query( + `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername" FROM "temporary_user"` + ); + await queryRunner.query(`DROP TABLE "temporary_user"`); + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query( + `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType" FROM "temporary_user"` + ); + await queryRunner.query(`DROP TABLE "temporary_user"`); + } +} diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 8c2a90fda..00d1ef9d3 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -48,13 +48,17 @@ authRoutes.post('/login', async (req, res, next) => { // Let's check if their plex token is up to date if (user.plexToken !== body.authToken) { user.plexToken = body.authToken; - await userRepository.save(user); } // Update the users avatar with their plex thumbnail (incase it changed) user.avatar = account.thumb; user.email = account.email; - user.username = account.username; + user.plexUsername = account.username; + + if (user.username === account.username) { + user.username = ''; + } + await userRepository.save(user); } else { // Here we check if it's the first user. If it is, we create the user with no check // and give them admin permissions @@ -63,7 +67,7 @@ authRoutes.post('/login', async (req, res, next) => { if (totalUsers === 0) { user = new User({ email: account.email, - username: account.username, + plexUsername: account.username, plexId: account.id, plexToken: account.authToken, permissions: Permission.ADMIN, @@ -86,7 +90,7 @@ authRoutes.post('/login', async (req, res, next) => { if (await mainPlexTv.checkUserAccess(account)) { user = new User({ email: account.email, - username: account.username, + plexUsername: account.username, plexId: account.id, plexToken: account.authToken, permissions: settings.main.defaultPermissions, @@ -141,7 +145,7 @@ authRoutes.post('/local', async (req, res, next) => { try { const user = await userRepository.findOne({ select: ['id', 'password'], - where: { email: body.email, userType: UserType.LOCAL }, + where: { email: body.email }, }); const isCorrectCredentials = await user?.passwordMatch(body.password); diff --git a/server/routes/user.ts b/server/routes/user.ts index 5b3887dca..896278efa 100644 --- a/server/routes/user.ts +++ b/server/routes/user.ts @@ -138,7 +138,11 @@ router.put<{ id: string }>('/:id', async (req, res, next) => { }); } - Object.assign(user, req.body); + Object.assign(user, { + username: req.body.username, + permissions: req.body.permissions, + }); + await userRepository.save(user); return res.status(200).json(user.filter()); @@ -213,20 +217,32 @@ router.post('/import-from-plex', async (req, res, next) => { const createdUsers: User[] = []; for (const rawUser of plexUsersResponse.MediaContainer.User) { const account = rawUser.$; + const user = await userRepository.findOne({ - where: { plexId: account.id }, + where: [{ plexId: account.id }, { email: account.email }], }); + if (user) { // Update the users avatar with their plex thumbnail (incase it changed) user.avatar = account.thumb; user.email = account.email; - user.username = account.username; + user.plexUsername = account.username; + + // in-case the user was previously a local account + if (user.userType === UserType.LOCAL) { + user.userType = UserType.PLEX; + user.plexId = parseInt(account.id); + + if (user.username === account.username) { + user.username = ''; + } + } await userRepository.save(user); } else { // Check to make sure it's a real account if (account.email && account.username) { const newUser = new User({ - username: account.username, + plexUsername: account.username, email: account.email, permissions: settings.main.defaultPermissions, plexId: parseInt(account.id), diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index e154f7e5b..aa925aef2 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -82,7 +82,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { /> - {request.requestedBy.username} + {request.requestedBy.displayName} {request.modifiedBy && ( @@ -101,7 +101,7 @@ const RequestBlock: React.FC = ({ request, onUpdate }) => { /> - {request.modifiedBy?.username} + {request.modifiedBy?.displayName} )} diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 7a1a082ab..196db861e 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -108,7 +108,7 @@ const RequestCard: React.FC = ({ request }) => {
{intl.formatMessage(messages.requestedby, { - username: requestData.requestedBy.username, + username: requestData.requestedBy.displayName, })}
{requestData.media.status && ( diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 9c337c0fb..53eb2d6a4 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -165,7 +165,7 @@ const RequestItem: React.FC = ({
{intl.formatMessage(messages.requestedby, { - username: requestData.requestedBy.username, + username: requestData.requestedBy.displayName, })}
{requestData.seasons.length > 0 && ( @@ -206,7 +206,8 @@ const RequestItem: React.FC = ({
{requestData.modifiedBy ? ( - {requestData.modifiedBy.username} ( + {requestData.modifiedBy.displayName} + ( = ({ {intl.formatMessage( is4k ? messages.request4kfrom : messages.requestfrom, { - username: activeRequest.requestedBy.username, + username: activeRequest.requestedBy.displayName, } )} {hasPermission(Permission.REQUEST_ADVANCED) && ( diff --git a/src/components/UserEdit/index.tsx b/src/components/UserEdit/index.tsx index d5b2a56cc..d8798dea3 100644 --- a/src/components/UserEdit/index.tsx +++ b/src/components/UserEdit/index.tsx @@ -8,10 +8,14 @@ import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; import Header from '../Common/Header'; import PermissionEdit from '../PermissionEdit'; +import { Field, Form, Formik } from 'formik'; +import * as Yup from 'yup'; +import { UserType } from '../../../server/constants/user'; export const messages = defineMessages({ edituser: 'Edit User', - username: 'Username', + plexUsername: 'Plex Username', + username: 'Display Name', avatar: 'Avatar', email: 'Email', permissions: 'Permissions', @@ -25,7 +29,6 @@ const UserEdit: React.FC = () => { const router = useRouter(); const intl = useIntl(); const { addToast } = useToasts(); - const [isUpdating, setIsUpdating] = useState(false); const { user: currentUser } = useUser(); const { user, error, revalidate } = useUser({ id: Number(router.query.userId), @@ -40,155 +43,184 @@ const UserEdit: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [user]); - const updateUser = async () => { - try { - setIsUpdating(true); - - await axios.put(`/api/v1/user/${user?.id}`, { - permissions: currentPermission, - email: user?.email, - }); - - addToast(intl.formatMessage(messages.usersaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.userfail), { - appearance: 'error', - autoDismiss: true, - }); - throw new Error(`Something went wrong saving the user: ${e.message}`); - } finally { - revalidate(); - setIsUpdating(false); - } - }; - if (!user && !error) { return ; } + const UserEditSchema = Yup.object().shape({ + username: Yup.string(), + }); + return ( - <> -
- -
-
-
-
-
- -
- -
-
-
- -
- + { + try { + await axios.put(`/api/v1/user/${user?.id}`, { + permissions: currentPermission, + email: user?.email, + username: values.username, + }); + addToast(intl.formatMessage(messages.usersaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.userfail), { + appearance: 'error', + autoDismiss: true, + }); + throw new Error(`Something went wrong saving the user: ${e.message}`); + } finally { + revalidate(); + } + }} + > + {({ isSubmitting, handleSubmit }) => ( +
+
+ +
+
+
+
+ {user?.userType === UserType.PLEX && ( +
+ +
+ +
+
+ )} +
+ +
+ +
+
+
+ +
+ +
+
-
-
-
- -
-
-
+ +
+
+ +
+
+ +
- -
- -
-
-
-
-
-
-
-
-
- +
+
+
+
+
+
+ +
+
+
+
+ + setCurrentPermission(newPermission) + } + /> +
+
-
-
- - setCurrentPermission(newPermission) - } - /> -
+
+
+
+ + +
-
-
- - - -
-
-
-
- + + )} + ); }; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 4c98db270..aa5d844e9 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -452,7 +452,7 @@ const UserList: React.FC = () => {
- {user.username} + {user.displayName}
{user.email} diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx index 24809a18c..ba1bc1a1f 100644 --- a/src/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { User, useUser } from '../hooks/useUser'; +import { useUser, User } from '../hooks/useUser'; import { useRouter } from 'next/dist/client/router'; interface UserContextProps { diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index c2102a009..18d67bf06 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -2,17 +2,19 @@ import useSwr from 'swr'; import { hasPermission, Permission } from '../../server/lib/permissions'; import { UserType } from '../../server/constants/user'; +export { Permission, UserType }; + export interface User { id: number; - username: string; + plexUsername?: string; + username?: string; + displayName: string; email: string; avatar: string; permissions: number; userType: number; } -export { Permission, UserType }; - interface UserHookResponse { user?: User; loading: boolean; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index be5768790..d04a01419 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -508,7 +508,7 @@ "components.UserEdit.save": "Save", "components.UserEdit.saving": "Saving…", "components.UserEdit.userfail": "Something went wrong saving the user.", - "components.UserEdit.username": "Username", + "components.UserEdit.username": "Display Name", "components.UserEdit.usersaved": "User saved", "components.UserList.admin": "Admin", "components.UserList.autogeneratepassword": "Automatically generate password",