@riligar/elysia-sqlite 1.5.2 → 1.7.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/README.md +52 -52
- package/package.json +1 -1
- package/src/index.js +198 -160
- package/src/ui/dist/assets/{_baseUniq-tp0gy_rC.js → _baseUniq-BMKkaOYG.js} +1 -1
- package/src/ui/dist/assets/{arc-CIKD5jh6.js → arc-DzwvCm0y.js} +1 -1
- package/src/ui/dist/assets/{architectureDiagram-VXUJARFQ-C-Zu1f5H.js → architectureDiagram-VXUJARFQ-CnkGS2CW.js} +1 -1
- package/src/ui/dist/assets/{blockDiagram-VD42YOAC-CwT44Fo1.js → blockDiagram-VD42YOAC-BAsWaVVt.js} +1 -1
- package/src/ui/dist/assets/{c4Diagram-YG6GDRKO-g1CkarVK.js → c4Diagram-YG6GDRKO-D3hSuEZw.js} +1 -1
- package/src/ui/dist/assets/channel-BDMhqIlJ.js +1 -0
- package/src/ui/dist/assets/{chunk-4BX2VUAB-BRSq_1my.js → chunk-4BX2VUAB-W3RfStVh.js} +1 -1
- package/src/ui/dist/assets/{chunk-55IACEB6-sJOVeCqP.js → chunk-55IACEB6-DNOq1VFt.js} +1 -1
- package/src/ui/dist/assets/{chunk-B4BG7PRW-CAJu4y9m.js → chunk-B4BG7PRW-BzO1OpN4.js} +1 -1
- package/src/ui/dist/assets/{chunk-DI55MBZ5-CqHNSLXG.js → chunk-DI55MBZ5-Y_GWh2zl.js} +1 -1
- package/src/ui/dist/assets/{chunk-FMBD7UC4-Cm8x3fGA.js → chunk-FMBD7UC4-Bi5H0CGx.js} +1 -1
- package/src/ui/dist/assets/{chunk-QN33PNHL-CXiOGhHB.js → chunk-QN33PNHL-Dtb7fGh4.js} +1 -1
- package/src/ui/dist/assets/{chunk-QZHKN3VN-Dac55Lwb.js → chunk-QZHKN3VN-ppMM_NQA.js} +1 -1
- package/src/ui/dist/assets/{chunk-TZMSLE5B-C2E0ascL.js → chunk-TZMSLE5B-BTmGFJO0.js} +1 -1
- package/src/ui/dist/assets/classDiagram-2ON5EDUG-BBbphcIu.js +1 -0
- package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-BBbphcIu.js +1 -0
- package/src/ui/dist/assets/clone-8HDXOICO.js +1 -0
- package/src/ui/dist/assets/{cose-bilkent-S5V4N54A-CJzZ2NKZ.js → cose-bilkent-S5V4N54A-DhSfb5hF.js} +1 -1
- package/src/ui/dist/assets/{dagre-6UL2VRFP-DqBYKK4u.js → dagre-6UL2VRFP-BeYsJtoG.js} +1 -1
- package/src/ui/dist/assets/{diagram-PSM6KHXK-Ud1GhyHV.js → diagram-PSM6KHXK-C-EWuuX_.js} +1 -1
- package/src/ui/dist/assets/{diagram-QEK2KX5R-C_PcM76a.js → diagram-QEK2KX5R-Bgl10GwF.js} +1 -1
- package/src/ui/dist/assets/{diagram-S2PKOQOG-Dxc59S6X.js → diagram-S2PKOQOG-BLljdZCM.js} +1 -1
- package/src/ui/dist/assets/{erDiagram-Q2GNP2WA-BM17VS0S.js → erDiagram-Q2GNP2WA-CjfXI2mB.js} +1 -1
- package/src/ui/dist/assets/{flowDiagram-NV44I4VS-DvifDv4F.js → flowDiagram-NV44I4VS-BILsxh5n.js} +1 -1
- package/src/ui/dist/assets/{ganttDiagram-JELNMOA3-BzdIaEbH.js → ganttDiagram-JELNMOA3-DpXhg3mE.js} +1 -1
- package/src/ui/dist/assets/{gitGraphDiagram-NY62KEGX-Df2I2D_a.js → gitGraphDiagram-NY62KEGX-DLD08RXZ.js} +1 -1
- package/src/ui/dist/assets/{graph-B581r9-3.js → graph-CavEq-OG.js} +1 -1
- package/src/ui/dist/assets/{index-BBKjGWlO.js → index-BG-h5MPT.js} +4 -4
- package/src/ui/dist/assets/{infoDiagram-WHAUD3N6-D7CJLVcM.js → infoDiagram-WHAUD3N6-cIe5ZWKq.js} +1 -1
- package/src/ui/dist/assets/{journeyDiagram-XKPGCS4Q-CX7LwWbu.js → journeyDiagram-XKPGCS4Q-CtJlkDMd.js} +1 -1
- package/src/ui/dist/assets/{kanban-definition-3W4ZIXB7-BIRhmBI5.js → kanban-definition-3W4ZIXB7-D5OeXcHJ.js} +1 -1
- package/src/ui/dist/assets/{layout-DC_20cjc.js → layout-BhMTPhmG.js} +1 -1
- package/src/ui/dist/assets/{linear-w2kfZjuH.js → linear-7JKJ0rc6.js} +1 -1
- package/src/ui/dist/assets/{min-DfeMYee7.js → min-DVfZQuXM.js} +1 -1
- package/src/ui/dist/assets/{mindmap-definition-VGOIOE7T-ooGmlDfN.js → mindmap-definition-VGOIOE7T-9WKTvEh6.js} +1 -1
- package/src/ui/dist/assets/{pieDiagram-ADFJNKIX-DIgElDn1.js → pieDiagram-ADFJNKIX-BhUn73AD.js} +1 -1
- package/src/ui/dist/assets/{quadrantDiagram-AYHSOK5B-Bpixm6gO.js → quadrantDiagram-AYHSOK5B-BZdz1yM3.js} +1 -1
- package/src/ui/dist/assets/{requirementDiagram-UZGBJVZJ-CTNj1gzp.js → requirementDiagram-UZGBJVZJ-Chqqws0m.js} +1 -1
- package/src/ui/dist/assets/{sankeyDiagram-TZEHDZUN-Bb0s_lf0.js → sankeyDiagram-TZEHDZUN-BOp8vVQ0.js} +1 -1
- package/src/ui/dist/assets/{sequenceDiagram-WL72ISMW-CMvbebl5.js → sequenceDiagram-WL72ISMW-DJ6x9fIe.js} +1 -1
- package/src/ui/dist/assets/{stateDiagram-FKZM4ZOC-VsagYS5z.js → stateDiagram-FKZM4ZOC-CL_VVFZQ.js} +1 -1
- package/src/ui/dist/assets/stateDiagram-v2-4FDKWEC3-vE8UUrO0.js +1 -0
- package/src/ui/dist/assets/{timeline-definition-IT6M3QCI-CD_E615Y.js → timeline-definition-IT6M3QCI-CTGuWGC8.js} +1 -1
- package/src/ui/dist/assets/{treemap-KMMF4GRG-Bmk930Gz.js → treemap-KMMF4GRG-DGWllLiC.js} +1 -1
- package/src/ui/dist/assets/{xychartDiagram-PRI3JC2R-CKe-Ew8V.js → xychartDiagram-PRI3JC2R-Drs9GbkQ.js} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/channel-1vrFD0pz.js +0 -1
- package/src/ui/dist/assets/classDiagram-2ON5EDUG-CZ5BrJQG.js +0 -1
- package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-CZ5BrJQG.js +0 -1
- package/src/ui/dist/assets/clone-B1UmMcxV.js +0 -1
- 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
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
|
37
|
-
import { sqliteAdmin } from
|
|
36
|
+
import { Elysia } from "elysia";
|
|
37
|
+
import { sqliteAdmin } from "@riligar/elysia-sqlite";
|
|
38
38
|
|
|
39
39
|
const app = new Elysia()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(
|
|
49
|
-
console.log(
|
|
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
|
|
61
|
-
| ------------ | ------ |
|
|
62
|
-
| `dbPath` | string | **Required**
|
|
63
|
-
| `prefix` | string | `"/sqlite"`
|
|
64
|
-
| `configPath` | string | Same directory as `dbPath`
|
|
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
|
|
71
|
-
|
|
|
72
|
-
| `username`
|
|
73
|
-
| `password`
|
|
74
|
-
| `totpSecret`
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
108
|
-
| ------ |
|
|
109
|
-
| POST | `/auth/login`
|
|
110
|
-
| POST | `/auth/logout`
|
|
111
|
-
| GET | `/auth/status`
|
|
112
|
-
| POST | `/api/setup`
|
|
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
|
|
117
|
-
| ------ |
|
|
118
|
-
| GET | `/api/tables`
|
|
119
|
-
| GET | `/api/table/:name/rows`
|
|
120
|
-
| POST | `/api/table/:name/insert
|
|
121
|
-
| POST | `/api/table/:name/update
|
|
122
|
-
| POST | `/api/table/:name/delete
|
|
123
|
-
| GET | `/api/table/:name`
|
|
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
|
|
128
|
-
| ------ |
|
|
129
|
-
| POST | `/api/query`
|
|
130
|
-
| POST | `/api/ai/sql`
|
|
131
|
-
| POST | `/api/resolve-fk`
|
|
132
|
-
| GET | `/api/meta/schema`
|
|
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
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
|
|
5
|
-
import { writeFile } from
|
|
6
|
-
import { createSessionManager } from
|
|
7
|
-
import { authenticator } from
|
|
8
|
-
import QRCode from
|
|
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
|
|
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,
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
+
return Response.redirect(prefix + "/", 301);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
// Permitir assets e HTML principal
|
|
85
|
-
if (path.includes(
|
|
86
|
-
if (path.endsWith(
|
|
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 (
|
|
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(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
249
|
+
if (!session) {
|
|
250
|
+
set.status = 401;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const { secret, code } = body;
|
|
229
254
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
267
|
+
if (!session) {
|
|
268
|
+
set.status = 401;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const { code } = body; // Confirm with code before disabling
|
|
244
272
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
312
|
-
|
|
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 };
|