From dda5b7f6d233f0f31e99ac5659ac944df4a83765 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Wed, 31 Jan 2024 12:19:57 -0500 Subject: [PATCH] Still figuring out how typescript works with rest APIs --- src/app.d.ts | 10 ++ src/lib/server/users.ts | 4 +- src/lib/utils.ts | 8 ++ src/routes/api/jellyfin/auth/+server.ts | 35 ++++++ .../api/users/[userId]/connections/+server.ts | 31 ++--- .../settings/connections/+page.server.ts | 113 +++++------------- 6 files changed, 94 insertions(+), 107 deletions(-) create mode 100644 src/routes/api/jellyfin/auth/+server.ts diff --git a/src/app.d.ts b/src/app.d.ts index aa2c951..8b7e535 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,6 +11,16 @@ declare global { // interface Platform {} } + namespace Jellyfin { + interface AuthData { + User: { + Name: string + Id: string + } + AccessToken: string + } + } + interface User { id: string username: string diff --git a/src/lib/server/users.ts b/src/lib/server/users.ts index e4dc828..fe410f1 100644 --- a/src/lib/server/users.ts +++ b/src/lib/server/users.ts @@ -15,7 +15,7 @@ type UserQueryParams = { includePassword?: boolean } -interface DBServiceData { +export interface DBServiceData { id: string type: ServiceType serviceUserId: string @@ -29,7 +29,7 @@ interface DBServiceRow { url: string } -interface DBConnectionData { +export interface DBConnectionData { id: string user: User service: DBServiceData diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5570ea4..03a169b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,11 @@ export const generateUUID = (): string => { return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) } + +export const isValidURL = (url: string): boolean => { + try { + return Boolean(new URL(url)) + } catch { + return false + } +} diff --git a/src/routes/api/jellyfin/auth/+server.ts b/src/routes/api/jellyfin/auth/+server.ts new file mode 100644 index 0000000..d7c3ad9 --- /dev/null +++ b/src/routes/api/jellyfin/auth/+server.ts @@ -0,0 +1,35 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { isValidURL } from '$lib/utils' +import { z } from 'zod' + +export const POST: RequestHandler = async ({ request, fetch }) => { + const jellyfinAuthSchema = z.object({ + serverUrl: z.string().refine((val) => isValidURL(val)), + username: z.string(), + password: z.string(), + deviceId: z.string(), + }) + + const jellyfinAuthData = await request.json() + const jellyfinAuthValidation = jellyfinAuthSchema.safeParse(jellyfinAuthData) + if (!jellyfinAuthValidation.success) return new Response(jellyfinAuthValidation.error.message, { 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"`, + }, + }) + + if (!authResponse.ok) return new Response('Failed to authenticate', { status: 400 }) + + const authData: Jellyfin.AuthData = await authResponse.json() + return new Response(JSON.stringify(authData)) +} diff --git a/src/routes/api/users/[userId]/connections/+server.ts b/src/routes/api/users/[userId]/connections/+server.ts index 84b1a66..4e9c3bd 100644 --- a/src/routes/api/users/[userId]/connections/+server.ts +++ b/src/routes/api/users/[userId]/connections/+server.ts @@ -1,16 +1,8 @@ import { Services, Connections } from '$lib/server/users' +import { isValidURL } from '$lib/utils' import type { RequestHandler } from '@sveltejs/kit' import { z } from 'zod' -const isValidURL = (url: string): boolean => { - try { - new URL(url) - return true - } catch { - return false - } -} - export const GET: RequestHandler = async ({ params }) => { const userId = params.userId as string @@ -18,17 +10,18 @@ export const GET: RequestHandler = async ({ params }) => { return new Response(JSON.stringify(connections)) } -export const PATCH: RequestHandler = async ({ params, request }) => { - const userId = params.userId as string +const connectionSchema = z.object({ + serviceType: z.enum(['jellyfin', 'youtube-music']), + serviceUserId: z.string(), + url: z.string().refine((val) => isValidURL(val)), + accessToken: z.string(), + refreshToken: z.string().nullable().optional(), + expiry: z.number().nullable().optional(), +}) +export type NewConnection = z.infer - const connectionSchema = z.object({ - serviceType: z.enum(['jellyfin', 'youtube-music']), - serviceUserId: z.string(), - url: z.string().refine((val) => isValidURL(val)), - accessToken: z.string(), - refreshToken: z.string().nullable().optional(), - expiry: z.number().nullable().optional(), - }) +export const POST: RequestHandler = async ({ params, request }) => { + const userId = params.userId as string const connection = await request.json() diff --git a/src/routes/settings/connections/+page.server.ts b/src/routes/settings/connections/+page.server.ts index 8fdca07..249f342 100644 --- a/src/routes/settings/connections/+page.server.ts +++ b/src/routes/settings/connections/+page.server.ts @@ -1,110 +1,51 @@ import { fail } from '@sveltejs/kit' import { SECRET_INTERNAL_API_KEY } from '$env/static/private' -import { Connections } from '$lib/server/users' +import type { DBConnectionData } from '$lib/server/users' +import type { NewConnection } from '../../api/users/[userId]/connections/+server' +import type { PageServerLoad, Actions } from './$types' -const createProfile = async (connectionData) => { - const { id, serviceType, serviceUserId, serviceUrl, accessToken, refreshToken, expiry } = connectionData - - switch (serviceType) { - case 'jellyfin': - const userUrl = new URL(`Users/${serviceUserId}`, serviceUrl).href - const systemUrl = new URL('System/Info', serviceUrl).href - - const reqHeaders = new Headers({ Authorization: `MediaBrowser Token="${accessToken}"` }) - - const userResponse = await fetch(userUrl, { headers: reqHeaders }) - const systemResponse = await fetch(systemUrl, { headers: reqHeaders }) - - const userData = await userResponse.json() - const systemData = await systemResponse.json() - - return { - connectionId: id, - serviceType, - userId: serviceUserId, - username: userData?.Name, - serviceUrl: serviceUrl, - serverName: systemData?.ServerName, - } - default: - return null - } -} - -/** @type {import('./$types').PageServerLoad} */ -export const load = async ({ fetch, locals }) => { - const response = await fetch(`/api/user/connections?userId=${locals.userId}`, { - headers: { - apikey: SECRET_INTERNAL_API_KEY, - }, +export const load: PageServerLoad = async ({ fetch, locals }) => { + const connectionsResponse = await fetch(`/api/user/${locals.user.id}/connections`, { + method: 'GET', + headers: { apikey: SECRET_INTERNAL_API_KEY }, }) - const allConnections = await response.json() - const connectionProfiles = [] - if (allConnections) { - for (const connection of allConnections) { - const connectionProfile = await createProfile(connection) - connectionProfiles.push(connectionProfile) - } - } - - return { connectionProfiles } + const userConnections: DBConnectionData[] = await connectionsResponse.json() + return { userConnections } } -/** @type {import('./$types').Actions}} */ -export const actions = { +export const actions: Actions = { authenticateJellyfin: async ({ request, fetch, locals }) => { const formData = await request.formData() const { serverUrl, username, password, deviceId } = Object.fromEntries(formData) - const serverUrlOrigin = new URL(serverUrl).origin const jellyfinAuthResponse = await fetch('/api/jellyfin/auth', { method: 'POST', - headers: { - apikey: SECRET_INTERNAL_API_KEY, - }, - body: JSON.stringify({ serverUrl: serverUrlOrigin, username, password, deviceId }), + headers: { apikey: SECRET_INTERNAL_API_KEY }, + body: JSON.stringify({ serverUrl, username, password, deviceId }), }) if (!jellyfinAuthResponse.ok) { - const jellyfinAuthError = await jellyfinAuthResponse.text() - return fail(jellyfinAuthResponse.status, { message: jellyfinAuthError }) + const authError = await jellyfinAuthResponse.text() + return fail(jellyfinAuthResponse.status, { message: authError }) } - const jellyfinAuthData = await jellyfinAuthResponse.json() - const accessToken = jellyfinAuthData.AccessToken - const jellyfinUserId = jellyfinAuthData.User.Id - const updateConnectionsResponse = await fetch(`/api/user/connections?userId=${locals.userId}`, { + const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json() + const newConnectionPayload: NewConnection = { + url: serverUrl.toString(), + serviceType: 'jellyfin', + serviceUserId: authData.User.Id, + accessToken: authData.AccessToken, + } + const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, { method: 'POST', - headers: { - apikey: SECRET_INTERNAL_API_KEY, - }, - body: JSON.stringify({ serviceType: 'jellyfin', serviceUserId: jellyfinUserId, serviceUrl: serverUrlOrigin, accessToken }), + headers: { apikey: SECRET_INTERNAL_API_KEY }, + body: JSON.stringify(newConnectionPayload), }) - if (!updateConnectionsResponse.ok) return fail(500, { message: 'Internal Server Error' }) + if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' }) - const newConnection = await updateConnectionsResponse.json() - const newConnectionData = UserConnections.getConnection(newConnection.id) - - const jellyfinProfile = await createProfile(newConnectionData) - - return { newConnection: jellyfinProfile } - }, - deleteConnection: async ({ request, fetch, locals }) => { - const formData = await request.formData() - const connectionId = formData.get('connectionId') - - const deleteConnectionResponse = await fetch(`/api/user/connections?userId=${locals.userId}`, { - method: 'DELETE', - headers: { - apikey: SECRET_INTERNAL_API_KEY, - }, - body: JSON.stringify({ connectionId }), - }) - - if (!deleteConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' }) - - return { deletedConnectionId: connectionId } + const newConnection: DBConnectionData = await newConnectionResponse.json() + return { newConnection } }, }