mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(users): add editable usernames (#715)
This commit is contained in:
@@ -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',
|
||||||
|
43
server/migration/1611508672722-AddDisplayNameToUser.ts
Normal file
43
server/migration/1611508672722-AddDisplayNameToUser.ts
Normal file
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const UserEditSchema = Yup.object().shape({
|
||||||
<>
|
username: Yup.string(),
|
||||||
<Header>
|
});
|
||||||
<FormattedMessage {...messages.edituser} />
|
|
||||||
</Header>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<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">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="block text-sm font-medium leading-5 text-gray-400"
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.username} />
|
|
||||||
</label>
|
|
||||||
<div className="flex rounded-md shadow-sm">
|
|
||||||
<input
|
|
||||||
id="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"
|
|
||||||
value={user?.username}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</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">
|
|
||||||
<input
|
|
||||||
id="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"
|
|
||||||
value={user?.email}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
|
return (
|
||||||
<p
|
<Formik
|
||||||
className="block text-sm font-medium leading-5 text-gray-400"
|
initialValues={{
|
||||||
aria-hidden="true"
|
plexUsername: user?.plexUsername,
|
||||||
>
|
username: user?.username,
|
||||||
<FormattedMessage {...messages.avatar} />
|
email: user?.email,
|
||||||
</p>
|
}}
|
||||||
<div className="lg:hidden">
|
validationSchema={UserEditSchema}
|
||||||
<div className="flex items-center">
|
onSubmit={async (values) => {
|
||||||
<div
|
try {
|
||||||
className="flex-shrink-0 inline-block w-12 h-12 overflow-hidden rounded-full"
|
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 }) => (
|
||||||
|
<Form>
|
||||||
|
<Header>
|
||||||
|
<FormattedMessage {...messages.edituser} />
|
||||||
|
</Header>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<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">
|
||||||
|
{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 className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
|
||||||
|
<p
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400"
|
||||||
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",
|
||||||
|
Reference in New Issue
Block a user