Changed the DB schema AGAIN
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Services, Connections } from '$lib/server/users'
|
||||
import { Connections } from '$lib/server/users'
|
||||
import { isValidURL } from '$lib/utils'
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { z } from 'zod'
|
||||
@@ -13,10 +13,8 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
const connectionSchema = z.object({
|
||||
serviceType: z.enum(['jellyfin', 'youtube-music']),
|
||||
serviceUserId: z.string(),
|
||||
url: z.string().refine((val) => isValidURL(val)),
|
||||
urlOrigin: 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>
|
||||
|
||||
@@ -28,9 +26,13 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
const connectionValidation = connectionSchema.safeParse(connection)
|
||||
if (!connectionValidation.success) return new Response(connectionValidation.error.message, { status: 400 })
|
||||
|
||||
const { serviceType, serviceUserId, url, accessToken, refreshToken, expiry } = connectionValidation.data
|
||||
const newService = Services.addService(serviceType as ServiceType, serviceUserId, new URL(url))
|
||||
const newConnection = Connections.addConnection(userId, newService.id, accessToken, refreshToken as string | null, expiry as number | null)
|
||||
const { serviceType, serviceUserId, urlOrigin, accessToken } = connectionValidation.data
|
||||
const service: Service = {
|
||||
type: serviceType,
|
||||
userId: serviceUserId,
|
||||
urlOrigin: new URL(urlOrigin).origin,
|
||||
}
|
||||
const newConnection = Connections.addConnection(userId, service, accessToken)
|
||||
return new Response(JSON.stringify(newConnection))
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,10 @@
|
||||
<input name="username" type="text" autocomplete="off" placeholder="Username" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4">
|
||||
<div class="w-full">
|
||||
<div class="w-full flex-shrink">
|
||||
<input name="password" type={passwordVisible ? 'text' : 'password'} placeholder="Password" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
||||
</div>
|
||||
<div class="overflow-hidden transition-[width] duration-300" style="width: {formMode === 'newUser' ? '100%' : 0};" aria-hidden={formMode !== 'newUser'}>
|
||||
<div class="flex-shrink overflow-hidden transition-[width] duration-300" style="width: {formMode === 'newUser' ? '100%' : 0};" aria-hidden={formMode !== 'newUser'}>
|
||||
<input
|
||||
name="confirmPassword"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
<script lang="ts">
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import type { LayoutServerData } from '../$types'
|
||||
|
||||
export let data: LayoutServerData
|
||||
|
||||
interface SettingRoute {
|
||||
pathname: string
|
||||
displayName: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const accountRoutes: SettingRoute[] = [
|
||||
{
|
||||
pathname: '/settings/connections',
|
||||
displayName: 'Connections',
|
||||
icon: 'fa-solid fa-circle-nodes',
|
||||
},
|
||||
{
|
||||
pathname: '/settings/devices',
|
||||
displayName: 'Devices',
|
||||
icon: 'fa-solid fa-mobile-screen',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<main class="h-full">
|
||||
<h1 class="sticky top-0 grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center text-2xl">
|
||||
<IconButton on:click={() => history.back()}>
|
||||
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||
</IconButton>
|
||||
<span>Account</span>
|
||||
<main class="grid h-full grid-rows-[min-content_auto] pb-12">
|
||||
<h1 class="sticky top-0 grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center p-6 text-2xl">
|
||||
<span class="h-12">
|
||||
<IconButton on:click={() => history.back()}>
|
||||
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||
</IconButton>
|
||||
</span>
|
||||
<span>Settings</span>
|
||||
</h1>
|
||||
<section class="px-[5vw]">
|
||||
<section class="grid grid-cols-[min-content_auto] grid-rows-1 gap-8 px-[5vw]">
|
||||
<nav class="h-full">
|
||||
<a class="whitespace-nowrap text-lg {data.url.pathname === '/settings' ? 'text-lazuli-primary' : 'text-neutral-400'}" href="/settings">
|
||||
<i class="fa-solid fa-user mr-1 w-4 text-center" />
|
||||
Account
|
||||
</a>
|
||||
<ol class="ml-2 mt-4 flex flex-col gap-3 border-2 border-transparent border-l-neutral-500 px-2">
|
||||
{#each accountRoutes as route}
|
||||
{@const isActive = route.pathname === data.url.pathname}
|
||||
<li class="w-60 px-3 py-1">
|
||||
<a class="whitespace-nowrap {isActive ? 'text-lazuli-primary' : 'text-neutral-400'}" href={route.pathname}>
|
||||
<i class="{route.icon} mr-1 w-4 text-center" />
|
||||
{route.displayName}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
height: 80px;
|
||||
padding: 16px 5vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,57 +1 @@
|
||||
<script lang="ts">
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import type { LayoutServerData } from '../$types.js'
|
||||
|
||||
export let data: LayoutServerData
|
||||
|
||||
interface SettingRoute {
|
||||
pathname: string
|
||||
displayName: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const settingRoutes: SettingRoute[] = [
|
||||
{
|
||||
pathname: '/settings/connections',
|
||||
displayName: 'Connections',
|
||||
icon: 'fa-solid fa-circle-nodes',
|
||||
},
|
||||
{
|
||||
pathname: '/settings/devices',
|
||||
displayName: 'Devices',
|
||||
icon: 'fa-solid fa-mobile-screen',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<nav class="h-full rounded-lg bg-neutral-950 p-6">
|
||||
<h1 class="flex h-6 justify-between text-neutral-400">
|
||||
<span>
|
||||
<i class="fa-solid fa-gear" />
|
||||
Settings
|
||||
</span>
|
||||
{#if data.url.pathname.split('/').at(-1) !== 'settings'}
|
||||
<IconButton on:click={() => goto('/settings')}>
|
||||
<i slot="icon" class="fa-solid fa-caret-left" />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</h1>
|
||||
<ol class="ml-2 mt-4 flex flex-col gap-3 border-2 border-transparent border-l-neutral-500 px-2">
|
||||
{#each settingRoutes as route}
|
||||
<li>
|
||||
{#if data.url.pathname === route.pathname}
|
||||
<div class="rounded-lg bg-neutral-500 px-3 py-1">
|
||||
<i class={route.icon} />
|
||||
{route.displayName}
|
||||
</div>
|
||||
{:else}
|
||||
<a href={route.pathname} class="block rounded-lg px-3 py-1 opacity-50 hover:bg-neutral-700">
|
||||
<i class={route.icon} />
|
||||
{route.displayName}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
<h1>Main Settings Page</h1>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { fail } from '@sveltejs/kit'
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import type { DBConnectionData } from '$lib/server/users'
|
||||
import type { NewConnection } from '../../api/users/[userId]/connections/+server'
|
||||
import type { PageServerLoad, Actions } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
const connectionsResponse = await fetch(`/api/user/${locals.user.id}/connections`, {
|
||||
const connectionsResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
})
|
||||
|
||||
const userConnections: DBConnectionData[] = await connectionsResponse.json()
|
||||
const userConnections: Connection[] = await connectionsResponse.json()
|
||||
return { userConnections }
|
||||
}
|
||||
|
||||
@@ -32,7 +31,7 @@ export const actions: Actions = {
|
||||
|
||||
const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json()
|
||||
const newConnectionPayload: NewConnection = {
|
||||
url: serverUrl.toString(),
|
||||
urlOrigin: serverUrl.toString(),
|
||||
serviceType: 'jellyfin',
|
||||
serviceUserId: authData.User.Id,
|
||||
accessToken: authData.AccessToken,
|
||||
@@ -45,7 +44,21 @@ export const actions: Actions = {
|
||||
|
||||
if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
||||
|
||||
const newConnection: DBConnectionData = await newConnectionResponse.json()
|
||||
const newConnection: Connection = await newConnectionResponse.json()
|
||||
return { newConnection }
|
||||
},
|
||||
deleteConnection: async ({ request, fetch, locals }) => {
|
||||
const formData = await request.formData()
|
||||
const connectionId = formData.get('connectionId')
|
||||
|
||||
const deleteConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||
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 }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import { fly } from 'svelte/transition'
|
||||
import Services from '$lib/services.json'
|
||||
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
||||
import { newestAlert } from '$lib/stores'
|
||||
import { newestAlert } from '$lib/stores.js'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import Toggle from '$lib/components/util/toggle.svelte'
|
||||
import type { PageServerData } from './$types.js'
|
||||
import type { SubmitFunction } from '@sveltejs/kit'
|
||||
|
||||
export let data
|
||||
let connectionProfiles = data.connectionProfiles
|
||||
export let data: PageServerData
|
||||
let connections = data.userConnections
|
||||
|
||||
const submitCredentials: SubmitFunction = ({ formData, action, cancel }) => {
|
||||
switch (action.search) {
|
||||
@@ -39,32 +40,26 @@
|
||||
return async ({ result }) => {
|
||||
switch (result.type) {
|
||||
case 'failure':
|
||||
$newestAlert = ['warning', result.data.message]
|
||||
return
|
||||
return ($newestAlert = ['warning', result.data?.message])
|
||||
case 'success':
|
||||
modal = null
|
||||
if (result.data?.newConnection) {
|
||||
const newConnection = result.data.newConnection
|
||||
connectionProfiles = [newConnection, ...connectionProfiles]
|
||||
const newConnection: Connection = result.data.newConnection
|
||||
connections = [newConnection, ...connections]
|
||||
|
||||
$newestAlert = ['success', `Added ${Services[newConnection.serviceType].displayName}`]
|
||||
return
|
||||
return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`])
|
||||
} else if (result.data?.deletedConnectionId) {
|
||||
const id = result.data.deletedConnectionId
|
||||
const indexToDelete = connectionProfiles.findIndex((profile) => profile.connectionId === id)
|
||||
const serviceType = connectionProfiles[indexToDelete].serviceType
|
||||
const indexToDelete = connections.findIndex((connection) => connection.id === id)
|
||||
const serviceType = connections[indexToDelete].service.type
|
||||
|
||||
connectionProfiles.splice(indexToDelete, 1)
|
||||
connectionProfiles = connectionProfiles
|
||||
connections.splice(indexToDelete, 1)
|
||||
connections = connections
|
||||
|
||||
$newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`]
|
||||
return
|
||||
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -72,35 +67,29 @@
|
||||
<h1 class="py-2 text-xl">Add Connection</h1>
|
||||
<div class="flex flex-wrap gap-2 pb-4">
|
||||
{#each Object.entries(Services) as [serviceType, serviceData]}
|
||||
<button
|
||||
class="bg-ne h-14 rounded-md"
|
||||
style="background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));"
|
||||
on:click={() => {
|
||||
if (serviceType === 'jellyfin') modal = JellyfinAuthBox
|
||||
}}
|
||||
>
|
||||
<button class="bg-ne h-14 rounded-md" style="background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));">
|
||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-2" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
<div class="grid gap-8">
|
||||
{#each connectionProfiles as connectionProfile}
|
||||
{@const serviceData = Services[connectionProfile.serviceType]}
|
||||
{#each connections as connection}
|
||||
{@const serviceData = Services[connection.service.type]}
|
||||
<section class="overflow-hidden rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
||||
<header class="flex h-20 items-center gap-4 p-4">
|
||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-1" />
|
||||
<div>
|
||||
<div>{connectionProfile?.username ? connectionProfile.username : 'Placeholder Account Name'}</div>
|
||||
<div>{connection.service?.username ? connection.service.username : 'Placeholder Account Name'}</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
{serviceData.displayName}
|
||||
{#if connectionProfile.serviceType === 'jellyfin' && connectionProfile?.serverName}
|
||||
- {connectionProfile.serverName}
|
||||
{#if connection.service.type === 'jellyfin' && connection.service?.serverName}
|
||||
- {connection.service.serverName}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto h-8">
|
||||
<IconButton on:click={() => (modal = `delete-${connectionProfile.connectionId}`)}>
|
||||
<IconButton>
|
||||
<i slot="icon" class="fa-solid fa-link-slash" />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -115,12 +104,12 @@
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{#if modal}
|
||||
<!-- {#if modal}
|
||||
<form method="post" use:enhance={submitCredentials} transition:fly={{ y: -15 }} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
{#if typeof modal === 'string'}
|
||||
{@const connectionId = modal.replace('delete-', '')}
|
||||
{@const connection = connectionProfiles.find((profile) => profile.connectionId === connectionId)}
|
||||
{@const serviceData = Services[connection.serviceType]}
|
||||
{@const connection = connections.find((connection) => connection.id === connectionId)}
|
||||
{@const serviceData = Services[connection.service.type]}
|
||||
<div class="rounded-lg bg-neutral-900 p-5">
|
||||
<h1 class="pb-4 text-center">Delete {serviceData.displayName} connection?</h1>
|
||||
<div class="flex w-60 justify-around">
|
||||
@@ -133,5 +122,5 @@
|
||||
<svelte:component this={modal} on:close={() => (modal = null)} />
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
{/if} -->
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user