@danieltmn/openbridge 0.3.0 → 0.5.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/CHANGELOG.md +46 -0
- package/README.md +116 -32
- package/docs/ARCHITECTURE.md +56 -11
- package/package.json +2 -1
- package/src/auth.js +44 -23
- package/src/bridge/bridge.js +44 -5
- package/src/cli.js +194 -9
- package/src/config.js +74 -4
- package/src/web/assets/app.js +142 -63
- package/src/web/assets/manifest.webmanifest +2 -0
- package/src/web/assets/sw.js +1 -1
- package/src/web/routes.js +48 -14
- package/src/web/server.js +7 -0
- package/src/web/templates/chat.html +14 -2
- package/src/web/templates/login.html +7 -3
package/src/cli.js
CHANGED
|
@@ -183,6 +183,12 @@ async function cmdInit(argv) {
|
|
|
183
183
|
console.log('Usa --force para reconfigurar (se pisan config.json/app.json).');
|
|
184
184
|
return 0;
|
|
185
185
|
}
|
|
186
|
+
if (paths.exists() && flags.force) {
|
|
187
|
+
const prev = config.readApp();
|
|
188
|
+
if ((prev.users || []).length > 1) {
|
|
189
|
+
console.log('aviso: --force deja un unico usuario admin; se pierden los demas usuarios.');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
186
192
|
|
|
187
193
|
if (stopRunningServer()) {
|
|
188
194
|
console.log('Habia un server corriendo; lo detuve para reconfigurar (reinicialo con `openbridge server`).');
|
|
@@ -235,12 +241,12 @@ async function cmdInit(argv) {
|
|
|
235
241
|
bridgeId: id,
|
|
236
242
|
bridgeName: name,
|
|
237
243
|
};
|
|
244
|
+
const adminName = flags.user || 'admin';
|
|
238
245
|
const appCfg = {
|
|
239
246
|
...config.DEFAULT_APP,
|
|
240
247
|
port,
|
|
241
248
|
host: '127.0.0.1',
|
|
242
|
-
|
|
243
|
-
password: config.hashPassword(password),
|
|
249
|
+
users: [config.makeUser(adminName, password, 'admin')],
|
|
244
250
|
csrfSecret,
|
|
245
251
|
bridgeToken,
|
|
246
252
|
vapid,
|
|
@@ -259,7 +265,7 @@ async function cmdInit(argv) {
|
|
|
259
265
|
console.log(' workspace : ' + workspace);
|
|
260
266
|
console.log(' puente : ' + id + ' (' + name + ')');
|
|
261
267
|
console.log(' URL local : http://127.0.0.1:' + port + '/chat.php');
|
|
262
|
-
console.log(' usuario : ' +
|
|
268
|
+
console.log(' usuario : ' + adminName);
|
|
263
269
|
console.log(' contrasena: ' + password + (generated ? ' (generada)' : ''));
|
|
264
270
|
console.log(' tunel : ' + provider);
|
|
265
271
|
console.log(' carpetas : ' + folders.length + ' en folders.json');
|
|
@@ -594,8 +600,8 @@ async function cmdLogs(argv) {
|
|
|
594
600
|
// ---------------------------------------------------------------------------
|
|
595
601
|
async function cmdBridge(argv) {
|
|
596
602
|
const { flags } = parseArgs(argv);
|
|
597
|
-
if (!paths.
|
|
598
|
-
console.error('No hay configuracion en ' + paths.home() + '. Corre primero: openbridge init');
|
|
603
|
+
if (!fs.existsSync(paths.configPath())) {
|
|
604
|
+
console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge init');
|
|
599
605
|
return 1;
|
|
600
606
|
}
|
|
601
607
|
// Permite apuntar a otro hub sin editar config.json a mano.
|
|
@@ -614,6 +620,41 @@ async function cmdBridge(argv) {
|
|
|
614
620
|
return new Promise((resolve) => child.on('exit', (code) => resolve(code || 0)));
|
|
615
621
|
}
|
|
616
622
|
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
// join: esta PC se suma como puente de un hub (otra PC con `openbridge server`).
|
|
625
|
+
// Guarda la URL/token/identidad en config.json y arranca el puente.
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
async function cmdJoin(argv) {
|
|
628
|
+
const { flags, _ } = parseArgs(argv);
|
|
629
|
+
const url = String(_[0] || flags.api || '').trim();
|
|
630
|
+
if (!url || !/^https?:\/\//i.test(url)) {
|
|
631
|
+
console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
|
|
632
|
+
console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
|
|
633
|
+
return 1;
|
|
634
|
+
}
|
|
635
|
+
paths.ensureDirs();
|
|
636
|
+
const cfg = config.readBridge();
|
|
637
|
+
cfg.apiUrl = url.replace(/\/+$/, '');
|
|
638
|
+
if (typeof flags.token === 'string') cfg.apiToken = flags.token;
|
|
639
|
+
if (typeof flags.id === 'string') cfg.bridgeId = sanitizeId(flags.id);
|
|
640
|
+
else if (!cfg.bridgeId) cfg.bridgeId = sanitizeId(os.hostname());
|
|
641
|
+
if (typeof flags.name === 'string' && flags.name.trim()) cfg.bridgeName = flags.name.trim().slice(0, 40);
|
|
642
|
+
else if (!cfg.bridgeName) cfg.bridgeName = cfg.bridgeId;
|
|
643
|
+
config.writeBridge(cfg);
|
|
644
|
+
|
|
645
|
+
console.log('Puente vinculado al hub: ' + cfg.apiUrl);
|
|
646
|
+
console.log(' id : ' + cfg.bridgeId);
|
|
647
|
+
console.log(' nombre : ' + cfg.bridgeName);
|
|
648
|
+
if (!cfg.apiToken) console.log('aviso: sin token (--token). Solo sirve si el hub no exige token del puente.');
|
|
649
|
+
if (flags['no-start']) {
|
|
650
|
+
console.log('Arrancalo cuando quieras con: openbridge bridge');
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
console.log('');
|
|
654
|
+
console.log('Arrancando el puente (Ctrl+C para salir)...');
|
|
655
|
+
return cmdBridge([]);
|
|
656
|
+
}
|
|
657
|
+
|
|
617
658
|
// ---------------------------------------------------------------------------
|
|
618
659
|
// import / reset (datos)
|
|
619
660
|
// ---------------------------------------------------------------------------
|
|
@@ -693,23 +734,114 @@ async function cmdPasswd(argv) {
|
|
|
693
734
|
return 1;
|
|
694
735
|
}
|
|
695
736
|
const app = config.readApp();
|
|
737
|
+
const name = String(flags.user || (app.users[0] && app.users[0].name) || 'admin');
|
|
738
|
+
const user = config.findUser(app, name);
|
|
739
|
+
if (!user) {
|
|
740
|
+
console.error('Usuario no encontrado: ' + name);
|
|
741
|
+
return 1;
|
|
742
|
+
}
|
|
696
743
|
const interactive = !flags.yes && process.stdin.isTTY;
|
|
697
744
|
let password = flags.password || '';
|
|
698
|
-
if (!password && interactive) password = await askHidden('Nueva contrasena (enter = generar): ');
|
|
745
|
+
if (!password && interactive) password = await askHidden('Nueva contrasena para "' + name + '" (enter = generar): ');
|
|
699
746
|
let generated = false;
|
|
700
747
|
if (!password) {
|
|
701
748
|
password = config.randomToken(12);
|
|
702
749
|
generated = true;
|
|
703
750
|
}
|
|
704
751
|
if (stopRunningServer()) console.log('Habia un server corriendo; lo detuve para aplicar el cambio.');
|
|
705
|
-
|
|
752
|
+
user.password = config.hashPassword(password);
|
|
753
|
+
user.pv = (parseInt(user.pv, 10) || 1) + 1; // invalida las sesiones abiertas
|
|
706
754
|
config.writeApp(app);
|
|
707
|
-
console.log('Contrasena actualizada para "' +
|
|
755
|
+
console.log('Contrasena actualizada para "' + user.name + '".');
|
|
708
756
|
console.log(' contrasena: ' + password + (generated ? ' (generada)' : ''));
|
|
709
757
|
console.log(' volve a arrancar: openbridge server');
|
|
710
758
|
return 0;
|
|
711
759
|
}
|
|
712
760
|
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
// users: administracion de usuarios y roles (solo desde la CLI local)
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
async function cmdUsers(argv) {
|
|
765
|
+
const { flags, _ } = parseArgs(argv);
|
|
766
|
+
if (!paths.exists()) {
|
|
767
|
+
console.error('No hay configuracion en ' + paths.home() + '. Corre primero: openbridge init');
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
const app = config.readApp();
|
|
771
|
+
const sub = String(_[0] || 'list').toLowerCase();
|
|
772
|
+
const name = _[1] || flags.name;
|
|
773
|
+
|
|
774
|
+
const persist = (msg) => {
|
|
775
|
+
if (stopRunningServer()) console.log('Habia un server corriendo; lo detuve para aplicar el cambio.');
|
|
776
|
+
config.writeApp(app);
|
|
777
|
+
console.log(msg);
|
|
778
|
+
console.log(' volve a arrancar: openbridge server');
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
if (sub === 'list' || sub === 'ls') {
|
|
782
|
+
const users = app.users || [];
|
|
783
|
+
if (!users.length) { console.log('(sin usuarios)'); return 0; }
|
|
784
|
+
for (const u of users) {
|
|
785
|
+
console.log((u.disabled ? 'x' : 'o') + ' ' + u.name.padEnd(16) + '[' + u.role + ']');
|
|
786
|
+
}
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (sub === 'add') {
|
|
791
|
+
if (!config.userValidName(name)) { console.error('Nombre invalido (letras, numeros, . _ -; 1-32).'); return 1; }
|
|
792
|
+
if (config.findUser(app, name)) { console.error('Ya existe el usuario "' + name + '".'); return 1; }
|
|
793
|
+
let password = flags.password || '';
|
|
794
|
+
if (!password && process.stdin.isTTY) password = await askHidden('Contrasena para "' + name + '" (enter = generar): ');
|
|
795
|
+
let generated = false;
|
|
796
|
+
if (!password) { password = config.randomToken(12); generated = true; }
|
|
797
|
+
const role = config.USER_ROLES.includes(flags.role) ? flags.role : 'user';
|
|
798
|
+
app.users.push(config.makeUser(name, password, role));
|
|
799
|
+
persist('Usuario "' + name + '" agregado [' + role + '].');
|
|
800
|
+
console.log(' contrasena: ' + password + (generated ? ' (generada)' : ''));
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (sub === 'remove' || sub === 'del' || sub === 'rm') {
|
|
805
|
+
const user = config.findUser(app, name);
|
|
806
|
+
if (!user) { console.error('Usuario no encontrado: ' + name); return 1; }
|
|
807
|
+
if (user.role === 'admin' && config.adminCount(app) <= 1) { console.error('No podes borrar al ultimo admin.'); return 1; }
|
|
808
|
+
app.users = app.users.filter((u) => u.id !== user.id);
|
|
809
|
+
persist('Usuario "' + user.name + '" borrado.');
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (sub === 'passwd' || sub === 'password') {
|
|
814
|
+
if (!name) { console.error('Uso: openbridge users passwd <nombre> [--password <clave>]'); return 1; }
|
|
815
|
+
const extra = ['--user', name];
|
|
816
|
+
if (flags.password) extra.push('--password', String(flags.password));
|
|
817
|
+
return cmdPasswd(extra);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (sub === 'role') {
|
|
821
|
+
const user = config.findUser(app, name);
|
|
822
|
+
if (!user) { console.error('Usuario no encontrado: ' + name); return 1; }
|
|
823
|
+
const role = String(_[2] || flags.role || '').toLowerCase();
|
|
824
|
+
if (!config.USER_ROLES.includes(role)) { console.error('Rol invalido (admin|user).'); return 1; }
|
|
825
|
+
if (user.role === 'admin' && role !== 'admin' && config.adminCount(app) <= 1) { console.error('No podes quitarle el admin al ultimo admin.'); return 1; }
|
|
826
|
+
user.role = role;
|
|
827
|
+
persist('Rol de "' + user.name + '" -> ' + role + '.');
|
|
828
|
+
return 0;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (sub === 'disable' || sub === 'enable') {
|
|
832
|
+
const user = config.findUser(app, name);
|
|
833
|
+
if (!user) { console.error('Usuario no encontrado: ' + name); return 1; }
|
|
834
|
+
const disabled = sub === 'disable';
|
|
835
|
+
if (disabled && user.role === 'admin' && config.adminCount(app) <= 1) { console.error('No podes deshabilitar al ultimo admin.'); return 1; }
|
|
836
|
+
user.disabled = disabled;
|
|
837
|
+
persist('Usuario "' + user.name + '" ' + (disabled ? 'deshabilitado' : 'habilitado') + '.');
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
console.error('Uso: openbridge users [list|add|remove|passwd|role|disable|enable] [nombre] [--role admin|user] [--password <clave>]');
|
|
842
|
+
return 1;
|
|
843
|
+
}
|
|
844
|
+
|
|
713
845
|
// ---------------------------------------------------------------------------
|
|
714
846
|
// autostart
|
|
715
847
|
// ---------------------------------------------------------------------------
|
|
@@ -818,6 +950,53 @@ async function cmdDoctor() {
|
|
|
818
950
|
return ok ? 0 : 1;
|
|
819
951
|
}
|
|
820
952
|
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
// update: compara con npm y, con --yes, actualiza la instalacion global.
|
|
955
|
+
// ---------------------------------------------------------------------------
|
|
956
|
+
function npmRun(args) {
|
|
957
|
+
return spawnSync('npm', args, { encoding: 'utf8', shell: process.platform === 'win32', windowsHide: true });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function cmdUpdate(argv) {
|
|
961
|
+
const { flags } = parseArgs(argv);
|
|
962
|
+
const tag = (typeof flags.tag === 'string' && flags.tag) ? flags.tag : 'latest';
|
|
963
|
+
const pkg = '@danieltmn/openbridge';
|
|
964
|
+
const spec = pkg + '@' + tag;
|
|
965
|
+
console.log('Version actual: ' + VERSION);
|
|
966
|
+
|
|
967
|
+
const view = npmRun(['view', spec, 'version', '--json']);
|
|
968
|
+
let latest = '';
|
|
969
|
+
try { latest = String(JSON.parse(view.stdout)).trim(); } catch (e) { latest = String(view.stdout || '').trim().replace(/"/g, ''); }
|
|
970
|
+
if (view.status !== 0 || !/^\d+\.\d+\.\d+/.test(latest)) {
|
|
971
|
+
console.error('No se pudo consultar npm (' + tag + '): ' + ((view.stderr || '').trim() || 'revisa tu conexion'));
|
|
972
|
+
return 1;
|
|
973
|
+
}
|
|
974
|
+
console.log('Disponible (' + tag + '): ' + latest);
|
|
975
|
+
if (latest === VERSION) {
|
|
976
|
+
console.log('Ya estas en la ultima version.');
|
|
977
|
+
return 0;
|
|
978
|
+
}
|
|
979
|
+
if (flags.check) {
|
|
980
|
+
console.log('Hay una version nueva. Corre: openbridge update --yes');
|
|
981
|
+
return 0;
|
|
982
|
+
}
|
|
983
|
+
if (!flags.yes && !flags.y) {
|
|
984
|
+
console.log('');
|
|
985
|
+
console.log('Para actualizar ahora:');
|
|
986
|
+
console.log(' openbridge update --yes (o) npm i -g ' + spec);
|
|
987
|
+
return 0;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
console.log('Actualizando a ' + spec + '...');
|
|
991
|
+
const r = spawnSync('npm', ['i', '-g', spec], { stdio: 'inherit', shell: process.platform === 'win32', windowsHide: true });
|
|
992
|
+
if (r.status !== 0) {
|
|
993
|
+
console.error('La actualizacion fallo. Proba a mano: npm i -g ' + spec);
|
|
994
|
+
return 1;
|
|
995
|
+
}
|
|
996
|
+
console.log('Listo. Reinicia el server para usar la version nueva: openbridge stop && openbridge server');
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
821
1000
|
// ---------------------------------------------------------------------------
|
|
822
1001
|
function usage() {
|
|
823
1002
|
console.log('OpenBridge ' + VERSION + ' — tu opencode en el celular, sin hosting');
|
|
@@ -826,17 +1005,20 @@ function usage() {
|
|
|
826
1005
|
console.log('');
|
|
827
1006
|
console.log(' init Configura la casa (workspace, contrasena, tunel)');
|
|
828
1007
|
console.log(' server Arranca en segundo plano (muestra el estado al levantar)');
|
|
829
|
-
console.log(' passwd Cambia la contrasena de acceso (--password <clave>)');
|
|
1008
|
+
console.log(' passwd Cambia la contrasena de acceso (--user <nombre> --password <clave>)');
|
|
1009
|
+
console.log(' users Usuarios y roles (list|add|remove|passwd|role|disable|enable)');
|
|
830
1010
|
console.log(' stop Detiene el server en segundo plano');
|
|
831
1011
|
console.log(' status Estado del server, puente y chats');
|
|
832
1012
|
console.log(' qr Muestra la URL (publica o local) como QR para el celular');
|
|
833
1013
|
console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
|
|
834
1014
|
console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
|
|
835
1015
|
console.log(' bridge Corre solo el puente (--api --token --id --name)');
|
|
1016
|
+
console.log(' join Vincula esta PC como puente de un hub (<url> --token --id --name)');
|
|
836
1017
|
console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');
|
|
837
1018
|
console.log(' reset Borra todos los chats/datos (--session <id> --yes)');
|
|
838
1019
|
console.log(' autostart Instala/quita el arranque automatico (install|remove)');
|
|
839
1020
|
console.log(' doctor Verifica Node, opencode, configuracion y puerto');
|
|
1021
|
+
console.log(' update Busca una version nueva en npm (--yes para actualizar)');
|
|
840
1022
|
console.log('');
|
|
841
1023
|
console.log('Opciones comunes: --dir <ruta> (casa portable; default: directorio actual)');
|
|
842
1024
|
console.log('init: --workspace --name --id --port --password --tunnel --domain --yes --force');
|
|
@@ -858,6 +1040,7 @@ async function main(argv) {
|
|
|
858
1040
|
switch (cmd) {
|
|
859
1041
|
case 'init': return cmdInit(args.slice(1));
|
|
860
1042
|
case 'passwd': case 'password': return cmdPasswd(args.slice(1));
|
|
1043
|
+
case 'users': case 'user': return cmdUsers(args.slice(1));
|
|
861
1044
|
case 'server': case 'start': return cmdServer(args.slice(1));
|
|
862
1045
|
case 'stop': return cmdStop();
|
|
863
1046
|
case 'status': return cmdStatus();
|
|
@@ -865,10 +1048,12 @@ async function main(argv) {
|
|
|
865
1048
|
case 'tunnel': return cmdTunnel(args.slice(1));
|
|
866
1049
|
case 'logs': return cmdLogs(args.slice(1));
|
|
867
1050
|
case 'bridge': return cmdBridge(args.slice(1));
|
|
1051
|
+
case 'join': return cmdJoin(args.slice(1));
|
|
868
1052
|
case 'import': return cmdImport(args.slice(1));
|
|
869
1053
|
case 'reset': return cmdReset(args.slice(1));
|
|
870
1054
|
case 'autostart': return cmdAutostart(args.slice(1));
|
|
871
1055
|
case 'doctor': return cmdDoctor();
|
|
1056
|
+
case 'update': return cmdUpdate(args.slice(1));
|
|
872
1057
|
case 'version': case '-v': case '--version': console.log(VERSION); return 0;
|
|
873
1058
|
case 'help': case '-h': case '--help': usage(); return 0;
|
|
874
1059
|
default:
|
package/src/config.js
CHANGED
|
@@ -27,14 +27,17 @@ const DEFAULT_APP = {
|
|
|
27
27
|
port: 8799,
|
|
28
28
|
host: '127.0.0.1',
|
|
29
29
|
baseUrl: '',
|
|
30
|
-
username: 'admin',
|
|
31
|
-
password: null, //
|
|
30
|
+
username: 'admin', // legado (migra a users[0])
|
|
31
|
+
password: null, // legado (migra a users[0])
|
|
32
|
+
users: [], // [{ id, name, role, password, pv, created, disabled }]
|
|
32
33
|
csrfSecret: '',
|
|
33
34
|
bridgeToken: '',
|
|
34
35
|
vapid: { publicKey: '', privateKey: '' },
|
|
35
36
|
tunnel: { provider: 'tunnelmole', domain: '' },
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
const USER_ROLES = ['admin', 'user'];
|
|
40
|
+
|
|
38
41
|
function readJsonFile(file, fallback) {
|
|
39
42
|
try {
|
|
40
43
|
const raw = fs.readFileSync(file, 'utf8');
|
|
@@ -62,10 +65,17 @@ function readApp() {
|
|
|
62
65
|
const data = { ...DEFAULT_APP, ...readJsonFile(paths.appConfigPath(), DEFAULT_APP) };
|
|
63
66
|
data.vapid = { ...DEFAULT_APP.vapid, ...(data.vapid || {}) };
|
|
64
67
|
data.tunnel = { ...DEFAULT_APP.tunnel, ...(data.tunnel || {}) };
|
|
68
|
+
normalizeApp(data);
|
|
65
69
|
return data;
|
|
66
70
|
}
|
|
67
71
|
function writeApp(cfg) {
|
|
68
|
-
|
|
72
|
+
const out = { ...cfg };
|
|
73
|
+
// Una vez migrado a `users`, no dejamos la contrasena legado suelta.
|
|
74
|
+
if (Array.isArray(out.users) && out.users.length) {
|
|
75
|
+
delete out.username;
|
|
76
|
+
delete out.password;
|
|
77
|
+
}
|
|
78
|
+
writeJsonFile(paths.appConfigPath(), out);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
function randomToken(bytes = 32) {
|
|
@@ -93,6 +103,65 @@ function verifyPassword(password, stored) {
|
|
|
93
103
|
return crypto.timingSafeEqual(calc, expected);
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Usuarios (multiusuario con roles admin|user). El modelo viejo de un solo
|
|
108
|
+
// `username`+`password` se migra solo a `users[0]`.
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
function userValidName(name) {
|
|
111
|
+
return typeof name === 'string' && /^[A-Za-z0-9._-]{1,32}$/.test(name);
|
|
112
|
+
}
|
|
113
|
+
function makeUser(name, password, role = 'user') {
|
|
114
|
+
return {
|
|
115
|
+
id: 'u_' + crypto.randomBytes(6).toString('hex'),
|
|
116
|
+
name: String(name),
|
|
117
|
+
role: USER_ROLES.includes(role) ? role : 'user',
|
|
118
|
+
password: hashPassword(password),
|
|
119
|
+
pv: 1,
|
|
120
|
+
created: new Date().toISOString(),
|
|
121
|
+
disabled: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Devuelve la lista de usuarios, sintetizando el admin legado si hace falta.
|
|
125
|
+
function userList(app) {
|
|
126
|
+
if (app && Array.isArray(app.users) && app.users.length) return app.users;
|
|
127
|
+
if (app && app.password && app.password.hash) {
|
|
128
|
+
return [{
|
|
129
|
+
id: 'u1',
|
|
130
|
+
name: (typeof app.username === 'string' && app.username) ? app.username : 'admin',
|
|
131
|
+
role: 'admin',
|
|
132
|
+
password: app.password,
|
|
133
|
+
pv: 1,
|
|
134
|
+
created: '',
|
|
135
|
+
disabled: false,
|
|
136
|
+
}];
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
function normalizeApp(app) {
|
|
141
|
+
if (!app || typeof app !== 'object') return app;
|
|
142
|
+
if (!Array.isArray(app.users)) app.users = [];
|
|
143
|
+
app.users = app.users.filter((u) => u && typeof u === 'object' && typeof u.name === 'string' && u.password);
|
|
144
|
+
if (!app.users.length) {
|
|
145
|
+
const migrated = userList(app);
|
|
146
|
+
if (migrated.length) app.users = migrated;
|
|
147
|
+
}
|
|
148
|
+
return app;
|
|
149
|
+
}
|
|
150
|
+
function findUser(app, name) {
|
|
151
|
+
const n = String(name || '').trim().toLowerCase();
|
|
152
|
+
if (n === '') return null;
|
|
153
|
+
return userList(app).find((u) => String(u.name).toLowerCase() === n) || null;
|
|
154
|
+
}
|
|
155
|
+
function userById(app, id) {
|
|
156
|
+
return userList(app).find((u) => u.id === id) || null;
|
|
157
|
+
}
|
|
158
|
+
function verifyUserPassword(user, password) {
|
|
159
|
+
return !!user && verifyPassword(password, user.password);
|
|
160
|
+
}
|
|
161
|
+
function adminCount(app) {
|
|
162
|
+
return userList(app).filter((u) => u.role === 'admin' && !u.disabled).length;
|
|
163
|
+
}
|
|
164
|
+
|
|
96
165
|
// Claves VAPID (P-256) en el formato que espera web-push (base64url).
|
|
97
166
|
function genVapid() {
|
|
98
167
|
const webpush = require('web-push');
|
|
@@ -101,8 +170,9 @@ function genVapid() {
|
|
|
101
170
|
}
|
|
102
171
|
|
|
103
172
|
module.exports = {
|
|
104
|
-
DEFAULT_BRIDGE, DEFAULT_APP,
|
|
173
|
+
DEFAULT_BRIDGE, DEFAULT_APP, USER_ROLES,
|
|
105
174
|
readBridge, writeBridge, readApp, writeApp,
|
|
106
175
|
randomToken, hashPassword, verifyPassword, genVapid,
|
|
176
|
+
userValidName, makeUser, userList, normalizeApp, findUser, userById, verifyUserPassword, adminCount,
|
|
107
177
|
readJsonFile, writeJsonFile,
|
|
108
178
|
};
|