more modularization
This commit is contained in:
@@ -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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user