feat(users): add editable usernames (#715)

pull/756/head
Jakob Ankarhem 3 years ago committed by GitHub
parent 82ac76b054
commit 20ca3f2f5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,6 +6,7 @@ import {
UpdateDateColumn, UpdateDateColumn,
OneToMany, OneToMany,
RelationCount, RelationCount,
AfterLoad,
} from 'typeorm'; } from 'typeorm';
import { Permission, hasPermission } from '../lib/permissions'; import { Permission, hasPermission } from '../lib/permissions';
import { MediaRequest } from './MediaRequest'; import { MediaRequest } from './MediaRequest';
@ -25,14 +26,19 @@ export class User {
static readonly filteredFields: string[] = ['plexToken', 'password']; static readonly filteredFields: string[] = ['plexToken', 'password'];
public displayName: string;
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id: number;
@Column({ unique: true }) @Column({ unique: true })
public email: string; public email: string;
@Column() @Column({ nullable: true })
public username: string; public plexUsername: string;
@Column({ nullable: true })
public username?: string;
@Column({ nullable: true, select: false }) @Column({ nullable: true, select: false })
public password?: string; public password?: string;
@ -125,4 +131,9 @@ export class User {
}); });
} }
} }
@AfterLoad()
public setDisplayName(): void {
this.displayName = this.username || this.plexUsername;
}
} }

@ -104,7 +104,7 @@ class DiscordAgent
fields.push( fields.push(
{ {
name: 'Requested By', name: 'Requested By',
value: payload.notifyUser.username ?? '', value: payload.notifyUser.displayName ?? '',
inline: true, inline: true,
}, },
{ {
@ -126,7 +126,7 @@ class DiscordAgent
fields.push( fields.push(
{ {
name: 'Requested By', name: 'Requested By',
value: payload.notifyUser.username ?? '', value: payload.notifyUser.displayName ?? '',
inline: true, inline: true,
}, },
{ {
@ -148,7 +148,7 @@ class DiscordAgent
fields.push( fields.push(
{ {
name: 'Requested By', name: 'Requested By',
value: payload.notifyUser.username ?? '', value: payload.notifyUser.displayName ?? '',
inline: true, inline: true,
}, },
{ {
@ -170,7 +170,7 @@ class DiscordAgent
fields.push( fields.push(
{ {
name: 'Requested By', name: 'Requested By',
value: payload.notifyUser.username ?? '', value: payload.notifyUser.displayName ?? '',
inline: true, inline: true,
}, },
{ {

@ -60,7 +60,7 @@ class EmailAgent
mediaName: payload.subject, mediaName: payload.subject,
imageUrl: payload.image, imageUrl: payload.image,
timestamp: new Date().toTimeString(), timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username, requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined, : undefined,
@ -106,7 +106,7 @@ class EmailAgent
mediaName: payload.subject, mediaName: payload.subject,
imageUrl: payload.image, imageUrl: payload.image,
timestamp: new Date().toTimeString(), timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username, requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined, : undefined,
@ -144,7 +144,7 @@ class EmailAgent
mediaName: payload.subject, mediaName: payload.subject,
imageUrl: payload.image, imageUrl: payload.image,
timestamp: new Date().toTimeString(), timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username, requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined, : undefined,
@ -181,7 +181,7 @@ class EmailAgent
mediaName: payload.subject, mediaName: payload.subject,
imageUrl: payload.image, imageUrl: payload.image,
timestamp: new Date().toTimeString(), timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username, requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined, : undefined,
@ -218,7 +218,7 @@ class EmailAgent
mediaName: payload.subject, mediaName: payload.subject,
imageUrl: payload.image, imageUrl: payload.image,
timestamp: new Date().toTimeString(), timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username, requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined, : undefined,

@ -48,42 +48,42 @@ class PushoverAgent
const title = payload.subject; const title = payload.subject;
const plot = payload.message; const plot = payload.message;
const user = payload.notifyUser.username; const username = payload.notifyUser.displayName;
switch (type) { switch (type) {
case Notification.MEDIA_PENDING: case Notification.MEDIA_PENDING:
messageTitle = 'New Request'; messageTitle = 'New Request';
message += `${title}\n\n`; message += `${title}\n\n`;
message += `${plot}\n\n`; message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`; message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nPending Approval\n`; message += `<b>Status</b>\nPending Approval\n`;
break; break;
case Notification.MEDIA_APPROVED: case Notification.MEDIA_APPROVED:
messageTitle = 'Request Approved'; messageTitle = 'Request Approved';
message += `${title}\n\n`; message += `${title}\n\n`;
message += `${plot}\n\n`; message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`; message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nProcessing Request\n`; message += `<b>Status</b>\nProcessing Request\n`;
break; break;
case Notification.MEDIA_AVAILABLE: case Notification.MEDIA_AVAILABLE:
messageTitle = 'Now available!'; messageTitle = 'Now available!';
message += `${title}\n\n`; message += `${title}\n\n`;
message += `${plot}\n\n`; message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`; message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nAvailable\n`; message += `<b>Status</b>\nAvailable\n`;
break; break;
case Notification.MEDIA_DECLINED: case Notification.MEDIA_DECLINED:
messageTitle = 'Request Declined'; messageTitle = 'Request Declined';
message += `${title}\n\n`; message += `${title}\n\n`;
message += `${plot}\n\n`; message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`; message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nDeclined\n`; message += `<b>Status</b>\nDeclined\n`;
break; break;
case Notification.TEST_NOTIFICATION: case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification'; messageTitle = 'Test Notification';
message += `${title}\n\n`; message += `${title}\n\n`;
message += `${plot}\n\n`; message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n`; message += `<b>Requested By</b>\n${username}\n`;
break; break;
} }

@ -69,7 +69,7 @@ class SlackAgent
fields.push( fields.push(
{ {
type: 'mrkdwn', type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
}, },
{ {
type: 'mrkdwn', type: 'mrkdwn',
@ -85,7 +85,7 @@ class SlackAgent
fields.push( fields.push(
{ {
type: 'mrkdwn', type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
}, },
{ {
type: 'mrkdwn', type: 'mrkdwn',
@ -101,7 +101,7 @@ class SlackAgent
fields.push( fields.push(
{ {
type: 'mrkdwn', type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
}, },
{ {
type: 'mrkdwn', type: 'mrkdwn',
@ -117,7 +117,7 @@ class SlackAgent
fields.push( fields.push(
{ {
type: 'mrkdwn', type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.username ?? ''}`, text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
}, },
{ {
type: 'mrkdwn', type: 'mrkdwn',

@ -51,7 +51,7 @@ class TelegramAgent
const title = this.escapeText(payload.subject); const title = this.escapeText(payload.subject);
const plot = this.escapeText(payload.message); 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 */ /* eslint-disable no-useless-escape */
switch (type) { switch (type) {

@ -16,7 +16,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
subject: 'subject', subject: 'subject',
message: 'message', message: 'message',
image: 'image', image: 'image',
notifyuser_username: 'notifyUser.username', notifyuser_username: 'notifyUser.displayName',
notifyuser_email: 'notifyUser.email', notifyuser_email: 'notifyUser.email',
notifyuser_avatar: 'notifyUser.avatar', notifyuser_avatar: 'notifyUser.avatar',
media_tmdbid: 'media.tmdbId', media_tmdbid: 'media.tmdbId',

@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDisplayNameToUser1611508672722 implements MigrationInterface {
name = 'AddDisplayNameToUser1611508672722';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}

@ -48,13 +48,17 @@ authRoutes.post('/login', async (req, res, next) => {
// Let's check if their plex token is up to date // Let's check if their plex token is up to date
if (user.plexToken !== body.authToken) { if (user.plexToken !== body.authToken) {
user.plexToken = body.authToken; user.plexToken = body.authToken;
await userRepository.save(user);
} }
// Update the users avatar with their plex thumbnail (incase it changed) // Update the users avatar with their plex thumbnail (incase it changed)
user.avatar = account.thumb; user.avatar = account.thumb;
user.email = account.email; user.email = account.email;
user.username = account.username; user.plexUsername = account.username;
if (user.username === account.username) {
user.username = '';
}
await userRepository.save(user);
} else { } else {
// Here we check if it's the first user. If it is, we create the user with no check // Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions // and give them admin permissions
@ -63,7 +67,7 @@ authRoutes.post('/login', async (req, res, next) => {
if (totalUsers === 0) { if (totalUsers === 0) {
user = new User({ user = new User({
email: account.email, email: account.email,
username: account.username, plexUsername: account.username,
plexId: account.id, plexId: account.id,
plexToken: account.authToken, plexToken: account.authToken,
permissions: Permission.ADMIN, permissions: Permission.ADMIN,
@ -86,7 +90,7 @@ authRoutes.post('/login', async (req, res, next) => {
if (await mainPlexTv.checkUserAccess(account)) { if (await mainPlexTv.checkUserAccess(account)) {
user = new User({ user = new User({
email: account.email, email: account.email,
username: account.username, plexUsername: account.username,
plexId: account.id, plexId: account.id,
plexToken: account.authToken, plexToken: account.authToken,
permissions: settings.main.defaultPermissions, permissions: settings.main.defaultPermissions,
@ -141,7 +145,7 @@ authRoutes.post('/local', async (req, res, next) => {
try { try {
const user = await userRepository.findOne({ const user = await userRepository.findOne({
select: ['id', 'password'], select: ['id', 'password'],
where: { email: body.email, userType: UserType.LOCAL }, where: { email: body.email },
}); });
const isCorrectCredentials = await user?.passwordMatch(body.password); const isCorrectCredentials = await user?.passwordMatch(body.password);

@ -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); await userRepository.save(user);
return res.status(200).json(user.filter()); return res.status(200).json(user.filter());
@ -213,20 +217,32 @@ router.post('/import-from-plex', async (req, res, next) => {
const createdUsers: User[] = []; const createdUsers: User[] = [];
for (const rawUser of plexUsersResponse.MediaContainer.User) { for (const rawUser of plexUsersResponse.MediaContainer.User) {
const account = rawUser.$; const account = rawUser.$;
const user = await userRepository.findOne({ const user = await userRepository.findOne({
where: { plexId: account.id }, where: [{ plexId: account.id }, { email: account.email }],
}); });
if (user) { if (user) {
// Update the users avatar with their plex thumbnail (incase it changed) // Update the users avatar with their plex thumbnail (incase it changed)
user.avatar = account.thumb; user.avatar = account.thumb;
user.email = account.email; 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); await userRepository.save(user);
} else { } else {
// Check to make sure it's a real account // Check to make sure it's a real account
if (account.email && account.username) { if (account.email && account.username) {
const newUser = new User({ const newUser = new User({
username: account.username, plexUsername: account.username,
email: account.email, email: account.email,
permissions: settings.main.defaultPermissions, permissions: settings.main.defaultPermissions,
plexId: parseInt(account.id), plexId: parseInt(account.id),

@ -82,7 +82,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
/> />
</svg> </svg>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
{request.requestedBy.username} {request.requestedBy.displayName}
</span> </span>
</div> </div>
{request.modifiedBy && ( {request.modifiedBy && (
@ -101,7 +101,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
/> />
</svg> </svg>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
{request.modifiedBy?.username} {request.modifiedBy?.displayName}
</span> </span>
</div> </div>
)} )}

@ -108,7 +108,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
</h2> </h2>
<div className="text-xs truncate sm:text-sm"> <div className="text-xs truncate sm:text-sm">
{intl.formatMessage(messages.requestedby, { {intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username, username: requestData.requestedBy.displayName,
})} })}
</div> </div>
{requestData.media.status && ( {requestData.media.status && (

@ -165,7 +165,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
</Link> </Link>
<div className="text-sm"> <div className="text-sm">
{intl.formatMessage(messages.requestedby, { {intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username, username: requestData.requestedBy.displayName,
})} })}
</div> </div>
{requestData.seasons.length > 0 && ( {requestData.seasons.length > 0 && (
@ -206,7 +206,8 @@ const RequestItem: React.FC<RequestItemProps> = ({
<div className="flex flex-col"> <div className="flex flex-col">
{requestData.modifiedBy ? ( {requestData.modifiedBy ? (
<span className="text-sm text-gray-300"> <span className="text-sm text-gray-300">
{requestData.modifiedBy.username} ( {requestData.modifiedBy.displayName}
(
<FormattedRelativeTime <FormattedRelativeTime
value={Math.floor( value={Math.floor(
(new Date(requestData.updatedAt).getTime() - Date.now()) / (new Date(requestData.updatedAt).getTime() - Date.now()) /

@ -224,7 +224,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
{intl.formatMessage( {intl.formatMessage(
is4k ? messages.request4kfrom : messages.requestfrom, is4k ? messages.request4kfrom : messages.requestfrom,
{ {
username: activeRequest.requestedBy.username, username: activeRequest.requestedBy.displayName,
} }
)} )}
{hasPermission(Permission.REQUEST_ADVANCED) && ( {hasPermission(Permission.REQUEST_ADVANCED) && (

@ -8,10 +8,14 @@ import axios from 'axios';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import Header from '../Common/Header'; import Header from '../Common/Header';
import PermissionEdit from '../PermissionEdit'; 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({ export const messages = defineMessages({
edituser: 'Edit User', edituser: 'Edit User',
username: 'Username', plexUsername: 'Plex Username',
username: 'Display Name',
avatar: 'Avatar', avatar: 'Avatar',
email: 'Email', email: 'Email',
permissions: 'Permissions', permissions: 'Permissions',
@ -25,7 +29,6 @@ const UserEdit: React.FC = () => {
const router = useRouter(); const router = useRouter();
const intl = useIntl(); const intl = useIntl();
const { addToast } = useToasts(); const { addToast } = useToasts();
const [isUpdating, setIsUpdating] = useState(false);
const { user: currentUser } = useUser(); const { user: currentUser } = useUser();
const { user, error, revalidate } = useUser({ const { user, error, revalidate } = useUser({
id: Number(router.query.userId), id: Number(router.query.userId),
@ -40,155 +43,184 @@ const UserEdit: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]); }, [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) { if (!user && !error) {
return <LoadingSpinner />; return <LoadingSpinner />;
} }
const UserEditSchema = Yup.object().shape({
username: Yup.string(),
});
return ( return (
<> <Formik
<Header> initialValues={{
<FormattedMessage {...messages.edituser} /> plexUsername: user?.plexUsername,
</Header> username: user?.username,
<div className="space-y-6"> email: user?.email,
<div className="flex flex-col space-y-6 text-white lg:flex-row lg:space-y-0 lg:space-x-6"> }}
<div className="flex-grow space-y-6"> validationSchema={UserEditSchema}
<div className="space-y-1"> onSubmit={async (values) => {
<label try {
htmlFor="username" await axios.put(`/api/v1/user/${user?.id}`, {
className="block text-sm font-medium leading-5 text-gray-400" permissions: currentPermission,
> email: user?.email,
<FormattedMessage {...messages.username} /> username: values.username,
</label> });
<div className="flex rounded-md shadow-sm"> addToast(intl.formatMessage(messages.usersaved), {
<input appearance: 'success',
id="username" autoDismiss: true,
type="text" });
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5" } catch (e) {
value={user?.username} addToast(intl.formatMessage(messages.userfail), {
readOnly appearance: 'error',
/> autoDismiss: true,
</div> });
</div> throw new Error(`Something went wrong saving the user: ${e.message}`);
<div className="space-y-1"> } finally {
<label revalidate();
htmlFor="email" }
className="block text-sm font-medium leading-5 text-gray-400" }}
> >
<FormattedMessage {...messages.email} /> {({ isSubmitting, handleSubmit }) => (
</label> <Form>
<div className="flex rounded-md shadow-sm"> <Header>
<input <FormattedMessage {...messages.edituser} />
id="email" </Header>
type="text" <div className="space-y-6">
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5" <div className="flex flex-col space-y-6 text-white lg:flex-row lg:space-y-0 lg:space-x-6">
value={user?.email} <div className="flex-grow space-y-6">
readOnly {user?.userType === UserType.PLEX && (
/> <div className="space-y-1">
<label
htmlFor="plexUsername"
className="block text-sm font-medium leading-5 text-gray-400"
>
{intl.formatMessage(messages.plexUsername)}
</label>
<div className="flex rounded-md shadow-sm">
<Field
id="plexUsername"
name="plexUsername"
type="text"
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
readOnly
/>
</div>
</div>
)}
<div className="space-y-1">
<label
htmlFor="username"
className="block text-sm font-medium leading-5 text-gray-400"
>
{intl.formatMessage(messages.username)}
</label>
<div className="flex rounded-md shadow-sm">
<Field
id="username"
name="username"
type="text"
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
</div>
<div className="space-y-1">
<label
htmlFor="email"
className="block text-sm font-medium leading-5 text-gray-400"
>
<FormattedMessage {...messages.email} />
</label>
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
className="flex-grow block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
readOnly
/>
</div>
</div>
</div> </div>
</div>
</div>
<div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0"> <div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
<p <p
className="block text-sm font-medium leading-5 text-gray-400" className="block text-sm font-medium leading-5 text-gray-400"
aria-hidden="true"
>
<FormattedMessage {...messages.avatar} />
</p>
<div className="lg:hidden">
<div className="flex items-center">
<div
className="flex-shrink-0 inline-block w-12 h-12 overflow-hidden rounded-full"
aria-hidden="true" aria-hidden="true"
> >
<FormattedMessage {...messages.avatar} />
</p>
<div className="lg:hidden">
<div className="flex items-center">
<div
className="flex-shrink-0 inline-block w-12 h-12 overflow-hidden rounded-full"
aria-hidden="true"
>
<img
className="w-full h-full rounded-full"
src={user?.avatar}
alt=""
/>
</div>
</div>
</div>
<div className="relative hidden overflow-hidden transition duration-150 ease-in-out rounded-full lg:block">
<img <img
className="w-full h-full rounded-full" className="relative w-40 h-40 rounded-full"
src={user?.avatar} src={user?.avatar}
alt="" alt=""
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="text-white">
<div className="relative hidden overflow-hidden transition duration-150 ease-in-out rounded-full lg:block"> <div className="sm:border-t sm:border-gray-200">
<img <div role="group" aria-labelledby="label-permissions">
className="relative w-40 h-40 rounded-full" <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
src={user?.avatar} <div>
alt="" <div
/> className="text-base font-medium leading-6 sm:text-sm sm:leading-5"
</div> id="label-permissions"
</div> >
</div> <FormattedMessage {...messages.permissions} />
<div className="text-white"> </div>
<div className="sm:border-t sm:border-gray-200"> </div>
<div role="group" aria-labelledby="label-permissions"> <div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline"> <div className="max-w-lg">
<div> <PermissionEdit
<div user={currentUser}
className="text-base font-medium leading-6 sm:text-sm sm:leading-5" currentPermission={currentPermission}
id="label-permissions" onUpdate={(newPermission) =>
> setCurrentPermission(newPermission)
<FormattedMessage {...messages.permissions} /> }
/>
</div>
</div>
</div> </div>
</div> </div>
<div className="mt-4 sm:mt-0 sm:col-span-2"> </div>
<div className="max-w-lg"> <div className="pt-5 mt-8 border-t border-gray-700">
<PermissionEdit <div className="flex justify-end">
user={currentUser} <span className="inline-flex ml-3 rounded-md shadow-sm">
currentPermission={currentPermission} <Button
onUpdate={(newPermission) => buttonType="primary"
setCurrentPermission(newPermission) type="submit"
} disabled={isSubmitting}
/> onClick={() => handleSubmit}
</div> >
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="pt-5 mt-8 border-t border-gray-700"> </Form>
<div className="flex justify-end"> )}
<span className="inline-flex ml-3 rounded-md shadow-sm"> </Formik>
<Button
buttonType="primary"
type="submit"
disabled={isUpdating}
onClick={() => updateUser()}
>
{isUpdating
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</div>
</div>
</>
); );
}; };

@ -452,7 +452,7 @@ const UserList: React.FC = () => {
</div> </div>
<div className="ml-4"> <div className="ml-4">
<div className="text-sm font-medium leading-5"> <div className="text-sm font-medium leading-5">
{user.username} {user.displayName}
</div> </div>
<div className="text-sm leading-5 text-gray-300"> <div className="text-sm leading-5 text-gray-300">
{user.email} {user.email}

@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react'; 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'; import { useRouter } from 'next/dist/client/router';
interface UserContextProps { interface UserContextProps {

@ -2,17 +2,19 @@ import useSwr from 'swr';
import { hasPermission, Permission } from '../../server/lib/permissions'; import { hasPermission, Permission } from '../../server/lib/permissions';
import { UserType } from '../../server/constants/user'; import { UserType } from '../../server/constants/user';
export { Permission, UserType };
export interface User { export interface User {
id: number; id: number;
username: string; plexUsername?: string;
username?: string;
displayName: string;
email: string; email: string;
avatar: string; avatar: string;
permissions: number; permissions: number;
userType: number; userType: number;
} }
export { Permission, UserType };
interface UserHookResponse { interface UserHookResponse {
user?: User; user?: User;
loading: boolean; loading: boolean;

@ -508,7 +508,7 @@
"components.UserEdit.save": "Save", "components.UserEdit.save": "Save",
"components.UserEdit.saving": "Saving…", "components.UserEdit.saving": "Saving…",
"components.UserEdit.userfail": "Something went wrong saving the user.", "components.UserEdit.userfail": "Something went wrong saving the user.",
"components.UserEdit.username": "Username", "components.UserEdit.username": "Display Name",
"components.UserEdit.usersaved": "User saved", "components.UserEdit.usersaved": "User saved",
"components.UserList.admin": "Admin", "components.UserList.admin": "Admin",
"components.UserList.autogeneratepassword": "Automatically generate password", "components.UserList.autogeneratepassword": "Automatically generate password",

Loading…
Cancel
Save