From b7daf9c27c0e6fb977e59d0079eca50e8ed9dbc0 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Wed, 31 Jan 2024 01:17:24 -0500 Subject: [PATCH] Experimenting with typescript API validation --- src/lib/server/users.ts | 18 +++---- .../api/users/[userId]/connections/+server.ts | 53 ++++++++++++------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/lib/server/users.ts b/src/lib/server/users.ts index fb703a7..e4dc828 100644 --- a/src/lib/server/users.ts +++ b/src/lib/server/users.ts @@ -4,7 +4,7 @@ import { generateUUID } 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, userId TEXT NOT NULL, url TEXT 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) @@ -15,21 +15,21 @@ type UserQueryParams = { includePassword?: boolean } -export interface DBServiceData { +interface DBServiceData { id: string type: ServiceType - userId: string + serviceUserId: string url: string } interface DBServiceRow { id: string type: string - userId: string + serviceUserId: string url: string } -export interface DBConnectionData { +interface DBConnectionData { id: string user: User service: DBServiceData @@ -80,14 +80,14 @@ export class Users { export class Services { static getService = (id: string): DBServiceData => { - const { type, userId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow - const service: DBServiceData = { id, type: type as ServiceType, userId, url } + 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, userId: string, url: URL): DBServiceData => { + static addService = (type: ServiceType, serviceUserId: string, url: URL): DBServiceData => { const serviceId = generateUUID() - db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin) + db.prepare('INSERT INTO Services(id, type, serviceUserId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, serviceUserId, url.origin) return this.getService(serviceId) } diff --git a/src/routes/api/users/[userId]/connections/+server.ts b/src/routes/api/users/[userId]/connections/+server.ts index fa2f87f..84b1a66 100644 --- a/src/routes/api/users/[userId]/connections/+server.ts +++ b/src/routes/api/users/[userId]/connections/+server.ts @@ -1,37 +1,52 @@ -import { Services, type DBServiceData, Connections, type DBConnectionData } from '$lib/server/users' +import { Services, Connections } from '$lib/server/users' 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 - const repsonseHeaders = new Headers({ - 'Content-Type': 'application/json', - }) - const connections = Connections.getUserConnections(userId) - return new Response(JSON.stringify(connections), { headers: repsonseHeaders }) + return new Response(JSON.stringify(connections)) } export const PATCH: RequestHandler = async ({ params, request }) => { const userId = params.userId as string - const serviceSchema = z.object({ - serviceType: z.enum(['jellyfin', 'youtube-music']), - userId: z.string(), - url: z.string(), - }) - const connectionSchema = z.object({ - userId: z.string(), - serviceId: z.string(), + serviceType: z.enum(['jellyfin', 'youtube-music']), + serviceUserId: z.string(), + url: z.string().refine((val) => isValidURL(val)), accessToken: z.string(), - refreshToken: z.string().nullable(), - expiry: z.number().nullable(), + refreshToken: z.string().nullable().optional(), + expiry: z.number().nullable().optional(), }) - const { service, connection } = await request.json() + const connection = await request.json() - const serviceValidation = serviceSchema.safeParse(service) - if (!serviceValidation.success) return new Response(serviceValidation.error.message, { status: 400 }) + 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) + return new Response(JSON.stringify(newConnection)) +} + +export const DELETE: RequestHandler = async ({ request }) => { + const connectionId: string = await request.json() + try { + Connections.deleteConnection(connectionId) + return new Response('Connection Deleted') + } catch { + return new Response('Connection does not exist', { status: 400 }) + } }