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 {}
|
||||
}
|
||||
|
||||
namespace Jellyfin {
|
||||
interface AuthData {
|
||||
User: {
|
||||
Name: string
|
||||
Id: string
|
||||
}
|
||||
AccessToken: string
|
||||
}
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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()
|
||||
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user