Skip to content

Commit a231eba

Browse files
author
Ryan Cohen
committed
refactor: decouple Plex as a requirement for setting up Overseerr
1 parent 30141f7 commit a231eba

File tree

22 files changed

+460
-123
lines changed

22 files changed

+460
-123
lines changed

cypress/e2e/discover.cy.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ describe('Discover', () => {
145145
plexUsername: null,
146146
username: '',
147147
recoveryLinkExpirationDate: null,
148-
userType: 2,
149148
avatar:
150149
'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200',
151150
movieQuotaLimit: null,

overseerr-api.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ components:
6161
plexUsername:
6262
type: string
6363
readOnly: true
64-
userType:
65-
type: integer
66-
example: 1
67-
readOnly: true
6864
permissions:
6965
type: number
7066
example: 0
@@ -83,6 +79,14 @@ components:
8379
type: number
8480
example: 5
8581
readOnly: true
82+
isPlexUser:
83+
type: boolean
84+
example: false
85+
readOnly: true
86+
isLocalUser:
87+
type: boolean
88+
example: true
89+
readOnly: true
8690
required:
8791
- id
8892
- email

server/constants/user.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

server/entity/User.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { MediaRequestStatus, MediaType } from '@server/constants/media';
2-
import { UserType } from '@server/constants/user';
32
import { getRepository } from '@server/datasource';
43
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
54
import PreparedEmail from '@server/lib/email';
@@ -35,14 +34,22 @@ export class User {
3534
public static filterMany(
3635
users: User[],
3736
showFiltered?: boolean
38-
): Partial<User>[] {
37+
): Omit<User, keyof typeof User.filteredFields>[] {
3938
return users.map((u) => u.filter(showFiltered));
4039
}
4140

42-
static readonly filteredFields: string[] = ['email'];
41+
// Fields that show only be shown to admins in user API responses
42+
static readonly filteredFields: (keyof User)[] = ['email', 'plexId'];
43+
44+
// Fields that should never be shown in API responses
45+
static readonly secureFields: (keyof User)[] = ['password'];
4346

4447
public displayName: string;
4548

49+
public isPlexUser: boolean;
50+
51+
public isLocalUser: boolean;
52+
4653
@PrimaryGeneratedColumn()
4754
public id: number;
4855

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

64-
@Column({ nullable: true, select: false })
71+
@Column({ nullable: true })
6572
public password?: string;
6673

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

73-
@Column({ type: 'integer', default: UserType.PLEX })
74-
public userType: UserType;
75-
76-
@Column({ nullable: true, select: false })
80+
@Column({ nullable: true })
7781
public plexId?: number;
7882

7983
@Column({ nullable: true, select: false })
@@ -126,13 +130,17 @@ export class User {
126130
Object.assign(this, init);
127131
}
128132

129-
public filter(showFiltered?: boolean): Partial<User> {
130-
const filtered: Partial<User> = Object.assign(
131-
{},
132-
...(Object.keys(this) as (keyof User)[])
133-
.filter((k) => showFiltered || !User.filteredFields.includes(k))
134-
.map((k) => ({ [k]: this[k] }))
135-
);
133+
public filter(
134+
showFiltered?: boolean
135+
): Omit<User, keyof typeof User.filteredFields> {
136+
const filtered: Omit<User, keyof typeof User.filteredFields> =
137+
Object.assign(
138+
{},
139+
...(Object.keys(this) as (keyof User)[])
140+
.filter((k) => showFiltered || !User.filteredFields.includes(k))
141+
.filter((k) => !User.secureFields.includes(k))
142+
.map((k) => ({ [k]: this[k] }))
143+
);
136144

137145
return filtered;
138146
}
@@ -229,8 +237,10 @@ export class User {
229237
}
230238

231239
@AfterLoad()
232-
public setDisplayName(): void {
240+
public setLocalProperties(): void {
233241
this.displayName = this.username || this.plexUsername || this.email;
242+
this.isPlexUser = !!this.plexId;
243+
this.isLocalUser = !!this.password;
234244
}
235245

236246
public async getQuota(): Promise<QuotaResponse> {

server/lib/scanners/plex/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class PlexScanner
7070
return this.log('No admin configured. Plex scan skipped.', 'warn');
7171
}
7272

73+
if (!admin.plexToken || !settings.plex.ip) {
74+
return this.log('Plex server is not configured.', 'warn');
75+
}
76+
7377
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
7478

7579
this.libraries = settings.plex.libraries.filter(

server/routes/auth.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import PlexTvAPI from '@server/api/plextv';
2-
import { UserType } from '@server/constants/user';
32
import { getRepository } from '@server/datasource';
43
import { User } from '@server/entity/User';
54
import { Permission } from '@server/lib/permissions';
65
import { getSettings } from '@server/lib/settings';
76
import logger from '@server/logger';
87
import { isAuthenticated } from '@server/middleware/auth';
98
import { Router } from 'express';
9+
import gravatarUrl from 'gravatar-url';
1010

1111
const authRoutes = Router();
1212

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

44-
// Next let's see if the user already exists
45-
let user = await userRepository
46-
.createQueryBuilder('user')
47-
.where('user.plexId = :id', { id: account.id })
48-
.orWhere('user.email = :email', {
49-
email: account.email.toLowerCase(),
50-
})
51-
.getOne();
44+
let user: User | null;
45+
46+
// If we are already logged in, we should just get the currently logged in user
47+
// otherwise we will try to match to an existing users email or plex ID
48+
if (req.user) {
49+
user = await userRepository.findOneBy({ id: req.user.id });
50+
} else {
51+
user = await userRepository
52+
.createQueryBuilder('user')
53+
.where('user.plexId = :id', { id: account.id })
54+
.orWhere('user.email = :email', {
55+
email: account.email.toLowerCase(),
56+
})
57+
.getOne();
58+
}
5259

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

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

7278
if (
7379
account.id === mainUser.plexId ||
80+
(user && user.id === 1 && !user.plexId) ||
7481
(await mainPlexTv.checkUserAccess(account.id))
7582
) {
7683
if (user) {
7784
if (!user.plexId) {
7885
logger.info(
79-
'Found matching Plex user; updating user with Plex data',
86+
'Found matching Plex user; updating user with Plex data. Notice: Emails are no longer synced.',
8087
{
8188
label: 'API',
8289
ip: req.ip,
@@ -91,9 +98,7 @@ authRoutes.post('/plex', async (req, res, next) => {
9198
user.plexToken = body.authToken;
9299
user.plexId = account.id;
93100
user.avatar = account.thumb;
94-
user.email = account.email;
95101
user.plexUsername = account.username;
96-
user.userType = UserType.PLEX;
97102

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

135139
await userRepository.save(user);
@@ -184,13 +188,22 @@ authRoutes.post('/local', async (req, res, next) => {
184188
});
185189
}
186190
try {
187-
const user = await userRepository
191+
let user = await userRepository
188192
.createQueryBuilder('user')
189193
.select(['user.id', 'user.email', 'user.password', 'user.plexId'])
190194
.where('user.email = :email', { email: body.email.toLowerCase() })
191195
.getOne();
192196

193-
if (!user || !(await user.passwordMatch(body.password))) {
197+
if (!user && !(await userRepository.count())) {
198+
const avatar = gravatarUrl(body.email, { default: 'mm', size: 200 });
199+
user = new User({
200+
email: body.email,
201+
permissions: Permission.ADMIN,
202+
avatar,
203+
});
204+
await user.setPassword(body.password);
205+
await userRepository.save(user);
206+
} else if (!user || !(await user.passwordMatch(body.password))) {
194207
logger.warn('Failed sign-in attempt using invalid Overseerr password', {
195208
label: 'API',
196209
ip: req.ip,
@@ -203,19 +216,19 @@ authRoutes.post('/local', async (req, res, next) => {
203216
});
204217
}
205218

206-
const mainUser = await userRepository.findOneOrFail({
219+
const mainUser = await userRepository.findOne({
207220
select: { id: true, plexToken: true, plexId: true },
208221
where: { id: 1 },
209222
});
210-
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
223+
const mainPlexTv = new PlexTvAPI(mainUser?.plexToken ?? '');
211224

212-
if (!user.plexId) {
225+
if (!user.plexId && mainUser?.isPlexUser) {
213226
try {
214227
const plexUsersResponse = await mainPlexTv.getUsers();
215228
const account = plexUsersResponse.MediaContainer.User.find(
216229
(account) =>
217230
account.$.email &&
218-
account.$.email.toLowerCase() === user.email.toLowerCase()
231+
account.$.email.toLowerCase() === user?.email.toLowerCase()
219232
)?.$;
220233

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

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

253265
if (
266+
mainUser?.isPlexUser &&
254267
user.plexId &&
255268
user.plexId !== mainUser.plexId &&
256269
!(await mainPlexTv.checkUserAccess(user.plexId))

server/routes/settings/index.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { AvailableCacheIds } from '@server/lib/cache';
1616
import cacheManager from '@server/lib/cache';
1717
import { Permission } from '@server/lib/permissions';
1818
import { plexFullScanner } from '@server/lib/scanners/plex';
19-
import type { MainSettings } from '@server/lib/settings';
19+
import type { MainSettings, PlexSettings } from '@server/lib/settings';
2020
import { getSettings } from '@server/lib/settings';
2121
import logger from '@server/logger';
2222
import { isAuthenticated } from '@server/middleware/auth';
@@ -82,10 +82,25 @@ settingsRoutes.post('/main/regenerate', (req, res, next) => {
8282
return res.status(200).json(filteredMainSettings(req.user, main));
8383
});
8484

85-
settingsRoutes.get('/plex', (_req, res) => {
85+
type PlexSettingsResponse = PlexSettings & {
86+
plexAvailable: boolean;
87+
};
88+
89+
settingsRoutes.get<never, PlexSettingsResponse>('/plex', async (_req, res) => {
8690
const settings = getSettings();
91+
const userRepository = getRepository(User);
92+
93+
const admin = await userRepository.findOneOrFail({
94+
select: { id: true, plexToken: true },
95+
where: { id: 1 },
96+
});
8797

88-
res.status(200).json(settings.plex);
98+
const settingsResponse: PlexSettingsResponse = {
99+
...settings.plex,
100+
plexAvailable: !!admin.plexToken,
101+
};
102+
103+
res.status(200).json(settingsResponse);
89104
});
90105

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

115+
if (!admin.plexToken) {
116+
throw new Error(
117+
'The administrator must have their account connected to Plex to be able to set up a Plex server.'
118+
);
119+
}
120+
100121
Object.assign(settings.plex, req.body);
101122

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

server/routes/user/index.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import PlexTvAPI from '@server/api/plextv';
22
import TautulliAPI from '@server/api/tautulli';
33
import { MediaType } from '@server/constants/media';
4-
import { UserType } from '@server/constants/user';
54
import { getRepository } from '@server/datasource';
65
import Media from '@server/entity/Media';
76
import { MediaRequest } from '@server/entity/MediaRequest';
@@ -121,7 +120,6 @@ router.post(
121120
password: body.password,
122121
permissions: settings.main.defaultPermissions,
123122
plexToken: '',
124-
userType: UserType.LOCAL,
125123
});
126124

127125
if (passedExplicitPassword) {
@@ -434,12 +432,7 @@ router.post(
434432
user.avatar = account.thumb;
435433
user.email = account.email;
436434
user.plexUsername = account.username;
437-
438-
// In case the user was previously a local account
439-
if (user.userType === UserType.LOCAL) {
440-
user.userType = UserType.PLEX;
441-
user.plexId = parseInt(account.id);
442-
}
435+
user.plexId = parseInt(account.id);
443436
await userRepository.save(user);
444437
} else if (!body || body.plexIds.includes(account.id)) {
445438
if (await mainPlexTv.checkUserAccess(parseInt(account.id))) {
@@ -450,7 +443,6 @@ router.post(
450443
plexId: parseInt(account.id),
451444
plexToken: '',
452445
avatar: account.thumb,
453-
userType: UserType.PLEX,
454446
});
455447
await userRepository.save(newUser);
456448
createdUsers.push(newUser);

server/scripts/prepareTestDb.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { UserType } from '@server/constants/user';
21
import dataSource, { getRepository } from '@server/datasource';
32
import { User } from '@server/entity/User';
43
import { copyFileSync } from 'fs';
@@ -43,7 +42,6 @@ const prepareDb = async () => {
4342
user.plexUsername = 'admin';
4443
user.username = 'admin';
4544
user.email = 'admin@seerr.dev';
46-
user.userType = UserType.PLEX;
4745
await user.setPassword('test1234');
4846
user.permissions = 2;
4947
user.avatar = gravatarUrl('admin@seerr.dev', { default: 'mm', size: 200 });
@@ -59,7 +57,6 @@ const prepareDb = async () => {
5957
otherUser.plexUsername = 'friend';
6058
otherUser.username = 'friend';
6159
otherUser.email = 'friend@seerr.dev';
62-
otherUser.userType = UserType.PLEX;
6360
await otherUser.setPassword('test1234');
6461
otherUser.permissions = 32;
6562
otherUser.avatar = gravatarUrl('friend@seerr.dev', {

0 commit comments

Comments
 (0)