@riligar/elysia-sqlite 1.6.0 → 1.8.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.
Files changed (54) hide show
  1. package/README.md +52 -52
  2. package/package.json +1 -1
  3. package/src/index.js +198 -160
  4. package/src/ui/dist/assets/{_baseUniq-tp0gy_rC.js → _baseUniq-q-EAkCrm.js} +1 -1
  5. package/src/ui/dist/assets/{arc-CIKD5jh6.js → arc-B4S-G_Zj.js} +1 -1
  6. package/src/ui/dist/assets/{architectureDiagram-VXUJARFQ-C-Zu1f5H.js → architectureDiagram-VXUJARFQ-DHQZgPXq.js} +1 -1
  7. package/src/ui/dist/assets/{blockDiagram-VD42YOAC-CwT44Fo1.js → blockDiagram-VD42YOAC-BhM55G8L.js} +1 -1
  8. package/src/ui/dist/assets/{c4Diagram-YG6GDRKO-g1CkarVK.js → c4Diagram-YG6GDRKO-Bg_svCII.js} +1 -1
  9. package/src/ui/dist/assets/channel-DoYfldvt.js +1 -0
  10. package/src/ui/dist/assets/{chunk-4BX2VUAB-BRSq_1my.js → chunk-4BX2VUAB-CXTInnOp.js} +1 -1
  11. package/src/ui/dist/assets/{chunk-55IACEB6-sJOVeCqP.js → chunk-55IACEB6-V4fY96y5.js} +1 -1
  12. package/src/ui/dist/assets/{chunk-B4BG7PRW-CAJu4y9m.js → chunk-B4BG7PRW-DJQoREWk.js} +1 -1
  13. package/src/ui/dist/assets/{chunk-DI55MBZ5-CqHNSLXG.js → chunk-DI55MBZ5-DygVHfVU.js} +1 -1
  14. package/src/ui/dist/assets/{chunk-FMBD7UC4-Cm8x3fGA.js → chunk-FMBD7UC4-DBhtmgS5.js} +1 -1
  15. package/src/ui/dist/assets/{chunk-QN33PNHL-CXiOGhHB.js → chunk-QN33PNHL-FzewbkmC.js} +1 -1
  16. package/src/ui/dist/assets/{chunk-QZHKN3VN-Dac55Lwb.js → chunk-QZHKN3VN-rUyvk0xL.js} +1 -1
  17. package/src/ui/dist/assets/{chunk-TZMSLE5B-C2E0ascL.js → chunk-TZMSLE5B-Dc5v8HSL.js} +1 -1
  18. package/src/ui/dist/assets/classDiagram-2ON5EDUG-D9veMMBn.js +1 -0
  19. package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-D9veMMBn.js +1 -0
  20. package/src/ui/dist/assets/clone-DtSiCwm9.js +1 -0
  21. package/src/ui/dist/assets/{cose-bilkent-S5V4N54A-CJzZ2NKZ.js → cose-bilkent-S5V4N54A-C025U-sf.js} +1 -1
  22. package/src/ui/dist/assets/{dagre-6UL2VRFP-DqBYKK4u.js → dagre-6UL2VRFP-1iB9U0lP.js} +1 -1
  23. package/src/ui/dist/assets/{diagram-PSM6KHXK-Ud1GhyHV.js → diagram-PSM6KHXK-CbkXP6KP.js} +1 -1
  24. package/src/ui/dist/assets/{diagram-QEK2KX5R-C_PcM76a.js → diagram-QEK2KX5R-B_tMo5mS.js} +1 -1
  25. package/src/ui/dist/assets/{diagram-S2PKOQOG-Dxc59S6X.js → diagram-S2PKOQOG-ChAZKJN6.js} +1 -1
  26. package/src/ui/dist/assets/{erDiagram-Q2GNP2WA-BM17VS0S.js → erDiagram-Q2GNP2WA-yLAcFWw7.js} +1 -1
  27. package/src/ui/dist/assets/{flowDiagram-NV44I4VS-DvifDv4F.js → flowDiagram-NV44I4VS-BlKkc3Nh.js} +1 -1
  28. package/src/ui/dist/assets/{ganttDiagram-JELNMOA3-BzdIaEbH.js → ganttDiagram-JELNMOA3-CwXkt89E.js} +1 -1
  29. package/src/ui/dist/assets/{gitGraphDiagram-NY62KEGX-Df2I2D_a.js → gitGraphDiagram-NY62KEGX-BChAOT8L.js} +1 -1
  30. package/src/ui/dist/assets/{graph-B581r9-3.js → graph-4il9X5Vw.js} +1 -1
  31. package/src/ui/dist/assets/{index-BBKjGWlO.js → index-Bkgmu5O1.js} +11 -11
  32. package/src/ui/dist/assets/{infoDiagram-WHAUD3N6-D7CJLVcM.js → infoDiagram-WHAUD3N6-DO2QNzqA.js} +1 -1
  33. package/src/ui/dist/assets/{journeyDiagram-XKPGCS4Q-CX7LwWbu.js → journeyDiagram-XKPGCS4Q-Cxft10rH.js} +1 -1
  34. package/src/ui/dist/assets/{kanban-definition-3W4ZIXB7-BIRhmBI5.js → kanban-definition-3W4ZIXB7-O6409vdq.js} +1 -1
  35. package/src/ui/dist/assets/{layout-DC_20cjc.js → layout-CrNa8f-Y.js} +1 -1
  36. package/src/ui/dist/assets/{linear-w2kfZjuH.js → linear-CtAuky1G.js} +1 -1
  37. package/src/ui/dist/assets/{min-DfeMYee7.js → min-llF8csIg.js} +1 -1
  38. package/src/ui/dist/assets/{mindmap-definition-VGOIOE7T-ooGmlDfN.js → mindmap-definition-VGOIOE7T-Cim6bQlk.js} +1 -1
  39. package/src/ui/dist/assets/{pieDiagram-ADFJNKIX-DIgElDn1.js → pieDiagram-ADFJNKIX-BtLD5oSN.js} +1 -1
  40. package/src/ui/dist/assets/{quadrantDiagram-AYHSOK5B-Bpixm6gO.js → quadrantDiagram-AYHSOK5B-BiqtJkih.js} +1 -1
  41. package/src/ui/dist/assets/{requirementDiagram-UZGBJVZJ-CTNj1gzp.js → requirementDiagram-UZGBJVZJ-Bqvt0J99.js} +1 -1
  42. package/src/ui/dist/assets/{sankeyDiagram-TZEHDZUN-Bb0s_lf0.js → sankeyDiagram-TZEHDZUN-CRKGtQsm.js} +1 -1
  43. package/src/ui/dist/assets/{sequenceDiagram-WL72ISMW-CMvbebl5.js → sequenceDiagram-WL72ISMW-BJfQuJfN.js} +1 -1
  44. package/src/ui/dist/assets/{stateDiagram-FKZM4ZOC-VsagYS5z.js → stateDiagram-FKZM4ZOC-Du-n3UOc.js} +1 -1
  45. package/src/ui/dist/assets/stateDiagram-v2-4FDKWEC3-BwDD2wYY.js +1 -0
  46. package/src/ui/dist/assets/{timeline-definition-IT6M3QCI-CD_E615Y.js → timeline-definition-IT6M3QCI-CZSIYiPY.js} +1 -1
  47. package/src/ui/dist/assets/{treemap-KMMF4GRG-Bmk930Gz.js → treemap-KMMF4GRG-BOqno8hS.js} +1 -1
  48. package/src/ui/dist/assets/{xychartDiagram-PRI3JC2R-CKe-Ew8V.js → xychartDiagram-PRI3JC2R-BhdrnCd3.js} +1 -1
  49. package/src/ui/dist/index.html +1 -1
  50. package/src/ui/dist/assets/channel-1vrFD0pz.js +0 -1
  51. package/src/ui/dist/assets/classDiagram-2ON5EDUG-CZ5BrJQG.js +0 -1
  52. package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-CZ5BrJQG.js +0 -1
  53. package/src/ui/dist/assets/clone-B1UmMcxV.js +0 -1
  54. package/src/ui/dist/assets/stateDiagram-v2-4FDKWEC3-lzsfBFxI.js +0 -1
package/README.md CHANGED
@@ -10,13 +10,13 @@ A powerful Elysia plugin for SQLite database management with a beautiful built-i
10
10
 
11
11
  ## ✨ Features
12
12
 
13
- - 📊 **Smart DataGrid** — Interactive table with pagination, sorting, and inline editing
14
- - 🔗 **Foreign Key Preview** — Intelligently resolves and displays foreign key relationships
15
- - 🔐 **Secure Admin** — Built-in authentication with session management and 2FA/TOTP
16
- - ⚡ **Zero Config** — Auto-detects database schema and provides instant CRUD interface
17
- - 📤 **Easy Export** — Export your data to CSV or JSON with a single click
18
- - 🧭 **Guided Onboarding** — Simple setup wizard for initial configuration
19
- - 🤖 **AI SQL** — Generate SQL queries using natural language (requires OpenRouter key)
13
+ - 📊 **Smart DataGrid** — Interactive table with pagination, sorting, and inline editing
14
+ - 🔗 **Foreign Key Preview** — Intelligently resolves and displays foreign key relationships
15
+ - 🔐 **Secure Admin** — Built-in authentication with session management and 2FA/TOTP
16
+ - ⚡ **Zero Config** — Auto-detects database schema and provides instant CRUD interface
17
+ - 📤 **Easy Export** — Export your data to CSV or JSON with a single click
18
+ - 🧭 **Guided Onboarding** — Simple setup wizard for initial configuration
19
+ - 🤖 **AI SQL** — Generate SQL queries using natural language (requires OpenRouter key)
20
20
 
21
21
  ## 📦 Installation
22
22
 
@@ -33,20 +33,20 @@ bun add elysia
33
33
  ## 🚀 Quick Start
34
34
 
35
35
  ```javascript
36
- import { Elysia } from 'elysia'
37
- import { sqliteAdmin } from '@riligar/elysia-sqlite'
36
+ import { Elysia } from "elysia";
37
+ import { sqliteAdmin } from "@riligar/elysia-sqlite";
38
38
 
39
39
  const app = new Elysia()
40
- .use(
41
- sqliteAdmin({
42
- dbPath: 'demo.db',
43
- prefix: '/sqlite', // Optional: defaults to /sqlite
44
- })
45
- )
46
- .listen(3000)
47
-
48
- console.log('🦊 Server running at http://localhost:3000')
49
- console.log('📊 Admin Dashboard at http://localhost:3000/sqlite')
40
+ .use(
41
+ sqliteAdmin({
42
+ dbPath: "demo.db",
43
+ prefix: "/sqlite", // Optional: defaults to /sqlite
44
+ })
45
+ )
46
+ .listen(3000);
47
+
48
+ console.log("🦊 Server running at http://localhost:3000");
49
+ console.log("📊 Admin Dashboard at http://localhost:3000/sqlite");
50
50
  ```
51
51
 
52
52
  On first run, navigate to `/sqlite` (or your configured prefix) to start the onboarding wizard and configure your admin credentials.
@@ -57,21 +57,21 @@ On first run, navigate to `/sqlite` (or your configured prefix) to start the onb
57
57
 
58
58
  These options are passed to the `sqliteAdmin` plugin at initialization.
59
59
 
60
- | Option | Type | Default | Description |
61
- | ------------ | ------ | ----------------------------- | --------------------------------------------------------------- |
62
- | `dbPath` | string | **Required** | Path to the SQLite database file |
63
- | `prefix` | string | `"/sqlite"` | URL prefix for the admin dashboard and API |
64
- | `configPath` | string | Same directory as `dbPath` | Path to save the runtime authentication config (JSON). Defaults to `sqlite-admin-config.json` in the same directory as your database file |
60
+ | Option | Type | Default | Description |
61
+ | ------------ | ------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
62
+ | `dbPath` | string | **Required** | Path to the SQLite database file |
63
+ | `prefix` | string | `"/sqlite"` | URL prefix for the admin dashboard and API |
64
+ | `configPath` | string | Same directory as `dbPath` | Path to save the runtime authentication config (JSON). Defaults to `sqlite-config.json` in the same directory as your database file |
65
65
 
66
66
  ### Runtime Configuration (via UI)
67
67
 
68
68
  The following settings are managed via the **Settings** tab in the dashboard and stored in the JSON file defined by `configPath`.
69
69
 
70
- | Option | Description |
71
- | ----------------- | -------------------------------------------------------- |
72
- | `username` | Admin username for accessing the dashboard |
73
- | `password` | Admin password (stored in plain text in config file - protect this file!) |
74
- | `totpSecret` | Secret key for Two-Factor Authentication (managed automatically) |
70
+ | Option | Description |
71
+ | ------------ | ------------------------------------------------------------------------- |
72
+ | `username` | Admin username for accessing the dashboard |
73
+ | `password` | Admin password (stored in plain text in config file - protect this file!) |
74
+ | `totpSecret` | Secret key for Two-Factor Authentication (managed automatically) |
75
75
 
76
76
  > **Note:** The configuration file contains sensitive credentials. Ensure it is included in your `.gitignore` if necessary or secured appropriately in production environments.
77
77
 
@@ -91,9 +91,9 @@ When deploying to cloud platforms with ephemeral filesystems, ensure your databa
91
91
  ```javascript
92
92
  // Your app
93
93
  sqliteAdmin({
94
- dbPath: '/data/app.db',
95
- // configPath automatically uses /data/sqlite-admin-config.json
96
- })
94
+ dbPath: "/data/app.db",
95
+ // configPath automatically uses /data/sqlite-config.json
96
+ });
97
97
  ```
98
98
 
99
99
  The config file is automatically stored alongside your database, so both will persist across deployments when using the same volume.
@@ -104,32 +104,32 @@ The plugin adds the following routes under your configured `prefix` (default `/s
104
104
 
105
105
  ### Authentication
106
106
 
107
- | Method | Path | Description |
108
- | ------ | ---------------- | ------------------------------------------------ |
109
- | POST | `/auth/login` | Authenticate user (username, password, 2FA code) |
110
- | POST | `/auth/logout` | End session |
111
- | GET | `/auth/status` | Check if system is configured and user authenticated |
112
- | POST | `/api/setup` | Initial setup (create admin credentials) |
107
+ | Method | Path | Description |
108
+ | ------ | -------------- | ---------------------------------------------------- |
109
+ | POST | `/auth/login` | Authenticate user (username, password, 2FA code) |
110
+ | POST | `/auth/logout` | End session |
111
+ | GET | `/auth/status` | Check if system is configured and user authenticated |
112
+ | POST | `/api/setup` | Initial setup (create admin credentials) |
113
113
 
114
114
  ### Data Operations
115
115
 
116
- | Method | Path | Description |
117
- | ------ | ------------------------ | --------------------------------------------- |
118
- | GET | `/api/tables` | List all tables in database |
119
- | GET | `/api/table/:name/rows` | Get rows for a table (pagination support) |
120
- | POST | `/api/table/:name/insert`| Insert new record |
121
- | POST | `/api/table/:name/update`| Update record (inline edit) |
122
- | POST | `/api/table/:name/delete`| Delete record |
123
- | GET | `/api/table/:name` | Get table schema (columns and foreign keys) |
116
+ | Method | Path | Description |
117
+ | ------ | ------------------------- | ------------------------------------------- |
118
+ | GET | `/api/tables` | List all tables in database |
119
+ | GET | `/api/table/:name/rows` | Get rows for a table (pagination support) |
120
+ | POST | `/api/table/:name/insert` | Insert new record |
121
+ | POST | `/api/table/:name/update` | Update record (inline edit) |
122
+ | POST | `/api/table/:name/delete` | Delete record |
123
+ | GET | `/api/table/:name` | Get table schema (columns and foreign keys) |
124
124
 
125
125
  ### Advanced Features
126
126
 
127
- | Method | Path | Description |
128
- | ------ | ------------------------ | --------------------------------------------- |
129
- | POST | `/api/query` | Execute raw SQL query |
130
- | POST | `/api/ai/sql` | Generate SQL from natural language prompt |
131
- | POST | `/api/resolve-fk` | Resolve IDs to display labels for foreign keys|
132
- | GET | `/api/meta/schema` | Get full database schema for ERD |
127
+ | Method | Path | Description |
128
+ | ------ | ------------------ | ---------------------------------------------- |
129
+ | POST | `/api/query` | Execute raw SQL query |
130
+ | POST | `/api/ai/sql` | Generate SQL from natural language prompt |
131
+ | POST | `/api/resolve-fk` | Resolve IDs to display labels for foreign keys |
132
+ | GET | `/api/meta/schema` | Get full database schema for ERD |
133
133
 
134
134
  ## 🤝 Contributing
135
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riligar/elysia-sqlite",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Plugin ElysiaJS para gerenciamento de bancos de dados SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Elysia } from "elysia";
2
2
  import { Database } from "bun:sqlite";
3
3
  import { join, dirname, resolve } from "path";
4
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
- import { writeFile } from 'node:fs/promises';
6
- import { createSessionManager } from './core/session.js';
7
- import { authenticator } from 'otplib';
8
- import QRCode from 'qrcode';
4
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
5
+ import { writeFile } from "node:fs/promises";
6
+ import { createSessionManager } from "./core/session.js";
7
+ import { authenticator } from "otplib";
8
+ import QRCode from "qrcode";
9
9
 
10
10
  // Mapeamento de extensões para MIME types
11
11
  const mimeTypes = {
@@ -34,8 +34,10 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
34
34
 
35
35
  // Se configPath não for especificado, deriva do diretório do banco de dados
36
36
  // Isso garante que a configuração fique no mesmo volume persistente do banco
37
- const resolvedConfigPath = configPath ? resolve(configPath) : join(dirname(absoluteDbPath), "sqlite-admin-config.json");
38
-
37
+ const resolvedConfigPath = configPath
38
+ ? resolve(configPath)
39
+ : join(dirname(absoluteDbPath), "sqlite-config.json");
40
+
39
41
  // Gerenciador de Sessão
40
42
  const sessionManager = createSessionManager();
41
43
 
@@ -44,7 +46,7 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
44
46
  const loadConfig = () => {
45
47
  if (existsSync(resolvedConfigPath)) {
46
48
  try {
47
- config = JSON.parse(readFileSync(resolvedConfigPath, 'utf-8'));
49
+ config = JSON.parse(readFileSync(resolvedConfigPath, "utf-8"));
48
50
  } catch (e) {
49
51
  console.error("Failed to load config", e);
50
52
  }
@@ -53,14 +55,14 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
53
55
  loadConfig();
54
56
 
55
57
  const saveConfig = async (newConfig) => {
56
- config = { ...config, ...newConfig };
57
- try {
58
- await writeFile(resolvedConfigPath, JSON.stringify(config, null, 2));
59
- return true;
60
- } catch (e) {
61
- console.error("Failed to save config", e);
62
- return false;
63
- }
58
+ config = { ...config, ...newConfig };
59
+ try {
60
+ await writeFile(resolvedConfigPath, JSON.stringify(config, null, 2));
61
+ return true;
62
+ } catch (e) {
63
+ console.error("Failed to save config", e);
64
+ return false;
65
+ }
64
66
  };
65
67
 
66
68
  const isConfigured = () => !!(config.username && config.password);
@@ -69,190 +71,220 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
69
71
  new Elysia({ prefix })
70
72
  // Middleware de Autenticação
71
73
  .derive(({ headers }) => {
72
- const cookies = headers.cookie || '';
73
- const sessionMatch = cookies.match(/admin-session=([^;]+)/);
74
- const token = sessionMatch ? sessionMatch[1] : null;
75
- const session = sessionManager.get(token);
76
- return { session };
74
+ const cookies = headers.cookie || "";
75
+ const sessionMatch = cookies.match(/admin-session=([^;]+)/);
76
+ const token = sessionMatch ? sessionMatch[1] : null;
77
+ const session = sessionManager.get(token);
78
+ return { session };
77
79
  })
78
80
  .onBeforeHandle(({ path, set, session, body }) => {
79
81
  // Enforce trailing slash for root to ensure relative assets work
80
82
  if (path === prefix) {
81
- return Response.redirect(prefix + '/', 301);
83
+ return Response.redirect(prefix + "/", 301);
82
84
  }
83
85
 
84
86
  // Permitir assets e HTML principal
85
- if (path.includes('/assets/') || path === prefix + '/') return;
86
- if (path.endsWith('index.html')) return;
87
+ if (path.includes("/assets/") || path === prefix + "/") return;
88
+ if (path.endsWith("index.html")) return;
87
89
 
88
90
  // Rotas Públicas de API
89
- if (path.endsWith('/auth/login') || path.endsWith('/auth/status') || path.endsWith('/auth/logout')) return;
90
-
91
+ if (
92
+ path.endsWith("/auth/login") ||
93
+ path.endsWith("/auth/status") ||
94
+ path.endsWith("/auth/logout")
95
+ )
96
+ return;
97
+
91
98
  // Rota de Setup (só permitida se não configurado)
92
- if (path.endsWith('/api/setup')) {
93
- if (isConfigured()) {
94
- set.status = 403;
95
- return { success: false, error: "System already configured" };
96
- }
97
- return;
99
+ if (path.endsWith("/api/setup")) {
100
+ if (isConfigured()) {
101
+ set.status = 403;
102
+ return { success: false, error: "System already configured" };
103
+ }
104
+ return;
98
105
  }
99
106
 
100
107
  // Para todas as outras rotas /api/, exigir configuração e autenticação
101
- if (path.includes('/api/')) {
102
- if (!isConfigured()) {
103
- set.status = 403;
104
- return { success: false, error: "System not configured", code: "NOT_CONFIGURED" };
105
- }
108
+ if (path.includes("/api/")) {
109
+ if (!isConfigured()) {
110
+ set.status = 403;
111
+ return {
112
+ success: false,
113
+ error: "System not configured",
114
+ code: "NOT_CONFIGURED",
115
+ };
116
+ }
106
117
 
107
- if (!session) {
108
- set.status = 401;
109
- return { success: false, error: "Unauthorized", code: "UNAUTHORIZED" };
110
- }
118
+ if (!session) {
119
+ set.status = 401;
120
+ return {
121
+ success: false,
122
+ error: "Unauthorized",
123
+ code: "UNAUTHORIZED",
124
+ };
125
+ }
111
126
  }
112
127
  })
113
128
 
114
129
  // AUTH: Status
115
130
  .get("/auth/status", ({ session }) => {
116
- return {
117
- configured: isConfigured(),
118
- authenticated: !!session,
119
- user: session?.username,
120
- totpEnabled: !!config.totpSecret
121
- };
131
+ return {
132
+ configured: isConfigured(),
133
+ authenticated: !!session,
134
+ user: session?.username,
135
+ totpEnabled: !!config.totpSecret,
136
+ };
122
137
  })
123
138
 
124
139
  // AUTH: Setup (Onboarding)
125
140
  .post("/api/setup", async ({ body }) => {
126
- if (isConfigured()) {
127
- return { success: false, error: "Already configured" };
128
- }
129
- const { username, password } = body;
130
- if (!username || !password) {
131
- return { success: false, error: "Username and password required" };
132
- }
133
-
134
- if (await saveConfig({ username, password })) {
135
- // Criar sessão automaticamente
136
- const { token, expiresAt } = sessionManager.create(username);
137
- const expiresDate = new Date(expiresAt);
138
-
139
- return new Response(JSON.stringify({ success: true }), {
140
- headers: {
141
- 'Content-Type': 'application/json',
142
- 'Set-Cookie': `admin-session=${token}; Path=${prefix}; HttpOnly; SameSite=Lax; Expires=${expiresDate.toUTCString()}`
143
- }
144
- });
145
- }
146
- return { success: false, error: "Failed to save config" };
141
+ if (isConfigured()) {
142
+ return { success: false, error: "Already configured" };
143
+ }
144
+ const { username, password } = body;
145
+ if (!username || !password) {
146
+ return { success: false, error: "Username and password required" };
147
+ }
148
+
149
+ if (await saveConfig({ username, password })) {
150
+ // Criar sessão automaticamente
151
+ const { token, expiresAt } = sessionManager.create(username);
152
+ const expiresDate = new Date(expiresAt);
153
+
154
+ return new Response(JSON.stringify({ success: true }), {
155
+ headers: {
156
+ "Content-Type": "application/json",
157
+ "Set-Cookie": `admin-session=${token}; Path=${prefix}; HttpOnly; SameSite=Lax; Expires=${expiresDate.toUTCString()}`,
158
+ },
159
+ });
160
+ }
161
+ return { success: false, error: "Failed to save config" };
147
162
  })
148
163
 
149
164
  // AUTH: Login
150
165
  .post("/auth/login", ({ body, set }) => {
151
- if (!isConfigured()) {
152
- set.status = 403;
153
- return { success: false, error: "Not configured" };
154
- }
155
-
156
- const { username, password, totpCode } = body;
157
-
158
- if (username === config.username && password === config.password) {
159
- // 2FA Verification
160
- if (config.totpSecret) {
161
- if (!totpCode) {
162
- set.status = 401; // Require 2FA
163
- return { success: false, error: "2FA code required", code: "2FA_REQUIRED" };
164
- }
165
-
166
- const isValid = authenticator.check(totpCode, config.totpSecret);
167
- if (!isValid) {
168
- set.status = 401;
169
- return { success: false, error: "Invalid 2FA code" };
170
- }
171
- }
172
-
173
- const { token, expiresAt } = sessionManager.create(username);
174
- const expiresDate = new Date(expiresAt);
175
-
176
- return new Response(JSON.stringify({ success: true }), {
177
- headers: {
178
- 'Content-Type': 'application/json',
179
- 'Set-Cookie': `admin-session=${token}; Path=${prefix}; HttpOnly; SameSite=Lax; Expires=${expiresDate.toUTCString()}`
180
- }
181
- });
166
+ if (!isConfigured()) {
167
+ set.status = 403;
168
+ return { success: false, error: "Not configured" };
169
+ }
170
+
171
+ const { username, password, totpCode } = body;
172
+
173
+ if (username === config.username && password === config.password) {
174
+ // 2FA Verification
175
+ if (config.totpSecret) {
176
+ if (!totpCode) {
177
+ set.status = 401; // Require 2FA
178
+ return {
179
+ success: false,
180
+ error: "2FA code required",
181
+ code: "2FA_REQUIRED",
182
+ };
183
+ }
184
+
185
+ const isValid = authenticator.check(totpCode, config.totpSecret);
186
+ if (!isValid) {
187
+ set.status = 401;
188
+ return { success: false, error: "Invalid 2FA code" };
189
+ }
182
190
  }
183
-
184
- set.status = 401;
185
- return { success: false, error: "Invalid credentials" };
191
+
192
+ const { token, expiresAt } = sessionManager.create(username);
193
+ const expiresDate = new Date(expiresAt);
194
+
195
+ return new Response(JSON.stringify({ success: true }), {
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ "Set-Cookie": `admin-session=${token}; Path=${prefix}; HttpOnly; SameSite=Lax; Expires=${expiresDate.toUTCString()}`,
199
+ },
200
+ });
201
+ }
202
+
203
+ set.status = 401;
204
+ return { success: false, error: "Invalid credentials" };
186
205
  })
187
206
 
188
207
  // AUTH: Logout
189
208
  .post("/auth/logout", ({ session }) => {
190
- return new Response(JSON.stringify({ success: true }), {
191
- headers: {
192
- 'Content-Type': 'application/json',
193
- 'Set-Cookie': `admin-session=; Path=${prefix}; HttpOnly; SameSite=Lax; Max-Age=0`
194
- }
195
- });
209
+ return new Response(JSON.stringify({ success: true }), {
210
+ headers: {
211
+ "Content-Type": "application/json",
212
+ "Set-Cookie": `admin-session=; Path=${prefix}; HttpOnly; SameSite=Lax; Max-Age=0`,
213
+ },
214
+ });
196
215
  })
197
216
 
198
217
  // DEBUG: List files in UI path
199
218
  .get("/api/debug/files", () => {
200
219
  try {
201
- const files = readdirSync(uiPath);
202
- const assetsPath = join(uiPath, 'assets');
203
- const assets = existsSync(assetsPath)
204
- ? readdirSync(assetsPath)
205
- : 'Assets folder missing';
206
- return { uiPath, files, assets };
220
+ const files = readdirSync(uiPath);
221
+ const assetsPath = join(uiPath, "assets");
222
+ const assets = existsSync(assetsPath)
223
+ ? readdirSync(assetsPath)
224
+ : "Assets folder missing";
225
+ return { uiPath, files, assets };
207
226
  } catch (e) {
208
- return { error: e.message, stack: e.stack, uiPath };
227
+ return { error: e.message, stack: e.stack, uiPath };
209
228
  }
210
229
  })
211
230
 
212
231
  // TOTP: Generate
213
232
  .post("/api/totp/generate", async ({ session, set }) => {
214
- if (!session) { set.status = 401; return; }
215
- const secret = authenticator.generateSecret();
216
- const otpauth = authenticator.keyuri(session.username, 'SQLite', secret);
217
- const qrCode = await QRCode.toDataURL(otpauth);
218
- return { success: true, secret, qrCode };
233
+ if (!session) {
234
+ set.status = 401;
235
+ return;
236
+ }
237
+ const secret = authenticator.generateSecret();
238
+ const otpauth = authenticator.keyuri(
239
+ session.username,
240
+ "SQLite",
241
+ secret
242
+ );
243
+ const qrCode = await QRCode.toDataURL(otpauth);
244
+ return { success: true, secret, qrCode };
219
245
  })
220
246
 
221
247
  // TOTP: Verify & Enable
222
248
  .post("/api/totp/verify", async ({ body, session, set }) => {
223
- if (!session) { set.status = 401; return; }
224
- const { secret, code } = body;
225
-
226
- if (!authenticator.check(code, secret)) {
227
- return { success: false, error: "Invalid code" };
228
- }
249
+ if (!session) {
250
+ set.status = 401;
251
+ return;
252
+ }
253
+ const { secret, code } = body;
229
254
 
230
- if (await saveConfig({ totpSecret: secret })) {
231
- return { success: true };
232
- }
233
- return { success: false, error: "Failed to save config" };
255
+ if (!authenticator.check(code, secret)) {
256
+ return { success: false, error: "Invalid code" };
257
+ }
258
+
259
+ if (await saveConfig({ totpSecret: secret })) {
260
+ return { success: true };
261
+ }
262
+ return { success: false, error: "Failed to save config" };
234
263
  })
235
264
 
236
265
  // TOTP: Disable
237
266
  .post("/api/totp/disable", async ({ body, session, set }) => {
238
- if (!session) { set.status = 401; return; }
239
- const { code } = body; // Confirm with code before disabling
240
-
241
- if (!authenticator.check(code, config.totpSecret)) {
242
- return { success: false, error: "Invalid code" };
243
- }
267
+ if (!session) {
268
+ set.status = 401;
269
+ return;
270
+ }
271
+ const { code } = body; // Confirm with code before disabling
244
272
 
245
- // Remove secret
246
- const newConfig = { ...config };
247
- delete newConfig.totpSecret;
248
- config = newConfig; // Local update
249
-
250
- try {
251
- await writeFile(resolvedConfigPath, JSON.stringify(config, null, 2));
252
- return { success: true };
253
- } catch(e) {
254
- return { success: false, error: "Failed to save" };
255
- }
273
+ if (!authenticator.check(code, config.totpSecret)) {
274
+ return { success: false, error: "Invalid code" };
275
+ }
276
+
277
+ // Remove secret
278
+ const newConfig = { ...config };
279
+ delete newConfig.totpSecret;
280
+ config = newConfig; // Local update
281
+
282
+ try {
283
+ await writeFile(resolvedConfigPath, JSON.stringify(config, null, 2));
284
+ return { success: true };
285
+ } catch (e) {
286
+ return { success: false, error: "Failed to save" };
287
+ }
256
288
  })
257
289
 
258
290
  // Servir index.html na raiz
@@ -262,9 +294,9 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
262
294
  headers: {
263
295
  "Content-Type": "text/html",
264
296
  "Cache-Control": "no-cache, no-store, must-revalidate",
265
- "Pragma": "no-cache",
266
- "Expires": "0"
267
- }
297
+ Pragma: "no-cache",
298
+ Expires: "0",
299
+ },
268
300
  });
269
301
  })
270
302
 
@@ -273,16 +305,16 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
273
305
  const filePath = join(uiPath, "assets", params["*"]);
274
306
  const file = Bun.file(filePath);
275
307
  const exists = await file.exists();
276
-
308
+
277
309
  if (!exists) {
278
- return new Response("Not found", { status: 404 });
310
+ return new Response("Not found", { status: 404 });
279
311
  }
280
-
312
+
281
313
  const ext = filePath.substring(filePath.lastIndexOf("."));
282
314
  return new Response(file, {
283
315
  headers: {
284
316
  "Content-Type": mimeTypes[ext] || "application/octet-stream",
285
- "Cache-Control": "public, max-age=31536000, immutable"
317
+ "Cache-Control": "public, max-age=31536000, immutable",
286
318
  },
287
319
  });
288
320
  })
@@ -307,11 +339,17 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
307
339
  const page = parseInt(query.page) || 1;
308
340
  const limit = parseInt(query.limit) || 50;
309
341
  const offset = (page - 1) * limit;
310
-
311
- const rows = db.query(`SELECT * FROM ${params.name} LIMIT ${limit} OFFSET ${offset}`).all();
312
- const countResult = db.query(`SELECT COUNT(*) as count FROM ${params.name}`).get();
342
+
343
+ const rows = db
344
+ .query(
345
+ `SELECT * FROM ${params.name} LIMIT ${limit} OFFSET ${offset}`
346
+ )
347
+ .all();
348
+ const countResult = db
349
+ .query(`SELECT COUNT(*) as count FROM ${params.name}`)
350
+ .get();
313
351
  const total = countResult.count;
314
-
352
+
315
353
  return {
316
354
  success: true,
317
355
  rows,
@@ -319,8 +357,8 @@ export const sqliteAdmin = ({ dbPath, prefix = "/sqlite", configPath }) => {
319
357
  page,
320
358
  limit,
321
359
  total,
322
- totalPages: Math.ceil(total / limit)
323
- }
360
+ totalPages: Math.ceil(total / limit),
361
+ },
324
362
  };
325
363
  } catch (error) {
326
364
  return { success: false, error: error.message };