Changed the DB schema AGAIN
This commit is contained in:
15
src/app.d.ts
vendored
15
src/app.d.ts
vendored
@@ -29,6 +29,21 @@ declare global {
|
||||
|
||||
type ServiceType = 'jellyfin' | 'youtube-music'
|
||||
|
||||
interface Service {
|
||||
type: ServiceType
|
||||
userId: string
|
||||
urlOrigin: string
|
||||
username?: string
|
||||
serverName?: string
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
id: string
|
||||
user: User
|
||||
service: Service
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
interface MediaItem {
|
||||
connectionId: string
|
||||
serviceType: string
|
||||
|
||||
Binary file not shown.
@@ -1,50 +1,22 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { generateUUID } from '$lib/utils'
|
||||
import { isValidURL } from '$lib/utils'
|
||||
|
||||
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
|
||||
db.pragma('foreign_keys = ON')
|
||||
const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id VARCHAR(36) PRIMARY KEY, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)'
|
||||
const initServicesTable = 'CREATE TABLE IF NOT EXISTS Services(id VARCHAR(36) PRIMARY KEY, type VARCHAR(64) NOT NULL, serviceUserId TEXT NOT NULL, url TEXT NOT NULL)'
|
||||
const initConnectionsTable =
|
||||
'CREATE TABLE IF NOT EXISTS Connections(id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, serviceId VARCHAR(36), accessToken TEXT NOT NULL, refreshToken TEXT, expiry INTEGER, FOREIGN KEY(userId) REFERENCES Users(id), FOREIGN KEY(serviceId) REFERENCES Services(id))'
|
||||
db.exec(initUsersTable)
|
||||
db.exec(initServicesTable)
|
||||
db.exec(initConnectionsTable)
|
||||
const initConnectionsTable = 'CREATE TABLE IF NOT EXISTS Connections(id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, service TEXT NOT NULL, accessToken TEXT NOT NULL, FOREIGN KEY(userId) REFERENCES Users(id))'
|
||||
db.exec(initUsersTable), db.exec(initConnectionsTable)
|
||||
|
||||
type UserQueryParams = {
|
||||
includePassword?: boolean
|
||||
}
|
||||
|
||||
export interface DBServiceData {
|
||||
id: string
|
||||
type: ServiceType
|
||||
serviceUserId: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface DBServiceRow {
|
||||
id: string
|
||||
type: string
|
||||
serviceUserId: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface DBConnectionData {
|
||||
id: string
|
||||
user: User
|
||||
service: DBServiceData
|
||||
accessToken: string
|
||||
refreshToken: string | null
|
||||
expiry: number | null
|
||||
}
|
||||
|
||||
interface DBConnectionRow {
|
||||
interface ConnectionsTableSchema {
|
||||
id: string
|
||||
userId: string
|
||||
serviceId: string
|
||||
service: string
|
||||
accessToken: string
|
||||
refreshToken: string | null
|
||||
expiry: number | null
|
||||
}
|
||||
|
||||
export class Users {
|
||||
@@ -78,46 +50,28 @@ export class Users {
|
||||
}
|
||||
}
|
||||
|
||||
export class Services {
|
||||
static getService = (id: string): DBServiceData => {
|
||||
const { type, serviceUserId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow
|
||||
const service: DBServiceData = { id, type: type as ServiceType, serviceUserId, url }
|
||||
return service
|
||||
}
|
||||
|
||||
static addService = (type: ServiceType, serviceUserId: string, url: URL): DBServiceData => {
|
||||
const serviceId = generateUUID()
|
||||
db.prepare('INSERT INTO Services(id, type, serviceUserId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, serviceUserId, url.origin)
|
||||
return this.getService(serviceId)
|
||||
}
|
||||
|
||||
static deleteService = (id: string): void => {
|
||||
const commandInfo = db.prepare('DELETE FROM Services WHERE id = ?').run(id)
|
||||
if (commandInfo.changes === 0) throw new Error(`Serivce with id ${id} does not exist`)
|
||||
}
|
||||
}
|
||||
|
||||
export class Connections {
|
||||
static getConnection = (id: string): DBConnectionData => {
|
||||
const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow
|
||||
const connection: DBConnectionData = { id, user: Users.getUser(userId)!, service: Services.getService(serviceId), accessToken, refreshToken, expiry }
|
||||
static getConnection = (id: string): Connection => {
|
||||
const { userId, service, accessToken } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as ConnectionsTableSchema
|
||||
const connection: Connection = { id, user: Users.getUser(userId)!, service: JSON.parse(service), accessToken }
|
||||
return connection
|
||||
}
|
||||
|
||||
static getUserConnections = (userId: string): DBConnectionData[] => {
|
||||
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as DBConnectionRow[]
|
||||
const connections: DBConnectionData[] = []
|
||||
static getUserConnections = (userId: string): Connection[] => {
|
||||
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as ConnectionsTableSchema[]
|
||||
const connections: Connection[] = []
|
||||
const user = Users.getUser(userId)!
|
||||
connectionRows.forEach((row) => {
|
||||
const { id, serviceId, accessToken, refreshToken, expiry } = row
|
||||
connections.push({ id, user, service: Services.getService(serviceId), accessToken, refreshToken, expiry })
|
||||
const { id, service, accessToken } = row
|
||||
connections.push({ id, user, service: JSON.parse(service), accessToken })
|
||||
})
|
||||
return connections
|
||||
}
|
||||
|
||||
static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): DBConnectionData => {
|
||||
static addConnection = (userId: string, service: Service, accessToken: string): Connection => {
|
||||
const connectionId = generateUUID()
|
||||
db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry)
|
||||
if (!isValidURL(service.urlOrigin)) throw new Error('Service does not have valid url')
|
||||
db.prepare('INSERT INTO Connections(id, userId, service, accessToken) VALUES(?, ?, ?, ?)').run(connectionId, userId, JSON.stringify(service), accessToken)
|
||||
return this.getConnection(connectionId)
|
||||
}
|
||||
|
||||
|
||||
@@ -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