Skip to content

Commit

Permalink
refactor: decouple Plex as a requirement for setting up Overseerr
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan Cohen committed Sep 12, 2022
1 parent 87825a0 commit 0f30ece
Show file tree
Hide file tree
Showing 22 changed files with 460 additions and 123 deletions.
1 change: 0 additions & 1 deletion cypress/e2e/discover.cy.ts
Expand Up @@ -145,7 +145,6 @@ describe('Discover', () => {
plexUsername: null,
username: '',
recoveryLinkExpirationDate: null,
userType: 2,
avatar:
'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200',
movieQuotaLimit: null,
Expand Down
12 changes: 8 additions & 4 deletions overseerr-api.yml
Expand Up @@ -61,10 +61,6 @@ components:
plexUsername:
type: string
readOnly: true
userType:
type: integer
example: 1
readOnly: true
permissions:
type: number
example: 0
Expand All @@ -83,6 +79,14 @@ components:
type: number
example: 5
readOnly: true
isPlexUser:
type: boolean
example: false
readOnly: true
isLocalUser:
type: boolean
example: true
readOnly: true
required:
- id
- email
Expand Down
4 changes: 0 additions & 4 deletions server/constants/user.ts

This file was deleted.

42 changes: 26 additions & 16 deletions server/entity/User.ts
@@ -1,5 +1,4 @@
import { MediaRequestStatus, MediaType } from '@server/constants/media';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import PreparedEmail from '@server/lib/email';
Expand Down Expand Up @@ -35,14 +34,22 @@ export class User {
public static filterMany(
users: User[],
showFiltered?: boolean
): Partial<User>[] {
): Omit<User, keyof typeof User.filteredFields>[] {
return users.map((u) => u.filter(showFiltered));
}

static readonly filteredFields: string[] = ['email'];
// Fields that show only be shown to admins in user API responses
static readonly filteredFields: (keyof User)[] = ['email', 'plexId'];

// Fields that should never be shown in API responses
static readonly secureFields: (keyof User)[] = ['password'];

public displayName: string;

public isPlexUser: boolean;

public isLocalUser: boolean;

@PrimaryGeneratedColumn()
public id: number;

Expand All @@ -61,7 +68,7 @@ export class User {
@Column({ nullable: true })
public username?: string;

@Column({ nullable: true, select: false })
@Column({ nullable: true })
public password?: string;

@Column({ nullable: true, select: false })
Expand All @@ -70,10 +77,7 @@ export class User {
@Column({ type: 'date', nullable: true })
public recoveryLinkExpirationDate?: Date | null;

@Column({ type: 'integer', default: UserType.PLEX })
public userType: UserType;

@Column({ nullable: true, select: false })
@Column({ nullable: true })
public plexId?: number;

@Column({ nullable: true, select: false })
Expand Down Expand Up @@ -126,13 +130,17 @@ export class User {
Object.assign(this, init);
}

public filter(showFiltered?: boolean): Partial<User> {
const filtered: Partial<User> = Object.assign(
{},
...(Object.keys(this) as (keyof User)[])
.filter((k) => showFiltered || !User.filteredFields.includes(k))
.map((k) => ({ [k]: this[k] }))
);
public filter(
showFiltered?: boolean
): Omit<User, keyof typeof User.filteredFields> {
const filtered: Omit<User, keyof typeof User.filteredFields> =
Object.assign(
{},
...(Object.keys(this) as (keyof User)[])
.filter((k) => showFiltered || !User.filteredFields.includes(k))
.filter((k) => !User.secureFields.includes(k))
.map((k) => ({ [k]: this[k] }))
);

return filtered;
}
Expand Down Expand Up @@ -229,8 +237,10 @@ export class User {
}

@AfterLoad()
public setDisplayName(): void {
public setLocalProperties(): void {
this.displayName = this.username || this.plexUsername || this.email;
this.isPlexUser = !!this.plexId;
this.isLocalUser = !!this.password;
}

public async getQuota(): Promise<QuotaResponse> {
Expand Down
4 changes: 4 additions & 0 deletions server/lib/scanners/plex/index.ts
Expand Up @@ -70,6 +70,10 @@ class PlexScanner
return this.log('No admin configured. Plex scan skipped.', 'warn');
}

if (!admin.plexToken || !settings.plex.ip) {
return this.log('Plex server is not configured.', 'warn');
}

this.plexClient = new PlexAPI({ plexToken: admin.plexToken });

this.libraries = settings.plex.libraries.filter(
Expand Down
55 changes: 34 additions & 21 deletions server/routes/auth.ts
@@ -1,12 +1,12 @@
import PlexTvAPI from '@server/api/plextv';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';

const authRoutes = Router();

Expand Down Expand Up @@ -41,14 +41,21 @@ authRoutes.post('/plex', async (req, res, next) => {
const plextv = new PlexTvAPI(body.authToken);
const account = await plextv.getUser();

// Next let's see if the user already exists
let user = await userRepository
.createQueryBuilder('user')
.where('user.plexId = :id', { id: account.id })
.orWhere('user.email = :email', {
email: account.email.toLowerCase(),
})
.getOne();
let user: User | null;

// If we are already logged in, we should just get the currently logged in user
// otherwise we will try to match to an existing users email or plex ID
if (req.user) {
user = await userRepository.findOneBy({ id: req.user.id });
} else {
user = await userRepository
.createQueryBuilder('user')
.where('user.plexId = :id', { id: account.id })
.orWhere('user.email = :email', {
email: account.email.toLowerCase(),
})
.getOne();
}

if (!user && !(await userRepository.count())) {
user = new User({
Expand All @@ -58,7 +65,6 @@ authRoutes.post('/plex', async (req, res, next) => {
plexToken: account.authToken,
permissions: Permission.ADMIN,
avatar: account.thumb,
userType: UserType.PLEX,
});

await userRepository.save(user);
Expand All @@ -71,12 +77,13 @@ authRoutes.post('/plex', async (req, res, next) => {

if (
account.id === mainUser.plexId ||
(user && user.id === 1 && !user.plexId) ||
(await mainPlexTv.checkUserAccess(account.id))
) {
if (user) {
if (!user.plexId) {
logger.info(
'Found matching Plex user; updating user with Plex data',
'Found matching Plex user; updating user with Plex data. Notice: Emails are no longer synced.',
{
label: 'API',
ip: req.ip,
Expand All @@ -91,9 +98,7 @@ authRoutes.post('/plex', async (req, res, next) => {
user.plexToken = body.authToken;
user.plexId = account.id;
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
user.userType = UserType.PLEX;

await userRepository.save(user);
} else if (!settings.main.newPlexLogin) {
Expand Down Expand Up @@ -129,7 +134,6 @@ authRoutes.post('/plex', async (req, res, next) => {
plexToken: account.authToken,
permissions: settings.main.defaultPermissions,
avatar: account.thumb,
userType: UserType.PLEX,
});

await userRepository.save(user);
Expand Down Expand Up @@ -184,13 +188,22 @@ authRoutes.post('/local', async (req, res, next) => {
});
}
try {
const user = await userRepository
let user = await userRepository
.createQueryBuilder('user')
.select(['user.id', 'user.email', 'user.password', 'user.plexId'])
.where('user.email = :email', { email: body.email.toLowerCase() })
.getOne();

if (!user || !(await user.passwordMatch(body.password))) {
if (!user && !(await userRepository.count())) {
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
user = new User({
email: body.email,
permissions: Permission.ADMIN,
avatar,
});
await user.setPassword(body.password);
await userRepository.save(user);
} else if (!user || !(await user.passwordMatch(body.password))) {
logger.warn('Failed sign-in attempt using invalid Overseerr password', {
label: 'API',
ip: req.ip,
Expand All @@ -203,19 +216,19 @@ authRoutes.post('/local', async (req, res, next) => {
});
}

const mainUser = await userRepository.findOneOrFail({
const mainUser = await userRepository.findOne({
select: { id: true, plexToken: true, plexId: true },
where: { id: 1 },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
const mainPlexTv = new PlexTvAPI(mainUser?.plexToken ?? '');

if (!user.plexId) {
if (!user.plexId && mainUser?.isPlexUser) {
try {
const plexUsersResponse = await mainPlexTv.getUsers();
const account = plexUsersResponse.MediaContainer.User.find(
(account) =>
account.$.email &&
account.$.email.toLowerCase() === user.email.toLowerCase()
account.$.email.toLowerCase() === user?.email.toLowerCase()
)?.$;

if (
Expand All @@ -238,7 +251,6 @@ authRoutes.post('/local', async (req, res, next) => {
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
user.userType = UserType.PLEX;

await userRepository.save(user);
}
Expand All @@ -251,6 +263,7 @@ authRoutes.post('/local', async (req, res, next) => {
}

if (
mainUser?.isPlexUser &&
user.plexId &&
user.plexId !== mainUser.plexId &&
!(await mainPlexTv.checkUserAccess(user.plexId))
Expand Down
27 changes: 24 additions & 3 deletions server/routes/settings/index.ts
Expand Up @@ -16,7 +16,7 @@ import type { AvailableCacheIds } from '@server/lib/cache';
import cacheManager from '@server/lib/cache';
import { Permission } from '@server/lib/permissions';
import { plexFullScanner } from '@server/lib/scanners/plex';
import type { MainSettings } from '@server/lib/settings';
import type { MainSettings, PlexSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
Expand Down Expand Up @@ -82,10 +82,25 @@ settingsRoutes.post('/main/regenerate', (req, res, next) => {
return res.status(200).json(filteredMainSettings(req.user, main));
});

settingsRoutes.get('/plex', (_req, res) => {
type PlexSettingsResponse = PlexSettings & {
plexAvailable: boolean;
};

settingsRoutes.get<never, PlexSettingsResponse>('/plex', async (_req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);

const admin = await userRepository.findOneOrFail({
select: { id: true, plexToken: true },
where: { id: 1 },
});

res.status(200).json(settings.plex);
const settingsResponse: PlexSettingsResponse = {
...settings.plex,
plexAvailable: !!admin.plexToken,
};

res.status(200).json(settingsResponse);
});

settingsRoutes.post('/plex', async (req, res, next) => {
Expand All @@ -97,6 +112,12 @@ settingsRoutes.post('/plex', async (req, res, next) => {
where: { id: 1 },
});

if (!admin.plexToken) {
throw new Error(
'The administrator must have their account connected to Plex to be able to set up a Plex server.'
);
}

Object.assign(settings.plex, req.body);

const plexClient = new PlexAPI({ plexToken: admin.plexToken });
Expand Down
10 changes: 1 addition & 9 deletions server/routes/user/index.ts
@@ -1,7 +1,6 @@
import PlexTvAPI from '@server/api/plextv';
import TautulliAPI from '@server/api/tautulli';
import { MediaType } from '@server/constants/media';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
Expand Down Expand Up @@ -121,7 +120,6 @@ router.post(
password: body.password,
permissions: settings.main.defaultPermissions,
plexToken: '',
userType: UserType.LOCAL,
});

if (passedExplicitPassword) {
Expand Down Expand Up @@ -434,12 +432,7 @@ router.post(
user.avatar = account.thumb;
user.email = account.email;
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);
}
user.plexId = parseInt(account.id);
await userRepository.save(user);
} else if (!body || body.plexIds.includes(account.id)) {
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
Expand All @@ -450,7 +443,6 @@ router.post(
plexId: parseInt(account.id),
plexToken: '',
avatar: account.thumb,
userType: UserType.PLEX,
});
await userRepository.save(newUser);
createdUsers.push(newUser);
Expand Down
3 changes: 0 additions & 3 deletions server/scripts/prepareTestDb.ts
@@ -1,4 +1,3 @@
import { UserType } from '@server/constants/user';
import dataSource, { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import { copyFileSync } from 'fs';
Expand Down Expand Up @@ -43,7 +42,6 @@ const prepareDb = async () => {
user.plexUsername = 'admin';
user.username = 'admin';
user.email = 'admin@seerr.dev';
user.userType = UserType.PLEX;
await user.setPassword('test1234');
user.permissions = 2;
user.avatar = gravatarUrl('admin@seerr.dev', { default: 'mm', size: 200 });
Expand All @@ -59,7 +57,6 @@ const prepareDb = async () => {
otherUser.plexUsername = 'friend';
otherUser.username = 'friend';
otherUser.email = 'friend@seerr.dev';
otherUser.userType = UserType.PLEX;
await otherUser.setPassword('test1234');
otherUser.permissions = 32;
otherUser.avatar = gravatarUrl('friend@seerr.dev', {
Expand Down

0 comments on commit 0f30ece

Please sign in to comment.