079project 6.0.0 → 8.0.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/.cache/38/9a0e6a4756f17b0edebad6a7be1eed.json +1 -0
- package/079project_frontend/README.md +70 -0
- package/079project_frontend/package-lock.json +17310 -0
- package/079project_frontend/package.json +40 -0
- package/079project_frontend/public/favicon.ico +0 -0
- package/079project_frontend/public/index.html +43 -0
- package/079project_frontend/public/logo192.png +0 -0
- package/079project_frontend/public/logo512.png +0 -0
- package/079project_frontend/public/manifest.json +25 -0
- package/079project_frontend/public/robots.txt +3 -0
- package/079project_frontend/src/App.css +515 -0
- package/079project_frontend/src/App.js +286 -0
- package/079project_frontend/src/App.test.js +8 -0
- package/079project_frontend/src/api/client.js +103 -0
- package/079project_frontend/src/components/AuthGate.js +153 -0
- package/079project_frontend/src/components/ConfigPanel.js +643 -0
- package/079project_frontend/src/index.css +21 -0
- package/079project_frontend/src/index.js +17 -0
- package/079project_frontend/src/logo.svg +1 -0
- package/079project_frontend/src/reportWebVitals.js +13 -0
- package/079project_frontend/src/setupTests.js +5 -0
- package/README.en.md +234 -0
- package/README.md +0 -0
- package/auth_frontend_server.cjs +312 -0
- package/main.cjs +2259 -83
- package/memeMergeWorker.cjs +256 -0
- package/package.json +28 -15
- package/robots/wikitext-something.txt +1 -39254
- package/tools_install.js +136 -0
- package/model_RNN.py +0 -209
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const reportWebVitals = onPerfEntry => {
|
|
2
|
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
3
|
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
4
|
+
getCLS(onPerfEntry);
|
|
5
|
+
getFID(onPerfEntry);
|
|
6
|
+
getFCP(onPerfEntry);
|
|
7
|
+
getLCP(onPerfEntry);
|
|
8
|
+
getTTFB(onPerfEntry);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default reportWebVitals;
|
package/README.en.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# 079Project
|
|
2
|
+
|
|
3
|
+
An experimental AI project that combines **graph-based reasoning**, **learning modules**, a **gateway API**, a **React control console**, and a **SQLite-backed identity system** in one repository.
|
|
4
|
+
|
|
5
|
+
The current runtime is split into **two Node.js processes**:
|
|
6
|
+
|
|
7
|
+
- **Main AI process**: `main.cjs` (serves `/api/*`; token required by default)
|
|
8
|
+
- **Identity + Web process**: `auth_frontend_server.cjs` (serves `/auth/*`, hosts the React production build, and reverse-proxies `/api/*` to the main AI process)
|
|
9
|
+
|
|
10
|
+
Note: This repository is still experimental; APIs/behavior may change.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Key Structure
|
|
15
|
+
|
|
16
|
+
- `main.cjs`: Main AI process (graph runtime, SparkArray, controller pool, MemeBarrier, RL/ADV learning, snapshots & export, `/api` routes).
|
|
17
|
+
- `auth_frontend_server.cjs`: Identity + frontend hosting (SQLite users/sessions, JWT auth, `/auth` routes, `/api` reverse proxy).
|
|
18
|
+
- `079project_frontend/`: React frontend (Chat + Config console).
|
|
19
|
+
- `robots/`: Robot corpora (TXT).
|
|
20
|
+
- `tests/`: Test cases / word lists (TXT; can be added at runtime and refreshed into RL).
|
|
21
|
+
- `snapshots/`: Snapshot files.
|
|
22
|
+
- `runtime_store/`: Runtime cache/export folder; default location for `runtime_store/auth.sqlite`.
|
|
23
|
+
- `lmdb/`: LMDB data directory (falls back to JSON storage if unavailable).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Windows + PowerShell (`pwsh.exe` examples below).
|
|
30
|
+
- Node.js >= 18 (LTS recommended).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Backend dependencies:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
cd "079Project/"
|
|
40
|
+
npm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Frontend dependencies:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
cd "079Project/079project_frontend"
|
|
47
|
+
npm install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Run (Two Processes)
|
|
53
|
+
|
|
54
|
+
Default ports:
|
|
55
|
+
|
|
56
|
+
- Main AI: `127.0.0.1:5080`
|
|
57
|
+
- Identity + Web: `127.0.0.1:5081`
|
|
58
|
+
|
|
59
|
+
### Mode A (Recommended): Identity+Web hosts UI and proxies `/api`
|
|
60
|
+
|
|
61
|
+
1) Start the main AI process (serves `/api/*` only):
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
cd "079Project/"
|
|
65
|
+
$env:AI_AUTH_ENABLED="true"
|
|
66
|
+
$env:AI_AUTH_JWT_SECRET="change-me"
|
|
67
|
+
node .\main.cjs
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2) Start the identity + web process (hosts UI and proxies `/api/*` to the main AI):
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
cd "079Project/"
|
|
74
|
+
$env:AUTH_JWT_SECRET="change-me"
|
|
75
|
+
$env:AI_API_BASE="http://127.0.0.1:5080"
|
|
76
|
+
node .\auth_frontend_server.cjs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
3) Open in browser:
|
|
80
|
+
|
|
81
|
+
- `http://127.0.0.1:5081/`
|
|
82
|
+
|
|
83
|
+
On first run, choose “bootstrap” in the UI and create the initial admin user (only allowed once).
|
|
84
|
+
|
|
85
|
+
Important: `AUTH_JWT_SECRET` must match `AI_AUTH_JWT_SECRET`, otherwise tokens cannot be verified by the main AI process.
|
|
86
|
+
|
|
87
|
+
### Mode B (Optional): Frontend dev server
|
|
88
|
+
|
|
89
|
+
The CRA dev server uses `079project_frontend/package.json` `proxy`.
|
|
90
|
+
If you use this mode, it is recommended to set `proxy` to `http://127.0.0.1:5081` so `/api/*` goes through the identity process (no CORS issues; consistent auth).
|
|
91
|
+
|
|
92
|
+
Start:
|
|
93
|
+
|
|
94
|
+
```powershell
|
|
95
|
+
cd "079Project/079project_frontend"
|
|
96
|
+
npm start
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mode C (Production): Build React and let identity process host it
|
|
100
|
+
|
|
101
|
+
1) Build the frontend:
|
|
102
|
+
|
|
103
|
+
```powershell
|
|
104
|
+
cd "079Project/079project_frontend"
|
|
105
|
+
npm run build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
2) Start main AI:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
cd "079Project/"
|
|
112
|
+
$env:AI_AUTH_ENABLED="true"
|
|
113
|
+
$env:AI_AUTH_JWT_SECRET="change-me"
|
|
114
|
+
node .\main.cjs
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
3) Start identity + web:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
cd "079Project/"
|
|
121
|
+
$env:AUTH_JWT_SECRET="change-me"
|
|
122
|
+
$env:AI_API_BASE="http://127.0.0.1:5080"
|
|
123
|
+
node .\auth_frontend_server.cjs
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Open: `http://127.0.0.1:5081/`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Main AI CLI Options (Common)
|
|
131
|
+
|
|
132
|
+
`main.cjs` supports some CLI / env options (partial list):
|
|
133
|
+
|
|
134
|
+
- `--gateway-host` / `AI_GATEWAY_HOST`: listen address (default `127.0.0.1`).
|
|
135
|
+
- `--port` / `CONTROLLER_PORT`: gateway port (default `5080`).
|
|
136
|
+
- `--disable-memebarrier`: disable MemeBarrier on startup (can be re-enabled at runtime).
|
|
137
|
+
- `--disable-learning`: disable learning modules on startup (can be re-enabled at runtime).
|
|
138
|
+
- `--disable-rl`: disable RL on startup (can be re-enabled at runtime).
|
|
139
|
+
- `--disable-adv`: disable ADV on startup (can be re-enabled at runtime).
|
|
140
|
+
- `--export-dir` / `AI_EXPORT_DIR`: default export directory for `/api/export/graph`.
|
|
141
|
+
|
|
142
|
+
Example: listen on `0.0.0.0:5080`:
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
node .\main.cjs --gateway-host=0.0.0.0 --port=5080
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Identity System (SQLite + JWT)
|
|
151
|
+
|
|
152
|
+
Default DB path: `079Project/runtime_store/auth.sqlite`.
|
|
153
|
+
|
|
154
|
+
Environment variables for `auth_frontend_server.cjs`:
|
|
155
|
+
|
|
156
|
+
- `AUTH_PORT`: default `5081`
|
|
157
|
+
- `AUTH_HOST`: default `127.0.0.1`
|
|
158
|
+
- `AUTH_DB_PATH`: default `079Project/runtime_store/auth.sqlite`
|
|
159
|
+
- `AUTH_JWT_SECRET`: JWT secret (must match main AI `AI_AUTH_JWT_SECRET`)
|
|
160
|
+
- `AI_API_BASE`: main AI base URL (default `http://127.0.0.1:5080`)
|
|
161
|
+
|
|
162
|
+
Environment variables for `main.cjs`:
|
|
163
|
+
|
|
164
|
+
- `AI_AUTH_ENABLED`: enabled by default (set to `false` to temporarily disable auth)
|
|
165
|
+
- `AI_AUTH_JWT_SECRET`: token verification secret
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Frontend
|
|
170
|
+
|
|
171
|
+
Left navigation:
|
|
172
|
+
|
|
173
|
+
- `Chat`: basic chat.
|
|
174
|
+
- `Config`: runtime operations console (MemeBarrier / RL / ADV, thresholds, tests refresh, robots retrain).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Selected HTTP APIs
|
|
179
|
+
|
|
180
|
+
### Chat
|
|
181
|
+
|
|
182
|
+
- `POST /api/chat`: single-model chat.
|
|
183
|
+
- `POST /api/array/chat`: SparkArray multi-AI / multi-layer chat.
|
|
184
|
+
|
|
185
|
+
### Runtime toggles (Config UI)
|
|
186
|
+
|
|
187
|
+
- `GET /api/runtime/features`
|
|
188
|
+
- `PATCH /api/runtime/features`
|
|
189
|
+
|
|
190
|
+
### MemeBarrier
|
|
191
|
+
|
|
192
|
+
- `POST /api/memebarrier/start`
|
|
193
|
+
- `POST /api/memebarrier/stop`
|
|
194
|
+
- `GET /api/memebarrier/stats`
|
|
195
|
+
|
|
196
|
+
### Learning
|
|
197
|
+
|
|
198
|
+
- `POST /api/learn/reinforce` / `GET /api/learn/reinforce/latest`
|
|
199
|
+
- `POST /api/learn/adversarial` / `GET /api/learn/adversarial/latest`
|
|
200
|
+
- `POST /api/learn/thresholds`
|
|
201
|
+
|
|
202
|
+
### Tests
|
|
203
|
+
|
|
204
|
+
- `GET /api/tests/list`
|
|
205
|
+
- `POST /api/tests/case`
|
|
206
|
+
- `POST /api/tests/refresh`
|
|
207
|
+
|
|
208
|
+
### Robots
|
|
209
|
+
|
|
210
|
+
- `GET /robots/list`
|
|
211
|
+
- `POST /robots/ingest`
|
|
212
|
+
- `POST /api/robots/retrain`
|
|
213
|
+
|
|
214
|
+
### Export
|
|
215
|
+
|
|
216
|
+
- `POST /api/export/graph`: export a windowed graph JSON to file and return inline content.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## FAQ
|
|
221
|
+
|
|
222
|
+
### Why is there no UI on `5080/`?
|
|
223
|
+
|
|
224
|
+
This is expected: the UI is hosted by the identity + web process (default `http://127.0.0.1:5081/`).
|
|
225
|
+
|
|
226
|
+
### LMDB cannot be opened
|
|
227
|
+
|
|
228
|
+
If LMDB fails, the system automatically falls back to JSON storage and continues to run. Check directory permissions and environment dependencies if you need LMDB.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
LGPL-3.0 (see `LICENSE`).
|
package/README.md
CHANGED
|
Binary file
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const bodyParser = require('body-parser');
|
|
5
|
+
const jwt = require('jsonwebtoken');
|
|
6
|
+
const bcrypt = require('bcryptjs');
|
|
7
|
+
|
|
8
|
+
let Database;
|
|
9
|
+
try {
|
|
10
|
+
Database = require('better-sqlite3');
|
|
11
|
+
} catch (e) {
|
|
12
|
+
Database = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getEnv(name, fallback) {
|
|
16
|
+
const v = process.env[name];
|
|
17
|
+
return (v === undefined || v === null || v === '') ? fallback : v;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function randomId() {
|
|
25
|
+
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function jsonError(res, status, error, extra) {
|
|
29
|
+
res.status(status).json({ ok: false, error, ...(extra || {}) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class IdentityStore {
|
|
33
|
+
constructor(dbPath) {
|
|
34
|
+
if (!Database) {
|
|
35
|
+
throw new Error('better-sqlite3 not installed/available');
|
|
36
|
+
}
|
|
37
|
+
this.dbPath = dbPath;
|
|
38
|
+
this.db = new Database(dbPath);
|
|
39
|
+
this.db.pragma('journal_mode = WAL');
|
|
40
|
+
this._init();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_init() {
|
|
44
|
+
this.db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
username TEXT NOT NULL UNIQUE,
|
|
48
|
+
password_hash TEXT NOT NULL,
|
|
49
|
+
role TEXT NOT NULL DEFAULT 'user',
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
last_login_at TEXT
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
user_id TEXT NOT NULL,
|
|
57
|
+
created_at TEXT NOT NULL,
|
|
58
|
+
expires_at TEXT NOT NULL,
|
|
59
|
+
revoked_at TEXT,
|
|
60
|
+
ip TEXT,
|
|
61
|
+
ua TEXT,
|
|
62
|
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hasAnyUser() {
|
|
71
|
+
const row = this.db.prepare('SELECT COUNT(1) as c FROM users').get();
|
|
72
|
+
return (row?.c || 0) > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
createUser({ username, password, role = 'admin' }) {
|
|
76
|
+
const id = randomId();
|
|
77
|
+
const passwordHash = bcrypt.hashSync(String(password), 10);
|
|
78
|
+
const createdAt = nowIso();
|
|
79
|
+
this.db
|
|
80
|
+
.prepare('INSERT INTO users (id, username, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
81
|
+
.run(id, username, passwordHash, role, createdAt);
|
|
82
|
+
return { id, username, role, createdAt };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getUserByUsername(username) {
|
|
86
|
+
return this.db.prepare('SELECT * FROM users WHERE username = ?').get(username) || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getUserById(id) {
|
|
90
|
+
return this.db.prepare('SELECT * FROM users WHERE id = ?').get(id) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
verifyPassword(user, password) {
|
|
94
|
+
return bcrypt.compareSync(String(password), String(user.password_hash));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
createSession({ userId, ttlSeconds = 60 * 60 * 24 * 7, ip, ua }) {
|
|
98
|
+
const id = randomId();
|
|
99
|
+
const createdAt = nowIso();
|
|
100
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
101
|
+
this.db
|
|
102
|
+
.prepare('INSERT INTO sessions (id, user_id, created_at, expires_at, ip, ua) VALUES (?, ?, ?, ?, ?, ?)')
|
|
103
|
+
.run(id, userId, createdAt, expiresAt, ip || null, ua || null);
|
|
104
|
+
return { id, userId, createdAt, expiresAt };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
revokeSession(sessionId) {
|
|
108
|
+
const revokedAt = nowIso();
|
|
109
|
+
this.db.prepare('UPDATE sessions SET revoked_at = ? WHERE id = ?').run(revokedAt, sessionId);
|
|
110
|
+
return { id: sessionId, revokedAt };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getSession(sessionId) {
|
|
114
|
+
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
|
|
115
|
+
return row || null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseBearer(req) {
|
|
120
|
+
const h = req.headers.authorization || '';
|
|
121
|
+
const m = /^Bearer\s+(.+)$/i.exec(String(h));
|
|
122
|
+
return m ? m[1] : '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function start() {
|
|
126
|
+
const port = Number(getEnv('AUTH_PORT', '5081'));
|
|
127
|
+
const host = getEnv('AUTH_HOST', '127.0.0.1');
|
|
128
|
+
const webRoot = getEnv('WEB_ROOT', path.join(__dirname, '079project_frontend', 'build'));
|
|
129
|
+
const dbPath = getEnv('AUTH_DB_PATH', path.join(__dirname, 'runtime_store', 'auth.sqlite'));
|
|
130
|
+
const jwtSecret = getEnv('AUTH_JWT_SECRET', 'dev-secret-change-me');
|
|
131
|
+
const aiApiBase = getEnv('AI_API_BASE', 'http://127.0.0.1:5080');
|
|
132
|
+
const tokenTtlSeconds = Number(getEnv('AUTH_TOKEN_TTL_SECONDS', String(60 * 60 * 24 * 7)));
|
|
133
|
+
|
|
134
|
+
if (!fs.existsSync(path.dirname(dbPath))) {
|
|
135
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const store = new IdentityStore(dbPath);
|
|
139
|
+
|
|
140
|
+
const app = express();
|
|
141
|
+
app.use(bodyParser.json({ limit: '2mb' }));
|
|
142
|
+
|
|
143
|
+
// Lightweight reverse proxy for /api/* to main AI process.
|
|
144
|
+
// This avoids CORS and keeps frontend API_BASE as same-origin (:5081).
|
|
145
|
+
app.all(/^\/api\/.*/, async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const base = aiApiBase.endsWith('/') ? aiApiBase.slice(0, -1) : aiApiBase;
|
|
148
|
+
const targetUrl = new URL(`${base}${req.originalUrl}`);
|
|
149
|
+
const headers = { ...req.headers };
|
|
150
|
+
delete headers.host;
|
|
151
|
+
|
|
152
|
+
let body;
|
|
153
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
154
|
+
body = JSON.stringify(req.body ?? {});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const r = await fetch(targetUrl.toString(), {
|
|
158
|
+
method: req.method,
|
|
159
|
+
headers: {
|
|
160
|
+
...headers,
|
|
161
|
+
'content-type': 'application/json'
|
|
162
|
+
},
|
|
163
|
+
body
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const text = await r.text();
|
|
167
|
+
res.status(r.status);
|
|
168
|
+
// pass through content-type if present
|
|
169
|
+
const ct = r.headers.get('content-type');
|
|
170
|
+
if (ct) res.setHeader('content-type', ct);
|
|
171
|
+
res.send(text);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
jsonError(res, 502, 'ai-proxy-failed', { message: e.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// health + config
|
|
178
|
+
app.get('/auth/health', (req, res) => {
|
|
179
|
+
res.json({ ok: true, ts: Date.now() });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.get('/auth/config', (req, res) => {
|
|
183
|
+
res.json({ ok: true, aiApiBase });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// First-run bootstrap: create initial admin if no users.
|
|
187
|
+
app.post('/auth/bootstrap', (req, res) => {
|
|
188
|
+
try {
|
|
189
|
+
if (store.hasAnyUser()) {
|
|
190
|
+
jsonError(res, 409, 'already-bootstrapped');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const username = String(req.body?.username || 'admin').trim();
|
|
194
|
+
const password = String(req.body?.password || '').trim();
|
|
195
|
+
if (!username) {
|
|
196
|
+
jsonError(res, 400, 'username-required');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!password || password.length < 6) {
|
|
200
|
+
jsonError(res, 400, 'password-too-short', { minLen: 6 });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const user = store.createUser({ username, password, role: 'admin' });
|
|
204
|
+
res.json({ ok: true, user: { id: user.id, username: user.username, role: user.role } });
|
|
205
|
+
} catch (e) {
|
|
206
|
+
jsonError(res, 500, 'bootstrap-failed', { message: e.message });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
app.post('/auth/login', (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const username = String(req.body?.username || '').trim();
|
|
213
|
+
const password = String(req.body?.password || '');
|
|
214
|
+
if (!username || !password) {
|
|
215
|
+
jsonError(res, 400, 'username-password-required');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const user = store.getUserByUsername(username);
|
|
219
|
+
if (!user || !store.verifyPassword(user, password)) {
|
|
220
|
+
jsonError(res, 401, 'invalid-credentials');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const session = store.createSession({
|
|
224
|
+
userId: user.id,
|
|
225
|
+
ttlSeconds: tokenTtlSeconds,
|
|
226
|
+
ip: req.ip,
|
|
227
|
+
ua: req.headers['user-agent']
|
|
228
|
+
});
|
|
229
|
+
const token = jwt.sign(
|
|
230
|
+
{ sub: user.id, username: user.username, role: user.role, sid: session.id },
|
|
231
|
+
jwtSecret,
|
|
232
|
+
{ expiresIn: tokenTtlSeconds }
|
|
233
|
+
);
|
|
234
|
+
res.json({
|
|
235
|
+
ok: true,
|
|
236
|
+
token,
|
|
237
|
+
user: { id: user.id, username: user.username, role: user.role },
|
|
238
|
+
expiresAt: session.expires_at
|
|
239
|
+
});
|
|
240
|
+
} catch (e) {
|
|
241
|
+
jsonError(res, 500, 'login-failed', { message: e.message });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
app.post('/auth/logout', (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const token = parseBearer(req);
|
|
248
|
+
if (!token) {
|
|
249
|
+
jsonError(res, 401, 'missing-token');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const payload = jwt.verify(token, jwtSecret);
|
|
253
|
+
if (payload?.sid) {
|
|
254
|
+
store.revokeSession(String(payload.sid));
|
|
255
|
+
}
|
|
256
|
+
res.json({ ok: true });
|
|
257
|
+
} catch (e) {
|
|
258
|
+
jsonError(res, 401, 'invalid-token', { message: e.message });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
app.get('/auth/me', (req, res) => {
|
|
263
|
+
try {
|
|
264
|
+
const token = parseBearer(req);
|
|
265
|
+
if (!token) {
|
|
266
|
+
jsonError(res, 401, 'missing-token');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const payload = jwt.verify(token, jwtSecret);
|
|
270
|
+
const user = store.getUserById(String(payload.sub));
|
|
271
|
+
if (!user) {
|
|
272
|
+
jsonError(res, 401, 'user-not-found');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const session = payload?.sid ? store.getSession(String(payload.sid)) : null;
|
|
276
|
+
if (session && session.revoked_at) {
|
|
277
|
+
jsonError(res, 401, 'session-revoked');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
res.json({ ok: true, user: { id: user.id, username: user.username, role: user.role } });
|
|
281
|
+
} catch (e) {
|
|
282
|
+
jsonError(res, 401, 'invalid-token', { message: e.message });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Serve CRA build (separate from main AI process)
|
|
287
|
+
if (fs.existsSync(webRoot)) {
|
|
288
|
+
app.use(express.static(webRoot));
|
|
289
|
+
|
|
290
|
+
// SPA fallback
|
|
291
|
+
app.get(/^\/(?!api\/|auth\/|robots\/).*/, (req, res) => {
|
|
292
|
+
const indexFile = path.join(webRoot, 'index.html');
|
|
293
|
+
if (!fs.existsSync(indexFile)) {
|
|
294
|
+
res.status(404).send('index.html not found');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
298
|
+
res.send(fs.readFileSync(indexFile, 'utf8'));
|
|
299
|
+
});
|
|
300
|
+
} else {
|
|
301
|
+
console.warn('[auth+web] WEB_ROOT does not exist:', webRoot);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
app.listen(port, host, () => {
|
|
305
|
+
console.log(`[auth+web] listening on http://${host}:${port}`);
|
|
306
|
+
console.log(`[auth+web] webRoot: ${webRoot}`);
|
|
307
|
+
console.log(`[auth+web] db: ${dbPath}`);
|
|
308
|
+
console.log(`[auth+web] AI_API_BASE: ${aiApiBase}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
start();
|