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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
{
|
||||
|
@@ -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,
|
||||
|
@@ -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 += `<b>Requested By</b>\n${user}\n\n`;
|
||||
message += `<b>Requested By</b>\n${username}\n\n`;
|
||||
message += `<b>Status</b>\nPending Approval\n`;
|
||||
break;
|
||||
case Notification.MEDIA_APPROVED:
|
||||
messageTitle = 'Request Approved';
|
||||
message += `${title}\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`;
|
||||
break;
|
||||
case Notification.MEDIA_AVAILABLE:
|
||||
messageTitle = 'Now available!';
|
||||
message += `${title}\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`;
|
||||
break;
|
||||
case Notification.MEDIA_DECLINED:
|
||||
messageTitle = 'Request Declined';
|
||||
message += `${title}\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`;
|
||||
break;
|
||||
case Notification.TEST_NOTIFICATION:
|
||||
messageTitle = 'Test Notification';
|
||||
message += `${title}\n\n`;
|
||||
message += `${plot}\n\n`;
|
||||
message += `<b>Requested By</b>\n${user}\n`;
|
||||
message += `<b>Requested By</b>\n${username}\n`;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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) {
|
||||
|
@@ -16,7 +16,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
|
||||
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',
|
||||
|
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
|
||||
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);
|
||||
|
@@ -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),
|
||||
|
Reference in New Issue
Block a user