diff --git a/docs/api/child.md b/docs/api/child.md index 93371c2..5ae4a66 100644 --- a/docs/api/child.md +++ b/docs/api/child.md @@ -16,8 +16,7 @@ On a invalid request body: HTTP status code 400 Bad request On a invalid add device token: HTTP status code 401 Unauthorized -On success: a JSON object with the properties ``deviceAuthToken`` and ``ownDeviceId``, -both of the type string +On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response) ## POST /child/update-primary-device diff --git a/docs/api/parent.md b/docs/api/parent.md index b1c7d1f..556157c 100644 --- a/docs/api/parent.md +++ b/docs/api/parent.md @@ -44,7 +44,7 @@ If the mail auth token is invalid/ expired: HTTP status code 401 Unauthorized If there is already a user with the mail address of the mail auth token: HTTP status code 409 Conflict -On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string) +On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response) ## POST /parent/sign-in-into-family @@ -60,7 +60,7 @@ On a invalid request body: HTTP status code 400 Bad Request If there is no user with the mail address of the mail auth token: HTTP status code 409 Conflict -On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string) +On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response) ## POST /parent/can-recover-password diff --git a/docs/schema/CreateFamilyByMailTokenRequest.schema.json b/docs/schema/CreateFamilyByMailTokenRequest.schema.json index 8860532..8317b23 100644 --- a/docs/schema/CreateFamilyByMailTokenRequest.schema.json +++ b/docs/schema/CreateFamilyByMailTokenRequest.schema.json @@ -18,6 +18,9 @@ }, "parentName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, diff --git a/docs/schema/RegisterChildDeviceRequest.schema.json b/docs/schema/RegisterChildDeviceRequest.schema.json index 3c7672f..71afe1c 100644 --- a/docs/schema/RegisterChildDeviceRequest.schema.json +++ b/docs/schema/RegisterChildDeviceRequest.schema.json @@ -9,6 +9,9 @@ }, "deviceName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, diff --git a/docs/schema/SignIntoFamilyRequest.schema.json b/docs/schema/SignIntoFamilyRequest.schema.json index 8faf9a4..adced5b 100644 --- a/docs/schema/SignIntoFamilyRequest.schema.json +++ b/docs/schema/SignIntoFamilyRequest.schema.json @@ -9,6 +9,9 @@ }, "deviceName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, diff --git a/docs/schema/createfamilybymailtokenrequest-properties-clientlevel.md b/docs/schema/createfamilybymailtokenrequest-properties-clientlevel.md new file mode 100644 index 0000000..7768c8d --- /dev/null +++ b/docs/schema/createfamilybymailtokenrequest-properties-clientlevel.md @@ -0,0 +1,15 @@ +# Untitled number in CreateFamilyByMailTokenRequest Schema + +```txt +https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [CreateFamilyByMailTokenRequest.schema.json\*](CreateFamilyByMailTokenRequest.schema.json "open original schema") | + +## clientLevel Type + +`number` diff --git a/docs/schema/createfamilybymailtokenrequest.md b/docs/schema/createfamilybymailtokenrequest.md index e99bf02..daca983 100644 --- a/docs/schema/createfamilybymailtokenrequest.md +++ b/docs/schema/createfamilybymailtokenrequest.md @@ -24,6 +24,7 @@ https://timelimit.io/CreateFamilyByMailTokenRequest | [deviceName](#devicename) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-devicename.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/deviceName") | | [timeZone](#timezone) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-timezone.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/timeZone") | | [parentName](#parentname) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-parentname.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentName") | +| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel") | ## mailAuthToken @@ -133,6 +134,24 @@ https://timelimit.io/CreateFamilyByMailTokenRequest `string` +## clientLevel + + + +`clientLevel` + +* is optional + +* Type: `number` + +* cannot be null + +* defined in: [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel") + +### clientLevel Type + +`number` + # CreateFamilyByMailTokenRequest Definitions ## Definitions group PlaintextParentPassword diff --git a/docs/schema/registerchilddevicerequest-properties-clientlevel.md b/docs/schema/registerchilddevicerequest-properties-clientlevel.md new file mode 100644 index 0000000..8cfc0ab --- /dev/null +++ b/docs/schema/registerchilddevicerequest-properties-clientlevel.md @@ -0,0 +1,15 @@ +# Untitled number in RegisterChildDeviceRequest Schema + +```txt +https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RegisterChildDeviceRequest.schema.json\*](RegisterChildDeviceRequest.schema.json "open original schema") | + +## clientLevel Type + +`number` diff --git a/docs/schema/registerchilddevicerequest.md b/docs/schema/registerchilddevicerequest.md index b015f0a..7f1d1e4 100644 --- a/docs/schema/registerchilddevicerequest.md +++ b/docs/schema/registerchilddevicerequest.md @@ -21,6 +21,7 @@ https://timelimit.io/RegisterChildDeviceRequest | [registerToken](#registertoken) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-registertoken.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/registerToken") | | [childDevice](#childdevice) | `object` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-definitions-newdeviceinfo.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice") | | [deviceName](#devicename) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-devicename.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/deviceName") | +| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel") | ## registerToken @@ -76,6 +77,24 @@ https://timelimit.io/RegisterChildDeviceRequest `string` +## clientLevel + + + +`clientLevel` + +* is optional + +* Type: `number` + +* cannot be null + +* defined in: [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel") + +### clientLevel Type + +`number` + # RegisterChildDeviceRequest Definitions ## Definitions group NewDeviceInfo diff --git a/docs/schema/signintofamilyrequest-properties-clientlevel.md b/docs/schema/signintofamilyrequest-properties-clientlevel.md new file mode 100644 index 0000000..1ab5892 --- /dev/null +++ b/docs/schema/signintofamilyrequest-properties-clientlevel.md @@ -0,0 +1,15 @@ +# Untitled number in SignIntoFamilyRequest Schema + +```txt +https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel +``` + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SignIntoFamilyRequest.schema.json\*](SignIntoFamilyRequest.schema.json "open original schema") | + +## clientLevel Type + +`number` diff --git a/docs/schema/signintofamilyrequest.md b/docs/schema/signintofamilyrequest.md index ea8583b..4c88af2 100644 --- a/docs/schema/signintofamilyrequest.md +++ b/docs/schema/signintofamilyrequest.md @@ -21,6 +21,7 @@ https://timelimit.io/SignIntoFamilyRequest | [mailAuthToken](#mailauthtoken) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-mailauthtoken.md "https://timelimit.io/SignIntoFamilyRequest#/properties/mailAuthToken") | | [parentDevice](#parentdevice) | `object` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-definitions-newdeviceinfo.md "https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice") | | [deviceName](#devicename) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-devicename.md "https://timelimit.io/SignIntoFamilyRequest#/properties/deviceName") | +| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel") | ## mailAuthToken @@ -76,6 +77,24 @@ https://timelimit.io/SignIntoFamilyRequest `string` +## clientLevel + + + +`clientLevel` + +* is optional + +* Type: `number` + +* cannot be null + +* defined in: [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel") + +### clientLevel Type + +`number` + # SignIntoFamilyRequest Definitions ## Definitions group NewDeviceInfo diff --git a/src/api/child.ts b/src/api/child.ts index f14ad7a..2e298b2 100644 --- a/src/api/child.ts +++ b/src/api/child.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -24,10 +24,12 @@ import { logoutAtPrimaryDevice } from '../function/child/logout-at-primary-devic import { setPrimaryDevice } from '../function/child/set-primary-device' import { WebsocketApi } from '../websocket' import { isRegisterChildDeviceRequest, isRequestWithAuthToken, isUpdatePrimaryDeviceRequest } from './validator' +import { EventHandler } from '../monitoring/eventhandler' -export const createChildRouter = ({ database, websocket }: { - database: Database, +export const createChildRouter = ({ database, websocket, eventHandler }: { + database: Database websocket: WebsocketApi + eventHandler: EventHandler }) => { const router = Router() @@ -37,15 +39,17 @@ export const createChildRouter = ({ database, websocket }: { throw new BadRequest() } - const { deviceAuthToken, deviceId } = await addChildDevice({ + const { deviceAuthToken, deviceId, data } = await addChildDevice({ request: req.body, database, + eventHandler, websocket }) res.json({ deviceAuthToken, - ownDeviceId: deviceId + ownDeviceId: deviceId, + data }) } catch (ex) { next(ex) diff --git a/src/api/index.ts b/src/api/index.ts index 81910d9..86f7c9e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2020 Jonas Lochmann + * Copyright (C) 2019 - 2022 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -47,8 +47,8 @@ export const createApi = ({ database, websocket, connectedDevicesManager, eventH }) app.use('/auth', createAuthRouter(database)) - app.use('/child', createChildRouter({ database, websocket })) - app.use('/parent', createParentRouter({ database, websocket })) + app.use('/child', createChildRouter({ database, websocket, eventHandler })) + app.use('/parent', createParentRouter({ database, websocket, eventHandler })) app.use('/purchase', createPurchaseRouter({ database, websocket })) app.use('/sync', createSyncRouter({ database, websocket, connectedDevicesManager, eventHandler })) diff --git a/src/api/parent.ts b/src/api/parent.ts index 8cd45c8..cbe0543 100644 --- a/src/api/parent.ts +++ b/src/api/parent.ts @@ -31,6 +31,7 @@ import { signInIntoFamily } from '../function/parent/sign-in-into-family' import { validateU2fIntegrity, U2fValidationError } from '../function/u2f' import { createIdentityToken, MissingSignSecretException } from '../util/identity-token' import { WebsocketApi } from '../websocket' +import { EventHandler } from '../monitoring/eventhandler' import { isCreateFamilyByMailTokenRequest, isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest, @@ -38,7 +39,13 @@ import { isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest } from './validator' -export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => { +export const createParentRouter = ({ + database, websocket, eventHandler +}: { + database: Database + websocket: WebsocketApi + eventHandler: EventHandler +}) => { const router = Router() router.post('/get-status-by-mail-address', json(), async (req, res, next) => { @@ -75,17 +82,20 @@ export const createParentRouter = ({ database, websocket }: {database: Database, const result = await createFamily({ database, + eventHandler, firstParentDevice: req.body.parentDevice, mailAuthToken: req.body.mailAuthToken, password: req.body.parentPassword, deviceName: req.body.deviceName, parentName: req.body.parentName, - timeZone: req.body.timeZone + timeZone: req.body.timeZone, + clientLevel: req.body.clientLevel || null }) res.json({ deviceAuthToken: result.deviceAuthToken, - ownDeviceId: result.deviceId + ownDeviceId: result.deviceId, + data: result.data }) } catch (ex) { next(ex) @@ -100,15 +110,18 @@ export const createParentRouter = ({ database, websocket }: {database: Database, const result = await signInIntoFamily({ database, + eventHandler, newDeviceInfo: req.body.parentDevice, mailAuthToken: req.body.mailAuthToken, deviceName: req.body.deviceName, + clientLevel: req.body.clientLevel || null, websocket }) res.json({ deviceAuthToken: result.deviceAuthToken, - ownDeviceId: result.deviceId + ownDeviceId: result.deviceId, + data: result.data }) } catch (ex) { next(ex) diff --git a/src/api/schema.ts b/src/api/schema.ts index d3d703a..5e16481 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -84,12 +84,14 @@ export interface CreateFamilyByMailTokenRequest { deviceName: string timeZone: string parentName: string + clientLevel?: number } export interface SignIntoFamilyRequest { mailAuthToken: string parentDevice: NewDeviceInfo deviceName: string + clientLevel?: number } export interface RecoverParentPasswordRequest { @@ -101,6 +103,7 @@ export interface RegisterChildDeviceRequest { registerToken: string childDevice: NewDeviceInfo deviceName: string + clientLevel?: number } export interface CreateRegisterDeviceTokenRequest { diff --git a/src/api/validator.ts b/src/api/validator.ts index 4d4f9ca..12f5a01 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -2930,6 +2930,9 @@ export const isCreateFamilyByMailTokenRequest: (value: unknown) => value is Crea }, "parentName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, @@ -2955,6 +2958,9 @@ export const isSignIntoFamilyRequest: (value: unknown) => value is SignIntoFamil }, "deviceName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, @@ -2995,6 +3001,9 @@ export const isRegisterChildDeviceRequest: (value: unknown) => value is Register }, "deviceName": { "type": "string" + }, + "clientLevel": { + "type": "number" } }, "additionalProperties": false, diff --git a/src/function/child/add-device.ts b/src/function/child/add-device.ts index 3c6d6de..ede948c 100644 --- a/src/function/child/add-device.ts +++ b/src/function/child/add-device.ts @@ -22,13 +22,22 @@ import { generateAuthToken, generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' import { prepareDeviceEntry } from '../device/prepare-device-entry' import { notifyClientsAboutChangesDelayed } from '../websocket' +import { generateServerDataStatus } from '../sync/get-server-data-status' +import { EventHandler } from '../../monitoring/eventhandler' +import { ServerDataStatus } from '../../object/serverdatastatus' +import { createEmptyClientDataStatus } from '../../object/clientdatastatus' -export const addChildDevice = async ({ database, websocket, request }: { +export const addChildDevice = async ({ database, eventHandler, websocket, request }: { database: Database + eventHandler: EventHandler websocket: WebsocketApi request: RegisterChildDeviceRequest // no transaction here because this is directly called from an API endpoint -}) => { +}): Promise<{ + deviceId: string + deviceAuthToken: string + data: ServerDataStatus +}> => { return database.transaction(async (transaction) => { const entry = await database.addDeviceToken.findOne({ where: { @@ -75,9 +84,19 @@ export const addChildDevice = async ({ database, websocket, request }: { transaction }) + const data = await generateServerDataStatus({ + database, + clientStatus: createEmptyClientDataStatus({ clientLevel: request.clientLevel || null }), + familyId: entry.familyId, + deviceId, + transaction, + eventHandler + }) + return { deviceId, - deviceAuthToken + deviceAuthToken, + data } }) } diff --git a/src/function/parent/create-family.ts b/src/function/parent/create-family.ts index e6af4ad..475a1cf 100644 --- a/src/function/parent/create-family.ts +++ b/src/function/parent/create-family.ts @@ -16,25 +16,38 @@ */ import { Conflict } from 'http-errors' +import { generateServerDataStatus } from '../sync/get-server-data-status' import { NewDeviceInfo, PlaintextParentPassword, assertPlaintextParentPasswordValid } from '../../api/schema' import { Database } from '../../database' import { maxMailNotificationFlags } from '../../database/user' +import { EventHandler } from '../../monitoring/eventhandler' +import { ServerDataStatus } from '../../object/serverdatastatus' +import { createEmptyClientDataStatus } from '../../object/clientdatastatus' import { generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId } from '../../util/token' import { requireMailAndLocaleByAuthToken } from '../authentication' import { prepareDeviceEntry } from '../device/prepare-device-entry' -export const createFamily = async ({ database, mailAuthToken, firstParentDevice, password, timeZone, parentName, deviceName }: { - database: Database, - mailAuthToken: string, - firstParentDevice: NewDeviceInfo, - password: PlaintextParentPassword, - timeZone: string, - parentName: string, +export async function createFamily ({ + database, eventHandler, mailAuthToken, firstParentDevice, + password, timeZone, parentName, deviceName, clientLevel +}: { + database: Database + eventHandler: EventHandler + mailAuthToken: string + firstParentDevice: NewDeviceInfo + password: PlaintextParentPassword + timeZone: string + parentName: string deviceName: string + clientLevel: number | null // no transaction here because this is directly called from an API endpoint -}) => { +}): Promise<{ + deviceAuthToken: string + deviceId: string + data: ServerDataStatus +}> { assertPlaintextParentPasswordValid(password) return database.transaction(async (transaction) => { @@ -42,14 +55,14 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) // ensure that no family was created for this mail yet - const exisitngUserEntry = await database.user.findOne({ + const existingUserEntry = await database.user.findOne({ where: { mail: mailInfo.mail }, transaction }) - if (exisitngUserEntry) { + if (existingUserEntry) { throw new Conflict() } @@ -103,9 +116,19 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, isUserKeptSignedIn: true }), { transaction }) + const data = await generateServerDataStatus({ + database, + clientStatus: createEmptyClientDataStatus({ clientLevel }), + familyId, + deviceId, + transaction, + eventHandler + }) + return { deviceAuthToken, - deviceId + deviceId, + data } }) } diff --git a/src/function/parent/sign-in-into-family.ts b/src/function/parent/sign-in-into-family.ts index 8a355ca..8637b4e 100644 --- a/src/function/parent/sign-in-into-family.ts +++ b/src/function/parent/sign-in-into-family.ts @@ -24,15 +24,21 @@ import { WebsocketApi } from '../../websocket' import { requireMailAndLocaleByAuthToken } from '../authentication' import { prepareDeviceEntry } from '../device/prepare-device-entry' import { notifyClientsAboutChangesDelayed } from '../websocket' +import { generateServerDataStatus } from '../sync/get-server-data-status' +import { EventHandler } from '../../monitoring/eventhandler' +import { ServerDataStatus } from '../../object/serverdatastatus' +import { createEmptyClientDataStatus } from '../../object/clientdatastatus' -export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, deviceName, websocket }: { +export const signInIntoFamily = async ({ database, eventHandler, mailAuthToken, newDeviceInfo, deviceName, websocket, clientLevel }: { database: Database + eventHandler: EventHandler mailAuthToken: string newDeviceInfo: NewDeviceInfo deviceName: string websocket: WebsocketApi + clientLevel: number | null // no transaction here because this is directly called from an API endpoint -}): Promise<{ deviceId: string; deviceAuthToken: string }> => { +}): Promise<{ deviceId: string; deviceAuthToken: string; data: ServerDataStatus }> => { return database.transaction(async (transaction) => { const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) @@ -94,9 +100,19 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, }) }) + const data = await generateServerDataStatus({ + database, + clientStatus: createEmptyClientDataStatus({ clientLevel }), + familyId: userEntry.familyId, + deviceId, + transaction, + eventHandler + }) + return { deviceId, - deviceAuthToken + deviceAuthToken, + data } }) } diff --git a/src/object/clientdatastatus.ts b/src/object/clientdatastatus.ts index 504c6b6..cece54a 100644 --- a/src/object/clientdatastatus.ts +++ b/src/object/clientdatastatus.ts @@ -28,6 +28,18 @@ export interface ClientDataStatus { u2f?: string // last u2f list version } +export function createEmptyClientDataStatus({ clientLevel }: { + clientLevel: number | null +}): ClientDataStatus { + return { + devices: '', + apps: {}, + categories: {}, + users: '', + clientLevel: clientLevel || undefined + } +} + export type ClientDataStatusApps = {[key: string]: string} // installedAppsVersionsByDeviceId export type ClientDataStatusCategories = {[key: string]: CategoryDataStatus} export type ClientDataStatusDevicesExtended = {[key: string]: DeviceDataStatus}