Still figuring out how typescript works with rest APIs
This commit is contained in:
10
src/app.d.ts
vendored
10
src/app.d.ts
vendored
@@ -11,6 +11,16 @@ declare global {
|
|||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Jellyfin {
|
||||||
|
interface AuthData {
|
||||||
|
User: {
|
||||||
|
Name: string
|
||||||
|
Id: string
|
||||||
|
}
|
||||||
|
AccessToken: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type UserQueryParams = {
|
|||||||
includePassword?: boolean
|
includePassword?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DBServiceData {
|
export interface DBServiceData {
|
||||||
id: string
|
id: string
|
||||||
type: ServiceType
|
type: ServiceType
|
||||||
serviceUserId: string
|
serviceUserId: string
|
||||||
@@ -29,7 +29,7 @@ interface DBServiceRow {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DBConnectionData {
|
export interface DBConnectionData {
|
||||||
id: string
|
id: string
|
||||||
user: User
|
user: User
|
||||||
service: DBServiceData
|
service: DBServiceData
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
export const generateUUID = (): string => {
|
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))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
src/routes/api/jellyfin/auth/+server.ts
Normal file
35
src/routes/api/jellyfin/auth/+server.ts
Normal 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))
|
||||||
|
}
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
import { Services, Connections } from '$lib/server/users'
|
import { Services, Connections } from '$lib/server/users'
|
||||||
|
import { isValidURL } from '$lib/utils'
|
||||||
import type { RequestHandler } from '@sveltejs/kit'
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const isValidURL = (url: string): boolean => {
|
|
||||||
try {
|
|
||||||
new URL(url)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const userId = params.userId as string
|
const userId = params.userId as string
|
||||||
|
|
||||||
@@ -18,17 +10,18 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
return new Response(JSON.stringify(connections))
|
return new Response(JSON.stringify(connections))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PATCH: RequestHandler = async ({ params, request }) => {
|
const connectionSchema = z.object({
|
||||||
const userId = params.userId as string
|
|
||||||
|
|
||||||
const connectionSchema = z.object({
|
|
||||||
serviceType: z.enum(['jellyfin', 'youtube-music']),
|
serviceType: z.enum(['jellyfin', 'youtube-music']),
|
||||||
serviceUserId: z.string(),
|
serviceUserId: z.string(),
|
||||||
url: z.string().refine((val) => isValidURL(val)),
|
url: z.string().refine((val) => isValidURL(val)),
|
||||||
accessToken: z.string(),
|
accessToken: z.string(),
|
||||||
refreshToken: z.string().nullable().optional(),
|
refreshToken: z.string().nullable().optional(),
|
||||||
expiry: z.number().nullable().optional(),
|
expiry: z.number().nullable().optional(),
|
||||||
})
|
})
|
||||||
|
export type NewConnection = z.infer<typeof connectionSchema>
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ params, request }) => {
|
||||||
|
const userId = params.userId as string
|
||||||
|
|
||||||
const connection = await request.json()
|
const connection = await request.json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +1,51 @@
|
|||||||
import { fail } from '@sveltejs/kit'
|
import { fail } from '@sveltejs/kit'
|
||||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
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) => {
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
const { id, serviceType, serviceUserId, serviceUrl, accessToken, refreshToken, expiry } = connectionData
|
const connectionsResponse = await fetch(`/api/user/${locals.user.id}/connections`, {
|
||||||
|
method: 'GET',
|
||||||
switch (serviceType) {
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const allConnections = await response.json()
|
const userConnections: DBConnectionData[] = await connectionsResponse.json()
|
||||||
const connectionProfiles = []
|
return { userConnections }
|
||||||
if (allConnections) {
|
|
||||||
for (const connection of allConnections) {
|
|
||||||
const connectionProfile = await createProfile(connection)
|
|
||||||
connectionProfiles.push(connectionProfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { connectionProfiles }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('./$types').Actions}} */
|
export const actions: Actions = {
|
||||||
export const actions = {
|
|
||||||
authenticateJellyfin: async ({ request, fetch, locals }) => {
|
authenticateJellyfin: async ({ request, fetch, locals }) => {
|
||||||
const formData = await request.formData()
|
const formData = await request.formData()
|
||||||
const { serverUrl, username, password, deviceId } = Object.fromEntries(formData)
|
const { serverUrl, username, password, deviceId } = Object.fromEntries(formData)
|
||||||
const serverUrlOrigin = new URL(serverUrl).origin
|
|
||||||
|
|
||||||
const jellyfinAuthResponse = await fetch('/api/jellyfin/auth', {
|
const jellyfinAuthResponse = await fetch('/api/jellyfin/auth', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
apikey: SECRET_INTERNAL_API_KEY,
|
body: JSON.stringify({ serverUrl, username, password, deviceId }),
|
||||||
},
|
|
||||||
body: JSON.stringify({ serverUrl: serverUrlOrigin, username, password, deviceId }),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!jellyfinAuthResponse.ok) {
|
if (!jellyfinAuthResponse.ok) {
|
||||||
const jellyfinAuthError = await jellyfinAuthResponse.text()
|
const authError = await jellyfinAuthResponse.text()
|
||||||
return fail(jellyfinAuthResponse.status, { message: jellyfinAuthError })
|
return fail(jellyfinAuthResponse.status, { message: authError })
|
||||||
}
|
}
|
||||||
|
|
||||||
const jellyfinAuthData = await jellyfinAuthResponse.json()
|
const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json()
|
||||||
const accessToken = jellyfinAuthData.AccessToken
|
const newConnectionPayload: NewConnection = {
|
||||||
const jellyfinUserId = jellyfinAuthData.User.Id
|
url: serverUrl.toString(),
|
||||||
const updateConnectionsResponse = await fetch(`/api/user/connections?userId=${locals.userId}`, {
|
serviceType: 'jellyfin',
|
||||||
|
serviceUserId: authData.User.Id,
|
||||||
|
accessToken: authData.AccessToken,
|
||||||
|
}
|
||||||
|
const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
apikey: SECRET_INTERNAL_API_KEY,
|
body: JSON.stringify(newConnectionPayload),
|
||||||
},
|
|
||||||
body: JSON.stringify({ serviceType: 'jellyfin', serviceUserId: jellyfinUserId, serviceUrl: serverUrlOrigin, accessToken }),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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 newConnection: DBConnectionData = await newConnectionResponse.json()
|
||||||
const newConnectionData = UserConnections.getConnection(newConnection.id)
|
return { newConnection }
|
||||||
|
|
||||||
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 }
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user