neuroalpha 0.3.17 → 0.5.2
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/config/certs/russian_trusted_root_ca.pem +33 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -4
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +2 -2
- package/package.json +60 -53
- package/public/activate.html +36 -0
- package/public/activate.js +33 -0
- package/public/admin.html +126 -7
- package/public/app.js +589 -66
- package/public/index.html +48 -21
- package/public/landing.html +106 -37
- package/public/legal.html +4 -4
- package/public/styles.css +152 -36
- package/server/api/routes.js +308 -37
- package/server/auth/index.js +79 -15
- package/server/brokers/index.js +1095 -168
- package/server/build-info.js +7 -0
- package/server/config.js +13 -6
- package/server/data/moex.js +6 -4
- package/server/db.js +22 -18
- package/server/desktop-entry.js +103 -0
- package/server/hub.js +28 -14
- package/server/index.js +82 -12
- package/server/instance/agent.js +259 -0
- package/server/instance/central.js +491 -0
- package/server/instance/identity.js +79 -0
- package/server/instance/provision.js +166 -0
- package/server/instance/signing.js +71 -0
- package/server/model/edge.js +19 -0
- package/server/model/forecast.js +107 -57
- package/server/model/live.js +1 -1
- package/server/model/models.js +12 -3
- package/server/model/research.js +218 -30
- package/server/notify/index.js +51 -16
- package/server/role.js +44 -0
- package/server/security.js +40 -0
- package/server/strategist/index.js +86 -47
- package/server/strategist/tools.js +231 -42
- package/server/tls-ca.js +32 -0
package/server/auth/index.js
CHANGED
|
@@ -5,9 +5,19 @@ import path from 'node:path';
|
|
|
5
5
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
6
|
import { ROOT, DATA_DIR, DEFAULT_CLOUD_URL } from '../config.js';
|
|
7
7
|
import { db, kvGet, kvSet, registerUidResolver } from '../db.js';
|
|
8
|
+
import { IS_INSTANCE, IS_DESKTOP } from '../role.js';
|
|
9
|
+
import { currentInstanceLicense } from '../instance/agent.js';
|
|
10
|
+
import { isActivated } from '../instance/identity.js';
|
|
8
11
|
|
|
9
12
|
const ctx = new AsyncLocalStorage();
|
|
10
|
-
|
|
13
|
+
// Фоновые задачи сервера (без контекста запроса) работают от имени владельца (id = 1).
|
|
14
|
+
// Запрос без авторизованного пользователя получает uid = 0 — изолированное пустое пространство данных.
|
|
15
|
+
const resolveUid = () => {
|
|
16
|
+
const store = ctx.getStore();
|
|
17
|
+
if (!store) return 1;
|
|
18
|
+
return store.user?.id || 0;
|
|
19
|
+
};
|
|
20
|
+
registerUidResolver(resolveUid);
|
|
11
21
|
|
|
12
22
|
// ---------- шифрование секретов (AES-256-GCM) ----------
|
|
13
23
|
let masterKey = null;
|
|
@@ -66,14 +76,39 @@ export function hashPassword(password) {
|
|
|
66
76
|
return `${salt}:${hash}`;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
export function verifyPassword(password, stored) {
|
|
79
|
+
export async function verifyPassword(password, stored) {
|
|
70
80
|
if (!stored || !stored.includes(':')) return false;
|
|
71
81
|
const [salt, hex] = stored.split(':');
|
|
72
|
-
const hash =
|
|
82
|
+
const hash = await new Promise((resolve, reject) => {
|
|
83
|
+
crypto.scrypt(String(password), salt, 64, (err, key) => (err ? reject(err) : resolve(key)));
|
|
84
|
+
});
|
|
73
85
|
const target = Buffer.from(hex, 'hex');
|
|
74
86
|
return hash.length === target.length && crypto.timingSafeEqual(hash, target);
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
// ---------- ограничение попыток входа (по IP и по email) ----------
|
|
90
|
+
const LOGIN_WINDOW_MS = 15 * 60000;
|
|
91
|
+
const LOGIN_MAX_FAILS = 8;
|
|
92
|
+
const loginFails = new Map(); // key -> { count, firstAt }
|
|
93
|
+
|
|
94
|
+
function loginBlockedFor(key) {
|
|
95
|
+
const rec = loginFails.get(key);
|
|
96
|
+
if (!rec) return 0;
|
|
97
|
+
const left = rec.firstAt + LOGIN_WINDOW_MS - Date.now();
|
|
98
|
+
if (left <= 0) { loginFails.delete(key); return 0; }
|
|
99
|
+
return rec.count >= LOGIN_MAX_FAILS ? left : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function noteLoginFail(key) {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const rec = loginFails.get(key);
|
|
105
|
+
if (!rec || now - rec.firstAt > LOGIN_WINDOW_MS) loginFails.set(key, { count: 1, firstAt: now });
|
|
106
|
+
else rec.count += 1;
|
|
107
|
+
if (loginFails.size > 50000) {
|
|
108
|
+
for (const [k, v] of loginFails) if (now - v.firstAt > LOGIN_WINDOW_MS) loginFails.delete(k);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
77
112
|
// ---------- пользователи, тарифные планы (Инвестор, Профи, VIP + режим Хозяин) и сессии ----------
|
|
78
113
|
export const PLANS = {
|
|
79
114
|
none: {
|
|
@@ -87,7 +122,6 @@ export const PLANS = {
|
|
|
87
122
|
allowShorts: false,
|
|
88
123
|
allowVipBoost: false,
|
|
89
124
|
allowResearch: false,
|
|
90
|
-
maxAnnualReturnPct: 0,
|
|
91
125
|
maxChatPerDay: 10,
|
|
92
126
|
},
|
|
93
127
|
investor: {
|
|
@@ -101,7 +135,6 @@ export const PLANS = {
|
|
|
101
135
|
allowShorts: false,
|
|
102
136
|
allowVipBoost: false,
|
|
103
137
|
allowResearch: false,
|
|
104
|
-
maxAnnualReturnPct: 60,
|
|
105
138
|
maxChatPerDay: 200,
|
|
106
139
|
},
|
|
107
140
|
pro: {
|
|
@@ -115,7 +148,6 @@ export const PLANS = {
|
|
|
115
148
|
allowShorts: true,
|
|
116
149
|
allowVipBoost: false,
|
|
117
150
|
allowResearch: false,
|
|
118
|
-
maxAnnualReturnPct: 120,
|
|
119
151
|
maxChatPerDay: 1000,
|
|
120
152
|
},
|
|
121
153
|
vip: {
|
|
@@ -130,7 +162,7 @@ export const PLANS = {
|
|
|
130
162
|
allowShorts: true,
|
|
131
163
|
allowVipBoost: true,
|
|
132
164
|
allowResearch: false,
|
|
133
|
-
|
|
165
|
+
allowDesktop: true, // установка на свой компьютер — по договору (VIP)
|
|
134
166
|
maxChatPerDay: 5000,
|
|
135
167
|
},
|
|
136
168
|
owner: {
|
|
@@ -145,7 +177,7 @@ export const PLANS = {
|
|
|
145
177
|
allowShorts: true,
|
|
146
178
|
allowVipBoost: true,
|
|
147
179
|
allowResearch: true,
|
|
148
|
-
|
|
180
|
+
allowDesktop: true,
|
|
149
181
|
maxChatPerDay: 10000,
|
|
150
182
|
},
|
|
151
183
|
};
|
|
@@ -227,6 +259,15 @@ async function patchUserLicense(cleanKey, baseUrl) {
|
|
|
227
259
|
|
|
228
260
|
export function effectivePlan(user) {
|
|
229
261
|
if (!user) return PLANS.none;
|
|
262
|
+
// Машина клиента: тариф только из действующей подписанной лицензии центра
|
|
263
|
+
if (IS_INSTANCE) {
|
|
264
|
+
if (user.id !== 1) return PLANS.none;
|
|
265
|
+
const lic = currentInstanceLicense();
|
|
266
|
+
if (!lic || lic.revoked) return PLANS.none;
|
|
267
|
+
const p = PLANS[lic.plan] || PLANS.none;
|
|
268
|
+
if (p.id !== 'none' && lic.planUntil > 0 && lic.planUntil < Date.now()) return PLANS.none;
|
|
269
|
+
return p;
|
|
270
|
+
}
|
|
230
271
|
if (process.env.CLOUD_MODE !== '1' && user.id === 1) {
|
|
231
272
|
if (user.plan && PLANS[user.plan] && user.plan !== 'owner') {
|
|
232
273
|
if (user.plan !== 'none' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.none;
|
|
@@ -286,13 +327,24 @@ export function getUserById(id) {
|
|
|
286
327
|
|
|
287
328
|
export function currentUser() {
|
|
288
329
|
const store = ctx.getStore();
|
|
289
|
-
if (store
|
|
330
|
+
if (store) return store.user || null;
|
|
290
331
|
return getUserById(1);
|
|
291
332
|
}
|
|
292
333
|
|
|
293
334
|
export function currentUserId() {
|
|
294
|
-
|
|
295
|
-
|
|
335
|
+
return resolveUid();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Локальный режим без входа допускается только с этого же компьютера (не из локальной сети и не через прокси).
|
|
339
|
+
// LOCAL_TRUST_LAN=1 возвращает прежнее поведение (доступ владельца любому устройству в сети).
|
|
340
|
+
export function isLoopbackRequest(req) {
|
|
341
|
+
if (process.env.LOCAL_TRUST_LAN === '1') return true;
|
|
342
|
+
if (req?.headers?.['x-forwarded-for'] || req?.headers?.['x-real-ip'] || req?.headers?.forwarded) return false;
|
|
343
|
+
const addr = String(req?.socket?.remoteAddress || '');
|
|
344
|
+
if (!(addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1')) return false;
|
|
345
|
+
// защита от DNS-rebinding: чужой домен, указывающий на 127.0.0.1, не получает доступ владельца
|
|
346
|
+
const host = String(req?.headers?.host || '').toLowerCase().replace(/:\d+$/, '');
|
|
347
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
|
|
296
348
|
}
|
|
297
349
|
|
|
298
350
|
export function runWithUser(user, fn) {
|
|
@@ -326,8 +378,10 @@ export function userFromRequest(req) {
|
|
|
326
378
|
if (u) return u;
|
|
327
379
|
}
|
|
328
380
|
}
|
|
329
|
-
// В локальном режиме (без CLOUD_MODE=1)
|
|
330
|
-
|
|
381
|
+
// В локальном режиме (без CLOUD_MODE=1) владелец (id = 1) открывается без входа только с этого компьютера
|
|
382
|
+
// Установка на компьютер клиента после активации — так же, без пароля только с этого компьютера
|
|
383
|
+
const localTrusted = !IS_INSTANCE || (IS_DESKTOP && isActivated());
|
|
384
|
+
if (process.env.CLOUD_MODE !== '1' && localTrusted && cookies.sid !== 'logged_out' && isLoopbackRequest(req)) {
|
|
331
385
|
return getUserById(1);
|
|
332
386
|
}
|
|
333
387
|
return null;
|
|
@@ -359,10 +413,20 @@ export function registerUser({ email, password, name }) {
|
|
|
359
413
|
return getUserById(Number(r.lastInsertRowid));
|
|
360
414
|
}
|
|
361
415
|
|
|
362
|
-
export function loginUser({ email, password }) {
|
|
416
|
+
export async function loginUser({ email, password }, { ip = '' } = {}) {
|
|
363
417
|
const cleanEmail = String(email || '').trim().toLowerCase();
|
|
418
|
+
const keys = [`e:${cleanEmail}`, ...(ip ? [`ip:${ip}`] : [])];
|
|
419
|
+
const blockedMs = Math.max(...keys.map(loginBlockedFor));
|
|
420
|
+
if (blockedMs > 0) {
|
|
421
|
+
throw new Error(`Слишком много неудачных попыток входа. Повторите через ${Math.ceil(blockedMs / 60000)} мин.`);
|
|
422
|
+
}
|
|
364
423
|
const u = db.prepare('SELECT * FROM users WHERE email = ?').get(cleanEmail);
|
|
365
|
-
|
|
424
|
+
const ok = u ? await verifyPassword(password, u.pass_hash) : false;
|
|
425
|
+
if (!ok) {
|
|
426
|
+
for (const k of keys) noteLoginFail(k);
|
|
427
|
+
throw new Error('Неверный email или пароль');
|
|
428
|
+
}
|
|
429
|
+
for (const k of keys) loginFails.delete(k);
|
|
366
430
|
return u;
|
|
367
431
|
}
|
|
368
432
|
|