diff --git a/overseerr-api.yml b/overseerr-api.yml index 80d80c5e9..8a929da31 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3100,6 +3100,15 @@ paths: type: string required: - authToken + /auth/plex/unlink: + get: + summary: Unlink Plex from currently logged in account + description: Will remove connected Plex account information from the currently logged-in user. + tags: + - auth + responses: + '204': + description: OK /auth/local: post: summary: Sign in using a local account diff --git a/server/entity/User.ts b/server/entity/User.ts index 6abf413e0..56ecf20c8 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -62,8 +62,8 @@ export class User { }) public email: string; - @Column({ nullable: true }) - public plexUsername?: string; + @Column({ type: 'varchar', nullable: true }) + public plexUsername?: string | null; @Column({ nullable: true }) public username?: string; @@ -77,11 +77,11 @@ export class User { @Column({ type: 'date', nullable: true }) public recoveryLinkExpirationDate?: Date | null; - @Column({ nullable: true }) - public plexId?: number; + @Column({ type: 'int', nullable: true }) + public plexId?: number | null; - @Column({ nullable: true, select: false }) - public plexToken?: string; + @Column({ type: 'varchar', nullable: true, select: false }) + public plexToken?: string | null; @Column({ type: 'integer', default: 0 }) public permissions = 0; diff --git a/server/index.ts b/server/index.ts index e78d5a577..e8859502d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -67,7 +67,7 @@ app where: { id: 1 }, }); - if (admin) { + if (admin?.plexToken) { logger.info('Migrating Plex libraries to include media type', { label: 'Settings', }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 402a4efc1..8ff11f965 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -175,6 +175,36 @@ authRoutes.post('/plex', async (req, res, next) => { } }); +authRoutes.get('/plex/unlink', isAuthenticated(), async (req, res, next) => { + const userRepository = getRepository(User); + try { + if (!req.user) { + throw new Error('User data is not present in request.'); + } + + const user = await userRepository.findOneByOrFail({ id: req.user.id }); + + user.plexId = null; + user.plexToken = null; + user.avatar = gravatarUrl(user.email, { default: 'mm', size: 200 }); + user.plexUsername = null; + + await userRepository.save(user); + + return res.status(204).send(); + } catch (e) { + logger.error('Something went wrong unlinking a Plex account', { + label: 'API', + errorMessage: e.message, + userId: req.user?.id, + }); + return next({ + status: 500, + message: 'Unable to unlink plex account.', + }); + } +}); + authRoutes.post('/local', async (req, res, next) => { const settings = getSettings(); const userRepository = getRepository(User); diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 29daee543..827fa01c5 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -153,9 +153,12 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { select: { id: true, plexToken: true }, where: { id: 1 }, }); - const plexTvClient = admin.plexToken - ? new PlexTvAPI(admin.plexToken) - : null; + + if (!admin.plexToken) { + throw new Error('Plex must be configured to retrieve servers.'); + } + + const plexTvClient = new PlexTvAPI(admin.plexToken); const devices = (await plexTvClient?.getDevices())?.filter((device) => { return device.provides.includes('server') && device.owned; }); @@ -192,7 +195,7 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { useSsl: connection.protocol === 'https', }; const plexClient = new PlexAPI({ - plexToken: admin.plexToken, + plexToken: admin.plexToken ?? '', plexSettings: plexDeviceSettings, timeout: 5000, }); @@ -223,7 +226,7 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => { } }); -settingsRoutes.get('/plex/library', async (req, res) => { +settingsRoutes.get('/plex/library', async (req, res, next) => { const settings = getSettings(); if (req.query.sync) { @@ -232,6 +235,14 @@ settingsRoutes.get('/plex/library', async (req, res) => { select: { id: true, plexToken: true }, where: { id: 1 }, }); + + if (!admin.plexToken) { + return next({ + status: '500', + message: 'Plex must be configured to retrieve libraries.', + }); + } + const plexapi = new PlexAPI({ plexToken: admin.plexToken }); await plexapi.syncLibraries(); diff --git a/src/components/Common/Button/index.tsx b/src/components/Common/Button/index.tsx index d3f96ae98..be0f17725 100644 --- a/src/components/Common/Button/index.tsx +++ b/src/components/Common/Button/index.tsx @@ -7,7 +7,8 @@ export type ButtonType = | 'danger' | 'warning' | 'success' - | 'ghost'; + | 'ghost' + | 'plex'; // Helper type to override types (overrides onClick) type MergeElementProps< @@ -74,6 +75,9 @@ function Button
(
'text-white bg-transaprent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100'
);
break;
+ case 'plex':
+ buttonStyle.push('plex-button');
+ break;
default:
buttonStyle.push(
'text-gray-200 bg-gray-800 bg-opacity-80 border-gray-600 hover:text-white hover:bg-gray-700 hover:border-gray-600 group-hover:text-white group-hover:bg-gray-700 group-hover:border-gray-600 focus:border-blue-300 focus:ring-blue active:text-gray-200 active:bg-gray-700 active:border-gray-600'
diff --git a/src/components/PlexLoginButton/index.tsx b/src/components/PlexLoginButton/index.tsx
index f28a1e23e..3c268cd46 100644
--- a/src/components/PlexLoginButton/index.tsx
+++ b/src/components/PlexLoginButton/index.tsx
@@ -1,3 +1,4 @@
+import Button from '@app/components/Common/Button';
import globalMessages from '@app/i18n/globalMessages';
import PlexOAuth from '@app/utils/plex';
import { LoginIcon } from '@heroicons/react/outline';
@@ -11,18 +12,25 @@ const messages = defineMessages({
const plexOAuth = new PlexOAuth();
-interface PlexLoginButtonProps {
+type PlexLoginButtonProps = Pick<
+ React.ComponentPropsWithoutRef