Still figuring out how typescript works with rest APIs

This commit is contained in:
Eclypsed
2024-01-31 12:19:57 -05:00
parent b7daf9c27c
commit dda5b7f6d2
6 changed files with 94 additions and 107 deletions

10
src/app.d.ts vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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))
}

View File

@@ -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<typeof connectionSchema>
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()

View File

@@ -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 }
},
}