more modularization
This commit is contained in:
@@ -4,210 +4,10 @@ import { getThumbnailSummaries } from "../thumbnails";
|
||||
import { db, dbPath } 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);
|
||||
}
|
||||
|
||||
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 * from "./repositories/chat";
|
||||
export * from "./repositories/revoked-tokens";
|
||||
export * from "./repositories/sessions";
|
||||
export * from "./repositories/shared-files";
|
||||
|
||||
export type ClientDbRow = Omit<Partial<ClientInfo>, "online"> & {
|
||||
id: string;
|
||||
@@ -534,153 +334,6 @@ export function isIpBanned(ip: string): boolean {
|
||||
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() {
|
||||
db.run(`UPDATE clients SET online=0`);
|
||||
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 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