import Database from 'better-sqlite3' 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, passwordHash VARCHAR(72) NOT NULL )` const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections( id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, type VARCHAR(36) NOT NULL, service TEXT, tokens TEXT FOREIGN KEY(userId) REFERENCES Users(id) )` db.exec(initUsersTable), db.exec(initConnectionsTable) interface ConnectionsTableSchema { id: string userId: string type: serviceType service?: string tokens?: string } export class Users { static getUser = (id: string): User | null => { const user = db.prepare(`SELECT * FROM Users WHERE id = ?`).get(id) as User | null return user } static getUsername = (username: string): User | null => { const user = db.prepare(`SELECT * FROM Users WHERE lower(username) = ?`).get(username.toLowerCase()) as User | null return user } static addUser = (username: string, passwordHash: string): User | null => { if (this.getUsername(username)) return null const userId = generateUUID() db.prepare(`INSERT INTO Users(id, username, passwordHash) VALUES(?, ?, ?)`).run(userId, username, passwordHash) return this.getUser(userId)! } static deleteUser = (id: string): void => { const commandInfo = db.prepare(`DELETE FROM Users WHERE id = ?`).run(id) if (commandInfo.changes === 0) throw new Error(`User with id ${id} does not exist`) } } type DBConnectionData = T extends 'jellyfin' ? Omit & { service: Pick } : T extends 'youtube-music' ? Omit & { service: Pick } : never export class Connections { static getConnection = (id: string): DBConnectionData => { const { userId, type, service, tokens } = db.prepare(`SELECT * FROM Connections WHERE id = ?`).get(id) as ConnectionsTableSchema const parsedService = service ? JSON.parse(service) : undefined const parsedTokens = tokens ? JSON.parse(tokens) : undefined const connection: DBConnectionData = { id, userId, type, service: parsedService, tokens: parsedTokens } return connection } static getUserConnections = (userId: string): DBConnectionData[] => { const connectionRows = db.prepare(`SELECT * FROM Connections WHERE userId = ?`).all(userId) as ConnectionsTableSchema[] const connections: DBConnectionData[] = [] for (const { id, type, service, tokens } of connectionRows) { const parsedService = service ? JSON.parse(service) : undefined const parsedTokens = tokens ? JSON.parse(tokens) : undefined connections.push({ id, userId, type, service: parsedService, tokens: parsedTokens }) } return connections } static addConnection(type: T, connectionData: Omit, 'id' | 'type'>): DBConnectionData { const connectionId = generateUUID() const { userId, service, tokens } = connectionData db.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, service, tokens) return this.getConnection(connectionId) as DBConnectionData } static deleteConnection = (id: string): void => { const commandInfo = db.prepare(`DELETE FROM Connections WHERE id = ?`).run(id) if (commandInfo.changes === 0) throw new Error(`Connection with id: ${id} does not exist`) } static updateTokens = (id: string, accessToken?: string, refreshToken?: string, expiry?: number): void => { const newTokens = { accessToken, refreshToken, expiry } const commandInfo = db.prepare(`UPDATE Connections SET tokens = ? WHERE id = ?`).run(newTokens, id) if (commandInfo.changes === 0) throw new Error('Failed to update tokens') } static getExpiredConnections = (userId: string): DBConnectionData[] => { const expiredRows = db.prepare(`SELECT * FROM Connections WHERE userId = ? AND json_extract(tokens, '$.expiry') < ?`).all(userId, Date.now()) as ConnectionsTableSchema[] const connections: DBConnectionData[] = [] for (const { id, userId, type, service, tokens } of expiredRows) { const parsedService = service ? JSON.parse(service) : undefined const parsedTokens = tokens ? JSON.parse(tokens) : undefined connections.push({ id, userId, type, service: parsedService, tokens: parsedTokens }) } return connections } }