From 20454e22d13bf4b15caa0b24748239310b1a7652 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Fri, 2 Feb 2024 03:05:42 -0500 Subject: [PATCH] Major improvements to how connections are managed, also interfaces, interfaces EVERYWHERE --- src/app.css | 4 -- src/app.d.ts | 54 +++++++++++---- src/hooks.server.ts | 2 +- src/lib/server/users.db | Bin 24576 -> 24576 bytes src/routes/api/jellyfin/auth/+server.ts | 35 +++++----- .../api/users/[userId]/connections/+server.ts | 29 ++++---- src/routes/login/+page.svelte | 4 +- .../settings/connections/+page.server.ts | 35 +++++++--- src/routes/settings/connections/+page.svelte | 64 +++--------------- .../connections/connectionProfile.svelte | 52 ++++++++++++++ .../connections/deleteConnectionModal.svelte | 14 ---- .../connections/jellyfinAuthBox.svelte | 30 ++++---- 12 files changed, 183 insertions(+), 140 deletions(-) create mode 100644 src/routes/settings/connections/connectionProfile.svelte delete mode 100644 src/routes/settings/connections/deleteConnectionModal.svelte diff --git a/src/app.css b/src/app.css index 5673d8e..54f0346 100644 --- a/src/app.css +++ b/src/app.css @@ -4,10 +4,6 @@ @tailwind components; @tailwind utilities; -* { - flex-shrink: 0; -} - img { max-width: 100%; } diff --git a/src/app.d.ts b/src/app.d.ts index 18c6af4..b2bddab 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,16 +11,6 @@ declare global { // interface Platform {} } - namespace Jellyfin { - interface AuthData { - User: { - Name: string - Id: string - } - AccessToken: string - } - } - interface User { id: string username: string @@ -33,8 +23,6 @@ declare global { type: ServiceType userId: string urlOrigin: string - username?: string - serverName?: string } interface Connection { @@ -44,6 +32,48 @@ declare global { accessToken: string } + namespace Jellyfin { + // The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will + // retrun the ServerName, it wont. This must be fetched from /System/Info. + // So, ONLY DEFINE THE INTERFACES FOR DATA THAT IS GARUNTEED TO BE RETURNED (unless the data value itself is inherently optional) + interface JFService extends Service { + type: 'jellyfin' + username: string + serverName: string + } + + interface JFConnection extends Connection { + service: JFService + } + + interface AuthData { + User: { + Id: string + } + AccessToken: string + } + + interface User { + Name: string + Id: string + } + + interface System { + ServerName: string + } + } + + namespace YouTubeMusic { + interface YTService extends Service { + type: 'youtube-music' + username: string + } + + interface YTConnection extends Connection { + service: YTService + } + } + interface MediaItem { connectionId: string serviceType: string diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 8ab2ee0..f38d5a4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -7,7 +7,7 @@ export const handle: Handle = async ({ event, resolve }) => { const urlpath = event.url.pathname if (urlpath.startsWith('/api') && event.request.headers.get('apikey') !== SECRET_INTERNAL_API_KEY && event.url.searchParams.get('apikey') !== SECRET_INTERNAL_API_KEY) { - return new Response('Unauthorized', { status: 400 }) + return new Response('Unauthorized', { status: 401 }) } if (!nonJwtProtectedRoutes.some((route) => urlpath.startsWith(route))) { diff --git a/src/lib/server/users.db b/src/lib/server/users.db index 898f96372420f8ec29294a85d0c8284bbe306e84..b173cb2726b5f0970a614e77aa4bb64c524f182c 100644 GIT binary patch delta 308 zcma)%Jx;?g7>4oEF+f)skkyeSPV#+r90@TXkQgXacVfq;L7_xRK#CBe>IpzxfeQc^ z=s|h~=B~Jc1F*h-kKXx`%$MY4j|^Y;Pss4&>iu{?y!Dp1{@UI7_WZ?w3?`Gs(M2p+ z#3Ylx297>RnfOYf_G61!2$q6k@@*B4o*!w~*M?^F##B{r%NaeS_pNEp3#W;MgElDR zRC7?0DHI$Ha-F9sFibYtrn+p(YuC%@wCn0D3?s$@jzI(vL?X&kCi&m!EWh2L#-*O# zRe9@F=}-@*8UGrmy6WrJoVm3pYmLl;{D delta 45 scmZoTz}Rqrae_1>^F$eEM&^wPOYE5#1U3sAJm#M`L7a_?0SMq+04}5qhyVZp diff --git a/src/routes/api/jellyfin/auth/+server.ts b/src/routes/api/jellyfin/auth/+server.ts index d7c3ad9..c8b926f 100644 --- a/src/routes/api/jellyfin/auth/+server.ts +++ b/src/routes/api/jellyfin/auth/+server.ts @@ -12,24 +12,27 @@ export const POST: RequestHandler = async ({ request, fetch }) => { const jellyfinAuthData = await request.json() const jellyfinAuthValidation = jellyfinAuthSchema.safeParse(jellyfinAuthData) - if (!jellyfinAuthValidation.success) return new Response(jellyfinAuthValidation.error.message, { status: 400 }) + if (!jellyfinAuthValidation.success) return new Response('Invalid data in request body', { status: 400 }) const { serverUrl, username, password, deviceId } = jellyfinAuthValidation.data const authUrl = new URL('/Users/AuthenticateByName', serverUrl).href - const authResponse = await fetch(authUrl, { - method: 'POST', - body: JSON.stringify({ - Username: username, - Pw: password, - }), - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'X-Emby-Authorization': `MediaBrowser Client="Lazuli", Device="Chrome", DeviceId="${deviceId}", Version="1.0.0.0"`, - }, - }) + try { + const authResponse = await fetch(authUrl, { + method: 'POST', + body: JSON.stringify({ + Username: username, + Pw: password, + }), + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-Emby-Authorization': `MediaBrowser Client="Lazuli", Device="Chrome", DeviceId="${deviceId}", Version="1.0.0.0"`, + }, + }) + if (!authResponse.ok) return new Response('Failed to authenticate', { status: 401 }) - if (!authResponse.ok) return new Response('Failed to authenticate', { status: 400 }) - - const authData: Jellyfin.AuthData = await authResponse.json() - return new Response(JSON.stringify(authData)) + const authData: Jellyfin.AuthData = await authResponse.json() + return Response.json(authData) + } catch { + return new Response('Fetch request failed', { status: 404 }) + } } diff --git a/src/routes/api/users/[userId]/connections/+server.ts b/src/routes/api/users/[userId]/connections/+server.ts index c478bdd..f9d8f81 100644 --- a/src/routes/api/users/[userId]/connections/+server.ts +++ b/src/routes/api/users/[userId]/connections/+server.ts @@ -10,38 +10,35 @@ export const GET: RequestHandler = async ({ params }) => { return new Response(JSON.stringify(connections)) } -const connectionSchema = z.object({ - serviceType: z.enum(['jellyfin', 'youtube-music']), - serviceUserId: z.string(), - urlOrigin: z.string().refine((val) => isValidURL(val)), +// This schema should be identical to the Connection Data Type but without the id and userId +const newConnectionSchema = z.object({ + service: z.object({ + type: z.enum(['jellyfin', 'youtube-music']), + userId: z.string(), + urlOrigin: z.string().refine((val) => isValidURL(val)), + }), accessToken: z.string(), }) -export type NewConnection = z.infer export const POST: RequestHandler = async ({ params, request }) => { const userId = params.userId as string - const connection = await request.json() + const connection: Connection = await request.json() - const connectionValidation = connectionSchema.safeParse(connection) + const connectionValidation = newConnectionSchema.safeParse(connection) if (!connectionValidation.success) return new Response(connectionValidation.error.message, { status: 400 }) - const { serviceType, serviceUserId, urlOrigin, accessToken } = connectionValidation.data - const service: Service = { - type: serviceType, - userId: serviceUserId, - urlOrigin: new URL(urlOrigin).origin, - } + const { service, accessToken } = connection const newConnection = Connections.addConnection(userId, service, accessToken) return new Response(JSON.stringify(newConnection)) } export const DELETE: RequestHandler = async ({ request }) => { - const connectionId: string = await request.json() + const requestData = await request.json() try { - Connections.deleteConnection(connectionId) + Connections.deleteConnection(requestData.connectionId) return new Response('Connection Deleted') - } catch { + } catch (error) { return new Response('Connection does not exist', { status: 400 }) } } diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 0f4c4b2..a031c36 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -64,10 +64,10 @@
-
+
-
+
{ @@ -25,26 +24,44 @@ export const actions: Actions = { }) if (!jellyfinAuthResponse.ok) { - const authError = await jellyfinAuthResponse.text() - return fail(jellyfinAuthResponse.status, { message: authError }) + if (jellyfinAuthResponse.status === 404) { + return fail(404, { message: 'Request failed, check Server URL' }) + } else if (jellyfinAuthResponse.status === 401) { + return fail(401, { message: 'Invalid Credentials' }) + } + + return fail(500, { message: 'Internal Server Error' }) } const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json() - const newConnectionPayload: NewConnection = { + + const userUrl = new URL(`Users/${authData.User.Id}`, serverUrl.toString()).href + const systemUrl = new URL('System/Info', serverUrl.toString()).href + + const reqHeaders = new Headers({ Authorization: `MediaBrowser Token="${authData.AccessToken}"` }) + + const userResponse = await fetch(userUrl, { headers: reqHeaders }) + const systemResponse = await fetch(systemUrl, { headers: reqHeaders }) + + const userData: Jellyfin.User = await userResponse.json() + const systemData: Jellyfin.System = await systemResponse.json() + + const serviceData: Jellyfin.JFService = { + type: 'jellyfin', + userId: authData.User.Id, urlOrigin: serverUrl.toString(), - serviceType: 'jellyfin', - serviceUserId: authData.User.Id, - accessToken: authData.AccessToken, + username: userData.Name, + serverName: systemData.ServerName, } const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, { method: 'POST', headers: { apikey: SECRET_INTERNAL_API_KEY }, - body: JSON.stringify(newConnectionPayload), + body: JSON.stringify({ service: serviceData, accessToken: authData.AccessToken }), }) if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' }) - const newConnection: Connection = await newConnectionResponse.json() + const newConnection: Jellyfin.JFConnection = await newConnectionResponse.json() return { newConnection } }, deleteConnection: async ({ request, fetch, locals }) => { diff --git a/src/routes/settings/connections/+page.svelte b/src/routes/settings/connections/+page.svelte index aa17ad1..b5c422e 100644 --- a/src/routes/settings/connections/+page.svelte +++ b/src/routes/settings/connections/+page.svelte @@ -1,15 +1,13 @@

Add Connection

- -
{#each connections as connection} - {@const serviceData = Services[connection.service.type]} -
-
- {serviceData.displayName} icon -
-
{connection.service?.username ? connection.service.username : 'Placeholder Account Name'}
-
- {serviceData.displayName} - {#if connection.service.type === 'jellyfin' && connection.service?.serverName} - - {connection.service.serverName} - {/if} -
-
-
- - - -
-
-
-
-
- console.log(event.detail.toggled)} /> - Enable Connection -
-
-
+ {/each}
-
+ (newConnectionModal = null)} />
\ No newline at end of file + diff --git a/src/routes/settings/connections/connectionProfile.svelte b/src/routes/settings/connections/connectionProfile.svelte new file mode 100644 index 0000000..bca17d5 --- /dev/null +++ b/src/routes/settings/connections/connectionProfile.svelte @@ -0,0 +1,52 @@ + + +
+
+ {serviceData.displayName} icon +
+
{'username' in connection.service ? connection.service.username : 'Placeholder Account Name'}
+
+ {serviceData.displayName} + {#if 'serverName' in connection.service} + - {connection.service.serverName} + {/if} +
+
+
+ (showUnlinkModal = !showUnlinkModal)}> + + + {#if showUnlinkModal} +
+ Delete Connection +
+ + +
+ +
+ {/if} +
+
+
+
+
+ console.log(event.detail.toggled)} /> + Enable Connection +
+
+
diff --git a/src/routes/settings/connections/deleteConnectionModal.svelte b/src/routes/settings/connections/deleteConnectionModal.svelte deleted file mode 100644 index 964b658..0000000 --- a/src/routes/settings/connections/deleteConnectionModal.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
-

Delete {Services[connection.service.type].displayName} connection?

-
- - - -
-
\ No newline at end of file diff --git a/src/routes/settings/connections/jellyfinAuthBox.svelte b/src/routes/settings/connections/jellyfinAuthBox.svelte index c311964..ca42fc2 100644 --- a/src/routes/settings/connections/jellyfinAuthBox.svelte +++ b/src/routes/settings/connections/jellyfinAuthBox.svelte @@ -1,23 +1,29 @@ -
-

Jellyfin Sign In

-
- -
- - +
+
+

Jellyfin Sign In

+
+ +
+ + +
+
+
+ +
-
- - -
-
+