@scrypted/server 0.4.6 → 0.4.8

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.

Potentially problematic release.


This version of @scrypted/server might be problematic. Click here for more details.

Files changed (49) hide show
  1. package/dist/db-types.js +1 -2
  2. package/dist/db-types.js.map +1 -1
  3. package/dist/plugin/acl.js +83 -0
  4. package/dist/plugin/acl.js.map +1 -0
  5. package/dist/plugin/plugin-api.js +28 -11
  6. package/dist/plugin/plugin-api.js.map +1 -1
  7. package/dist/plugin/plugin-device.js +4 -0
  8. package/dist/plugin/plugin-device.js.map +1 -1
  9. package/dist/plugin/plugin-host-api.js +0 -16
  10. package/dist/plugin/plugin-host-api.js.map +1 -1
  11. package/dist/plugin/plugin-host.js +13 -7
  12. package/dist/plugin/plugin-host.js.map +1 -1
  13. package/dist/plugin/plugin-http.js +1 -0
  14. package/dist/plugin/plugin-http.js.map +1 -1
  15. package/dist/plugin/plugin-remote-websocket.js +25 -14
  16. package/dist/plugin/plugin-remote-websocket.js.map +1 -1
  17. package/dist/plugin/plugin-remote-worker.js +3 -0
  18. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  19. package/dist/plugin/plugin-remote.js +41 -8
  20. package/dist/plugin/plugin-remote.js.map +1 -1
  21. package/dist/rpc.js +37 -8
  22. package/dist/rpc.js.map +1 -1
  23. package/dist/runtime.js +44 -14
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/scrypted-server-main.js +11 -11
  26. package/dist/scrypted-server-main.js.map +1 -1
  27. package/dist/services/plugin.js +1 -12
  28. package/dist/services/plugin.js.map +1 -1
  29. package/dist/services/users.js +46 -0
  30. package/dist/services/users.js.map +1 -0
  31. package/dist/usertoken.js +9 -5
  32. package/dist/usertoken.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/db-types.ts +2 -2
  35. package/src/plugin/acl.ts +104 -0
  36. package/src/plugin/plugin-api.ts +41 -25
  37. package/src/plugin/plugin-device.ts +6 -0
  38. package/src/plugin/plugin-host-api.ts +1 -20
  39. package/src/plugin/plugin-host.ts +21 -12
  40. package/src/plugin/plugin-http.ts +1 -0
  41. package/src/plugin/plugin-remote-websocket.ts +26 -17
  42. package/src/plugin/plugin-remote-worker.ts +3 -0
  43. package/src/plugin/plugin-remote.ts +49 -11
  44. package/src/rpc.ts +43 -9
  45. package/src/runtime.ts +48 -20
  46. package/src/scrypted-server-main.ts +11 -11
  47. package/src/services/plugin.ts +2 -12
  48. package/src/services/users.ts +43 -0
  49. package/src/usertoken.ts +13 -6
@@ -1,5 +1,6 @@
1
- import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, HttpRequest, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
1
+ import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
2
2
  import { RpcPeer, RPCResultError } from '../rpc';
3
+ import { AccessControls } from './acl';
3
4
  import { BufferSerializer } from './buffer-serializer';
4
5
  import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
5
6
  import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketConnection, WebSocketMethods, WebSocketSerializer } from './plugin-remote-websocket';
@@ -121,10 +122,6 @@ class EndpointManagerImpl implements EndpointManager {
121
122
  return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
122
123
  }
123
124
 
124
- async deliverPush(id: string, request: HttpRequest) {
125
- return this.api.deliverPush(id, request);
126
- }
127
-
128
125
  async getPath(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
129
126
  return `/endpoint/${this.getEndpoint(nativeId)}/${options?.public ? 'public/' : ''}`
130
127
  }
@@ -133,7 +130,7 @@ class EndpointManagerImpl implements EndpointManager {
133
130
  const protocol = options?.insecure ? 'http' : 'https';
134
131
  const port = await this.api.getComponent(options?.insecure ? 'SCRYPTED_INSECURE_PORT' : 'SCRYPTED_SECURE_PORT');
135
132
  const path = await this.getPath(nativeId, options);
136
- const url = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
133
+ const url = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
137
134
  return url;
138
135
  }
139
136
 
@@ -368,8 +365,48 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
368
365
  const getRemote = await peer.getParam('getRemote');
369
366
  const remote = await getRemote(api, pluginId) as PluginRemote;
370
367
 
371
- await remote.setSystemState(getSystemState());
368
+ const accessControls: AccessControls = peer.tags.acl;
369
+
370
+ const getAccessControlDeviceState = (id: string, state?: { [property: string]: SystemDeviceState } ) => {
371
+ state = state || getSystemState()[id];
372
+ if (accessControls && state) {
373
+ state = Object.assign({}, state);
374
+ for (const property of Object.keys(state)) {
375
+ if (accessControls.shouldRejectProperty(id, property))
376
+ delete state[property];
377
+ }
378
+ let interfaces: ScryptedInterface[] = state.interfaces?.value;
379
+ if (interfaces) {
380
+ interfaces = interfaces.filter(scryptedInterface => !accessControls.shouldRejectInterface(id, scryptedInterface));
381
+ state.interfaces = {
382
+ value: interfaces,
383
+ }
384
+ }
385
+ }
386
+ return state;
387
+ }
388
+
389
+ const getAccessControlSystemState = () => {
390
+ let state = getSystemState();
391
+ if (accessControls) {
392
+ state = Object.assign({}, state);
393
+ for (const id of Object.keys(state)) {
394
+ if (accessControls.shouldRejectDevice(id)) {
395
+ delete state[id];
396
+ continue;
397
+ }
398
+ state[id] = getAccessControlDeviceState(id, state[id]);
399
+ }
400
+ }
401
+
402
+ return state;
403
+ }
404
+
405
+ await remote.setSystemState(getAccessControlSystemState());
372
406
  api.listen((id, eventDetails, eventData) => {
407
+ if (accessControls?.shouldRejectEvent(eventDetails.property === ScryptedInterfaceProperty.id ? eventData : id, eventDetails))
408
+ return;
409
+
373
410
  // ScryptedDevice events will be handled specially and repropagated by the remote.
374
411
  if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice) {
375
412
  if (eventDetails.property === ScryptedInterfaceProperty.id) {
@@ -378,7 +415,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
378
415
  }
379
416
  else {
380
417
  // a change on anything else is a descriptor update
381
- remote.updateDeviceState(id, getSystemState()[id]);
418
+ remote.updateDeviceState(id, getAccessControlDeviceState(id));
382
419
  }
383
420
  return;
384
421
  }
@@ -429,15 +466,16 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
429
466
  const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
430
467
 
431
468
  peer.params.getRemote = async (api: PluginAPI, pluginId: string) => {
432
- websocketSerializer.WebSocket = createWebSocketClass((url: string, callbacks: WebSocketConnectCallbacks) => {
469
+ websocketSerializer.WebSocket = createWebSocketClass((connection, callbacks) => {
470
+ const { url } = connection;
433
471
  if (url.startsWith('io://') || url.startsWith('ws://')) {
434
472
  const id = url.substring('xx://'.length);
435
473
 
436
474
  ioSockets[id] = callbacks;
437
475
 
438
476
  callbacks.connect(undefined, {
439
- close: () => api.ioClose(id),
440
- send: (message: string) => api.ioSend(id, message),
477
+ close: (message) => connection.close(message),
478
+ send: (message) => connection.send(message),
441
479
  });
442
480
  }
443
481
  else {
package/src/rpc.ts CHANGED
@@ -4,19 +4,29 @@ type CompileFunction = (code: string, params?: ReadonlyArray<string>, options?:
4
4
  export function startPeriodicGarbageCollection() {
5
5
  if (!global.gc) {
6
6
  console.warn('rpc peer garbage collection not available: global.gc is not exposed.');
7
- return;
8
7
  }
8
+ let g: typeof global;
9
9
  try {
10
- const g = global;
11
- if (g.gc) {
12
- return setInterval(() => {
13
- g.gc!();
14
- }, 10000);
15
- }
10
+ g = global;
16
11
  }
17
12
  catch (e) {
18
-
19
13
  }
14
+
15
+ // periodically see if new objects were created or finalized,
16
+ // and collect gc if so.
17
+ let lastCollection = 0;
18
+ return setInterval(() => {
19
+ const now = Date.now();
20
+ const sinceLastCollection = now - lastCollection;
21
+ const remotesCreated = RpcPeer.remotesCreated;
22
+ RpcPeer.remotesCreated = 0;
23
+ const remotesCollected = RpcPeer.remotesCollected;
24
+ RpcPeer.remotesCollected = 0;
25
+ if (remotesCreated || remotesCollected || sinceLastCollection > 5 * 60 * 1000) {
26
+ lastCollection = now;
27
+ g?.gc?.();
28
+ }
29
+ }, 10000);
20
30
  }
21
31
 
22
32
  export interface RpcMessage {
@@ -219,8 +229,12 @@ export class RpcPeer {
219
229
  transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes();
220
230
  killed: Promise<void>;
221
231
  killedDeferred: Deferred;
232
+ tags: any = {};
222
233
 
223
234
  static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
235
+ static remotesCollected = 0;
236
+ static remotesCreated = 0;
237
+ static activeRpcPeer: RpcPeer;
224
238
 
225
239
  static isRpcProxy(value: any) {
226
240
  return !!value?.[RpcPeer.PROPERTY_PROXY_ID];
@@ -315,6 +329,8 @@ export class RpcPeer {
315
329
  }
316
330
 
317
331
  finalize(entry: LocalProxiedEntry) {
332
+ RpcPeer.remotesCollected++;
333
+
318
334
  delete this.remoteWeakProxies[entry.id];
319
335
  const rpcFinalize: RpcFinalize = {
320
336
  __local_proxy_id: entry.id,
@@ -378,6 +394,12 @@ export class RpcPeer {
378
394
  if (!proxy)
379
395
  proxy = this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods);
380
396
  proxy[RpcPeer.finalizerIdSymbol] = __remote_proxy_finalizer_id;
397
+
398
+ const deserializer = this.nameDeserializerMap.get(__remote_constructor_name);
399
+ if (deserializer) {
400
+ return deserializer.deserialize(proxy, deserializationContext);
401
+ }
402
+
381
403
  return proxy;
382
404
  }
383
405
 
@@ -471,6 +493,8 @@ export class RpcPeer {
471
493
  }
472
494
 
473
495
  newProxy(proxyId: string, proxyConstructorName: string, proxyProps: any, proxyOneWayMethods: string[]) {
496
+ RpcPeer.remotesCreated++;
497
+
474
498
  const localProxiedEntry: LocalProxiedEntry = {
475
499
  id: proxyId,
476
500
  finalizerId: undefined,
@@ -484,7 +508,17 @@ export class RpcPeer {
484
508
  return proxy;
485
509
  }
486
510
 
487
- async handleMessage(message: RpcMessage, deserializationContext?: any) {
511
+ handleMessage(message: RpcMessage, deserializationContext?: any) {
512
+ try {
513
+ RpcPeer.activeRpcPeer = this;
514
+ this.handleMessageInternal(message, deserializationContext);
515
+ }
516
+ finally {
517
+ RpcPeer.activeRpcPeer = undefined;
518
+ }
519
+ }
520
+
521
+ private async handleMessageInternal(message: RpcMessage, deserializationContext?: any) {
488
522
  if (Object.isFrozen(this.pendingResults))
489
523
  return;
490
524
 
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Device, DeviceInformation, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedNativeId } from '@scrypted/types';
1
+ import { Device, DeviceInformation, DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, ScryptedDevice, ScryptedInterface, ScryptedInterfaceMethod, ScryptedInterfaceProperty, ScryptedNativeId, ScryptedUser as SU } from '@scrypted/types';
2
2
  import AdmZip from 'adm-zip';
3
3
  import axios from 'axios';
4
4
  import * as io from 'engine.io';
@@ -14,13 +14,14 @@ import { PassThrough } from 'stream';
14
14
  import tar from 'tar';
15
15
  import { URL } from "url";
16
16
  import WebSocket, { Server as WebSocketServer } from "ws";
17
- import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
17
+ import { Plugin, PluginDevice, ScryptedAlert, ScryptedUser } from './db-types';
18
18
  import { createResponseInterface } from './http-interfaces';
19
19
  import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults';
20
20
  import { IOServer } from './io';
21
21
  import { Level } from './level';
22
22
  import { LogEntry, Logger, makeAlertId } from './logger';
23
23
  import { hasMixinCycle } from './mixin/mixin-cycle';
24
+ import { AccessControls } from './plugin/acl';
24
25
  import { PluginDebug } from './plugin/plugin-debug';
25
26
  import { PluginDeviceProxyHandler } from './plugin/plugin-device';
26
27
  import { PluginHost } from './plugin/plugin-host';
@@ -34,6 +35,7 @@ import { CORSControl, CORSServer } from './services/cors';
34
35
  import { Info } from './services/info';
35
36
  import { PluginComponent } from './services/plugin';
36
37
  import { ServiceControl } from './services/service-control';
38
+ import { UsersService } from './services/users';
37
39
  import { getState, ScryptedStateManager, setState } from './state';
38
40
 
39
41
  interface DeviceProxyPair {
@@ -76,6 +78,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
76
78
  alerts = new Alerts(this);
77
79
  corsControl = new CORSControl(this);
78
80
  addressSettings = new AddressSettings(this);
81
+ usersService = new UsersService(this);
79
82
 
80
83
  constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
81
84
  super(app);
@@ -91,6 +94,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
91
94
  });
92
95
 
93
96
  app.all('/engine.io/shell', (req, res) => {
97
+ if (res.locals.aclId) {
98
+ res.writeHead(401);
99
+ res.end();
100
+ return;
101
+ }
94
102
  this.shellHandler(req, res);
95
103
  });
96
104
 
@@ -251,21 +259,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
251
259
  };
252
260
  }
253
261
 
254
- async deliverPush(endpoint: string, request: HttpRequest) {
255
- const { pluginHost, pluginDevice } = await this.getPluginForEndpoint(endpoint);
256
- if (!pluginDevice) {
257
- console.error('plugin device missing for', endpoint);
258
- return;
259
- }
260
-
261
- if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.PushHandler)) {
262
- return;
263
- }
264
-
265
- const handler = this.getDevice<PushHandler>(pluginDevice._id);
266
- return handler.onPush(request);
267
- }
268
-
269
262
  async shellHandler(req: Request, res: Response) {
270
263
  const isUpgrade = isConnectionUpgrade(req.headers);
271
264
 
@@ -345,7 +338,14 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
345
338
  });
346
339
 
347
340
  // @ts-expect-error
348
- await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`));
341
+ await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`, {
342
+ send(message) {
343
+ ws.send(message);
344
+ },
345
+ close(message) {
346
+ ws.close();
347
+ },
348
+ }));
349
349
  }
350
350
 
351
351
  async getComponent(componentId: string): Promise<any> {
@@ -370,6 +370,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
370
370
  return this.corsControl;
371
371
  case 'addresses':
372
372
  return this.addressSettings;
373
+ case "users":
374
+ return this.usersService;
373
375
  }
374
376
  }
375
377
 
@@ -385,9 +387,31 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
385
387
  return packageJson;
386
388
  }
387
389
 
388
- handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
390
+ async handleEngineIOEndpoint(req: Request, res: ServerResponse & { locals: any }, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
389
391
  const { pluginHost, pluginDevice } = pluginData;
390
392
 
393
+ const { username } = res.locals;
394
+ let accessControls: AccessControls;
395
+ if (username) {
396
+ const user = await this.datastore.tryGet(ScryptedUser, username);
397
+ if (user.aclId) {
398
+ const accessControl = this.getDevice<SU>(user.aclId);
399
+ try {
400
+ const acls = await accessControl.getScryptedUserAccessControl();
401
+ if (acls) {
402
+ accessControls = new AccessControls(acls);
403
+ if (accessControls.shouldRejectMethod(pluginDevice._id, ScryptedInterfaceMethod.onConnection))
404
+ accessControls.deny();
405
+ }
406
+ }
407
+ catch (e) {
408
+ res.writeHead(401);
409
+ res.end();
410
+ return;
411
+ }
412
+ }
413
+ }
414
+
391
415
  if (!pluginHost || !pluginDevice) {
392
416
  console.error('plugin does not exist or is still starting up.');
393
417
  res.writeHead(500);
@@ -398,6 +422,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
398
422
  (req as any).scrypted = {
399
423
  endpointRequest,
400
424
  pluginDevice,
425
+ accessControls,
401
426
  };
402
427
  if ((req as any).upgradeHead)
403
428
  pluginHost.io.handleUpgrade(req, res.socket, (req as any).upgradeHead)
@@ -628,7 +653,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
628
653
  this.plugins[pluginId] = pluginHost;
629
654
 
630
655
  for (const pluginDevice of pluginDevices) {
631
- this.getDevice(pluginDevice._id)?.probe().catch(() => {});
656
+ this.getDevice(pluginDevice._id)?.probe().catch(() => { });
632
657
  }
633
658
 
634
659
  return pluginHost;
@@ -684,6 +709,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
684
709
  continue;
685
710
  await this.removeDevice(provided);
686
711
  }
712
+ const providerId = device.state?.providerId?.value;
687
713
  device.state = undefined;
688
714
 
689
715
  this.invalidatePluginDevice(device._id);
@@ -706,6 +732,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
706
732
  // notify the plugin that a device was removed.
707
733
  const plugin = this.plugins[device.pluginId];
708
734
  await plugin.remote.setNativeId(device.nativeId, undefined, undefined);
735
+ const provider = this.getDevice<DeviceProvider>(providerId);
736
+ await provider?.releaseDevice(device._id, device.nativeId);
709
737
  }
710
738
  catch (e) {
711
739
  // may throw if the plugin is killed, etc.
@@ -25,6 +25,7 @@ import { PluginError } from './plugin/plugin-error';
25
25
  import { getScryptedVolume } from './plugin/plugin-volume';
26
26
  import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
27
27
  import os from 'os';
28
+ import { setScryptedUserPassword } from './services/users';
28
29
 
29
30
  if (!semver.gte(process.version, '16.0.0')) {
30
31
  throw new Error('"node" version out of date. Please update node to v16 or higher.')
@@ -123,7 +124,8 @@ async function start() {
123
124
  realm: 'Scrypted',
124
125
  }, async (username, password, callback) => {
125
126
  const user = await db.tryGet(ScryptedUser, username);
126
- if (!user) {
127
+ // disallow basic auth for non-admin as it can deploy plugins, etc.
128
+ if (!user || user.aclId) {
127
129
  callback(false);
128
130
  return;
129
131
  }
@@ -188,7 +190,7 @@ async function start() {
188
190
 
189
191
  const userToken = getSignedLoginUserToken(req);
190
192
  if (userToken) {
191
- const { username } = userToken;
193
+ const { username, aclId } = userToken;
192
194
 
193
195
  // this database lookup on every web request is not necessary, the cookie
194
196
  // itself is the auth, and is signed. furthermore, this is currently
@@ -202,6 +204,7 @@ async function start() {
202
204
  // }
203
205
 
204
206
  res.locals.username = username;
207
+ res.locals.aclId = aclId;
205
208
  }
206
209
  else if (req.headers.authorization?.startsWith('Bearer ')) {
207
210
  const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
@@ -216,6 +219,7 @@ async function start() {
216
219
  const userToken = validateToken(tokenPart);
217
220
  if (userToken)
218
221
  res.locals.username = userToken.username;
222
+ res.locals.aclId = userToken.aclId;
219
223
  }
220
224
  }
221
225
  }
@@ -240,7 +244,7 @@ async function start() {
240
244
 
241
245
  // verify all plugin related requests have some sort of auth
242
246
  app.all('/web/component/*', (req, res, next) => {
243
- if (!res.locals.username) {
247
+ if (!res.locals.username || res.locals.aclId) {
244
248
  res.status(401);
245
249
  res.send('Not Authorized');
246
250
  return;
@@ -466,7 +470,7 @@ async function start() {
466
470
  return;
467
471
  }
468
472
 
469
- const userToken = new UserToken(username, timestamp, maxAge);
473
+ const userToken = new UserToken(username, user.aclId, timestamp, maxAge);
470
474
  const login_user_token = userToken.toString();
471
475
  res.cookie(getLoginUserToken(req.secure), login_user_token, {
472
476
  maxAge,
@@ -476,9 +480,7 @@ async function start() {
476
480
  });
477
481
 
478
482
  if (change_password) {
479
- user.salt = crypto.randomBytes(64).toString('base64');
480
- user.passwordHash = crypto.createHash('sha256').update(user.salt + change_password).digest().toString('hex');
481
- user.passwordDate = timestamp;
483
+ setScryptedUserPassword(user, change_password, timestamp);
482
484
  await db.upsert(user);
483
485
  }
484
486
 
@@ -502,14 +504,12 @@ async function start() {
502
504
 
503
505
  const user = new ScryptedUser();
504
506
  user._id = username;
505
- user.salt = crypto.randomBytes(64).toString('base64');
506
- user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
507
- user.passwordDate = timestamp;
507
+ setScryptedUserPassword(user, password, timestamp);
508
508
  user.token = crypto.randomBytes(16).toString('hex');
509
509
  await db.upsert(user);
510
510
  hasLogin = true;
511
511
 
512
- const userToken = new UserToken(username, timestamp);
512
+ const userToken = new UserToken(username, user.aclId, timestamp);
513
513
  const login_user_token = userToken.toString();
514
514
  res.cookie(getLoginUserToken(req.secure), login_user_token, {
515
515
  maxAge,
@@ -29,12 +29,10 @@ export class PluginComponent {
29
29
  await this.reload(pluginDevice.pluginId);
30
30
  }
31
31
 
32
- getNativeId(id: string) {
33
- return this.scrypted.findPluginDeviceById(id)?.nativeId;
34
- }
35
32
  getStorage(id: string) {
36
33
  return this.scrypted.findPluginDeviceById(id)?.storage || {};
37
34
  }
35
+
38
36
  async setStorage(id: string, storage: { [key: string]: string }) {
39
37
  const pluginDevice = this.scrypted.findPluginDeviceById(id);
40
38
  pluginDevice.storage = storage;
@@ -65,17 +63,9 @@ export class PluginComponent {
65
63
  async getIdForNativeId(pluginId: string, nativeId: ScryptedNativeId) {
66
64
  return this.scrypted.findPluginDevice(pluginId, nativeId)?._id;
67
65
  }
68
- /**
69
- * @deprecated available as device.pluginId now.
70
- * Remove at some point after core/ui rolls out 6/20/2022.
71
- */
72
- async getPluginId(id: string) {
73
- const pluginDevice = this.scrypted.findPluginDeviceById(id);
74
- return pluginDevice.pluginId;
75
- }
76
66
  async reload(pluginId: string) {
77
67
  const plugin = await this.scrypted.datastore.tryGet(Plugin, pluginId);
78
- await this.scrypted.runPlugin(plugin);
68
+ this.scrypted.runPlugin(plugin);
79
69
  }
80
70
  async kill(pluginId: string) {
81
71
  return this.scrypted.plugins[pluginId]?.kill();
@@ -0,0 +1,43 @@
1
+ import { ScryptedUser } from "../db-types";
2
+ import { ScryptedRuntime } from "../runtime";
3
+ import crypto from 'crypto';
4
+
5
+ export class UsersService {
6
+ constructor(public scrypted: ScryptedRuntime) {
7
+ }
8
+
9
+ async getAllUsers() {
10
+ const users: ScryptedUser[] = [];
11
+ for await (const user of this.scrypted.datastore.getAll(ScryptedUser)) {
12
+ users.push(user);
13
+ }
14
+
15
+ return users.map(user => ({
16
+ username: user._id,
17
+ admin: !user.aclId,
18
+ }));
19
+ }
20
+
21
+ async removeUser(username: string) {
22
+ await this.scrypted.datastore.removeId(ScryptedUser, username);
23
+ }
24
+
25
+ async removeAllUsers() {
26
+ await this.scrypted.datastore.removeAll(ScryptedUser);
27
+ }
28
+
29
+ async addUser(username: string, password: string, aclId: string) {
30
+ const user = new ScryptedUser();
31
+ user._id = username;
32
+ user.aclId = aclId;
33
+ setScryptedUserPassword(user, password, Date.now());
34
+ await this.scrypted.datastore.upsert(user);
35
+ }
36
+ }
37
+
38
+ export function setScryptedUserPassword(user: ScryptedUser, password: string, timestamp: number) {
39
+ user.salt = crypto.randomBytes(64).toString('base64');
40
+ user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
41
+ user.passwordDate = timestamp;
42
+ user.token = crypto.randomBytes(16).toString('hex');
43
+ }
package/src/usertoken.ts CHANGED
@@ -2,21 +2,27 @@ export const ONE_DAY_MILLISECONDS = 86400000;
2
2
  export const ONE_YEAR_MILLISECONDS = ONE_DAY_MILLISECONDS * 365;
3
3
 
4
4
  export class UserToken {
5
- constructor(public username: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
5
+ constructor(public username: string, public aclId: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
6
6
  }
7
7
 
8
8
  static validateToken(token: string): UserToken {
9
- let json: any;
9
+ let json: {
10
+ u: string,
11
+ a: string,
12
+ t: number,
13
+ d: number,
14
+ };
10
15
  try {
11
16
  json = JSON.parse(token);
12
17
  }
13
18
  catch (e) {
14
19
  throw new Error('Token malformed, unparseable.');
15
20
  }
16
- let { u, t, d } = json;
21
+ let { u, a, t, d } = json;
17
22
  u = u?.toString();
18
- t = parseInt(t);
19
- d = parseInt(d);
23
+ t = parseInt(t?.toString());
24
+ d = parseInt(d?.toString());
25
+ a = a?.toString();
20
26
  if (!u || !t || !d)
21
27
  throw new Error('Token malformed, missing properties.');
22
28
  if (d > ONE_YEAR_MILLISECONDS)
@@ -25,12 +31,13 @@ export class UserToken {
25
31
  throw new Error('Token from the future.');
26
32
  if (t + d < Date.now())
27
33
  throw new Error('Token expired.');
28
- return new UserToken(u, t, d);
34
+ return new UserToken(u, a, t, d);
29
35
  }
30
36
 
31
37
  toString(): string {
32
38
  return JSON.stringify({
33
39
  u: this.username,
40
+ a: this.aclId,
34
41
  t: this.timestamp,
35
42
  d: this.duration,
36
43
  })