diff --git a/src/app.d.ts b/src/app.d.ts index 8b7e535..18c6af4 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -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 diff --git a/src/lib/server/users.db b/src/lib/server/users.db index f24c00c..898f963 100644 Binary files a/src/lib/server/users.db and b/src/lib/server/users.db differ diff --git a/src/lib/server/users.ts b/src/lib/server/users.ts index fe410f1..6b37049 100644 --- a/src/lib/server/users.ts +++ b/src/lib/server/users.ts @@ -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) } diff --git a/src/routes/api/users/[userId]/connections/+server.ts b/src/routes/api/users/[userId]/connections/+server.ts index 4e9c3bd..c478bdd 100644 --- a/src/routes/api/users/[userId]/connections/+server.ts +++ b/src/routes/api/users/[userId]/connections/+server.ts @@ -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 @@ -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)) } diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index a031c36..0f4c4b2 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -64,10 +64,10 @@
-
+
-
+
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', + }, + ] -
-

- history.back()}> - - - Account +
+

+ + history.back()}> + + + + Settings

-
+
+
- - diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 21c225a..4c288a3 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,57 +1 @@ - - - +

Main Settings Page

diff --git a/src/routes/settings/connections/+page.server.ts b/src/routes/settings/connections/+page.server.ts index 249f342..e15f47b 100644 --- a/src/routes/settings/connections/+page.server.ts +++ b/src/routes/settings/connections/+page.server.ts @@ -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 } + }, } diff --git a/src/routes/settings/connections/+page.svelte b/src/routes/settings/connections/+page.svelte index b3a22e9..a311d00 100644 --- a/src/routes/settings/connections/+page.svelte +++ b/src/routes/settings/connections/+page.svelte @@ -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
@@ -72,35 +67,29 @@

Add Connection

{#each Object.entries(Services) as [serviceType, serviceData]} - {/each}
- {#each connectionProfiles as connectionProfile} - {@const serviceData = Services[connectionProfile.serviceType]} + {#each connections as connection} + {@const serviceData = Services[connection.service.type]}
{serviceData.displayName} icon
-
{connectionProfile?.username ? connectionProfile.username : 'Placeholder Account Name'}
+
{connection.service?.username ? connection.service.username : 'Placeholder Account Name'}
{serviceData.displayName} - {#if connectionProfile.serviceType === 'jellyfin' && connectionProfile?.serverName} - - {connectionProfile.serverName} + {#if connection.service.type === 'jellyfin' && connection.service?.serverName} + - {connection.service.serverName} {/if}
- (modal = `delete-${connectionProfile.connectionId}`)}> +
@@ -115,12 +104,12 @@
{/each}
- {#if modal} +