Now can be powered by postgres sql
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import { config } from "./config.ts";
|
||||
import { db, initDb, closeDb, dbKind } from "./db/client.ts";
|
||||
import type { DbKind } from "./db/client.ts";
|
||||
|
||||
export { db, initDb, closeDb, dbKind };
|
||||
export type { DbKind };
|
||||
|
||||
// --- row types ---
|
||||
|
||||
@@ -54,94 +55,6 @@ export interface PublicUser {
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
// --- db handle ---
|
||||
|
||||
let db: Database | null = null;
|
||||
|
||||
export function getDb(): Database {
|
||||
if (db) return db;
|
||||
mkdirSync(config.dataDir, { recursive: true });
|
||||
db = new Database(join(config.dataDir, "kisassistant.db"), { create: true });
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
db.exec("PRAGMA foreign_keys = ON;");
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL DEFAULT '',
|
||||
theme TEXT NOT NULL DEFAULT 'auto',
|
||||
key_hash TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sessions(
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS conversations(
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT 'New chat',
|
||||
topic TEXT NOT NULL DEFAULT '',
|
||||
model TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_conv_user ON conversations(user_id, updated_at DESC);
|
||||
CREATE TABLE IF NOT EXISTS messages(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conv_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conv_id, id);
|
||||
CREATE TABLE IF NOT EXISTS shares(
|
||||
conv_id TEXT PRIMARY KEY REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
public_id TEXT UNIQUE NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS resets(
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS settings(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS user_provider_keys(
|
||||
user_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, provider)
|
||||
);
|
||||
`);
|
||||
// Email column added after the initial schema — keep idempotent.
|
||||
const cols = db.query("PRAGMA table_info(users)").all() as { name: string }[];
|
||||
if (!cols.some((c) => c.name === "email")) {
|
||||
db.exec("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
// Archive flag added after the initial schema — keep idempotent.
|
||||
const convCols = db.query("PRAGMA table_info(conversations)").all() as { name: string }[];
|
||||
if (!convCols.some((c) => c.name === "archived_at")) {
|
||||
db.exec("ALTER TABLE conversations ADD COLUMN archived_at INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
// Admin pseudo-account row (auth still goes through APP_PASSWORD from .env).
|
||||
const admin = db.query("SELECT id FROM users WHERE username = 'admin'").get() as { id: string } | null;
|
||||
if (!admin) {
|
||||
db.query(
|
||||
"INSERT INTO users (id, username, display_name, avatar_url, theme, key_hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(randomUUID(), "admin", "Administrator", "", "auto", "", Date.now());
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export function toPublicUser(u: UserRow): PublicUser {
|
||||
return {
|
||||
id: u.id,
|
||||
@@ -157,20 +70,19 @@ export function toPublicUser(u: UserRow): PublicUser {
|
||||
|
||||
// --- users ---
|
||||
|
||||
export function getUserByUsername(username: string): UserRow | null {
|
||||
return getDb().query("SELECT * FROM users WHERE username = ?").get(username) as UserRow | null;
|
||||
export async function getUserByUsername(username: string): Promise<UserRow | null> {
|
||||
return db().get<UserRow>("SELECT * FROM users WHERE username = ?", username);
|
||||
}
|
||||
|
||||
export function getUserById(id: string): UserRow | null {
|
||||
return getDb().query("SELECT * FROM users WHERE id = ?").get(id) as UserRow | null;
|
||||
export async function getUserById(id: string): Promise<UserRow | null> {
|
||||
return db().get<UserRow>("SELECT * FROM users WHERE id = ?", id);
|
||||
}
|
||||
|
||||
export function listUsers(): UserRow[] {
|
||||
return getDb().query("SELECT * FROM users ORDER BY created_at ASC").all() as UserRow[];
|
||||
export async function listUsers(): Promise<UserRow[]> {
|
||||
return db().all<UserRow>("SELECT * FROM users ORDER BY created_at ASC");
|
||||
}
|
||||
|
||||
export function createUser(opts: { username: string; displayName: string; avatarUrl: string; theme: string; email: string; keyHash: string }): UserRow {
|
||||
const d = getDb();
|
||||
export async function createUser(opts: { username: string; displayName: string; avatarUrl: string; theme: string; email: string; keyHash: string }): Promise<UserRow> {
|
||||
const row: UserRow = {
|
||||
id: randomUUID(),
|
||||
username: opts.username,
|
||||
@@ -181,14 +93,14 @@ export function createUser(opts: { username: string; displayName: string; avatar
|
||||
key_hash: opts.keyHash,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
d.query("INSERT INTO users (id, username, display_name, avatar_url, theme, email, key_hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(
|
||||
await db().run(
|
||||
"INSERT INTO users (id, username, display_name, avatar_url, theme, email, key_hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
row.id, row.username, row.display_name, row.avatar_url, row.theme, row.email, row.key_hash, row.created_at,
|
||||
);
|
||||
return row;
|
||||
}
|
||||
|
||||
export function updateUser(id: string, patch: { username?: string; displayName?: string; avatarUrl?: string; theme?: string; email?: string; keyHash?: string }): UserRow | null {
|
||||
const d = getDb();
|
||||
export async function updateUser(id: string, patch: { username?: string; displayName?: string; avatarUrl?: string; theme?: string; email?: string; keyHash?: string }): Promise<UserRow | null> {
|
||||
const sets: string[] = [];
|
||||
const vals: (string | number)[] = [];
|
||||
if (patch.username !== undefined) { sets.push("username = ?"); vals.push(patch.username); }
|
||||
@@ -198,62 +110,62 @@ export function updateUser(id: string, patch: { username?: string; displayName?:
|
||||
if (patch.email !== undefined) { sets.push("email = ?"); vals.push(patch.email); }
|
||||
if (patch.keyHash !== undefined) { sets.push("key_hash = ?"); vals.push(patch.keyHash); }
|
||||
if (sets.length > 0) {
|
||||
d.query(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`).run(...vals, id);
|
||||
await db().run(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, ...vals, id);
|
||||
}
|
||||
if (patch.username !== undefined) {
|
||||
// Username is denormalized onto past reports — keep the triage list consistent.
|
||||
d.query("UPDATE reports SET reporter_name = ? WHERE reporter_id = ?").run(patch.username, id);
|
||||
await db().run("UPDATE reports SET reporter_name = ? WHERE reporter_id = ?", patch.username, id);
|
||||
}
|
||||
return getUserById(id);
|
||||
}
|
||||
|
||||
export function deleteUser(id: string): void {
|
||||
const d = getDb();
|
||||
const convs = d.query("SELECT id FROM conversations WHERE user_id = ?").all(id) as { id: string }[];
|
||||
const delMsg = d.query("DELETE FROM messages WHERE conv_id = ?");
|
||||
const delShare = d.query("DELETE FROM shares WHERE conv_id = ?");
|
||||
const delConv = d.query("DELETE FROM conversations WHERE id = ?");
|
||||
const txn = d.transaction((cid: string) => {
|
||||
delMsg.run(cid);
|
||||
delShare.run(cid);
|
||||
delConv.run(cid);
|
||||
export async function deleteUser(id: string): Promise<void> {
|
||||
const d = db();
|
||||
const convs = await d.all<{ id: string }>("SELECT id FROM conversations WHERE user_id = ?", id);
|
||||
await d.transaction(async (tx) => {
|
||||
for (const c of convs) {
|
||||
await tx.run("DELETE FROM messages WHERE conv_id = ?", c.id);
|
||||
await tx.run("DELETE FROM shares WHERE conv_id = ?", c.id);
|
||||
await tx.run("DELETE FROM conversations WHERE id = ?", c.id);
|
||||
}
|
||||
await tx.run("DELETE FROM sessions WHERE user_id = ?", id);
|
||||
await tx.run("DELETE FROM resets WHERE user_id = ?", id);
|
||||
await tx.run("DELETE FROM user_provider_keys WHERE user_id = ?", id);
|
||||
await tx.run("DELETE FROM users WHERE id = ?", id);
|
||||
});
|
||||
for (const c of convs) txn(c.id);
|
||||
d.query("DELETE FROM sessions WHERE user_id = ?").run(id);
|
||||
d.query("DELETE FROM resets WHERE user_id = ?").run(id);
|
||||
d.query("DELETE FROM user_provider_keys WHERE user_id = ?").run(id);
|
||||
d.query("DELETE FROM users WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
// --- sessions ---
|
||||
|
||||
export function createSession(tokenHash: string, userId: string, expiresAt: number): void {
|
||||
export async function createSession(tokenHash: string, userId: string, expiresAt: number): Promise<void> {
|
||||
const now = Date.now();
|
||||
getDb().query("INSERT INTO sessions (token_hash, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)").run(tokenHash, userId, now, expiresAt);
|
||||
await db().run(
|
||||
"INSERT INTO sessions (token_hash, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)",
|
||||
tokenHash, userId, now, expiresAt,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSession(tokenHash: string): SessionRow | null {
|
||||
const d = getDb();
|
||||
const s = d.query("SELECT * FROM sessions WHERE token_hash = ?").get(tokenHash) as SessionRow | null;
|
||||
export async function getSession(tokenHash: string): Promise<SessionRow | null> {
|
||||
const s = await db().get<SessionRow>("SELECT * FROM sessions WHERE token_hash = ?", tokenHash);
|
||||
if (!s) return null;
|
||||
if (s.expires_at <= Date.now()) {
|
||||
d.query("DELETE FROM sessions WHERE token_hash = ?").run(tokenHash);
|
||||
await db().run("DELETE FROM sessions WHERE token_hash = ?", tokenHash);
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function deleteSession(tokenHash: string): void {
|
||||
getDb().query("DELETE FROM sessions WHERE token_hash = ?").run(tokenHash);
|
||||
export async function deleteSession(tokenHash: string): Promise<void> {
|
||||
await db().run("DELETE FROM sessions WHERE token_hash = ?", tokenHash);
|
||||
}
|
||||
|
||||
export function deleteUserSessions(userId: string): void {
|
||||
getDb().query("DELETE FROM sessions WHERE user_id = ?").run(userId);
|
||||
export async function deleteUserSessions(userId: string): Promise<void> {
|
||||
await db().run("DELETE FROM sessions WHERE user_id = ?", userId);
|
||||
}
|
||||
|
||||
// --- conversations ---
|
||||
|
||||
export function createConversation(userId: string, opts: { title?: string; topic?: string; model?: string }): ConversationRow {
|
||||
export async function createConversation(userId: string, opts: { title?: string; topic?: string; model?: string }): Promise<ConversationRow> {
|
||||
const now = Date.now();
|
||||
const row: ConversationRow = {
|
||||
id: randomUUID(),
|
||||
@@ -265,69 +177,74 @@ export function createConversation(userId: string, opts: { title?: string; topic
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
getDb().query(
|
||||
await db().run(
|
||||
"INSERT INTO conversations (id, user_id, title, topic, model, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(row.id, row.user_id, row.title, row.topic, row.model, row.created_at, row.updated_at);
|
||||
row.id, row.user_id, row.title, row.topic, row.model, row.created_at, row.updated_at,
|
||||
);
|
||||
return row;
|
||||
}
|
||||
|
||||
export function listConversations(userId: string): ConversationRow[] {
|
||||
return getDb().query("SELECT * FROM conversations WHERE user_id = ? AND archived_at = 0 ORDER BY updated_at DESC").all(userId) as ConversationRow[];
|
||||
export async function listConversations(userId: string): Promise<ConversationRow[]> {
|
||||
return db().all<ConversationRow>(
|
||||
"SELECT * FROM conversations WHERE user_id = ? AND archived_at = 0 ORDER BY updated_at DESC", userId,
|
||||
);
|
||||
}
|
||||
|
||||
/** Archived chats: hidden from the sidebar, managed in settings. */
|
||||
export function listArchivedConversations(userId: string): ConversationRow[] {
|
||||
return getDb().query("SELECT * FROM conversations WHERE user_id = ? AND archived_at != 0 ORDER BY updated_at DESC").all(userId) as ConversationRow[];
|
||||
export async function listArchivedConversations(userId: string): Promise<ConversationRow[]> {
|
||||
return db().all<ConversationRow>(
|
||||
"SELECT * FROM conversations WHERE user_id = ? AND archived_at != 0 ORDER BY updated_at DESC", userId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* (Un)archive a conversation. archived_at is left out of updated_at on
|
||||
* purpose so an unarchived chat returns to its chronological place.
|
||||
*/
|
||||
export function setConversationArchived(id: string, archived: boolean): ConversationRow | null {
|
||||
getDb().query("UPDATE conversations SET archived_at = ? WHERE id = ?").run(archived ? Date.now() : 0, id);
|
||||
export async function setConversationArchived(id: string, archived: boolean): Promise<ConversationRow | null> {
|
||||
await db().run("UPDATE conversations SET archived_at = ? WHERE id = ?", archived ? Date.now() : 0, id);
|
||||
return getConversation(id);
|
||||
}
|
||||
|
||||
export function getConversation(id: string): ConversationRow | null {
|
||||
return getDb().query("SELECT * FROM conversations WHERE id = ?").get(id) as ConversationRow | null;
|
||||
export async function getConversation(id: string): Promise<ConversationRow | null> {
|
||||
return db().get<ConversationRow>("SELECT * FROM conversations WHERE id = ?", id);
|
||||
}
|
||||
|
||||
export function updateConversation(id: string, patch: { title?: string; topic?: string; model?: string }): ConversationRow | null {
|
||||
const d = getDb();
|
||||
export async function updateConversation(id: string, patch: { title?: string; topic?: string; model?: string }): Promise<ConversationRow | null> {
|
||||
const sets: string[] = ["updated_at = ?"];
|
||||
const vals: (string | number)[] = [Date.now()];
|
||||
if (patch.title !== undefined) { sets.push("title = ?"); vals.push(patch.title.slice(0, 120) || "New chat"); }
|
||||
if (patch.topic !== undefined) { sets.push("topic = ?"); vals.push(patch.topic.slice(0, 120)); }
|
||||
if (patch.model !== undefined) { sets.push("model = ?"); vals.push(patch.model.slice(0, 160)); }
|
||||
d.query(`UPDATE conversations SET ${sets.join(", ")} WHERE id = ?`).run(...vals, id);
|
||||
await db().run(`UPDATE conversations SET ${sets.join(", ")} WHERE id = ?`, ...vals, id);
|
||||
return getConversation(id);
|
||||
}
|
||||
|
||||
export function touchConversation(id: string, model?: string): void {
|
||||
export async function touchConversation(id: string, model?: string): Promise<void> {
|
||||
if (model) {
|
||||
getDb().query("UPDATE conversations SET updated_at = ?, model = ? WHERE id = ?").run(Date.now(), model.slice(0, 160), id);
|
||||
await db().run("UPDATE conversations SET updated_at = ?, model = ? WHERE id = ?", Date.now(), model.slice(0, 160), id);
|
||||
} else {
|
||||
getDb().query("UPDATE conversations SET updated_at = ? WHERE id = ?").run(Date.now(), id);
|
||||
await db().run("UPDATE conversations SET updated_at = ? WHERE id = ?", Date.now(), id);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteConversation(id: string): void {
|
||||
const d = getDb();
|
||||
const txn = d.transaction(() => {
|
||||
d.query("DELETE FROM messages WHERE conv_id = ?").run(id);
|
||||
d.query("DELETE FROM shares WHERE conv_id = ?").run(id);
|
||||
d.query("DELETE FROM conversations WHERE id = ?").run(id);
|
||||
export async function deleteConversation(id: string): Promise<void> {
|
||||
await db().transaction(async (tx) => {
|
||||
await tx.run("DELETE FROM messages WHERE conv_id = ?", id);
|
||||
await tx.run("DELETE FROM shares WHERE conv_id = ?", id);
|
||||
await tx.run("DELETE FROM conversations WHERE id = ?", id);
|
||||
});
|
||||
txn();
|
||||
}
|
||||
|
||||
export function addMessage(convId: string, role: string, content: string): void {
|
||||
getDb().query("INSERT INTO messages (conv_id, role, content, created_at) VALUES (?, ?, ?, ?)").run(convId, role, content, Date.now());
|
||||
export async function addMessage(convId: string, role: string, content: string): Promise<void> {
|
||||
await db().run(
|
||||
"INSERT INTO messages (conv_id, role, content, created_at) VALUES (?, ?, ?, ?)",
|
||||
convId, role, content, Date.now(),
|
||||
);
|
||||
}
|
||||
|
||||
export function getMessages(convId: string): MessageRow[] {
|
||||
return getDb().query("SELECT * FROM messages WHERE conv_id = ? ORDER BY id ASC LIMIT 500").all(convId) as MessageRow[];
|
||||
export async function getMessages(convId: string): Promise<MessageRow[]> {
|
||||
return db().all<MessageRow>("SELECT * FROM messages WHERE conv_id = ? ORDER BY id ASC LIMIT 500", convId);
|
||||
}
|
||||
|
||||
// --- password-recovery resets (one-time, hashed, expiring) ---
|
||||
@@ -339,81 +256,80 @@ export interface ResetRow {
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export function createReset(tokenHash: string, userId: string, expiresAt: number): void {
|
||||
const d = getDb();
|
||||
export async function createReset(tokenHash: string, userId: string, expiresAt: number): Promise<void> {
|
||||
// One active reset per account: replace any previous one.
|
||||
d.query("DELETE FROM resets WHERE user_id = ?").run(userId);
|
||||
d.query("INSERT INTO resets (token_hash, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)").run(
|
||||
await db().run("DELETE FROM resets WHERE user_id = ?", userId);
|
||||
await db().run(
|
||||
"INSERT INTO resets (token_hash, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)",
|
||||
tokenHash, userId, Date.now(), expiresAt,
|
||||
);
|
||||
}
|
||||
|
||||
export function peekReset(tokenHash: string): ResetRow | null {
|
||||
const d = getDb();
|
||||
const r = d.query("SELECT * FROM resets WHERE token_hash = ?").get(tokenHash) as ResetRow | null;
|
||||
export async function peekReset(tokenHash: string): Promise<ResetRow | null> {
|
||||
const r = await db().get<ResetRow>("SELECT * FROM resets WHERE token_hash = ?", tokenHash);
|
||||
if (!r) return null;
|
||||
if (r.expires_at <= Date.now()) {
|
||||
d.query("DELETE FROM resets WHERE token_hash = ?").run(tokenHash);
|
||||
await db().run("DELETE FROM resets WHERE token_hash = ?", tokenHash);
|
||||
return null;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Consume a reset token (single use). Returns the row, or null if invalid/expired. */
|
||||
export function consumeReset(tokenHash: string): ResetRow | null {
|
||||
const r = peekReset(tokenHash);
|
||||
export async function consumeReset(tokenHash: string): Promise<ResetRow | null> {
|
||||
const r = await peekReset(tokenHash);
|
||||
if (!r) return null;
|
||||
getDb().query("DELETE FROM resets WHERE token_hash = ?").run(tokenHash);
|
||||
await db().run("DELETE FROM resets WHERE token_hash = ?", tokenHash);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function deleteUserResets(userId: string): void {
|
||||
getDb().query("DELETE FROM resets WHERE user_id = ?").run(userId);
|
||||
export async function deleteUserResets(userId: string): Promise<void> {
|
||||
await db().run("DELETE FROM resets WHERE user_id = ?", userId);
|
||||
}
|
||||
|
||||
// --- platform settings (admin-controlled feature flags) ---
|
||||
|
||||
export function getSetting(key: string, fallback: string): string {
|
||||
const r = getDb().query("SELECT value FROM settings WHERE key = ?").get(key) as { value: string } | null;
|
||||
export async function getSetting(key: string, fallback: string): Promise<string> {
|
||||
const r = await db().get<{ value: string }>("SELECT value FROM settings WHERE key = ?", key);
|
||||
return r?.value ?? fallback;
|
||||
}
|
||||
|
||||
export function setSetting(key: string, value: string): void {
|
||||
getDb().query(
|
||||
export async function setSetting(key: string, value: string): Promise<void> {
|
||||
await db().run(
|
||||
"INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
|
||||
).run(key, value, Date.now());
|
||||
key, value, Date.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/** Public self-registration switch (default off = admin-created accounts only). */
|
||||
export function isRegistrationEnabled(): boolean {
|
||||
return getSetting("registration_enabled", "0") === "1";
|
||||
export async function isRegistrationEnabled(): Promise<boolean> {
|
||||
return (await getSetting("registration_enabled", "0")) === "1";
|
||||
}
|
||||
|
||||
/** Admin kill-switch for the OCR platform tool (default on). */
|
||||
export function isOcrToolEnabled(): boolean {
|
||||
return getSetting("tools_ocr_enabled", "1") === "1";
|
||||
export async function isOcrToolEnabled(): Promise<boolean> {
|
||||
return (await getSetting("tools_ocr_enabled", "1")) === "1";
|
||||
}
|
||||
|
||||
/** Admin kill-switch for message reports / flag button (default on). */
|
||||
export function isReportsEnabled(): boolean {
|
||||
return getSetting("reports_enabled", "1") === "1";
|
||||
export async function isReportsEnabled(): Promise<boolean> {
|
||||
return (await getSetting("reports_enabled", "1")) === "1";
|
||||
}
|
||||
|
||||
/** Admin kill-switch for self-service username changes (default on; admins bypass). */
|
||||
export function isUsernameChangeEnabled(): boolean {
|
||||
return getSetting("username_change_enabled", "1") === "1";
|
||||
export async function isUsernameChangeEnabled(): Promise<boolean> {
|
||||
return (await getSetting("username_change_enabled", "1")) === "1";
|
||||
}
|
||||
|
||||
// --- public shares ---
|
||||
|
||||
export function ensureShare(convId: string): string {
|
||||
const d = getDb();
|
||||
const existing = d.query("SELECT public_id FROM shares WHERE conv_id = ?").get(convId) as { public_id: string } | null;
|
||||
export async function ensureShare(convId: string): Promise<string> {
|
||||
const existing = await db().get<{ public_id: string }>("SELECT public_id FROM shares WHERE conv_id = ?", convId);
|
||||
if (existing) return existing.public_id;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const publicId = randomBytes(9).toString("base64url");
|
||||
try {
|
||||
d.query("INSERT INTO shares (conv_id, public_id, created_at) VALUES (?, ?, ?)").run(convId, publicId, Date.now());
|
||||
await db().run("INSERT INTO shares (conv_id, public_id, created_at) VALUES (?, ?, ?)", convId, publicId, Date.now());
|
||||
return publicId;
|
||||
} catch {
|
||||
// collision — retry
|
||||
@@ -422,25 +338,24 @@ export function ensureShare(convId: string): string {
|
||||
throw new Error("could not generate share id");
|
||||
}
|
||||
|
||||
export function getShareByConv(convId: string): string | null {
|
||||
const r = getDb().query("SELECT public_id FROM shares WHERE conv_id = ?").get(convId) as { public_id: string } | null;
|
||||
export async function getShareByConv(convId: string): Promise<string | null> {
|
||||
const r = await db().get<{ public_id: string }>("SELECT public_id FROM shares WHERE conv_id = ?", convId);
|
||||
return r?.public_id ?? null;
|
||||
}
|
||||
|
||||
export function deleteShare(convId: string): void {
|
||||
getDb().query("DELETE FROM shares WHERE conv_id = ?").run(convId);
|
||||
export async function deleteShare(convId: string): Promise<void> {
|
||||
await db().run("DELETE FROM shares WHERE conv_id = ?", convId);
|
||||
}
|
||||
|
||||
export function getShareByPublic(publicId: string): { conv: ConversationRow; messages: MessageRow[]; authorName: string; authorAvatarUrl: string; archived: boolean; sharedAt: number } | null {
|
||||
const d = getDb();
|
||||
const s = d.query("SELECT * FROM shares WHERE public_id = ?").get(publicId) as { conv_id: string; created_at: number } | null;
|
||||
export async function getShareByPublic(publicId: string): Promise<{ conv: ConversationRow; messages: MessageRow[]; authorName: string; authorAvatarUrl: string; archived: boolean; sharedAt: number } | null> {
|
||||
const s = await db().get<{ conv_id: string; created_at: number }>("SELECT * FROM shares WHERE public_id = ?", publicId);
|
||||
if (!s) return null;
|
||||
const conv = getConversation(s.conv_id);
|
||||
const conv = await getConversation(s.conv_id);
|
||||
if (!conv) return null;
|
||||
const author = getUserById(conv.user_id);
|
||||
const author = await getUserById(conv.user_id);
|
||||
return {
|
||||
conv,
|
||||
messages: getMessages(conv.id).filter((m) => m.role !== "system"),
|
||||
messages: (await getMessages(conv.id)).filter((m) => m.role !== "system"),
|
||||
authorName: author?.display_name || "Someone",
|
||||
authorAvatarUrl: author?.avatar_url || "",
|
||||
archived: conv.archived_at !== 0,
|
||||
|
||||
Reference in New Issue
Block a user