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

This commit is contained in:
Jakob Ankarhem
2021-01-27 00:09:42 +01:00
committed by GitHub
parent 82ac76b054
commit 20ca3f2f5f
19 changed files with 284 additions and 175 deletions

View File

@@ -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;
}
}

View File

@@ -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,
},
{

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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',

View 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"`);
}
}

View File

@@ -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);

View File

@@ -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),