more modularization

This commit is contained in:
KingKDot
2026-07-07 10:17:10 -05:00
parent 2db677af8d
commit 97e9bf8f3d
6 changed files with 377 additions and 361 deletions
+4 -361
View File
@@ -4,210 +4,10 @@ import { getThumbnailSummaries } from "../thumbnails";
import { db, dbPath } from "./connection"; import { db, dbPath } from "./connection";
import "./schema"; import "./schema";
export type ChatMessageRecord = { export * from "./repositories/chat";
id: number; export * from "./repositories/revoked-tokens";
userId: number; export * from "./repositories/sessions";
username: string; export * from "./repositories/shared-files";
userRole: string;
message: string;
createdAt: number;
};
export function insertChatMessage(userId: number, username: string, userRole: string, message: string): ChatMessageRecord {
const createdAt = Date.now();
const result = db.run(
`INSERT INTO chat_messages (user_id, username, user_role, message, created_at) VALUES (?, ?, ?, ?, ?)`,
userId, username, userRole, message, createdAt,
);
const id = Number((result as any).lastInsertRowid);
return { id, userId, username, userRole, message, createdAt };
}
export function getChatHistory(before?: number, limit: number = 50, retentionMs?: number): ChatMessageRecord[] {
const maxLimit = Math.min(Math.max(1, limit), 200);
const cutoff = retentionMs && retentionMs > 0 ? Date.now() - retentionMs : 0;
let rows: any[];
if (before) {
if (cutoff > 0) {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at < ? AND created_at > ? ORDER BY created_at DESC LIMIT ?`,
).all(before, cutoff, maxLimit);
} else {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at < ? ORDER BY created_at DESC LIMIT ?`,
).all(before, maxLimit);
}
} else {
if (cutoff > 0) {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at > ? ORDER BY created_at DESC LIMIT ?`,
).all(cutoff, maxLimit);
} else {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages ORDER BY created_at DESC LIMIT ?`,
).all(maxLimit);
}
}
return rows.map((r: any) => ({
id: r.id,
userId: r.user_id,
username: r.username,
userRole: r.user_role,
message: r.message,
createdAt: r.created_at,
})).reverse();
}
export function deleteExpiredChatMessages(retentionMs: number): number {
if (retentionMs <= 0) return 0;
const cutoff = Date.now() - retentionMs;
const result = db.run(`DELETE FROM chat_messages WHERE created_at < ?`, cutoff);
return Number((result as any).changes ?? 0);
}
export type SharedFileRecord = {
id: string;
filename: string;
storedPath: string;
size: number;
mimeType: string;
uploadedBy: number;
uploadedByUsername: string;
passwordHash: string | null;
maxDownloads: number | null;
downloadCount: number;
expiresAt: number | null;
createdAt: number;
description: string | null;
};
export function insertSharedFile(file: SharedFileRecord): void {
db.run(
`INSERT INTO shared_files (id, filename, stored_path, size, mime_type, uploaded_by, uploaded_by_username, password_hash, max_downloads, download_count, expires_at, created_at, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
file.id,
file.filename,
file.storedPath,
file.size,
file.mimeType,
file.uploadedBy,
file.uploadedByUsername,
file.passwordHash,
file.maxDownloads,
file.downloadCount,
file.expiresAt,
file.createdAt,
file.description,
);
}
export function getSharedFile(id: string): SharedFileRecord | null {
const row = db.query<any>(`SELECT * FROM shared_files WHERE id=?`).get(id);
if (!row) return null;
return {
id: row.id,
filename: row.filename,
storedPath: row.stored_path,
size: row.size,
mimeType: row.mime_type,
uploadedBy: row.uploaded_by,
uploadedByUsername: row.uploaded_by_username,
passwordHash: row.password_hash,
maxDownloads: row.max_downloads,
downloadCount: row.download_count,
expiresAt: row.expires_at,
createdAt: row.created_at,
description: row.description,
};
}
export function listSharedFiles(): SharedFileRecord[] {
return db
.query<any>(`SELECT * FROM shared_files ORDER BY created_at DESC`)
.all()
.map((row: any) => ({
id: row.id,
filename: row.filename,
storedPath: row.stored_path,
size: row.size,
mimeType: row.mime_type,
uploadedBy: row.uploaded_by,
uploadedByUsername: row.uploaded_by_username,
passwordHash: row.password_hash,
maxDownloads: row.max_downloads,
downloadCount: row.download_count,
expiresAt: row.expires_at,
createdAt: row.created_at,
description: row.description,
}));
}
export function deleteSharedFile(id: string): boolean {
const result = db.run(`DELETE FROM shared_files WHERE id=?`, id);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function updateSharedFile(
id: string,
updates: {
passwordHash?: string | null;
maxDownloads?: number | null;
expiresAt?: number | null;
description?: string | null;
},
): boolean {
const setClauses: string[] = [];
const values: any[] = [];
if (updates.passwordHash !== undefined) {
setClauses.push("password_hash=?");
values.push(updates.passwordHash);
}
if (updates.maxDownloads !== undefined) {
setClauses.push("max_downloads=?");
values.push(updates.maxDownloads);
}
if (updates.expiresAt !== undefined) {
setClauses.push("expires_at=?");
values.push(updates.expiresAt);
}
if (updates.description !== undefined) {
setClauses.push("description=?");
values.push(updates.description);
}
if (setClauses.length === 0) return false;
values.push(id);
const result = db.run(
`UPDATE shared_files SET ${setClauses.join(", ")} WHERE id=?`,
...values,
);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function incrementSharedFileDownloadCount(id: string): boolean {
const result = db.run(
`UPDATE shared_files SET download_count = download_count + 1 WHERE id=?`,
id,
);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function deleteExpiredSharedFiles(): string[] {
const now = Date.now();
const expired = db
.query<any>(`SELECT id, stored_path FROM shared_files WHERE expires_at IS NOT NULL AND expires_at < ?`)
.all(now);
const ids = expired.map((r: any) => r.id);
const paths = expired.map((r: any) => r.stored_path);
if (ids.length > 0) {
for (const id of ids) {
db.run(`DELETE FROM shared_files WHERE id=?`, id);
}
}
return paths;
}
export type ClientDbRow = Omit<Partial<ClientInfo>, "online"> & { export type ClientDbRow = Omit<Partial<ClientInfo>, "online"> & {
id: string; id: string;
@@ -534,153 +334,6 @@ export function isIpBanned(ip: string): boolean {
return !!row?.ip; return !!row?.ip;
} }
// ── Revoked token persistence ──
function hashToken(token: string): string {
return new Bun.CryptoHasher("sha256").update(token).digest("hex");
}
export function persistRevokedToken(token: string, expiresAt: number): void {
db.run(
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)`,
hashToken(token),
expiresAt,
);
}
export function persistRevokedTokenHash(tokenHash: string, expiresAt: number): void {
db.run(
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)`,
tokenHash,
expiresAt,
);
}
export function isTokenRevoked(token: string): boolean {
const row = db.query<{ token_hash: string }>(
`SELECT token_hash FROM revoked_tokens WHERE token_hash=?`,
).get(hashToken(token));
return !!row;
}
export function loadAllRevokedTokenHashes(): Set<string> {
const now = Math.floor(Date.now() / 1000);
const rows = db.query<{ token_hash: string }>(
`SELECT token_hash FROM revoked_tokens WHERE expires_at > ?`,
).all(now);
return new Set(rows.map((r) => r.token_hash));
}
export function pruneExpiredRevokedTokens(): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(`DELETE FROM revoked_tokens WHERE expires_at <= ?`, now);
return result.changes;
}
export type SessionRecord = {
id: string;
userId: number;
tokenHash: string;
ip: string | null;
userAgent: string | null;
createdAt: number;
lastActivity: number;
expiresAt: number;
revoked: boolean;
};
export function createSession(session: {
id: string;
userId: number;
tokenHash: string;
ip: string | null;
userAgent: string | null;
createdAt: number;
expiresAt: number;
}): void {
db.run(
`INSERT INTO sessions (id, user_id, token_hash, ip, user_agent, created_at, last_activity, expires_at, revoked)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`,
session.id,
session.userId,
session.tokenHash,
session.ip,
session.userAgent,
session.createdAt,
session.createdAt,
session.expiresAt,
);
}
export function getSessionByTokenHash(tokenHash: string): SessionRecord | null {
const row = db.query<any>(
`SELECT * FROM sessions WHERE token_hash=? AND revoked=0`,
).get(tokenHash);
if (!row) return null;
return mapSessionRow(row);
}
export function getSessionById(id: string): SessionRecord | null {
const row = db.query<any>(`SELECT * FROM sessions WHERE id=?`).get(id);
if (!row) return null;
return mapSessionRow(row);
}
export function listUserSessions(userId: number): SessionRecord[] {
const rows = db.query<any>(
`SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC`,
).all(userId);
return rows.map(mapSessionRow);
}
export function updateSessionActivity(tokenHash: string): void {
const now = Math.floor(Date.now() / 1000);
db.run(`UPDATE sessions SET last_activity=? WHERE token_hash=? AND revoked=0`, now, tokenHash);
}
export function revokeSessionByTokenHash(tokenHash: string): boolean {
const result = db.run(`UPDATE sessions SET revoked=1 WHERE token_hash=? AND revoked=0`, tokenHash);
return result.changes > 0;
}
export function revokeSessionById(sessionId: string): { tokenHash: string | null } {
const row = db.query<{ token_hash: string }>(
`SELECT token_hash FROM sessions WHERE id=? AND revoked=0`,
).get(sessionId);
if (!row) return { tokenHash: null };
db.run(`UPDATE sessions SET revoked=1 WHERE id=?`, sessionId);
return { tokenHash: row.token_hash };
}
export function revokeAllUserSessions(userId: number): number {
const result = db.run(`UPDATE sessions SET revoked=1 WHERE user_id=? AND revoked=0`, userId);
return result.changes;
}
export function pruneExpiredSessions(): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(`DELETE FROM sessions WHERE expires_at <= ?`, now);
return result.changes;
}
export function hashTokenForSession(token: string): string {
return hashToken(token);
}
function mapSessionRow(row: any): SessionRecord {
return {
id: row.id,
userId: row.user_id,
tokenHash: row.token_hash,
ip: row.ip,
userAgent: row.user_agent,
createdAt: row.created_at,
lastActivity: row.last_activity,
expiresAt: row.expires_at,
revoked: !!row.revoked,
};
}
export function markAllClientsOffline() { export function markAllClientsOffline() {
db.run(`UPDATE clients SET online=0`); db.run(`UPDATE clients SET online=0`);
invalidateClientMetricsSummaryCache(); invalidateClientMetricsSummaryCache();
@@ -1444,16 +1097,6 @@ export function getOldestBuildForUser(userId: number): BuildRecord | null {
}; };
} }
export function deleteInactiveSessions(userId: number): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(
`DELETE FROM sessions WHERE user_id = ? AND (revoked = 1 OR expires_at <= ?)`,
userId,
now,
);
return result.changes;
}
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once"; export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once";
export type AutoScript = { export type AutoScript = {
@@ -0,0 +1,67 @@
import { db } from "../connection";
import "../schema";
export type ChatMessageRecord = {
id: number;
userId: number;
username: string;
userRole: string;
message: string;
createdAt: number;
};
export function insertChatMessage(userId: number, username: string, userRole: string, message: string): ChatMessageRecord {
const createdAt = Date.now();
const result = db.run(
`INSERT INTO chat_messages (user_id, username, user_role, message, created_at) VALUES (?, ?, ?, ?, ?)`,
userId,
username,
userRole,
message,
createdAt,
);
const id = Number((result as any).lastInsertRowid);
return { id, userId, username, userRole, message, createdAt };
}
export function getChatHistory(before?: number, limit: number = 50, retentionMs?: number): ChatMessageRecord[] {
const maxLimit = Math.min(Math.max(1, limit), 200);
const cutoff = retentionMs && retentionMs > 0 ? Date.now() - retentionMs : 0;
let rows: any[];
if (before) {
if (cutoff > 0) {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at < ? AND created_at > ? ORDER BY created_at DESC LIMIT ?`,
).all(before, cutoff, maxLimit);
} else {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at < ? ORDER BY created_at DESC LIMIT ?`,
).all(before, maxLimit);
}
} else {
if (cutoff > 0) {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages WHERE created_at > ? ORDER BY created_at DESC LIMIT ?`,
).all(cutoff, maxLimit);
} else {
rows = db.query<any>(
`SELECT id, user_id, username, user_role, message, created_at FROM chat_messages ORDER BY created_at DESC LIMIT ?`,
).all(maxLimit);
}
}
return rows.map((r: any) => ({
id: r.id,
userId: r.user_id,
username: r.username,
userRole: r.user_role,
message: r.message,
createdAt: r.created_at,
})).reverse();
}
export function deleteExpiredChatMessages(retentionMs: number): number {
if (retentionMs <= 0) return 0;
const cutoff = Date.now() - retentionMs;
const result = db.run(`DELETE FROM chat_messages WHERE created_at < ?`, cutoff);
return Number((result as any).changes ?? 0);
}
@@ -0,0 +1,40 @@
import { db } from "../connection";
import "../schema";
import { hashToken } from "./token-hash";
export function persistRevokedToken(token: string, expiresAt: number): void {
db.run(
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)`,
hashToken(token),
expiresAt,
);
}
export function persistRevokedTokenHash(tokenHash: string, expiresAt: number): void {
db.run(
`INSERT OR IGNORE INTO revoked_tokens (token_hash, expires_at) VALUES (?, ?)`,
tokenHash,
expiresAt,
);
}
export function isTokenRevoked(token: string): boolean {
const row = db.query<{ token_hash: string }>(
`SELECT token_hash FROM revoked_tokens WHERE token_hash=?`,
).get(hashToken(token));
return !!row;
}
export function loadAllRevokedTokenHashes(): Set<string> {
const now = Math.floor(Date.now() / 1000);
const rows = db.query<{ token_hash: string }>(
`SELECT token_hash FROM revoked_tokens WHERE expires_at > ?`,
).all(now);
return new Set(rows.map((r) => r.token_hash));
}
export function pruneExpiredRevokedTokens(): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(`DELETE FROM revoked_tokens WHERE expires_at <= ?`, now);
return result.changes;
}
@@ -0,0 +1,117 @@
import { db } from "../connection";
import "../schema";
import { hashToken } from "./token-hash";
export type SessionRecord = {
id: string;
userId: number;
tokenHash: string;
ip: string | null;
userAgent: string | null;
createdAt: number;
lastActivity: number;
expiresAt: number;
revoked: boolean;
};
export function createSession(session: {
id: string;
userId: number;
tokenHash: string;
ip: string | null;
userAgent: string | null;
createdAt: number;
expiresAt: number;
}): void {
db.run(
`INSERT INTO sessions (id, user_id, token_hash, ip, user_agent, created_at, last_activity, expires_at, revoked)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`,
session.id,
session.userId,
session.tokenHash,
session.ip,
session.userAgent,
session.createdAt,
session.createdAt,
session.expiresAt,
);
}
export function getSessionByTokenHash(tokenHash: string): SessionRecord | null {
const row = db.query<any>(
`SELECT * FROM sessions WHERE token_hash=? AND revoked=0`,
).get(tokenHash);
if (!row) return null;
return mapSessionRow(row);
}
export function getSessionById(id: string): SessionRecord | null {
const row = db.query<any>(`SELECT * FROM sessions WHERE id=?`).get(id);
if (!row) return null;
return mapSessionRow(row);
}
export function listUserSessions(userId: number): SessionRecord[] {
const rows = db.query<any>(
`SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC`,
).all(userId);
return rows.map(mapSessionRow);
}
export function updateSessionActivity(tokenHash: string): void {
const now = Math.floor(Date.now() / 1000);
db.run(`UPDATE sessions SET last_activity=? WHERE token_hash=? AND revoked=0`, now, tokenHash);
}
export function revokeSessionByTokenHash(tokenHash: string): boolean {
const result = db.run(`UPDATE sessions SET revoked=1 WHERE token_hash=? AND revoked=0`, tokenHash);
return result.changes > 0;
}
export function revokeSessionById(sessionId: string): { tokenHash: string | null } {
const row = db.query<{ token_hash: string }>(
`SELECT token_hash FROM sessions WHERE id=? AND revoked=0`,
).get(sessionId);
if (!row) return { tokenHash: null };
db.run(`UPDATE sessions SET revoked=1 WHERE id=?`, sessionId);
return { tokenHash: row.token_hash };
}
export function revokeAllUserSessions(userId: number): number {
const result = db.run(`UPDATE sessions SET revoked=1 WHERE user_id=? AND revoked=0`, userId);
return result.changes;
}
export function pruneExpiredSessions(): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(`DELETE FROM sessions WHERE expires_at <= ?`, now);
return result.changes;
}
export function hashTokenForSession(token: string): string {
return hashToken(token);
}
export function deleteInactiveSessions(userId: number): number {
const now = Math.floor(Date.now() / 1000);
const result = db.run(
`DELETE FROM sessions WHERE user_id = ? AND (revoked = 1 OR expires_at <= ?)`,
userId,
now,
);
return result.changes;
}
function mapSessionRow(row: any): SessionRecord {
return {
id: row.id,
userId: row.user_id,
tokenHash: row.token_hash,
ip: row.ip,
userAgent: row.user_agent,
createdAt: row.created_at,
lastActivity: row.last_activity,
expiresAt: row.expires_at,
revoked: !!row.revoked,
};
}
@@ -0,0 +1,146 @@
import { db } from "../connection";
import "../schema";
export type SharedFileRecord = {
id: string;
filename: string;
storedPath: string;
size: number;
mimeType: string;
uploadedBy: number;
uploadedByUsername: string;
passwordHash: string | null;
maxDownloads: number | null;
downloadCount: number;
expiresAt: number | null;
createdAt: number;
description: string | null;
};
export function insertSharedFile(file: SharedFileRecord): void {
db.run(
`INSERT INTO shared_files (id, filename, stored_path, size, mime_type, uploaded_by, uploaded_by_username, password_hash, max_downloads, download_count, expires_at, created_at, description)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
file.id,
file.filename,
file.storedPath,
file.size,
file.mimeType,
file.uploadedBy,
file.uploadedByUsername,
file.passwordHash,
file.maxDownloads,
file.downloadCount,
file.expiresAt,
file.createdAt,
file.description,
);
}
export function getSharedFile(id: string): SharedFileRecord | null {
const row = db.query<any>(`SELECT * FROM shared_files WHERE id=?`).get(id);
if (!row) return null;
return {
id: row.id,
filename: row.filename,
storedPath: row.stored_path,
size: row.size,
mimeType: row.mime_type,
uploadedBy: row.uploaded_by,
uploadedByUsername: row.uploaded_by_username,
passwordHash: row.password_hash,
maxDownloads: row.max_downloads,
downloadCount: row.download_count,
expiresAt: row.expires_at,
createdAt: row.created_at,
description: row.description,
};
}
export function listSharedFiles(): SharedFileRecord[] {
return db
.query<any>(`SELECT * FROM shared_files ORDER BY created_at DESC`)
.all()
.map((row: any) => ({
id: row.id,
filename: row.filename,
storedPath: row.stored_path,
size: row.size,
mimeType: row.mime_type,
uploadedBy: row.uploaded_by,
uploadedByUsername: row.uploaded_by_username,
passwordHash: row.password_hash,
maxDownloads: row.max_downloads,
downloadCount: row.download_count,
expiresAt: row.expires_at,
createdAt: row.created_at,
description: row.description,
}));
}
export function deleteSharedFile(id: string): boolean {
const result = db.run(`DELETE FROM shared_files WHERE id=?`, id);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function updateSharedFile(
id: string,
updates: {
passwordHash?: string | null;
maxDownloads?: number | null;
expiresAt?: number | null;
description?: string | null;
},
): boolean {
const setClauses: string[] = [];
const values: any[] = [];
if (updates.passwordHash !== undefined) {
setClauses.push("password_hash=?");
values.push(updates.passwordHash);
}
if (updates.maxDownloads !== undefined) {
setClauses.push("max_downloads=?");
values.push(updates.maxDownloads);
}
if (updates.expiresAt !== undefined) {
setClauses.push("expires_at=?");
values.push(updates.expiresAt);
}
if (updates.description !== undefined) {
setClauses.push("description=?");
values.push(updates.description);
}
if (setClauses.length === 0) return false;
values.push(id);
const result = db.run(
`UPDATE shared_files SET ${setClauses.join(", ")} WHERE id=?`,
...values,
);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function incrementSharedFileDownloadCount(id: string): boolean {
const result = db.run(
`UPDATE shared_files SET download_count = download_count + 1 WHERE id=?`,
id,
);
return (result as any)?.changes ? (result as any).changes > 0 : false;
}
export function deleteExpiredSharedFiles(): string[] {
const now = Date.now();
const expired = db
.query<any>(`SELECT id, stored_path FROM shared_files WHERE expires_at IS NOT NULL AND expires_at < ?`)
.all(now);
const ids = expired.map((r: any) => r.id);
const paths = expired.map((r: any) => r.stored_path);
if (ids.length > 0) {
for (const id of ids) {
db.run(`DELETE FROM shared_files WHERE id=?`, id);
}
}
return paths;
}
@@ -0,0 +1,3 @@
export function hashToken(token: string): string {
return new Bun.CryptoHasher("sha256").update(token).digest("hex");
}