@pixelbyte-software/pixcode 1.32.0 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-DcAn_NI0.js → index-CZKV8Tbj.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/server/database/db.js +542 -460
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/database/json-store.js +184 -0
- package/dist-server/server/database/json-store.js.map +1 -0
- package/dist-server/server/services/vapid-keys.js +3 -3
- package/dist-server/server/services/vapid-keys.js.map +1 -1
- package/package.json +1 -1
- package/server/database/db.js +794 -696
- package/server/database/json-store.js +194 -0
- package/server/services/vapid-keys.js +36 -35
- package/dist-server/server/database/schema.js +0 -130
- package/dist-server/server/database/schema.js.map +0 -1
- package/server/database/schema.js +0 -138
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
|
|
6
|
-
import {
|
|
6
|
+
import { JsonStore, nowIso } from './json-store.js';
|
|
7
|
+
// CommonJS `require` shim — we only reach for it once, during the
|
|
8
|
+
// legacy-to-JSON migration below, to dynamically import better-sqlite3
|
|
9
|
+
// only when an old auth.db is actually present on disk.
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
7
11
|
const __dirname = getModuleDir(import.meta.url);
|
|
8
12
|
// The compiled backend lives under dist-server/server/database, but the install root we log
|
|
9
13
|
// should still point at the project/app root. Resolving it here avoids build-layout drift.
|
|
@@ -20,468 +24,546 @@ const c = {
|
|
|
20
24
|
bright: (text) => `${colors.bright}${text}${colors.reset}`,
|
|
21
25
|
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
22
26
|
};
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// DATABASE_PATH keeps its historical meaning (user override), but points
|
|
28
|
+
// at a `.json` file now. If the user's env var still names a `.db`
|
|
29
|
+
// extension, we swap to the corresponding `.json` sibling — the auth
|
|
30
|
+
// store moved off SQLite in v1.33.0.
|
|
31
|
+
const rawDbPath = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
|
|
32
|
+
const JSON_PATH = rawDbPath.endsWith('.db')
|
|
33
|
+
? rawDbPath.replace(/\.db$/, '.json')
|
|
34
|
+
: (rawDbPath.endsWith('.json') ? rawDbPath : `${rawDbPath}.json`);
|
|
35
|
+
const LEGACY_SQLITE_PATH = rawDbPath.endsWith('.db')
|
|
36
|
+
? rawDbPath
|
|
37
|
+
: rawDbPath.replace(/\.json$/, '.db');
|
|
38
|
+
// Ensure parent dir exists before migration / initial write.
|
|
39
|
+
{
|
|
40
|
+
const dir = path.dirname(JSON_PATH);
|
|
41
|
+
if (!fs.existsSync(dir)) {
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error(`Failed to create database directory ${dir}:`, err.message);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One-time migration from the previous better-sqlite3 auth.db to the
|
|
53
|
+
* new JSON format. Triggered only when:
|
|
54
|
+
* - The legacy .db exists
|
|
55
|
+
* - AND no .json has been created yet
|
|
56
|
+
* The legacy file is kept in place as `<name>.db.migrated-<timestamp>`
|
|
57
|
+
* so a user can roll back by moving it into place and reinstalling
|
|
58
|
+
* better-sqlite3 if they hit a showstopper. Runs synchronously because
|
|
59
|
+
* all downstream modules (auth.js, vapid-keys.js) import `db` at module
|
|
60
|
+
* load and can't wait for async startup.
|
|
61
|
+
*/
|
|
62
|
+
function migrateSqliteIfPresent() {
|
|
63
|
+
if (fs.existsSync(JSON_PATH))
|
|
64
|
+
return; // Already migrated or fresh install.
|
|
65
|
+
if (!fs.existsSync(LEGACY_SQLITE_PATH))
|
|
66
|
+
return; // Nothing to migrate.
|
|
67
|
+
console.log(`${c.info('[MIGRATION]')} Converting ${c.bright(LEGACY_SQLITE_PATH)} → ${c.bright(JSON_PATH)} (JSON auth store, v1.33.0)`);
|
|
68
|
+
let Database;
|
|
28
69
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
const mod = require('better-sqlite3');
|
|
71
|
+
Database = mod.default || mod;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Auto-install path fell through — the user has a legacy file but
|
|
75
|
+
// better-sqlite3 isn't present (they may have a trimmed dep tree).
|
|
76
|
+
// Surface a clear error; we'd rather fail startup than silently
|
|
77
|
+
// skip migration and strand the user's saved credentials.
|
|
78
|
+
console.error('[MIGRATION] Legacy auth.db present but better-sqlite3 not installed.');
|
|
79
|
+
console.error('[MIGRATION] Install it once to migrate: `npm install better-sqlite3` in your pixcode install dir, then restart.');
|
|
80
|
+
throw new Error('Auth DB migration requires better-sqlite3 (legacy file detected).');
|
|
81
|
+
}
|
|
82
|
+
const legacy = new Database(LEGACY_SQLITE_PATH, { readonly: true });
|
|
83
|
+
const store = new JsonStore(JSON_PATH);
|
|
84
|
+
// Pull every table, skipping silently when it doesn't exist (old
|
|
85
|
+
// installs that never ran some migrations). We rely on `IF NOT EXISTS`
|
|
86
|
+
// patterns from the old schema.js — missing tables throw a SQLite
|
|
87
|
+
// error which we catch per-table.
|
|
88
|
+
const safeAll = (sql) => {
|
|
89
|
+
try {
|
|
90
|
+
return legacy.prepare(sql).all();
|
|
32
91
|
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const users = safeAll('SELECT id, username, password_hash, created_at, last_login, is_active, git_name, git_email, has_completed_onboarding FROM users');
|
|
97
|
+
for (const u of users) {
|
|
98
|
+
store.raw.users.push({
|
|
99
|
+
id: u.id,
|
|
100
|
+
username: u.username,
|
|
101
|
+
password_hash: u.password_hash,
|
|
102
|
+
created_at: u.created_at || nowIso(),
|
|
103
|
+
last_login: u.last_login || null,
|
|
104
|
+
is_active: u.is_active !== 0,
|
|
105
|
+
git_name: u.git_name || null,
|
|
106
|
+
git_email: u.git_email || null,
|
|
107
|
+
has_completed_onboarding: u.has_completed_onboarding === 1,
|
|
108
|
+
});
|
|
109
|
+
store.raw._sequences.users = Math.max(store.raw._sequences.users, u.id);
|
|
33
110
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
111
|
+
const apiKeys = safeAll('SELECT id, user_id, key_name, api_key, created_at, last_used, is_active FROM api_keys');
|
|
112
|
+
for (const k of apiKeys) {
|
|
113
|
+
store.raw.api_keys.push({
|
|
114
|
+
id: k.id,
|
|
115
|
+
user_id: k.user_id,
|
|
116
|
+
key_name: k.key_name,
|
|
117
|
+
api_key: k.api_key,
|
|
118
|
+
created_at: k.created_at || nowIso(),
|
|
119
|
+
last_used: k.last_used || null,
|
|
120
|
+
is_active: k.is_active !== 0,
|
|
121
|
+
});
|
|
122
|
+
store.raw._sequences.api_keys = Math.max(store.raw._sequences.api_keys, k.id);
|
|
37
123
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
124
|
+
const credentials = safeAll('SELECT id, user_id, credential_name, credential_type, credential_value, description, created_at, is_active FROM user_credentials');
|
|
125
|
+
for (const cr of credentials) {
|
|
126
|
+
store.raw.user_credentials.push({
|
|
127
|
+
id: cr.id,
|
|
128
|
+
user_id: cr.user_id,
|
|
129
|
+
credential_name: cr.credential_name,
|
|
130
|
+
credential_type: cr.credential_type,
|
|
131
|
+
credential_value: cr.credential_value,
|
|
132
|
+
description: cr.description || null,
|
|
133
|
+
created_at: cr.created_at || nowIso(),
|
|
134
|
+
is_active: cr.is_active !== 0,
|
|
135
|
+
});
|
|
136
|
+
store.raw._sequences.user_credentials = Math.max(store.raw._sequences.user_credentials, cr.id);
|
|
137
|
+
}
|
|
138
|
+
const prefs = safeAll('SELECT user_id, preferences_json, updated_at FROM user_notification_preferences');
|
|
139
|
+
for (const p of prefs) {
|
|
140
|
+
store.raw.user_notification_preferences.push({
|
|
141
|
+
user_id: p.user_id,
|
|
142
|
+
preferences_json: p.preferences_json,
|
|
143
|
+
updated_at: p.updated_at || nowIso(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const vapid = safeAll('SELECT id, public_key, private_key, created_at FROM vapid_keys');
|
|
147
|
+
for (const v of vapid) {
|
|
148
|
+
store.raw.vapid_keys.push({
|
|
149
|
+
id: v.id,
|
|
150
|
+
public_key: v.public_key,
|
|
151
|
+
private_key: v.private_key,
|
|
152
|
+
created_at: v.created_at || nowIso(),
|
|
153
|
+
});
|
|
154
|
+
store.raw._sequences.vapid_keys = Math.max(store.raw._sequences.vapid_keys, v.id);
|
|
155
|
+
}
|
|
156
|
+
const pushSubs = safeAll('SELECT id, user_id, endpoint, keys_p256dh, keys_auth, created_at FROM push_subscriptions');
|
|
157
|
+
for (const s of pushSubs) {
|
|
158
|
+
store.raw.push_subscriptions.push({
|
|
159
|
+
id: s.id,
|
|
160
|
+
user_id: s.user_id,
|
|
161
|
+
endpoint: s.endpoint,
|
|
162
|
+
keys_p256dh: s.keys_p256dh,
|
|
163
|
+
keys_auth: s.keys_auth,
|
|
164
|
+
created_at: s.created_at || nowIso(),
|
|
165
|
+
});
|
|
166
|
+
store.raw._sequences.push_subscriptions = Math.max(store.raw._sequences.push_subscriptions, s.id);
|
|
167
|
+
}
|
|
168
|
+
const sessionNames = safeAll('SELECT session_id, provider, custom_name, created_at, updated_at FROM session_names');
|
|
169
|
+
for (const sn of sessionNames) {
|
|
170
|
+
store.raw.session_names.push({
|
|
171
|
+
session_id: sn.session_id,
|
|
172
|
+
provider: sn.provider,
|
|
173
|
+
custom_name: sn.custom_name,
|
|
174
|
+
created_at: sn.created_at || nowIso(),
|
|
175
|
+
updated_at: sn.updated_at || nowIso(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const appConfig = safeAll('SELECT key, value, created_at FROM app_config');
|
|
179
|
+
for (const a of appConfig) {
|
|
180
|
+
store.raw.app_config.push({
|
|
181
|
+
key: a.key,
|
|
182
|
+
value: a.value,
|
|
183
|
+
created_at: a.created_at || nowIso(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const telegramConfig = safeAll('SELECT id, bot_token, bot_username, updated_at FROM telegram_config');
|
|
187
|
+
for (const t of telegramConfig) {
|
|
188
|
+
store.raw.telegram_config.push({
|
|
189
|
+
id: 1,
|
|
190
|
+
bot_token: t.bot_token,
|
|
191
|
+
bot_username: t.bot_username || null,
|
|
192
|
+
updated_at: t.updated_at || nowIso(),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const telegramLinks = safeAll('SELECT user_id, chat_id, telegram_username, language, pairing_code, pairing_code_expires_at, verified_at, notifications_enabled, bridge_enabled, updated_at FROM telegram_links');
|
|
196
|
+
for (const tl of telegramLinks) {
|
|
197
|
+
store.raw.telegram_links.push({
|
|
198
|
+
user_id: tl.user_id,
|
|
199
|
+
chat_id: tl.chat_id || null,
|
|
200
|
+
telegram_username: tl.telegram_username || null,
|
|
201
|
+
language: tl.language || 'en',
|
|
202
|
+
pairing_code: tl.pairing_code || null,
|
|
203
|
+
pairing_code_expires_at: tl.pairing_code_expires_at || null,
|
|
204
|
+
verified_at: tl.verified_at || null,
|
|
205
|
+
notifications_enabled: tl.notifications_enabled !== 0,
|
|
206
|
+
bridge_enabled: tl.bridge_enabled !== 0,
|
|
207
|
+
updated_at: tl.updated_at || nowIso(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
store.save();
|
|
211
|
+
legacy.close();
|
|
212
|
+
// Rename the old .db out of the way so we never migrate it twice.
|
|
213
|
+
// Keep it (not delete) so the user can roll back if needed.
|
|
214
|
+
const backup = `${LEGACY_SQLITE_PATH}.migrated-${Date.now()}`;
|
|
42
215
|
try {
|
|
43
|
-
fs.
|
|
44
|
-
console.log(`[MIGRATION] Copied database from ${LEGACY_DB_PATH} to ${DB_PATH}`);
|
|
216
|
+
fs.renameSync(LEGACY_SQLITE_PATH, backup);
|
|
45
217
|
for (const suffix of ['-wal', '-shm']) {
|
|
46
|
-
if (fs.existsSync(
|
|
47
|
-
|
|
218
|
+
if (fs.existsSync(LEGACY_SQLITE_PATH + suffix)) {
|
|
219
|
+
try {
|
|
220
|
+
fs.renameSync(LEGACY_SQLITE_PATH + suffix, backup + suffix);
|
|
221
|
+
}
|
|
222
|
+
catch { /* noop */ }
|
|
48
223
|
}
|
|
49
224
|
}
|
|
50
225
|
}
|
|
51
226
|
catch (err) {
|
|
52
|
-
console.warn(`[MIGRATION]
|
|
227
|
+
console.warn(`[MIGRATION] Migration succeeded but could not rename old DB: ${err.message}`);
|
|
53
228
|
}
|
|
229
|
+
console.log(`${c.info('[MIGRATION]')} Migration complete. Old DB preserved as ${c.dim(backup)}.`);
|
|
54
230
|
}
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
db.exec(APP_CONFIG_TABLE_SQL);
|
|
61
|
-
// Show app installation path prominently
|
|
62
|
-
const appInstallPath = APP_ROOT;
|
|
231
|
+
// Module-load order matters — auth middleware imports `db` and reads the
|
|
232
|
+
// JWT secret before anything else gets to boot. So the store has to be
|
|
233
|
+
// ready synchronously at the first `import { db } from './db.js'`.
|
|
234
|
+
migrateSqliteIfPresent();
|
|
235
|
+
const store = new JsonStore(JSON_PATH);
|
|
63
236
|
console.log('');
|
|
64
237
|
console.log(c.dim('═'.repeat(60)));
|
|
65
|
-
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(
|
|
66
|
-
console.log(`${c.info('[INFO]')}
|
|
238
|
+
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(APP_ROOT)}`);
|
|
239
|
+
console.log(`${c.info('[INFO]')} Auth store: ${c.dim(path.relative(APP_ROOT, JSON_PATH))}`);
|
|
67
240
|
if (process.env.DATABASE_PATH) {
|
|
68
241
|
console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
|
|
69
242
|
}
|
|
70
243
|
console.log(c.dim('═'.repeat(60)));
|
|
71
244
|
console.log('');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
// Initialize database with schema
|
|
106
|
-
const initializeDatabase = async () => {
|
|
107
|
-
try {
|
|
108
|
-
db.exec(DATABASE_SCHEMA_SQL);
|
|
109
|
-
console.log('Database initialized successfully');
|
|
110
|
-
runMigrations();
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
console.error('Error initializing database:', error.message);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
245
|
+
// initializeDatabase used to run `CREATE TABLE IF NOT EXISTS` + migrations.
|
|
246
|
+
// The JSON store always has its shape pre-baked, so this is a no-op —
|
|
247
|
+
// keeping the export preserves the server boot sequence.
|
|
248
|
+
const initializeDatabase = async () => { };
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Back-compat `db` shim — a few modules outside database/db.js imported `db`
|
|
251
|
+
// and called `db.prepare(sql)` directly. Most of those uses were for trivial
|
|
252
|
+
// transaction markers (BEGIN/COMMIT/ROLLBACK — no-op under our JSON store)
|
|
253
|
+
// or very specific SELECTs we've wrapped in explicit helpers. This shim
|
|
254
|
+
// lets those call sites keep working without a bigger refactor.
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
const db = {
|
|
257
|
+
prepare(sql) {
|
|
258
|
+
const normalized = sql.trim().toUpperCase();
|
|
259
|
+
// Transaction markers — our store writes atomically per op, so
|
|
260
|
+
// there's no real transaction to start. No-op + success result.
|
|
261
|
+
if (normalized === 'BEGIN' || normalized === 'COMMIT' || normalized === 'ROLLBACK') {
|
|
262
|
+
return {
|
|
263
|
+
run: () => ({ changes: 0, lastInsertRowid: 0 }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Specific query routed through the new helpers — used by
|
|
267
|
+
// server/routes/projects.js to fetch a single github credential.
|
|
268
|
+
if (/FROM USER_CREDENTIALS\s+WHERE ID = \?\s+AND USER_ID = \?\s+AND CREDENTIAL_TYPE = \?\s+AND IS_ACTIVE = 1/.test(normalized)) {
|
|
269
|
+
return {
|
|
270
|
+
get: (id, userId, credentialType) => store.findWhere('user_credentials', (r) => r.id === id && r.user_id === userId && r.credential_type === credentialType && r.is_active)
|
|
271
|
+
|| undefined,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`db.prepare: unsupported SQL passed through the compat shim: ${sql.slice(0, 80)}`
|
|
275
|
+
+ '\nExtend server/database/db.js or use a dedicated helper (userDb, credentialsDb, …).');
|
|
276
|
+
},
|
|
116
277
|
};
|
|
117
|
-
//
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// User operations
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
118
281
|
const userDb = {
|
|
119
|
-
|
|
120
|
-
hasUsers: () => {
|
|
121
|
-
try {
|
|
122
|
-
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
|
123
|
-
return row.count > 0;
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
// Create a new user
|
|
282
|
+
hasUsers: () => store.count('users', (r) => r.is_active) > 0,
|
|
130
283
|
createUser: (username, passwordHash) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return row;
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
// Update last login time (non-fatal — logged but not thrown)
|
|
284
|
+
const row = store.insert('users', {
|
|
285
|
+
username,
|
|
286
|
+
password_hash: passwordHash,
|
|
287
|
+
created_at: nowIso(),
|
|
288
|
+
last_login: null,
|
|
289
|
+
is_active: true,
|
|
290
|
+
git_name: null,
|
|
291
|
+
git_email: null,
|
|
292
|
+
has_completed_onboarding: false,
|
|
293
|
+
});
|
|
294
|
+
return { id: row.id, username: row.username };
|
|
295
|
+
},
|
|
296
|
+
getUserByUsername: (username) => store.findWhere('users', (r) => r.username === username && r.is_active) || undefined,
|
|
151
297
|
updateLastLogin: (userId) => {
|
|
152
298
|
try {
|
|
153
|
-
|
|
299
|
+
store.updateWhere('users', (r) => r.id === userId, { last_login: nowIso() });
|
|
154
300
|
}
|
|
155
301
|
catch (err) {
|
|
156
302
|
console.warn('Failed to update last login:', err.message);
|
|
157
303
|
}
|
|
158
304
|
},
|
|
159
|
-
// Get user by ID
|
|
160
305
|
getUserById: (userId) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
306
|
+
const row = store.findWhere('users', (r) => r.id === userId && r.is_active);
|
|
307
|
+
if (!row)
|
|
308
|
+
return undefined;
|
|
309
|
+
return {
|
|
310
|
+
id: row.id,
|
|
311
|
+
username: row.username,
|
|
312
|
+
created_at: row.created_at,
|
|
313
|
+
last_login: row.last_login,
|
|
314
|
+
};
|
|
168
315
|
},
|
|
169
316
|
getFirstUser: () => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
317
|
+
const row = store.raw.users.find((r) => r.is_active);
|
|
318
|
+
if (!row)
|
|
319
|
+
return undefined;
|
|
320
|
+
return {
|
|
321
|
+
id: row.id,
|
|
322
|
+
username: row.username,
|
|
323
|
+
created_at: row.created_at,
|
|
324
|
+
last_login: row.last_login,
|
|
325
|
+
};
|
|
177
326
|
},
|
|
178
327
|
updateGitConfig: (userId, gitName, gitEmail) => {
|
|
179
|
-
|
|
180
|
-
const stmt = db.prepare('UPDATE users SET git_name = ?, git_email = ? WHERE id = ?');
|
|
181
|
-
stmt.run(gitName, gitEmail, userId);
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
throw err;
|
|
185
|
-
}
|
|
328
|
+
store.updateWhere('users', (r) => r.id === userId, { git_name: gitName, git_email: gitEmail });
|
|
186
329
|
},
|
|
187
330
|
getGitConfig: (userId) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
throw err;
|
|
194
|
-
}
|
|
331
|
+
const row = store.findWhere('users', (r) => r.id === userId);
|
|
332
|
+
if (!row)
|
|
333
|
+
return undefined;
|
|
334
|
+
return { git_name: row.git_name || null, git_email: row.git_email || null };
|
|
195
335
|
},
|
|
196
336
|
completeOnboarding: (userId) => {
|
|
197
|
-
|
|
198
|
-
const stmt = db.prepare('UPDATE users SET has_completed_onboarding = 1 WHERE id = ?');
|
|
199
|
-
stmt.run(userId);
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
337
|
+
store.updateWhere('users', (r) => r.id === userId, { has_completed_onboarding: true });
|
|
204
338
|
},
|
|
205
339
|
hasCompletedOnboarding: (userId) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
throw err;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
340
|
+
const row = store.findWhere('users', (r) => r.id === userId);
|
|
341
|
+
return Boolean(row?.has_completed_onboarding);
|
|
342
|
+
},
|
|
214
343
|
};
|
|
215
|
-
//
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// API key operations
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
216
347
|
const apiKeysDb = {
|
|
217
|
-
|
|
218
|
-
generateApiKey: () => {
|
|
219
|
-
return 'ck_' + crypto.randomBytes(32).toString('hex');
|
|
220
|
-
},
|
|
221
|
-
// Create a new API key
|
|
348
|
+
generateApiKey: () => 'ck_' + crypto.randomBytes(32).toString('hex'),
|
|
222
349
|
createApiKey: (userId, keyName) => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
350
|
+
const apiKey = apiKeysDb.generateApiKey();
|
|
351
|
+
const row = store.insert('api_keys', {
|
|
352
|
+
user_id: userId,
|
|
353
|
+
key_name: keyName,
|
|
354
|
+
api_key: apiKey,
|
|
355
|
+
created_at: nowIso(),
|
|
356
|
+
last_used: null,
|
|
357
|
+
is_active: true,
|
|
358
|
+
});
|
|
359
|
+
return { id: row.id, keyName, apiKey };
|
|
232
360
|
},
|
|
233
|
-
// Get all API keys for a user
|
|
234
361
|
getApiKeys: (userId) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
362
|
+
const rows = store.filterWhere('api_keys', (r) => r.user_id === userId);
|
|
363
|
+
// Match the old ORDER BY created_at DESC behaviour.
|
|
364
|
+
return rows
|
|
365
|
+
.slice()
|
|
366
|
+
.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
|
|
367
|
+
.map((r) => ({
|
|
368
|
+
id: r.id,
|
|
369
|
+
key_name: r.key_name,
|
|
370
|
+
api_key: r.api_key,
|
|
371
|
+
created_at: r.created_at,
|
|
372
|
+
last_used: r.last_used,
|
|
373
|
+
is_active: r.is_active ? 1 : 0,
|
|
374
|
+
}));
|
|
242
375
|
},
|
|
243
|
-
// Validate API key and get user
|
|
244
376
|
validateApiKey: (apiKey) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
// Delete an API key
|
|
263
|
-
deleteApiKey: (userId, apiKeyId) => {
|
|
264
|
-
try {
|
|
265
|
-
const stmt = db.prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?');
|
|
266
|
-
const result = stmt.run(apiKeyId, userId);
|
|
267
|
-
return result.changes > 0;
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
throw err;
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
// Toggle API key active status
|
|
274
|
-
toggleApiKey: (userId, apiKeyId, isActive) => {
|
|
275
|
-
try {
|
|
276
|
-
const stmt = db.prepare('UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?');
|
|
277
|
-
const result = stmt.run(isActive ? 1 : 0, apiKeyId, userId);
|
|
278
|
-
return result.changes > 0;
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
throw err;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
377
|
+
const key = store.findWhere('api_keys', (r) => r.api_key === apiKey && r.is_active);
|
|
378
|
+
if (!key)
|
|
379
|
+
return undefined;
|
|
380
|
+
const user = store.findWhere('users', (r) => r.id === key.user_id && r.is_active);
|
|
381
|
+
if (!user)
|
|
382
|
+
return undefined;
|
|
383
|
+
// Mirror the SQL-era side effect: stamp last_used. Only relevant
|
|
384
|
+
// for the "which key was used last" display in the UI.
|
|
385
|
+
store.updateWhere('api_keys', (r) => r.id === key.id, { last_used: nowIso() });
|
|
386
|
+
return {
|
|
387
|
+
id: user.id,
|
|
388
|
+
username: user.username,
|
|
389
|
+
api_key_id: key.id,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
deleteApiKey: (userId, apiKeyId) => store.deleteWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId) > 0,
|
|
393
|
+
toggleApiKey: (userId, apiKeyId, isActive) => store.updateWhere('api_keys', (r) => r.id === apiKeyId && r.user_id === userId, { is_active: Boolean(isActive) }) > 0,
|
|
284
394
|
};
|
|
285
|
-
//
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Credentials (GitHub tokens, etc.) operations
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
286
398
|
const credentialsDb = {
|
|
287
|
-
// Create a new credential
|
|
288
399
|
createCredential: (userId, credentialName, credentialType, credentialValue, description = null) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
400
|
+
const row = store.insert('user_credentials', {
|
|
401
|
+
user_id: userId,
|
|
402
|
+
credential_name: credentialName,
|
|
403
|
+
credential_type: credentialType,
|
|
404
|
+
credential_value: credentialValue,
|
|
405
|
+
description,
|
|
406
|
+
created_at: nowIso(),
|
|
407
|
+
is_active: true,
|
|
408
|
+
});
|
|
409
|
+
return { id: row.id, credentialName, credentialType };
|
|
297
410
|
},
|
|
298
|
-
// Get all credentials for a user, optionally filtered by type
|
|
299
411
|
getCredentials: (userId, credentialType = null) => {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
throw err;
|
|
313
|
-
}
|
|
412
|
+
const rows = store.filterWhere('user_credentials', (r) => r.user_id === userId && (credentialType == null || r.credential_type === credentialType));
|
|
413
|
+
return rows
|
|
414
|
+
.slice()
|
|
415
|
+
.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
|
|
416
|
+
.map((r) => ({
|
|
417
|
+
id: r.id,
|
|
418
|
+
credential_name: r.credential_name,
|
|
419
|
+
credential_type: r.credential_type,
|
|
420
|
+
description: r.description,
|
|
421
|
+
created_at: r.created_at,
|
|
422
|
+
is_active: r.is_active ? 1 : 0,
|
|
423
|
+
}));
|
|
314
424
|
},
|
|
315
|
-
// Get active credential value for a user by type (returns most recent active)
|
|
316
425
|
getActiveCredential: (userId, credentialType) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
426
|
+
const rows = store.filterWhere('user_credentials', (r) => r.user_id === userId && r.credential_type === credentialType && r.is_active);
|
|
427
|
+
if (rows.length === 0)
|
|
428
|
+
return null;
|
|
429
|
+
// "Most recent active" — mirror ORDER BY created_at DESC LIMIT 1
|
|
430
|
+
rows.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
|
|
431
|
+
return rows[0].credential_value;
|
|
324
432
|
},
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const result = stmt.run(credentialId, userId);
|
|
330
|
-
return result.changes > 0;
|
|
331
|
-
}
|
|
332
|
-
catch (err) {
|
|
333
|
-
throw err;
|
|
334
|
-
}
|
|
433
|
+
getCredentialById: (userId, credentialId, credentialType = null) => {
|
|
434
|
+
const row = store.findWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId && r.is_active
|
|
435
|
+
&& (credentialType == null || r.credential_type === credentialType));
|
|
436
|
+
return row || undefined;
|
|
335
437
|
},
|
|
336
|
-
|
|
337
|
-
toggleCredential: (userId, credentialId, isActive) => {
|
|
338
|
-
try {
|
|
339
|
-
const stmt = db.prepare('UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?');
|
|
340
|
-
const result = stmt.run(isActive ? 1 : 0, credentialId, userId);
|
|
341
|
-
return result.changes > 0;
|
|
342
|
-
}
|
|
343
|
-
catch (err) {
|
|
344
|
-
throw err;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
438
|
+
deleteCredential: (userId, credentialId) => store.deleteWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId) > 0,
|
|
439
|
+
toggleCredential: (userId, credentialId, isActive) => store.updateWhere('user_credentials', (r) => r.id === credentialId && r.user_id === userId, { is_active: Boolean(isActive) }) > 0,
|
|
347
440
|
};
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Notification preferences
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
348
444
|
const DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
349
|
-
channels: {
|
|
350
|
-
|
|
351
|
-
webPush: false
|
|
352
|
-
},
|
|
353
|
-
events: {
|
|
354
|
-
actionRequired: true,
|
|
355
|
-
stop: true,
|
|
356
|
-
error: true
|
|
357
|
-
}
|
|
445
|
+
channels: { inApp: false, webPush: false },
|
|
446
|
+
events: { actionRequired: true, stop: true, error: true },
|
|
358
447
|
};
|
|
359
448
|
const normalizeNotificationPreferences = (value) => {
|
|
360
449
|
const source = value && typeof value === 'object' ? value : {};
|
|
361
450
|
return {
|
|
362
451
|
channels: {
|
|
363
452
|
inApp: source.channels?.inApp === true,
|
|
364
|
-
webPush: source.channels?.webPush === true
|
|
453
|
+
webPush: source.channels?.webPush === true,
|
|
365
454
|
},
|
|
366
455
|
events: {
|
|
367
456
|
actionRequired: source.events?.actionRequired !== false,
|
|
368
457
|
stop: source.events?.stop !== false,
|
|
369
|
-
error: source.events?.error !== false
|
|
370
|
-
}
|
|
458
|
+
error: source.events?.error !== false,
|
|
459
|
+
},
|
|
371
460
|
};
|
|
372
461
|
};
|
|
373
462
|
const notificationPreferencesDb = {
|
|
374
463
|
getPreferences: (userId) => {
|
|
464
|
+
const row = store.findWhere('user_notification_preferences', (r) => r.user_id === userId);
|
|
465
|
+
if (!row) {
|
|
466
|
+
const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
|
|
467
|
+
store.insert('user_notification_preferences', {
|
|
468
|
+
user_id: userId,
|
|
469
|
+
preferences_json: JSON.stringify(defaults),
|
|
470
|
+
updated_at: nowIso(),
|
|
471
|
+
}, { autoId: false });
|
|
472
|
+
return defaults;
|
|
473
|
+
}
|
|
375
474
|
try {
|
|
376
|
-
|
|
377
|
-
if (!row) {
|
|
378
|
-
const defaults = normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
|
|
379
|
-
db.prepare('INSERT INTO user_notification_preferences (user_id, preferences_json, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)').run(userId, JSON.stringify(defaults));
|
|
380
|
-
return defaults;
|
|
381
|
-
}
|
|
382
|
-
let parsed;
|
|
383
|
-
try {
|
|
384
|
-
parsed = JSON.parse(row.preferences_json);
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
parsed = DEFAULT_NOTIFICATION_PREFERENCES;
|
|
388
|
-
}
|
|
389
|
-
return normalizeNotificationPreferences(parsed);
|
|
475
|
+
return normalizeNotificationPreferences(JSON.parse(row.preferences_json));
|
|
390
476
|
}
|
|
391
|
-
catch
|
|
392
|
-
|
|
477
|
+
catch {
|
|
478
|
+
return normalizeNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES);
|
|
393
479
|
}
|
|
394
480
|
},
|
|
395
481
|
updatePreferences: (userId, preferences) => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
ON CONFLICT(user_id) DO UPDATE SET
|
|
401
|
-
preferences_json = excluded.preferences_json,
|
|
402
|
-
updated_at = CURRENT_TIMESTAMP`).run(userId, JSON.stringify(normalized));
|
|
403
|
-
return normalized;
|
|
404
|
-
}
|
|
405
|
-
catch (err) {
|
|
406
|
-
throw err;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
482
|
+
const normalized = normalizeNotificationPreferences(preferences);
|
|
483
|
+
store.upsertWhere('user_notification_preferences', (r) => r.user_id === userId, { user_id: userId, preferences_json: JSON.stringify(normalized), updated_at: nowIso() });
|
|
484
|
+
return normalized;
|
|
485
|
+
},
|
|
409
486
|
};
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// Push subscriptions
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
410
490
|
const pushSubscriptionsDb = {
|
|
411
491
|
saveSubscription: (userId, endpoint, keysP256dh, keysAuth) => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
getSubscriptions: (userId) => {
|
|
425
|
-
try {
|
|
426
|
-
return db.prepare('SELECT endpoint, keys_p256dh, keys_auth FROM push_subscriptions WHERE user_id = ?').all(userId);
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
throw err;
|
|
492
|
+
// The old SQL path upserted on `endpoint`, updating user_id as
|
|
493
|
+
// well. Preserve that exact behaviour.
|
|
494
|
+
const existing = store.findWhere('push_subscriptions', (r) => r.endpoint === endpoint);
|
|
495
|
+
if (existing) {
|
|
496
|
+
store.updateWhere('push_subscriptions', (r) => r.endpoint === endpoint, {
|
|
497
|
+
user_id: userId,
|
|
498
|
+
keys_p256dh: keysP256dh,
|
|
499
|
+
keys_auth: keysAuth,
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
430
502
|
}
|
|
431
|
-
|
|
503
|
+
store.insert('push_subscriptions', {
|
|
504
|
+
user_id: userId,
|
|
505
|
+
endpoint,
|
|
506
|
+
keys_p256dh: keysP256dh,
|
|
507
|
+
keys_auth: keysAuth,
|
|
508
|
+
created_at: nowIso(),
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
getSubscriptions: (userId) => store.filterWhere('push_subscriptions', (r) => r.user_id === userId).map((r) => ({
|
|
512
|
+
endpoint: r.endpoint,
|
|
513
|
+
keys_p256dh: r.keys_p256dh,
|
|
514
|
+
keys_auth: r.keys_auth,
|
|
515
|
+
})),
|
|
432
516
|
removeSubscription: (endpoint) => {
|
|
433
|
-
|
|
434
|
-
db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?').run(endpoint);
|
|
435
|
-
}
|
|
436
|
-
catch (err) {
|
|
437
|
-
throw err;
|
|
438
|
-
}
|
|
517
|
+
store.deleteWhere('push_subscriptions', (r) => r.endpoint === endpoint);
|
|
439
518
|
},
|
|
440
519
|
removeAllForUser: (userId) => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
520
|
+
store.deleteWhere('push_subscriptions', (r) => r.user_id === userId);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// VAPID key storage (for web push)
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
const vapidKeysDb = {
|
|
527
|
+
getLatest: () => {
|
|
528
|
+
if (store.raw.vapid_keys.length === 0)
|
|
529
|
+
return null;
|
|
530
|
+
// Mirror ORDER BY id DESC LIMIT 1 — take the newest row.
|
|
531
|
+
const rows = store.raw.vapid_keys.slice().sort((a, b) => b.id - a.id);
|
|
532
|
+
return { public_key: rows[0].public_key, private_key: rows[0].private_key };
|
|
533
|
+
},
|
|
534
|
+
insert: (publicKey, privateKey) => {
|
|
535
|
+
store.insert('vapid_keys', {
|
|
536
|
+
public_key: publicKey,
|
|
537
|
+
private_key: privateKey,
|
|
538
|
+
created_at: nowIso(),
|
|
539
|
+
});
|
|
540
|
+
},
|
|
448
541
|
};
|
|
449
|
-
//
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Session custom names
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
450
545
|
const sessionNamesDb = {
|
|
451
|
-
// Set (insert or update) a custom session name
|
|
452
546
|
setName: (sessionId, provider, customName) => {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
VALUES (?, ?, ?)
|
|
456
|
-
ON CONFLICT(session_id, provider)
|
|
457
|
-
DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP
|
|
458
|
-
`).run(sessionId, provider, customName);
|
|
459
|
-
},
|
|
460
|
-
// Get a single custom session name
|
|
547
|
+
store.upsertWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider, { session_id: sessionId, provider, custom_name: customName, updated_at: nowIso(), created_at: nowIso() });
|
|
548
|
+
},
|
|
461
549
|
getName: (sessionId, provider) => {
|
|
462
|
-
const row =
|
|
550
|
+
const row = store.findWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider);
|
|
463
551
|
return row?.custom_name || null;
|
|
464
552
|
},
|
|
465
|
-
// Batch lookup — returns Map<sessionId, customName>
|
|
466
553
|
getNames: (sessionIds, provider) => {
|
|
467
554
|
if (!sessionIds.length)
|
|
468
555
|
return new Map();
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
return new Map(rows.map(r => [r.session_id, r.custom_name]));
|
|
473
|
-
},
|
|
474
|
-
// Delete a custom session name
|
|
475
|
-
deleteName: (sessionId, provider) => {
|
|
476
|
-
return db.prepare('DELETE FROM session_names WHERE session_id = ? AND provider = ?').run(sessionId, provider).changes > 0;
|
|
556
|
+
const lookup = new Set(sessionIds);
|
|
557
|
+
const matches = store.filterWhere('session_names', (r) => r.provider === provider && lookup.has(r.session_id));
|
|
558
|
+
return new Map(matches.map((r) => [r.session_id, r.custom_name]));
|
|
477
559
|
},
|
|
560
|
+
deleteName: (sessionId, provider) => store.deleteWhere('session_names', (r) => r.session_id === sessionId && r.provider === provider) > 0,
|
|
478
561
|
};
|
|
479
|
-
// Apply custom session names from the database (overrides CLI-generated summaries)
|
|
480
562
|
function applyCustomSessionNames(sessions, provider) {
|
|
481
563
|
if (!sessions?.length)
|
|
482
564
|
return;
|
|
483
565
|
try {
|
|
484
|
-
const ids = sessions.map(s => s.id);
|
|
566
|
+
const ids = sessions.map((s) => s.id);
|
|
485
567
|
const customNames = sessionNamesDb.getNames(ids, provider);
|
|
486
568
|
for (const session of sessions) {
|
|
487
569
|
const custom = customNames.get(session.id);
|
|
@@ -493,19 +575,16 @@ function applyCustomSessionNames(sessions, provider) {
|
|
|
493
575
|
console.warn(`[DB] Failed to apply custom session names for ${provider}:`, error.message);
|
|
494
576
|
}
|
|
495
577
|
}
|
|
496
|
-
//
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// App config (key/value)
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
497
581
|
const appConfigDb = {
|
|
498
582
|
get: (key) => {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return row?.value || null;
|
|
502
|
-
}
|
|
503
|
-
catch (err) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
583
|
+
const row = store.findWhere('app_config', (r) => r.key === key);
|
|
584
|
+
return row?.value || null;
|
|
506
585
|
},
|
|
507
586
|
set: (key, value) => {
|
|
508
|
-
|
|
587
|
+
store.upsertWhere('app_config', (r) => r.key === key, { key, value, created_at: nowIso() });
|
|
509
588
|
},
|
|
510
589
|
getOrCreateJwtSecret: () => {
|
|
511
590
|
let secret = appConfigDb.get('jwt_secret');
|
|
@@ -514,114 +593,117 @@ const appConfigDb = {
|
|
|
514
593
|
appConfigDb.set('jwt_secret', secret);
|
|
515
594
|
}
|
|
516
595
|
return secret;
|
|
517
|
-
}
|
|
596
|
+
},
|
|
518
597
|
};
|
|
519
|
-
//
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// Telegram — singleton config + per-user links
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
520
601
|
const telegramConfigDb = {
|
|
521
602
|
get: () => {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
catch (err) {
|
|
526
|
-
console.warn('telegramConfigDb.get failed:', err.message);
|
|
603
|
+
const row = store.raw.telegram_config[0];
|
|
604
|
+
if (!row)
|
|
527
605
|
return null;
|
|
528
|
-
}
|
|
606
|
+
return { bot_token: row.bot_token, bot_username: row.bot_username, updated_at: row.updated_at };
|
|
529
607
|
},
|
|
530
608
|
set: (botToken, botUsername = null) => {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
609
|
+
store.raw.telegram_config = [{
|
|
610
|
+
id: 1,
|
|
611
|
+
bot_token: botToken,
|
|
612
|
+
bot_username: botUsername,
|
|
613
|
+
updated_at: nowIso(),
|
|
614
|
+
}];
|
|
615
|
+
store.save();
|
|
537
616
|
},
|
|
538
617
|
clear: () => {
|
|
539
|
-
|
|
618
|
+
store.raw.telegram_config = [];
|
|
619
|
+
store.save();
|
|
540
620
|
},
|
|
541
621
|
};
|
|
542
622
|
const telegramLinksDb = {
|
|
543
|
-
// Write a fresh pairing code for a user and wipe any prior verification —
|
|
544
|
-
// regenerating a code implies "start over", not "keep the old binding".
|
|
545
623
|
setPairingCode: (userId, code, expiresAt, language) => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
624
|
+
store.upsertWhere('telegram_links', (r) => r.user_id === userId, {
|
|
625
|
+
user_id: userId,
|
|
626
|
+
pairing_code: code,
|
|
627
|
+
pairing_code_expires_at: expiresAt,
|
|
628
|
+
language,
|
|
629
|
+
chat_id: null,
|
|
630
|
+
telegram_username: null,
|
|
631
|
+
verified_at: null,
|
|
632
|
+
notifications_enabled: true,
|
|
633
|
+
bridge_enabled: true,
|
|
634
|
+
updated_at: nowIso(),
|
|
635
|
+
});
|
|
556
636
|
},
|
|
557
637
|
findByPairingCode: (code) => {
|
|
558
|
-
|
|
559
|
-
|
|
638
|
+
const row = store.findWhere('telegram_links', (r) => r.pairing_code === code);
|
|
639
|
+
if (!row)
|
|
640
|
+
return null;
|
|
641
|
+
return {
|
|
642
|
+
user_id: row.user_id,
|
|
643
|
+
pairing_code: row.pairing_code,
|
|
644
|
+
pairing_code_expires_at: row.pairing_code_expires_at,
|
|
645
|
+
language: row.language,
|
|
646
|
+
};
|
|
560
647
|
},
|
|
561
648
|
verify: (userId, chatId, telegramUsername) => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
649
|
+
store.updateWhere('telegram_links', (r) => r.user_id === userId, {
|
|
650
|
+
chat_id: chatId,
|
|
651
|
+
telegram_username: telegramUsername,
|
|
652
|
+
verified_at: nowIso(),
|
|
653
|
+
pairing_code: null,
|
|
654
|
+
pairing_code_expires_at: null,
|
|
655
|
+
updated_at: nowIso(),
|
|
656
|
+
});
|
|
566
657
|
},
|
|
567
658
|
getByUserId: (userId) => {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
FROM telegram_links WHERE user_id = ?`).get(userId) || null;
|
|
659
|
+
const row = store.findWhere('telegram_links', (r) => r.user_id === userId);
|
|
660
|
+
return row ? { ...row } : null;
|
|
571
661
|
},
|
|
572
662
|
getByChatId: (chatId) => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
663
|
+
const row = store.findWhere('telegram_links', (r) => r.chat_id === chatId);
|
|
664
|
+
if (!row)
|
|
665
|
+
return null;
|
|
666
|
+
return {
|
|
667
|
+
user_id: row.user_id,
|
|
668
|
+
chat_id: row.chat_id,
|
|
669
|
+
telegram_username: row.telegram_username,
|
|
670
|
+
language: row.language,
|
|
671
|
+
notifications_enabled: row.notifications_enabled,
|
|
672
|
+
bridge_enabled: row.bridge_enabled,
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
listVerified: () => store.filterWhere('telegram_links', (r) => r.chat_id && r.verified_at).map((r) => ({
|
|
676
|
+
user_id: r.user_id,
|
|
677
|
+
chat_id: r.chat_id,
|
|
678
|
+
telegram_username: r.telegram_username,
|
|
679
|
+
language: r.language,
|
|
680
|
+
notifications_enabled: r.notifications_enabled,
|
|
681
|
+
bridge_enabled: r.bridge_enabled,
|
|
682
|
+
})),
|
|
580
683
|
updatePreferences: (userId, { language, notificationsEnabled, bridgeEnabled }) => {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
params.push(notificationsEnabled ? 1 : 0);
|
|
592
|
-
}
|
|
593
|
-
if (bridgeEnabled !== undefined) {
|
|
594
|
-
sets.push('bridge_enabled = ?');
|
|
595
|
-
params.push(bridgeEnabled ? 1 : 0);
|
|
596
|
-
}
|
|
597
|
-
if (!sets.length)
|
|
598
|
-
return;
|
|
599
|
-
sets.push('updated_at = CURRENT_TIMESTAMP');
|
|
600
|
-
params.push(userId);
|
|
601
|
-
db.prepare(`UPDATE telegram_links SET ${sets.join(', ')} WHERE user_id = ?`).run(...params);
|
|
684
|
+
const patch = { updated_at: nowIso() };
|
|
685
|
+
if (language !== undefined)
|
|
686
|
+
patch.language = language;
|
|
687
|
+
if (notificationsEnabled !== undefined)
|
|
688
|
+
patch.notifications_enabled = Boolean(notificationsEnabled);
|
|
689
|
+
if (bridgeEnabled !== undefined)
|
|
690
|
+
patch.bridge_enabled = Boolean(bridgeEnabled);
|
|
691
|
+
if (Object.keys(patch).length === 1)
|
|
692
|
+
return; // only updated_at → no real change
|
|
693
|
+
store.updateWhere('telegram_links', (r) => r.user_id === userId, patch);
|
|
602
694
|
},
|
|
603
695
|
unlink: (userId) => {
|
|
604
|
-
|
|
696
|
+
store.deleteWhere('telegram_links', (r) => r.user_id === userId);
|
|
605
697
|
},
|
|
606
698
|
};
|
|
607
|
-
//
|
|
699
|
+
// Back-compat surface — older callers used `githubTokensDb.*`; internally
|
|
700
|
+
// they always delegated to credentialsDb with `credential_type='github_token'`.
|
|
608
701
|
const githubTokensDb = {
|
|
609
|
-
createGithubToken: (userId, tokenName, githubToken, description = null) =>
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
getActiveGithubToken: (userId) => {
|
|
616
|
-
return credentialsDb.getActiveCredential(userId, 'github_token');
|
|
617
|
-
},
|
|
618
|
-
deleteGithubToken: (userId, tokenId) => {
|
|
619
|
-
return credentialsDb.deleteCredential(userId, tokenId);
|
|
620
|
-
},
|
|
621
|
-
toggleGithubToken: (userId, tokenId, isActive) => {
|
|
622
|
-
return credentialsDb.toggleCredential(userId, tokenId, isActive);
|
|
623
|
-
}
|
|
702
|
+
createGithubToken: (userId, tokenName, githubToken, description = null) => credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description),
|
|
703
|
+
getGithubTokens: (userId) => credentialsDb.getCredentials(userId, 'github_token'),
|
|
704
|
+
getActiveGithubToken: (userId) => credentialsDb.getActiveCredential(userId, 'github_token'),
|
|
705
|
+
deleteGithubToken: (userId, tokenId) => credentialsDb.deleteCredential(userId, tokenId),
|
|
706
|
+
toggleGithubToken: (userId, tokenId, isActive) => credentialsDb.toggleCredential(userId, tokenId, isActive),
|
|
624
707
|
};
|
|
625
|
-
export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramConfigDb, telegramLinksDb, githubTokensDb
|
|
626
|
-
};
|
|
708
|
+
export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb, vapidKeysDb, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramConfigDb, telegramLinksDb, githubTokensDb, };
|
|
627
709
|
//# sourceMappingURL=db.js.map
|