FINALLY, Beautiful connection types!

This commit is contained in:
Eclypsed
2024-02-23 00:53:54 -05:00
parent c2236ab8ac
commit c7b9b214b7
16 changed files with 200 additions and 214 deletions

Binary file not shown.

View File

@@ -1,22 +1,19 @@
import Database, { SqliteError } from 'better-sqlite3'
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
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 NOT NULL,
accessToken TEXT NOT NULL,
refreshToken TEXT,
expiry NUMBER,
service TEXT,
tokens TEXT
FOREIGN KEY(userId) REFERENCES Users(id)
)`
db.exec(initUsersTable), db.exec(initConnectionsTable)
@@ -24,88 +21,87 @@ db.exec(initUsersTable), db.exec(initConnectionsTable)
interface ConnectionsTableSchema {
id: string
userId: string
type: string
service: string
accessToken: string
refreshToken?: string
expiry?: number
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
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
const user = db.prepare(`SELECT * FROM Users WHERE lower(username) = ?`).get(username.toLowerCase()) as User | null
return user
}
static addUser = (username: string, hashedPassword: string): User | null => {
static addUser = (username: string, passwordHash: string): User | null => {
if (this.getUsername(username)) return null
const userId = generateUUID()
db.prepare('INSERT INTO Users(id, username, password) VALUES(?, ?, ?)').run(userId, username, hashedPassword)
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)
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 serviceType> = T extends 'jellyfin'
? Omit<Jellyfin.Connection, 'service'> & { service: Pick<Jellyfin.Connection['service'], 'userId' | 'urlOrigin'> }
: T extends 'youtube-music'
? Omit<YouTubeMusic.Connection, 'service'> & { service: Pick<YouTubeMusic.Connection['service'], 'userId'> }
: never
export class Connections {
static getConnection = (id: string): BaseConnection => {
const { userId, service, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as ConnectionsTableSchema
const connection: BaseConnection = { id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry }
static getConnection = (id: string): DBConnectionData<serviceType> => {
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<typeof type> = { id, userId, type, service: parsedService, tokens: parsedTokens }
return connection
}
static getUserConnections = (userId: string): Connection[] => {
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as ConnectionsTableSchema[]
const connections: Connection[] = []
for (const row of connectionRows) {
const { id, service, accessToken, refreshToken, expiry } = row
connections.push({ id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry })
static getUserConnections = (userId: string): DBConnectionData<serviceType>[] => {
const connectionRows = db.prepare(`SELECT * FROM Connections WHERE userId = ?`).all(userId) as ConnectionsTableSchema[]
const connections: DBConnectionData<serviceType>[] = []
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 = (userId: string, service: Service, accessToken?: string, refreshToken?: string, expiry?: number): Connection => {
static addConnection<T extends serviceType>(type: T, connectionData: Omit<DBConnectionData<T>, 'id' | 'type'>): DBConnectionData<T> {
const connectionId = generateUUID()
const test: Connection = {
id: 'test',
userId: 'test',
type: 'jellyfin',
service: {
userId: 'test',
urlOrigin: 'test',
},
accessToken: 'test',
}
// if (!isValidURL(service.urlOrigin)) throw new Error('Service does not have valid url')
db.prepare('INSERT INTO Connections(id, userId, service, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, JSON.stringify(service), accessToken, refreshToken, expiry)
return this.getConnection(connectionId)
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<T>
}
static deleteConnection = (id: string): void => {
const commandInfo = db.prepare('DELETE FROM Connections WHERE id = ?').run(id)
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 commandInfo = db.prepare('UPDATE Connections SET accessToken = ?, refreshToken = ?, expiry = ? WHERE id = ?').run(accessToken, refreshToken, expiry, id)
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): Connection[] => {
const expiredRows = db.prepare('SELECT * FROM Connections WHERE userId = ? AND expiry < ?').all(userId, Date.now()) as ConnectionsTableSchema[]
const connections: Connection[] = []
for (const row of expiredRows) {
const { id, userId, service, accessToken, refreshToken, expiry } = row
connections.push({ id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry })
static getExpiredConnections = (userId: string): DBConnectionData<serviceType>[] => {
const expiredRows = db.prepare(`SELECT * FROM Connections WHERE userId = ? AND json_extract(tokens, '$.expiry') < ?`).all(userId, Date.now()) as ConnectionsTableSchema[]
const connections: DBConnectionData<serviceType>[] = []
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
}