From abc2102da55d2810b7413a88a016a962f12f6c09 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 28 Sep 2020 02:00:00 +0200 Subject: [PATCH] Extend transaction usage --- .../ClientPushChangesRequest.schema.json | 68 ++-- docs/schema/README.md | 95 ++--- ...srequestaction-properties-encodedaction.md | 16 + ...angesrequestaction-properties-integrity.md | 16 + ...requestaction-properties-sequencenumber.md | 16 + ...ushchangesrequestaction-properties-type.md | 26 ++ ...hchangesrequestaction-properties-userid.md | 16 + ...ientpushchangesrequestaction-properties.md | 16 + ...initions-clientpushchangesrequestaction.md | 116 ++++++ .../clientpushchangesrequest-definitions.md | 16 + ...ntpushchangesrequest-properties-actions.md | 2 +- docs/schema/clientpushchangesrequest.md | 114 +++++- src/api/admin.ts | 40 +- src/api/parent.ts | 57 +-- src/api/purchase.ts | 99 ++--- src/api/schema.ts | 16 +- src/api/sync.ts | 15 +- src/api/validator.ts | 66 ++-- src/database/config.ts | 5 +- src/database/index.ts | 58 ++- src/function/authentication/index.ts | 17 +- src/function/authentication/login-by-mail.ts | 62 ++-- src/function/child/add-device.ts | 20 +- .../child/logout-at-primary-device.ts | 3 +- src/function/child/set-primary-device.ts | 42 +-- src/function/cleanup/delete-families.ts | 3 +- src/function/cleanup/delete-old-families.ts | 54 +-- src/function/device/remove-device.ts | 164 ++++---- src/function/device/report-device-removed.ts | 41 +- src/function/parent/can-recover-password.ts | 26 +- .../parent/create-add-device-token.ts | 12 +- src/function/parent/create-family.ts | 9 +- .../parent/get-status-by-mail-address.ts | 19 +- src/function/parent/link-mail-address.ts | 71 ++-- .../parent/recover-parent-password.ts | 36 +- src/function/parent/sign-in-into-family.ts | 36 +- src/function/purchase/add-purchase.ts | 110 +++--- src/function/purchase/require-family-entry.ts | 13 +- src/function/statusmessage/index.ts | 29 +- src/function/sync/apply-actions/cache.ts | 18 +- src/function/sync/apply-actions/index.ts | 350 ++++++++---------- src/function/sync/apply-actions/integrity.ts | 89 +++++ src/function/warningmail/manipulation.ts | 38 +- src/function/warningmail/uninstall.ts | 41 +- src/function/websocket/index.ts | 18 +- src/index.ts | 4 +- src/worker/delete-deprecated-purchases.ts | 29 +- 47 files changed, 1367 insertions(+), 860 deletions(-) create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties.md create mode 100644 docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction.md create mode 100644 docs/schema/clientpushchangesrequest-definitions.md create mode 100644 src/function/sync/apply-actions/integrity.ts diff --git a/docs/schema/ClientPushChangesRequest.schema.json b/docs/schema/ClientPushChangesRequest.schema.json index e70f4ea..dffb364 100644 --- a/docs/schema/ClientPushChangesRequest.schema.json +++ b/docs/schema/ClientPushChangesRequest.schema.json @@ -7,37 +7,7 @@ "actions": { "type": "array", "items": { - "type": "object", - "properties": { - "encodedAction": { - "type": "string" - }, - "sequenceNumber": { - "type": "number" - }, - "integrity": { - "type": "string" - }, - "type": { - "enum": [ - "appLogic", - "child", - "parent" - ], - "type": "string" - }, - "userId": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "encodedAction", - "integrity", - "sequenceNumber", - "type", - "userId" - ] + "$ref": "#/definitions/ClientPushChangesRequestAction" } } }, @@ -46,6 +16,42 @@ "actions", "deviceAuthToken" ], + "definitions": { + "ClientPushChangesRequestAction": { + "type": "object", + "properties": { + "encodedAction": { + "type": "string" + }, + "sequenceNumber": { + "type": "number" + }, + "integrity": { + "type": "string" + }, + "type": { + "enum": [ + "appLogic", + "child", + "parent" + ], + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "encodedAction", + "integrity", + "sequenceNumber", + "type", + "userId" + ], + "title": "ClientPushChangesRequestAction" + } + }, "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClientPushChangesRequest", "$id": "https://timelimit.io/ClientPushChangesRequest" diff --git a/docs/schema/README.md b/docs/schema/README.md index 3c106ac..3f0297c 100644 --- a/docs/schema/README.md +++ b/docs/schema/README.md @@ -33,26 +33,28 @@ - [CategoryDataStatus](./clientpullchangesrequest-definitions-categorydatastatus.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/CategoryDataStatus` - [ClientDataStatus](./clientpullchangesrequest-properties-clientdatastatus.md) – `https://timelimit.io/ClientPullChangesRequest#/properties/status` - [ClientDataStatus](./clientpullchangesrequest-definitions-clientdatastatus.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus` -- [NewDeviceInfo](./registerchilddevicerequest-definitions-newdeviceinfo.md) – `https://timelimit.io/RegisterChildDeviceRequest#/definitions/NewDeviceInfo` -- [NewDeviceInfo](./registerchilddevicerequest-properties-newdeviceinfo.md) – `https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice` +- [ClientPushChangesRequestAction](./clientpushchangesrequest-properties-actions-clientpushchangesrequestaction.md) – `https://timelimit.io/ClientPushChangesRequest#/properties/actions/items` +- [ClientPushChangesRequestAction](./clientpushchangesrequest-definitions-clientpushchangesrequestaction.md) – `https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction` - [NewDeviceInfo](./signintofamilyrequest-definitions-newdeviceinfo.md) – `https://timelimit.io/SignIntoFamilyRequest#/definitions/NewDeviceInfo` - [NewDeviceInfo](./signintofamilyrequest-properties-newdeviceinfo.md) – `https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice` +- [NewDeviceInfo](./registerchilddevicerequest-definitions-newdeviceinfo.md) – `https://timelimit.io/RegisterChildDeviceRequest#/definitions/NewDeviceInfo` +- [NewDeviceInfo](./registerchilddevicerequest-properties-newdeviceinfo.md) – `https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice` - [NewDeviceInfo](./createfamilybymailtokenrequest-definitions-newdeviceinfo.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/NewDeviceInfo` - [NewDeviceInfo](./createfamilybymailtokenrequest-properties-newdeviceinfo.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentDevice` - [ParentPassword](./serializedparentaction-definitions-serializedadduseraction-properties-parentpassword.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction/properties/password` -- [ParentPassword](./createfamilybymailtokenrequest-properties-parentpassword.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentPassword` -- [ParentPassword](./recoverparentpasswordrequest-definitions-parentpassword.md) – `https://timelimit.io/RecoverParentPasswordRequest#/definitions/ParentPassword` +- [ParentPassword](./createfamilybymailtokenrequest-definitions-parentpassword.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/ParentPassword` +- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) – `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password` - [ParentPassword](./serializedparentaction-definitions-serializedsetchildpasswordaction-properties-parentpassword.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction/properties/newPassword` - [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) – `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password` - [ParentPassword](./serializedparentaction-definitions-parentpassword.md) – `https://timelimit.io/SerializedParentAction#/definitions/ParentPassword` -- [ParentPassword](./recoverparentpasswordrequest-properties-parentpassword.md) – `https://timelimit.io/RecoverParentPasswordRequest#/properties/password` -- [ParentPassword](./createfamilybymailtokenrequest-definitions-parentpassword.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/ParentPassword` -- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) – `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password` +- [ParentPassword](./recoverparentpasswordrequest-definitions-parentpassword.md) – `https://timelimit.io/RecoverParentPasswordRequest#/definitions/ParentPassword` +- [ParentPassword](./createfamilybymailtokenrequest-properties-parentpassword.md) – `https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentPassword` - [ParentPassword](./serializedchildaction-definitions-parentpassword.md) – `https://timelimit.io/SerializedChildAction#/definitions/ParentPassword` +- [ParentPassword](./recoverparentpasswordrequest-properties-parentpassword.md) – `https://timelimit.io/RecoverParentPasswordRequest#/properties/password` - [ParentPassword](./serializedparentaction-definitions-serializedsetchildpasswordaction-properties-parentpassword.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction/properties/newPassword` - [ParentPassword](./serializedparentaction-definitions-serializedadduseraction-properties-parentpassword.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction/properties/password` -- [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-anyof-serialiezdtrieddisablingdeviceadminaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/6` - [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-definitions-serialiezdtrieddisablingdeviceadminaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerialiezdTriedDisablingDeviceAdminAction` +- [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-anyof-serialiezdtrieddisablingdeviceadminaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/6` - [SerialiizedUpdateNetworkTimeVerificationAction](./serializedparentaction-definitions-serialiizedupdatenetworktimeverificationaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerialiizedUpdateNetworkTimeVerificationAction` - [SerialiizedUpdateNetworkTimeVerificationAction](./serializedparentaction-anyof-serialiizedupdatenetworktimeverificationaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/37` - [SerializeResetCategoryNetworkIdsAction](./serializedparentaction-anyof-serializeresetcategorynetworkidsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/13` @@ -63,18 +65,18 @@ - [SerializedAddCategoryNetworkIdAction](./serializedparentaction-definitions-serializedaddcategorynetworkidaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction` - [SerializedAddInstalledAppsAction](./serializedapplogicaction-definitions-serializedaddinstalledappsaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction` - [SerializedAddInstalledAppsAction](./serializedapplogicaction-anyof-serializedaddinstalledappsaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/0` -- [SerializedAddUsedTimeAction](./serializedapplogicaction-definitions-serializedaddusedtimeaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeAction` - [SerializedAddUsedTimeAction](./serializedapplogicaction-anyof-serializedaddusedtimeaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/1` -- [SerializedAddUsedTimeActionVersion2](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2` +- [SerializedAddUsedTimeAction](./serializedapplogicaction-definitions-serializedaddusedtimeaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeAction` - [SerializedAddUsedTimeActionVersion2](./serializedapplogicaction-anyof-serializedaddusedtimeactionversion2.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/2` -- [SerializedAddUserAction](./serializedparentaction-definitions-serializedadduseraction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction` +- [SerializedAddUsedTimeActionVersion2](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2` - [SerializedAddUserAction](./serializedparentaction-anyof-serializedadduseraction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/2` -- [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items` -- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedappactivityitem.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAppActivityItem` -- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items` +- [SerializedAddUserAction](./serializedparentaction-definitions-serializedadduseraction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction` - [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items` - [SerializedAppActivityItem](./serverdatastatus-definitions-serializedappactivityitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/SerializedAppActivityItem` - [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items` +- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedappactivityitem.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAppActivityItem` +- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items` +- [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items` - [SerializedChangeParentPasswordAction](./serializedparentaction-definitions-serializedchangeparentpasswordaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedChangeParentPasswordAction` - [SerializedChangeParentPasswordAction](./serializedparentaction-anyof-serializedchangeparentpasswordaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/3` - [SerializedChildChangePasswordAction](./serializedchildaction-definitions-serializedchildchangepasswordaction.md) – `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction` @@ -83,26 +85,26 @@ - [SerializedChildSignInAction](./serializedchildaction-anyof-serializedchildsigninaction.md) – `https://timelimit.io/SerializedChildAction#/anyOf/1` - [SerializedCreateCategoryAction](./serializedparentaction-definitions-serializedcreatecategoryaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateCategoryAction` - [SerializedCreateCategoryAction](./serializedparentaction-anyof-serializedcreatecategoryaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/4` -- [SerializedCreateTimelimtRuleAction](./serializedparentaction-definitions-serializedcreatetimelimtruleaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction` - [SerializedCreateTimelimtRuleAction](./serializedparentaction-anyof-serializedcreatetimelimtruleaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/5` -- [SerializedDeleteCategoryAction](./serializedparentaction-definitions-serializeddeletecategoryaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedDeleteCategoryAction` +- [SerializedCreateTimelimtRuleAction](./serializedparentaction-definitions-serializedcreatetimelimtruleaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction` - [SerializedDeleteCategoryAction](./serializedparentaction-anyof-serializeddeletecategoryaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/6` +- [SerializedDeleteCategoryAction](./serializedparentaction-definitions-serializeddeletecategoryaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedDeleteCategoryAction` - [SerializedDeleteTimeLimitRuleAction](./serializedparentaction-anyof-serializeddeletetimelimitruleaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/7` - [SerializedDeleteTimeLimitRuleAction](./serializedparentaction-definitions-serializeddeletetimelimitruleaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedDeleteTimeLimitRuleAction` - [SerializedForceSyncAction](./serializedapplogicaction-definitions-serializedforcesyncaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedForceSyncAction` - [SerializedForceSyncAction](./serializedapplogicaction-anyof-serializedforcesyncaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/3` -- [SerializedIgnoreManipulationAction](./serializedparentaction-anyof-serializedignoremanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/8` - [SerializedIgnoreManipulationAction](./serializedparentaction-definitions-serializedignoremanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedIgnoreManipulationAction` -- [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-anyof-serializedincrementcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/9` +- [SerializedIgnoreManipulationAction](./serializedparentaction-anyof-serializedignoremanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/8` - [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-definitions-serializedincrementcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedIncrementCategoryExtraTimeAction` -- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items` -- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items` -- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedInstalledApp` -- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items` -- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items` +- [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-anyof-serializedincrementcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/9` - [SerializedInstalledApp](./serverdatastatus-definitions-serializedinstalledapp.md) – `https://timelimit.io/ServerDataStatus#/definitions/SerializedInstalledApp` -- [SerializedRemoveCategoryAppsAction](./serializedparentaction-definitions-serializedremovecategoryappsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedRemoveCategoryAppsAction` +- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items` +- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items` +- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedInstalledApp` +- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items` +- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items` - [SerializedRemoveCategoryAppsAction](./serializedparentaction-anyof-serializedremovecategoryappsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/10` +- [SerializedRemoveCategoryAppsAction](./serializedparentaction-definitions-serializedremovecategoryappsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedRemoveCategoryAppsAction` - [SerializedRemoveInstalledAppsAction](./serializedapplogicaction-definitions-serializedremoveinstalledappsaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedRemoveInstalledAppsAction` - [SerializedRemoveInstalledAppsAction](./serializedapplogicaction-anyof-serializedremoveinstalledappsaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/4` - [SerializedRemoveUserAction](./serializedparentaction-definitions-serializedremoveuseraction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedRemoveUserAction` @@ -111,26 +113,26 @@ - [SerializedRenameChildAction](./serializedparentaction-anyof-serializedrenamechildaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/12` - [SerializedResetParentBlockedTimesAction](./serializedparentaction-anyof-serializedresetparentblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/14` - [SerializedResetParentBlockedTimesAction](./serializedparentaction-definitions-serializedresetparentblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedResetParentBlockedTimesAction` -- [SerializedSetCategoryExtraTimeAction](./serializedparentaction-anyof-serializedsetcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/15` - [SerializedSetCategoryExtraTimeAction](./serializedparentaction-definitions-serializedsetcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetCategoryExtraTimeAction` -- [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-anyof-serializedsetcategoryforunassignedappsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/16` +- [SerializedSetCategoryExtraTimeAction](./serializedparentaction-anyof-serializedsetcategoryextratimeaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/15` - [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-definitions-serializedsetcategoryforunassignedappsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetCategoryForUnassignedAppsAction` +- [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-anyof-serializedsetcategoryforunassignedappsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/16` - [SerializedSetChildPasswordAction](./serializedparentaction-definitions-serializedsetchildpasswordaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction` - [SerializedSetChildPasswordAction](./serializedparentaction-anyof-serializedsetchildpasswordaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/17` -- [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-definitions-serializedsetconsiderrebootmanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetConsiderRebootManipulationAction` - [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-anyof-serializedsetconsiderrebootmanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/18` +- [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-definitions-serializedsetconsiderrebootmanipulationaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetConsiderRebootManipulationAction` - [SerializedSetDeviceDefaultUserAction](./serializedparentaction-definitions-serializedsetdevicedefaultuseraction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceDefaultUserAction` - [SerializedSetDeviceDefaultUserAction](./serializedparentaction-anyof-serializedsetdevicedefaultuseraction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/19` -- [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-definitions-serializedsetdevicedefaultusertimeoutaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceDefaultUserTimeoutAction` - [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-anyof-serializedsetdevicedefaultusertimeoutaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/20` -- [SerializedSetDeviceUserAction](./serializedparentaction-anyof-serializedsetdeviceuseraction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/21` +- [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-definitions-serializedsetdevicedefaultusertimeoutaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceDefaultUserTimeoutAction` - [SerializedSetDeviceUserAction](./serializedparentaction-definitions-serializedsetdeviceuseraction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceUserAction` +- [SerializedSetDeviceUserAction](./serializedparentaction-anyof-serializedsetdeviceuseraction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/21` - [SerializedSetKeepSignedInAction](./serializedparentaction-definitions-serializedsetkeepsignedinaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetKeepSignedInAction` - [SerializedSetKeepSignedInAction](./serializedparentaction-anyof-serializedsetkeepsignedinaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/22` - [SerializedSetParentCategoryAction](./serializedparentaction-anyof-serializedsetparentcategoryaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/23` - [SerializedSetParentCategoryAction](./serializedparentaction-definitions-serializedsetparentcategoryaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetParentCategoryAction` -- [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-definitions-serializedsetrelaxprimarydeviceaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetRelaxPrimaryDeviceAction` - [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-anyof-serializedsetrelaxprimarydeviceaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/24` +- [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-definitions-serializedsetrelaxprimarydeviceaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetRelaxPrimaryDeviceAction` - [SerializedSetSendDeviceConnected](./serializedparentaction-definitions-serializedsetsenddeviceconnected.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetSendDeviceConnected` - [SerializedSetSendDeviceConnected](./serializedparentaction-anyof-serializedsetsenddeviceconnected.md) – `https://timelimit.io/SerializedParentAction#/anyOf/25` - [SerializedSetUserDisableLimitsUntilAction](./serializedparentaction-definitions-serializedsetuserdisablelimitsuntilaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetUserDisableLimitsUntilAction` @@ -139,13 +141,13 @@ - [SerializedSetUserTimezoneAction](./serializedparentaction-anyof-serializedsetusertimezoneaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/27` - [SerializedSignOutAtDeviceAction](./serializedapplogicaction-definitions-serializedsignoutatdeviceaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedSignOutAtDeviceAction` - [SerializedSignOutAtDeviceAction](./serializedapplogicaction-anyof-serializedsignoutatdeviceaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/5` -- [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedcreatetimelimtruleaction-properties-serializedtimelimitrule.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction/properties/rule` - [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedtimelimitrule.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedTimeLimitRule` - [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedcreatetimelimtruleaction-properties-serializedtimelimitrule.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction/properties/rule` +- [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedcreatetimelimtruleaction-properties-serializedtimelimitrule.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction/properties/rule` - [SerializedUpdateAppActivitiesAction](./serializedapplogicaction-anyof-serializedupdateappactivitiesaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/7` - [SerializedUpdateAppActivitiesAction](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction` -- [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-definitions-serializedupdatecategorybatterylimitaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBatteryLimitAction` - [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-anyof-serializedupdatecategorybatterylimitaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/28` +- [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-definitions-serializedupdatecategorybatterylimitaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBatteryLimitAction` - [SerializedUpdateCategoryBlockAllNotificationsAction](./serializedparentaction-definitions-serializedupdatecategoryblockallnotificationsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBlockAllNotificationsAction` - [SerializedUpdateCategoryBlockAllNotificationsAction](./serializedparentaction-anyof-serializedupdatecategoryblockallnotificationsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/29` - [SerializedUpdateCategoryBlockedTimesAction](./serializedparentaction-definitions-serializedupdatecategoryblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBlockedTimesAction` @@ -154,63 +156,62 @@ - [SerializedUpdateCategorySortingAction](./serializedparentaction-anyof-serializedupdatecategorysortingaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/31` - [SerializedUpdateCategoryTemporarilyBlockedAction](./serializedparentaction-definitions-serializedupdatecategorytemporarilyblockedaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTemporarilyBlockedAction` - [SerializedUpdateCategoryTemporarilyBlockedAction](./serializedparentaction-anyof-serializedupdatecategorytemporarilyblockedaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/32` -- [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-definitions-serializedupdatecategorytimewarningsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTimeWarningsAction` - [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-anyof-serializedupdatecategorytimewarningsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/33` +- [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-definitions-serializedupdatecategorytimewarningsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTimeWarningsAction` - [SerializedUpdateCategoryTitleAction](./serializedparentaction-anyof-serializedupdatecategorytitleaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/34` - [SerializedUpdateCategoryTitleAction](./serializedparentaction-definitions-serializedupdatecategorytitleaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTitleAction` -- [SerializedUpdateDeviceNameAction](./serializedparentaction-anyof-serializedupdatedevicenameaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/35` - [SerializedUpdateDeviceNameAction](./serializedparentaction-definitions-serializedupdatedevicenameaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateDeviceNameAction` +- [SerializedUpdateDeviceNameAction](./serializedparentaction-anyof-serializedupdatedevicenameaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/35` - [SerializedUpdateDeviceStatusAction](./serializedapplogicaction-anyof-serializedupdatedevicestatusaction.md) – `https://timelimit.io/SerializedAppLogicAction#/anyOf/8` - [SerializedUpdateDeviceStatusAction](./serializedapplogicaction-definitions-serializedupdatedevicestatusaction.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction` - [SerializedUpdateEnableActivityLevelBlockingAction](./serializedparentaction-definitions-serializedupdateenableactivitylevelblockingaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateEnableActivityLevelBlockingAction` - [SerializedUpdateEnableActivityLevelBlockingAction](./serializedparentaction-anyof-serializedupdateenableactivitylevelblockingaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/36` -- [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-anyof-serializedupdateparentblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/38` - [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-definitions-serializedupdateparentblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateParentBlockedTimesAction` +- [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-anyof-serializedupdateparentblockedtimesaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/38` - [SerializedUpdateParentNotificationFlagsAction](./serializedparentaction-definitions-serializedupdateparentnotificationflagsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateParentNotificationFlagsAction` - [SerializedUpdateParentNotificationFlagsAction](./serializedparentaction-anyof-serializedupdateparentnotificationflagsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/39` - [SerializedUpdateTimelimitRuleAction](./serializedparentaction-definitions-serializedupdatetimelimitruleaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateTimelimitRuleAction` - [SerializedUpdateTimelimitRuleAction](./serializedparentaction-anyof-serializedupdatetimelimitruleaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/40` - [SerializedUpdateUserFlagsAction](./serializedparentaction-definitions-serializedupdateuserflagsaction.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateUserFlagsAction` - [SerializedUpdateUserFlagsAction](./serializedparentaction-anyof-serializedupdateuserflagsaction.md) – `https://timelimit.io/SerializedParentAction#/anyOf/41` -- [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-anyof-serializedupdateuserlimitlogincategory.md) – `https://timelimit.io/SerializedParentAction#/anyOf/42` - [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-definitions-serializedupdateuserlimitlogincategory.md) – `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateUserLimitLoginCategory` +- [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-anyof-serializedupdateuserlimitlogincategory.md) – `https://timelimit.io/SerializedParentAction#/anyOf/42` - [ServerCategoryNetworkId](./serverdatastatus-definitions-servercategorynetworkid.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId` - [ServerCategoryNetworkId](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks-servercategorynetworkid.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks/items` - [ServerCategoryNetworkId](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks-servercategorynetworkid.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks/items` - [ServerDeviceData](./serverdatastatus-definitions-serverdevicelist-properties-data-serverdevicedata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList/properties/data/items` -- [ServerDeviceData](./serverdatastatus-definitions-serverdevicedata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData` - [ServerDeviceData](./serverdatastatus-definitions-serverdevicelist-properties-data-serverdevicedata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList/properties/data/items` -- [ServerDeviceList](./serverdatastatus-properties-serverdevicelist.md) – `https://timelimit.io/ServerDataStatus#/properties/devices` +- [ServerDeviceData](./serverdatastatus-definitions-serverdevicedata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData` - [ServerDeviceList](./serverdatastatus-definitions-serverdevicelist.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList` -- [ServerInstalledAppsData](./serverdatastatus-properties-apps-serverinstalledappsdata.md) – `https://timelimit.io/ServerDataStatus#/properties/apps/items` +- [ServerDeviceList](./serverdatastatus-properties-serverdevicelist.md) – `https://timelimit.io/ServerDataStatus#/properties/devices` - [ServerInstalledAppsData](./serverdatastatus-definitions-serverinstalledappsdata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData` +- [ServerInstalledAppsData](./serverdatastatus-properties-apps-serverinstalledappsdata.md) – `https://timelimit.io/ServerDataStatus#/properties/apps/items` +- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items` - [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items` - [ServerSessionDurationItem](./serverdatastatus-definitions-serversessiondurationitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerSessionDurationItem` -- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items` +- [ServerTimeLimitRule](./serverdatastatus-definitions-serverupdatedtimelimitrules-properties-rules-servertimelimitrule.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules/properties/rules/items` - [ServerTimeLimitRule](./serverdatastatus-definitions-servertimelimitrule.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerTimeLimitRule` - [ServerTimeLimitRule](./serverdatastatus-definitions-serverupdatedtimelimitrules-properties-rules-servertimelimitrule.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules/properties/rules/items` -- [ServerTimeLimitRule](./serverdatastatus-definitions-serverupdatedtimelimitrules-properties-rules-servertimelimitrule.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules/properties/rules/items` -- [ServerUpdatedCategoryAssignedApps](./serverdatastatus-definitions-serverupdatedcategoryassignedapps.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryAssignedApps` - [ServerUpdatedCategoryAssignedApps](./serverdatastatus-properties-categoryapp-serverupdatedcategoryassignedapps.md) – `https://timelimit.io/ServerDataStatus#/properties/categoryApp/items` +- [ServerUpdatedCategoryAssignedApps](./serverdatastatus-definitions-serverupdatedcategoryassignedapps.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryAssignedApps` - [ServerUpdatedCategoryBaseData](./serverdatastatus-properties-categorybase-serverupdatedcategorybasedata.md) – `https://timelimit.io/ServerDataStatus#/properties/categoryBase/items` - [ServerUpdatedCategoryBaseData](./serverdatastatus-definitions-serverupdatedcategorybasedata.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData` - [ServerUpdatedCategoryUsedTimes](./serverdatastatus-definitions-serverupdatedcategoryusedtimes.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes` - [ServerUpdatedCategoryUsedTimes](./serverdatastatus-properties-usedtimes-serverupdatedcategoryusedtimes.md) – `https://timelimit.io/ServerDataStatus#/properties/usedTimes/items` -- [ServerUpdatedTimeLimitRules](./serverdatastatus-definitions-serverupdatedtimelimitrules.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules` - [ServerUpdatedTimeLimitRules](./serverdatastatus-properties-rules-serverupdatedtimelimitrules.md) – `https://timelimit.io/ServerDataStatus#/properties/rules/items` +- [ServerUpdatedTimeLimitRules](./serverdatastatus-definitions-serverupdatedtimelimitrules.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules` - [ServerUsedTimeItem](./serverdatastatus-definitions-serverusedtimeitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUsedTimeItem` - [ServerUsedTimeItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times-serverusedtimeitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times/items` - [ServerUsedTimeItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times-serverusedtimeitem.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times/items` +- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items` +- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items` - [ServerUserEntry](./serverdatastatus-definitions-serveruserentry.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserEntry` -- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items` -- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items` - [ServerUserList](./serverdatastatus-properties-serveruserlist.md) – `https://timelimit.io/ServerDataStatus#/properties/users` - [ServerUserList](./serverdatastatus-definitions-serveruserlist.md) – `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList` - [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-apps.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/apps` -- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories` - [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-apps.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/apps` - [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories` -- [Untitled object in ClientPushChangesRequest](./clientpushchangesrequest-properties-actions-items.md) – `https://timelimit.io/ClientPushChangesRequest#/properties/actions/items` +- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) – `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories` - [Untitled object in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i-items.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i/items` - [Untitled object in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i-items.md) – `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i/items` diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md new file mode 100644 index 0000000..8de87f5 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md @@ -0,0 +1,16 @@ +# Untitled string in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/encodedAction +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## encodedAction Type + +`string` diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md new file mode 100644 index 0000000..5d69653 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md @@ -0,0 +1,16 @@ +# Untitled string in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/integrity +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## integrity Type + +`string` diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md new file mode 100644 index 0000000..f67f966 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md @@ -0,0 +1,16 @@ +# Untitled number in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/sequenceNumber +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## sequenceNumber Type + +`number` diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md new file mode 100644 index 0000000..538818b --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md @@ -0,0 +1,26 @@ +# Untitled string in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/type +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## type Type + +`string` + +## type Constraints + +**enum**: the value of this property must be equal to one of the following values: + +| Value | Explanation | +| :----------- | ----------- | +| `"appLogic"` | | +| `"child"` | | +| `"parent"` | | diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md new file mode 100644 index 0000000..289ce60 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md @@ -0,0 +1,16 @@ +# Untitled string in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/userId +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## userId Type + +`string` diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties.md new file mode 100644 index 0000000..79c1155 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties.md @@ -0,0 +1,16 @@ +# Untitled undefined type in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## properties Type + +unknown diff --git a/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction.md b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction.md new file mode 100644 index 0000000..aa1538d --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions-clientpushchangesrequestaction.md @@ -0,0 +1,116 @@ +# ClientPushChangesRequestAction Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ------------ | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | No | Forbidden | Forbidden | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## ClientPushChangesRequestAction Type + +`object` ([ClientPushChangesRequestAction](clientpushchangesrequest-definitions-clientpushchangesrequestaction.md)) + +# ClientPushChangesRequestAction Properties + +| Property | Type | Required | Nullable | Defined by | +| :-------------------------------- | -------- | -------- | -------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [encodedAction](#encodedAction) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/encodedAction") | +| [sequenceNumber](#sequenceNumber) | `number` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/sequenceNumber") | +| [integrity](#integrity) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/integrity") | +| [type](#type) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/type") | +| [userId](#userId) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/userId") | + +## encodedAction + + + + +`encodedAction` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/encodedAction") + +### encodedAction Type + +`string` + +## sequenceNumber + + + + +`sequenceNumber` + +- is required +- Type: `number` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/sequenceNumber") + +### sequenceNumber Type + +`number` + +## integrity + + + + +`integrity` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/integrity") + +### integrity Type + +`string` + +## type + + + + +`type` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/type") + +### type Type + +`string` + +### type Constraints + +**enum**: the value of this property must be equal to one of the following values: + +| Value | Explanation | +| :----------- | ----------- | +| `"appLogic"` | | +| `"child"` | | +| `"parent"` | | + +## userId + + + + +`userId` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/userId") + +### userId Type + +`string` diff --git a/docs/schema/clientpushchangesrequest-definitions.md b/docs/schema/clientpushchangesrequest-definitions.md new file mode 100644 index 0000000..01e77a2 --- /dev/null +++ b/docs/schema/clientpushchangesrequest-definitions.md @@ -0,0 +1,16 @@ +# Untitled undefined type in ClientPushChangesRequest Schema + +```txt +https://timelimit.io/ClientPushChangesRequest#/definitions +``` + + + + +| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | +| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | +| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ClientPushChangesRequest.schema.json\*](ClientPushChangesRequest.schema.json "open original schema") | + +## definitions Type + +unknown diff --git a/docs/schema/clientpushchangesrequest-properties-actions.md b/docs/schema/clientpushchangesrequest-properties-actions.md index 974bae5..af63a0d 100644 --- a/docs/schema/clientpushchangesrequest-properties-actions.md +++ b/docs/schema/clientpushchangesrequest-properties-actions.md @@ -13,4 +13,4 @@ https://timelimit.io/ClientPushChangesRequest#/properties/actions ## actions Type -`object[]` ([Details](clientpushchangesrequest-properties-actions-items.md)) +`object[]` ([ClientPushChangesRequestAction](clientpushchangesrequest-definitions-clientpushchangesrequestaction.md)) diff --git a/docs/schema/clientpushchangesrequest.md b/docs/schema/clientpushchangesrequest.md index 47bf6e0..4989645 100644 --- a/docs/schema/clientpushchangesrequest.md +++ b/docs/schema/clientpushchangesrequest.md @@ -9,12 +9,120 @@ https://timelimit.io/ClientPushChangesRequest | Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In | | :------------------ | ---------- | -------------- | ------------ | :---------------- | --------------------- | ------------------- | --------------------------------------------------------------------------------------------------- | -| Can be instantiated | No | Unknown status | No | Forbidden | Forbidden | none | [ClientPushChangesRequest.schema.json](ClientPushChangesRequest.schema.json "open original schema") | +| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [ClientPushChangesRequest.schema.json](ClientPushChangesRequest.schema.json "open original schema") | ## ClientPushChangesRequest Type `object` ([ClientPushChangesRequest](clientpushchangesrequest.md)) +# ClientPushChangesRequest Definitions + +## Definitions group ClientPushChangesRequestAction + +Reference this group by using + +```json +{"$ref":"https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction"} +``` + +| Property | Type | Required | Nullable | Defined by | +| :-------------------------------- | -------- | -------- | -------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [encodedAction](#encodedAction) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/encodedAction") | +| [sequenceNumber](#sequenceNumber) | `number` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/sequenceNumber") | +| [integrity](#integrity) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/integrity") | +| [type](#type) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/type") | +| [userId](#userId) | `string` | Required | cannot be null | [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/userId") | + +### encodedAction + + + + +`encodedAction` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-encodedaction.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/encodedAction") + +#### encodedAction Type + +`string` + +### sequenceNumber + + + + +`sequenceNumber` + +- is required +- Type: `number` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-sequencenumber.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/sequenceNumber") + +#### sequenceNumber Type + +`number` + +### integrity + + + + +`integrity` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-integrity.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/integrity") + +#### integrity Type + +`string` + +### type + + + + +`type` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-type.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/type") + +#### type Type + +`string` + +#### type Constraints + +**enum**: the value of this property must be equal to one of the following values: + +| Value | Explanation | +| :----------- | ----------- | +| `"appLogic"` | | +| `"child"` | | +| `"parent"` | | + +### userId + + + + +`userId` + +- is required +- Type: `string` +- cannot be null +- defined in: [ClientPushChangesRequest](clientpushchangesrequest-definitions-clientpushchangesrequestaction-properties-userid.md "https://timelimit.io/ClientPushChangesRequest#/definitions/ClientPushChangesRequestAction/properties/userId") + +#### userId Type + +`string` + # ClientPushChangesRequest Properties | Property | Type | Required | Nullable | Defined by | @@ -46,10 +154,10 @@ https://timelimit.io/ClientPushChangesRequest `actions` - is required -- Type: `object[]` ([Details](clientpushchangesrequest-properties-actions-items.md)) +- Type: `object[]` ([ClientPushChangesRequestAction](clientpushchangesrequest-definitions-clientpushchangesrequestaction.md)) - cannot be null - defined in: [ClientPushChangesRequest](clientpushchangesrequest-properties-actions.md "https://timelimit.io/ClientPushChangesRequest#/properties/actions") ### actions Type -`object[]` ([Details](clientpushchangesrequest-properties-actions-items.md)) +`object[]` ([ClientPushChangesRequestAction](clientpushchangesrequest-definitions-clientpushchangesrequestaction.md)) diff --git a/src/api/admin.ts b/src/api/admin.ts index 45db694..67e9adc 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -96,27 +96,31 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: { throw new BadRequest() } - const userEntryUnsafe = await database.user.findOne({ - where: { - mail - }, - attributes: ['familyId'] - }) + await database.transaction(async (transaction) => { + const userEntryUnsafe = await database.user.findOne({ + where: { + mail + }, + attributes: ['familyId'], + transaction + }) - if (!userEntryUnsafe) { - throw new Conflict('no user with specified mail address') - } + if (!userEntryUnsafe) { + throw new Conflict('no user with specified mail address') + } - const userEntry = { - familyId: userEntryUnsafe.familyId - } + const userEntry = { + familyId: userEntryUnsafe.familyId + } - await addPurchase({ - database, - familyId: userEntry.familyId, - type, - transactionId: 'manual-' + type + '-' + generatePurchaseId(), - websocket + await addPurchase({ + database, + familyId: userEntry.familyId, + type, + transactionId: 'manual-' + type + '-' + generatePurchaseId(), + websocket, + transaction + }) }) res.json({ ok: true }) diff --git a/src/api/parent.ts b/src/api/parent.ts index 0f28552..e9c0030 100644 --- a/src/api/parent.ts +++ b/src/api/parent.ts @@ -19,7 +19,7 @@ import { json } from 'body-parser' import { Router } from 'express' import { BadRequest, Forbidden, Unauthorized } from 'http-errors' import { config } from '../config' -import { Database } from '../database' +import { Database, Transaction } from '../database' import { removeDevice } from '../function/device/remove-device' import { canRecoverPassword } from '../function/parent/can-recover-password' import { createAddDeviceToken } from '../function/parent/create-add-device-token' @@ -46,7 +46,9 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } const { mailAuthToken } = req.body - const { status, mail } = await getStatusByMailToken({ database, mailAuthToken }) + const { status, mail } = await database.transaction(async (transaction) => { + return getStatusByMailToken({ database, mailAuthToken, transaction }) + }) res.json({ status, @@ -148,15 +150,17 @@ export const createParentRouter = ({ database, websocket }: {database: Database, } }) - async function assertAuthValidAndReturnDeviceEntry ({ deviceAuthToken, parentId, secondPasswordHash }: { + async function assertAuthValidAndReturnDeviceEntry ({ deviceAuthToken, parentId, secondPasswordHash, transaction }: { deviceAuthToken: string parentId: string secondPasswordHash: string + transaction: Transaction }) { const deviceEntry = await database.device.findOne({ where: { deviceAuthToken: deviceAuthToken - } + }, + transaction }) if (!deviceEntry) { @@ -173,7 +177,8 @@ export const createParentRouter = ({ database, websocket }: {database: Database, familyId: deviceEntry.familyId, type: 'parent', userId: deviceEntry.currentUserId - } + }, + transaction }) if (!parentEntry) { @@ -186,7 +191,8 @@ export const createParentRouter = ({ database, websocket }: {database: Database, type: 'parent', userId: parentId, secondPasswordHash: secondPasswordHash - } + }, + transaction }) if (!parentEntry) { @@ -203,13 +209,16 @@ export const createParentRouter = ({ database, websocket }: {database: Database, throw new BadRequest() } - const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ - deviceAuthToken: req.body.deviceAuthToken, - parentId: req.body.parentId, - secondPasswordHash: req.body.parentPasswordSecondHash - }) + const { token, deviceId } = await database.transaction(async (transaction) => { + const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ + deviceAuthToken: req.body.deviceAuthToken, + parentId: req.body.parentId, + secondPasswordHash: req.body.parentPasswordSecondHash, + transaction + }) - const { token, deviceId } = await createAddDeviceToken({ familyId: deviceEntry.familyId, database }) + return createAddDeviceToken({ familyId: deviceEntry.familyId, database, transaction }) + }) res.json({ token, deviceId }) } catch (ex) { @@ -244,17 +253,21 @@ export const createParentRouter = ({ database, websocket }: {database: Database, throw new BadRequest() } - const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ - deviceAuthToken: req.body.deviceAuthToken, - parentId: req.body.parentUserId, - secondPasswordHash: req.body.parentPasswordSecondHash - }) + await database.transaction(async (transaction) => { + const deviceEntry = await assertAuthValidAndReturnDeviceEntry({ + deviceAuthToken: req.body.deviceAuthToken, + parentId: req.body.parentUserId, + secondPasswordHash: req.body.parentPasswordSecondHash, + transaction + }) - await removeDevice({ - database, - familyId: deviceEntry.familyId, - deviceId: req.body.deviceId, - websocket + await removeDevice({ + database, + familyId: deviceEntry.familyId, + deviceId: req.body.deviceId, + websocket, + transaction + }) }) res.json({ ok: true }) diff --git a/src/api/purchase.ts b/src/api/purchase.ts index 2a53f6f..d88e17c 100644 --- a/src/api/purchase.ts +++ b/src/api/purchase.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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,12 +47,15 @@ export const createPurchaseRouter = ({ database, websocket }: { throw new BadRequest() } - const familyEntry = await requireFamilyEntry({ - database, - deviceAuthToken: req.body.deviceAuthToken - }) + const result: boolean = await database.transaction(async (transaction) => { + const familyEntry = await requireFamilyEntry({ + database, + deviceAuthToken: req.body.deviceAuthToken, + transaction + }) - const result = canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10) }) + return canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10) }) + }) res.json({ canDoPurchase: result ? 'yes' : 'no due to old purchase', @@ -69,56 +72,60 @@ export const createPurchaseRouter = ({ database, websocket }: { throw new BadRequest() } - const deviceEntryUnsafe = await database.device.findOne({ - where: { - deviceAuthToken: req.body.deviceAuthToken - }, - attributes: ['familyId'] - }) + await database.transaction(async (transaction) => { + const deviceEntryUnsafe = await database.device.findOne({ + where: { + deviceAuthToken: req.body.deviceAuthToken + }, + attributes: ['familyId'], + transaction + }) - if (!deviceEntryUnsafe) { - throw new Unauthorized() - } + if (!deviceEntryUnsafe) { + throw new Unauthorized() + } - const deviceEntry = { - familyId: deviceEntryUnsafe.familyId - } + const deviceEntry = { + familyId: deviceEntryUnsafe.familyId + } - if (!isGooglePlayPurchaseSignatureValid({ - receipt: req.body.receipt, - signature: req.body.signature - })) { - throw new Conflict() - } + if (!isGooglePlayPurchaseSignatureValid({ + receipt: req.body.receipt, + signature: req.body.signature + })) { + throw new Conflict() + } - const receipt = JSON.parse(req.body.receipt) + const receipt = JSON.parse(req.body.receipt) - if (typeof receipt !== 'object') { - throw new Conflict() - } + if (typeof receipt !== 'object') { + throw new Conflict() + } - let type: 'month' | 'year' + let type: 'month' | 'year' - if (receipt.productId === 'premium_year_2018') { - type = 'year' - } else if (receipt.productId === 'premium_month_2018') { - type = 'month' - } else { - throw new Conflict() - } + if (receipt.productId === 'premium_year_2018') { + type = 'year' + } else if (receipt.productId === 'premium_month_2018') { + type = 'month' + } else { + throw new Conflict() + } - const orderId = receipt.orderId + const orderId = receipt.orderId - if (typeof orderId !== 'string') { - throw new Conflict() - } + if (typeof orderId !== 'string') { + throw new Conflict() + } - await addPurchase({ - database, - familyId: deviceEntry.familyId, - type, - transactionId: orderId, - websocket + await addPurchase({ + database, + familyId: deviceEntry.familyId, + type, + transactionId: orderId, + websocket, + transaction + }) }) res.json({ ok: true }) diff --git a/src/api/schema.ts b/src/api/schema.ts index 6e8c162..7103c4e 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -20,13 +20,15 @@ import { optionalPasswordRegex, optionalSaltRegex } from '../util/password' export interface ClientPushChangesRequest { deviceAuthToken: string - actions: Array<{ - encodedAction: string - sequenceNumber: number - integrity: string - type: 'appLogic' | 'parent' | 'child' - userId: string - }> + actions: Array +} + +export interface ClientPushChangesRequestAction { + encodedAction: string + sequenceNumber: number + integrity: string + type: 'appLogic' | 'parent' | 'child' + userId: string } export interface ClientPullChangesRequest { diff --git a/src/api/sync.ts b/src/api/sync.ts index f2b5423..f5c7d38 100644 --- a/src/api/sync.ts +++ b/src/api/sync.ts @@ -158,14 +158,19 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager, throw new BadRequest() } - const removedEntry = await database.oldDevice.findOne({ - where: { - deviceAuthToken: req.body.deviceAuthToken - } + const isDeviceRemoved: boolean = await database.transaction(async (transaction) => { + const removedEntry = await database.oldDevice.findOne({ + where: { + deviceAuthToken: req.body.deviceAuthToken + }, + transaction + }) + + return !!removedEntry }) res.json({ - isDeviceRemoved: !!removedEntry + isDeviceRemoved }) } catch (ex) { next(ex) diff --git a/src/api/validator.ts b/src/api/validator.ts index fb3210c..1757f6d 100644 --- a/src/api/validator.ts +++ b/src/api/validator.ts @@ -4,6 +4,39 @@ const Ajv = require('ajv') const ajv = new Ajv() const definitions = { + "ClientPushChangesRequestAction": { + "type": "object", + "properties": { + "encodedAction": { + "type": "string" + }, + "sequenceNumber": { + "type": "number" + }, + "integrity": { + "type": "string" + }, + "type": { + "enum": [ + "appLogic", + "child", + "parent" + ], + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "encodedAction", + "integrity", + "sequenceNumber", + "type", + "userId" + ] + }, "ClientDataStatus": { "type": "object", "properties": { @@ -2219,37 +2252,7 @@ export const isClientPushChangesRequest: (value: object) => value is ClientPushC "actions": { "type": "array", "items": { - "type": "object", - "properties": { - "encodedAction": { - "type": "string" - }, - "sequenceNumber": { - "type": "number" - }, - "integrity": { - "type": "string" - }, - "type": { - "enum": [ - "appLogic", - "child", - "parent" - ], - "type": "string" - }, - "userId": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "encodedAction", - "integrity", - "sequenceNumber", - "type", - "userId" - ] + "$ref": "#/definitions/ClientPushChangesRequestAction" } } }, @@ -2258,6 +2261,7 @@ export const isClientPushChangesRequest: (value: object) => value is ClientPushC "actions", "deviceAuthToken" ], + "definitions": definitions, "$schema": "http://json-schema.org/draft-07/schema#" }) export const isClientPullChangesRequest: (value: object) => value is ClientPullChangesRequest = ajv.compile({ diff --git a/src/database/config.ts b/src/database/config.ts index 6fed9f4..22d19f9 100644 --- a/src/database/config.ts +++ b/src/database/config.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -46,5 +46,6 @@ export const attributes: SequelizeAttributes = { export const createConfigModel = (sequelize: Sequelize.Sequelize): ConfigModelStatic => sequelize.define('Config', attributes) as ConfigModelStatic export const configItemIds = { - statusMessage: 'status_message' + statusMessage: 'status_message', + selfTestData: 'self_test_data' } diff --git a/src/database/index.ts b/src/database/index.ts index 852af1f..fdddd9d 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -15,7 +15,9 @@ * along with this program. If not, see . */ +import { Promise as BluePromise } from 'bluebird' import * as Sequelize from 'sequelize' +import { generateIdWithinFamily } from '../util/token' import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken' import { AppModelStatic, createAppModel } from './app' import { AppActivityModelStatic, createAppActivityModel } from './appactivity' @@ -23,7 +25,7 @@ import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken' import { CategoryModelStatic, createCategoryModel } from './category' import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp' import { CategoryNetworkIdModelStatic, createCategoryNetworkIdModel } from './categorynetworkid' -import { ConfigModelStatic, createConfigModel } from './config' +import { configItemIds, ConfigModelStatic, createConfigModel } from './config' import { createDeviceModel, DeviceModelStatic } from './device' import { createFamilyModel, FamilyModelStatic } from './family' import { createMailLoginTokenModel, MailLoginTokenModelStatic } from './maillogintoken' @@ -36,6 +38,8 @@ import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime' import { createUserModel, UserModelStatic } from './user' import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory' +export type Transaction = Sequelize.Transaction + export interface Database { addDeviceToken: AddDeviceTokenModelStatic authtoken: AuthTokenModelStatic @@ -55,7 +59,7 @@ export interface Database { usedTime: UsedTimeModelStatic user: UserModelStatic userLimitLoginCategory: UserLimitLoginCategoryModelStatic - transaction: (autoCallback: (t: Sequelize.Transaction) => Promise) => Promise + transaction: (autoCallback: (t: Transaction) => Promise, options?: { transaction: Transaction }) => Promise dialect: string } @@ -78,8 +82,9 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({ usedTime: createUsedTimeModel(sequelize), user: createUserModel(sequelize), userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize), - transaction: (autoCallback: (transaction: Sequelize.Transaction) => Promise) => (sequelize.transaction({ - isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED + transaction: (autoCallback: (transaction: Transaction) => Promise, options?: { transaction: Transaction }) => (sequelize.transaction({ + isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED, + transaction: options?.transaction }, autoCallback) as any) as Promise, dialect: sequelize.getDialect() }) @@ -93,3 +98,48 @@ export const sequelize = new Sequelize.Sequelize(process.env.DATABASE_URL || 'sq export const defaultDatabase = createDatabase(sequelize) export const defaultUmzug = createUmzug(sequelize) + +class NestedTransactionTestException extends Error {} +class TestRollbackException extends NestedTransactionTestException {} +class NestedTransactionsNotWorkingException extends NestedTransactionTestException { constructor () { super('NestedTransactionsNotWorkingException') } } +class IllegalStateException extends NestedTransactionTestException {} + +export const wrapPromise = (promise: Promise) => BluePromise.resolve(promise) +export const warpPromiseReturner = (fun: () => Promise) => () => wrapPromise(fun()) + +export async function assertNestedTransactionsAreWorking (database: Database) { + const testValue = generateIdWithinFamily() + + // clean up just for the case + await database.config.destroy({ where: { id: configItemIds.selfTestData } }) + + await database.transaction(async (transaction) => { + const readOne = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction }) + + if (readOne) throw new IllegalStateException() + + await database.transaction(async (transaction) => { + await database.config.create({ id: configItemIds.selfTestData, value: testValue }, { transaction }) + + const readTwo = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction }) + + if (readTwo?.value !== testValue) throw new IllegalStateException() + + try { + await database.transaction(async (transaction) => { + await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction }) + + throw new TestRollbackException() + }, { transaction }) + } catch (ex) { + if (!(ex instanceof TestRollbackException)) throw ex + } + + const readThree = await database.config.findOne({ where: { id: configItemIds.selfTestData }, transaction }) + + if (readThree?.value !== testValue) throw new NestedTransactionsNotWorkingException() + + await database.config.destroy({ where: { id: configItemIds.selfTestData }, transaction }) + }, { transaction }) + }) +} diff --git a/src/function/authentication/index.ts b/src/function/authentication/index.ts index 1a6afae..7af460e 100644 --- a/src/function/authentication/index.ts +++ b/src/function/authentication/index.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -16,26 +16,27 @@ */ import { Unauthorized } from 'http-errors' -import { Database } from '../../database' +import { Database, Transaction } from '../../database' import { generateAuthToken } from '../../util/token' -export const createAuthTokenByMailAddress = async ({ mail, database }: {mail: string, database: Database}) => { +export const createAuthTokenByMailAddress = async ({ mail, database, transaction }: { mail: string, database: Database, transaction: Transaction }) => { const token = generateAuthToken() await database.authtoken.create({ token, mail, createdAt: Date.now().toString() - }) + }, { transaction }) return token } -export const getMailByAuthToken = async ({ mailAuthToken, database }: {mailAuthToken: string, database: Database}) => { +export const getMailByAuthToken = async ({ mailAuthToken, database, transaction }: { mailAuthToken: string, database: Database, transaction: Transaction }) => { const entry = await database.authtoken.findOne({ where: { token: mailAuthToken - } + }, + transaction }) if (entry) { @@ -45,8 +46,8 @@ export const getMailByAuthToken = async ({ mailAuthToken, database }: {mailAuthT } } -export const requireMailByAuthToken = async ({ mailAuthToken, database }: {mailAuthToken: string, database: Database}) => { - const mail = await getMailByAuthToken({ mailAuthToken, database }) +export const requireMailByAuthToken = async ({ mailAuthToken, database, transaction }: { mailAuthToken: string, database: Database, transaction: Transaction }) => { + const mail = await getMailByAuthToken({ mailAuthToken, database, transaction }) if (!mail) { throw new Unauthorized() diff --git a/src/function/authentication/login-by-mail.ts b/src/function/authentication/login-by-mail.ts index 1adc917..5247abb 100644 --- a/src/function/authentication/login-by-mail.ts +++ b/src/function/authentication/login-by-mail.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { Forbidden, Gone, InternalServerError, TooManyRequests } from 'http-errors' +import { Forbidden, Gone, TooManyRequests } from 'http-errors' import { Database } from '../../database' import { sendAuthenticationMail } from '../../util/mail' import { areWordSequencesEqual, randomWords } from '../../util/random-words' @@ -27,7 +27,8 @@ export const sendLoginCode = async ({ mail, locale, database }: { mail: string locale: string database: Database -}): Promise<{mailLoginToken: string}> => { + // no transaction here because this is directly called from an API endpoint +}): Promise<{ mailLoginToken: string }> => { try { await checkMailSendLimit(mail) } catch (ex) { @@ -43,12 +44,14 @@ export const sendLoginCode = async ({ mail, locale, database }: { locale }) - await database.mailLoginToken.create({ - mailLoginToken, - receivedCode: code, - mail, - createdAt: Date.now().toString(10), - remainingAttempts: 3 + await database.transaction(async (transaction) => { + await database.mailLoginToken.create({ + mailLoginToken, + receivedCode: code, + mail, + createdAt: Date.now().toString(10), + remainingAttempts: 3 + }, { transaction }) }) return { @@ -62,8 +65,9 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database mailLoginToken: string receivedCode: string database: Database -}): Promise<{mailAuthToken: string}> => { - const { mail, status } = await database.transaction(async (transaction) => { + // no transaction here because this is directly called from an API endpoint +}): Promise<{ mailAuthToken: string }> => { + return database.transaction(async (transaction) => { const entry = await database.mailLoginToken.findOne({ where: { mailLoginToken @@ -72,10 +76,7 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database }) if ((!entry) || entry.remainingAttempts === 0) { - return { - mail: null, - status: 'gone' - } + throw new Gone() } if (!areWordSequencesEqual(entry.receivedCode, receivedCode)) { @@ -84,35 +85,14 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database await entry.save({ transaction }) if (entry.remainingAttempts === 0) { - return { - mail: null, - status: 'gone' - } + throw new Gone() } else { - return { - mail: null, - status: 'forbidden' - } + throw new Forbidden() } } - return { - mail: entry.mail, - status: null - } + const mailAuthToken = await createAuthTokenByMailAddress({ mail: entry.mail, database, transaction }) + + return { mailAuthToken } }) - - if (!mail) { - if (status === 'gone') { - throw new Gone() - } else if (status === 'forbidden') { - throw new Forbidden() - } else { - throw new InternalServerError() - } - } - - const mailAuthToken = await createAuthTokenByMailAddress({ mail, database }) - - return { mailAuthToken } } diff --git a/src/function/child/add-device.ts b/src/function/child/add-device.ts index 6926ac6..710b569 100644 --- a/src/function/child/add-device.ts +++ b/src/function/child/add-device.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -21,14 +21,15 @@ import { Database } from '../../database' import { generateAuthToken, generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' import { prepareDeviceEntry } from '../device/prepare-device-entry' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export const addChildDevice = async ({ database, websocket, request }: { database: Database websocket: WebsocketApi request: RegisterChildDeviceRequest + // no transaction here because this is directly called from an API endpoint }) => { - const { response, familyId } = await database.transaction(async (transaction) => { + return database.transaction(async (transaction) => { const entry = await database.addDeviceToken.findOne({ where: { token: request.registerToken.toLowerCase() @@ -63,16 +64,11 @@ export const addChildDevice = async ({ database, websocket, request }: { transaction }) + await notifyClientsAboutChangesDelayed({ familyId, websocket, database, isImportant: true, sourceDeviceId: deviceId, transaction }) + return { - response: { - deviceId, - deviceAuthToken - }, - familyId + deviceId, + deviceAuthToken } }) - - await notifyClientsAboutChanges({ familyId, websocket, database, isImportant: true, sourceDeviceId: response.deviceId }) - - return response } diff --git a/src/function/child/logout-at-primary-device.ts b/src/function/child/logout-at-primary-device.ts index ea364b7..36fde61 100644 --- a/src/function/child/logout-at-primary-device.ts +++ b/src/function/child/logout-at-primary-device.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -23,6 +23,7 @@ export const logoutAtPrimaryDevice = async ({ deviceAuthToken, database, websock deviceAuthToken: string database: Database websocket: WebsocketApi + // no transaction here because this is directly called from an API endpoint }) => { await database.transaction(async (transaction) => { const ownDeviceEntryUnsafe = await database.device.findOne({ diff --git a/src/function/child/set-primary-device.ts b/src/function/child/set-primary-device.ts index 9877079..0d63390 100644 --- a/src/function/child/set-primary-device.ts +++ b/src/function/child/set-primary-device.ts @@ -21,7 +21,7 @@ import { config } from '../../config' import { Database } from '../../database' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, currentUserId, action }: { database: Database @@ -29,12 +29,9 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c deviceAuthToken: string currentUserId: string action: 'set this device' | 'unset this device' + // no transaction here because this is directly called from an API endpoint }): Promise<'assigned to other device' | 'requires full version' | 'success'> => { - const response = await database.transaction(async (transaction): Promise<{ - response: 'assigned to other device' | 'requires full version' | 'success', - sourceDeviceId: string, - familyId: string - }> => { + return database.transaction(async (transaction): Promise<'assigned to other device' | 'requires full version' | 'success'> => { const deviceEntryUnsafe = await database.device.findOne({ where: { deviceAuthToken @@ -106,22 +103,14 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c } if (!(familyEntry.hasFullVersion || config.alwaysPro)) { - return { - response: 'requires full version', - sourceDeviceId: deviceEntry.deviceId, - familyId: deviceEntry.familyId - } + return 'requires full version' } } if (action === 'set this device') { // check that no other device is selected if (userDeviceEntries.find((item) => item.deviceId === userEntry.currentDevice)) { - return { - response: 'assigned to other device', - sourceDeviceId: deviceEntry.deviceId, - familyId: deviceEntry.familyId - } + return 'assigned to other device' } // update @@ -171,23 +160,16 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c } }) - return { - response: 'success', - sourceDeviceId: deviceEntry.deviceId, - familyId: deviceEntry.familyId - } - }) - - if (response.response === 'success') { // trigger sync - await notifyClientsAboutChanges({ - familyId: response.familyId, - sourceDeviceId: response.sourceDeviceId, + await notifyClientsAboutChangesDelayed({ + familyId: deviceEntry.familyId, + sourceDeviceId: deviceEntry.deviceId, websocket, database, - isImportant: false // the source device knows it already + isImportant: false, // the source device knows it already + transaction }) - } - return response.response + return 'success' + }) } diff --git a/src/function/cleanup/delete-families.ts b/src/function/cleanup/delete-families.ts index 5174238..6e19993 100644 --- a/src/function/cleanup/delete-families.ts +++ b/src/function/cleanup/delete-families.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -22,6 +22,7 @@ import { Database } from '../../database' export async function deleteFamilies ({ database, familiyIds }: { database: Database familiyIds: Array + // no transaction here because this should run isolated }) { if (familiyIds.length === 0) { return diff --git a/src/function/cleanup/delete-old-families.ts b/src/function/cleanup/delete-old-families.ts index 4b1c89c..f4a407c 100644 --- a/src/function/cleanup/delete-old-families.ts +++ b/src/function/cleanup/delete-old-families.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -34,30 +34,34 @@ export async function deleteOldFamilies (database: Database) { } export async function findOldFamilyIds (database: Database) { - const familyIdsWithExpiredLicenses = await database.family.findAll({ - where: { - fullVersionUntil: { - [Sequelize.Op.lt]: (Date.now() - 1000 * 60 * 60 * 24 * 90 /* 90 days */).toString(10) - } - }, - attributes: ['familyId'] - }).map((item) => item.familyId) - - if (familyIdsWithExpiredLicenses.length === 0) { - return [] - } - - const recentlyUsedFamilyIds = await database.device.findAll({ - where: { - familyId: { - [Sequelize.Op.in]: familyIdsWithExpiredLicenses + return database.transaction(async (transaction) => { + const familyIdsWithExpiredLicenses = await database.family.findAll({ + where: { + fullVersionUntil: { + [Sequelize.Op.lt]: (Date.now() - 1000 * 60 * 60 * 24 * 90 /* 90 days */).toString(10) + } }, - lastConnectivity: { - [Sequelize.Op.gt]: (Date.now() - 1000 * 60 * 60 * 24 * 90 /* 90 days */).toString(10) - } - }, - attributes: ['familyId'] - }).map((item) => item.familyId) + attributes: ['familyId'], + transaction + }).map((item) => item.familyId) - return difference(familyIdsWithExpiredLicenses, recentlyUsedFamilyIds) + if (familyIdsWithExpiredLicenses.length === 0) { + return [] + } + + const recentlyUsedFamilyIds = await database.device.findAll({ + where: { + familyId: { + [Sequelize.Op.in]: familyIdsWithExpiredLicenses + }, + lastConnectivity: { + [Sequelize.Op.gt]: (Date.now() - 1000 * 60 * 60 * 24 * 90 /* 90 days */).toString(10) + } + }, + attributes: ['familyId'], + transaction + }).map((item) => item.familyId) + + return difference(familyIdsWithExpiredLicenses, recentlyUsedFamilyIds) + }) } diff --git a/src/function/device/remove-device.ts b/src/function/device/remove-device.ts index 43e1ff6..c4fad70 100644 --- a/src/function/device/remove-device.ts +++ b/src/function/device/remove-device.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -16,102 +16,102 @@ */ import { Conflict } from 'http-errors' -import { Database } from '../../database' +import { Database, Transaction } from '../../database' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' -export async function removeDevice ({ database, familyId, deviceId, websocket }: { +export async function removeDevice ({ database, familyId, deviceId, websocket, transaction }: { database: Database familyId: string deviceId: string websocket: WebsocketApi + transaction: Transaction }) { - const { oldDeviceAuthToken } = await database.transaction(async (transaction) => { - const deviceEntry = await database.device.findOne({ - where: { - familyId, - deviceId - }, - transaction - }) - - if (!deviceEntry) { - throw new Conflict() - } - - // remove app entries - await database.app.destroy({ - where: { - familyId, - deviceId - }, - transaction - }) - - await database.appActivity.destroy({ - where: { - familyId, - deviceId - }, - transaction - }) - - // remove as current device - await database.user.update({ - currentDevice: '' - }, { - where: { - familyId, - currentDevice: deviceId - }, - transaction - }) - - // add to old devices if it is not yet there (it could be there if it reported a uninstall) - const oldOldDeviceEntry = await database.oldDevice.findOne({ - where: { - deviceAuthToken: deviceEntry.deviceAuthToken - }, - transaction - }) - - if (!oldOldDeviceEntry) { - await database.oldDevice.create({ - deviceAuthToken: deviceEntry.deviceAuthToken - }, { - transaction - }) - } - - // remove from the device list - await deviceEntry.destroy({ transaction }) - - // invalidiate the caches - await database.family.update({ - deviceListVersion: generateVersionId(), - // the device could have become unassigned during this - userListVersion: generateVersionId() - }, { - where: { - familyId: deviceEntry.familyId - }, - transaction - }) - - return { oldDeviceAuthToken: deviceEntry.deviceAuthToken } + const deviceEntry = await database.device.findOne({ + where: { + familyId, + deviceId + }, + transaction }) - await notifyClientsAboutChanges({ + if (!deviceEntry) { + throw new Conflict() + } + + // remove app entries + await database.app.destroy({ + where: { + familyId, + deviceId + }, + transaction + }) + + await database.appActivity.destroy({ + where: { + familyId, + deviceId + }, + transaction + }) + + // remove as current device + await database.user.update({ + currentDevice: '' + }, { + where: { + familyId, + currentDevice: deviceId + }, + transaction + }) + + // add to old devices if it is not yet there (it could be there if it reported a uninstall) + const oldOldDeviceEntry = await database.oldDevice.findOne({ + where: { + deviceAuthToken: deviceEntry.deviceAuthToken + }, + transaction + }) + + if (!oldOldDeviceEntry) { + await database.oldDevice.create({ + deviceAuthToken: deviceEntry.deviceAuthToken + }, { + transaction + }) + } + + // remove from the device list + await deviceEntry.destroy({ transaction }) + + // invalidiate the caches + await database.family.update({ + deviceListVersion: generateVersionId(), + // the device could have become unassigned during this + userListVersion: generateVersionId() + }, { + where: { + familyId: deviceEntry.familyId + }, + transaction + }) + + await notifyClientsAboutChangesDelayed({ database, websocket, familyId, sourceDeviceId: null, - isImportant: false + isImportant: false, + transaction }) - websocket.triggerSyncByDeviceAuthToken({ - deviceAuthToken: oldDeviceAuthToken, - isImportant: true + transaction.afterCommit(() => { + websocket.triggerSyncByDeviceAuthToken({ + deviceAuthToken: deviceEntry.deviceAuthToken, + isImportant: true + }) }) } diff --git a/src/function/device/report-device-removed.ts b/src/function/device/report-device-removed.ts index f5c2cde..c500363 100644 --- a/src/function/device/report-device-removed.ts +++ b/src/function/device/report-device-removed.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -19,14 +19,15 @@ import { Database } from '../../database' import { generateAuthToken, generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' import { sendUninstallWarnings } from '../warningmail/uninstall' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export async function reportDeviceRemoved ({ database, deviceAuthToken, websocket }: { database: Database deviceAuthToken: string websocket: WebsocketApi + // no transaction here because this is directly called from an API endpoint }) { - const result = await database.transaction(async (transaction) => { + await database.transaction(async (transaction) => { const deviceEntry = await database.device.findOne({ where: { deviceAuthToken @@ -58,7 +59,21 @@ export async function reportDeviceRemoved ({ database, deviceAuthToken, websocke transaction }) - return { familyId: deviceEntry.familyId, deviceName: deviceEntry.name } + await notifyClientsAboutChangesDelayed({ + database, + websocket, + familyId: deviceEntry.familyId, + sourceDeviceId: null, + isImportant: false, + transaction + }) + + await sendUninstallWarnings({ + database, + familyId: deviceEntry.familyId, + deviceName: deviceEntry.name, + transaction + }) } else { const oldDeviceEntry = await database.oldDevice.findOne({ where: { @@ -70,24 +85,6 @@ export async function reportDeviceRemoved ({ database, deviceAuthToken, websocke if (!oldDeviceEntry) { throw new Error('device not found') } - - return null } }) - - if (result) { - await notifyClientsAboutChanges({ - database, - websocket, - familyId: result.familyId, - sourceDeviceId: null, - isImportant: false - }) - - await sendUninstallWarnings({ - database, - familyId: result.familyId, - deviceName: result.deviceName - }) - } } diff --git a/src/function/parent/can-recover-password.ts b/src/function/parent/can-recover-password.ts index e0b7bb0..302a526 100644 --- a/src/function/parent/can-recover-password.ts +++ b/src/function/parent/can-recover-password.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -22,16 +22,20 @@ export const canRecoverPassword = async ({ database, mailAuthToken, parentUserId database: Database mailAuthToken: string parentUserId: string -}) => { - const mail = await requireMailByAuthToken({ mailAuthToken, database }) + // no transaction here because this is directly called from an API endpoint +}): Promise => { + return database.transaction(async (transaction) => { + const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction }) - const entry = await database.user.findOne({ - where: { - mail, - userId: parentUserId, - type: 'parent' - } + const entry = await database.user.findOne({ + where: { + mail, + userId: parentUserId, + type: 'parent' + }, + transaction + }) + + return !!entry }) - - return !!entry } diff --git a/src/function/parent/create-add-device-token.ts b/src/function/parent/create-add-device-token.ts index 9064608..d314175 100644 --- a/src/function/parent/create-add-device-token.ts +++ b/src/function/parent/create-add-device-token.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -15,13 +15,14 @@ * along with this program. If not, see . */ -import { Database } from '../../database' +import { Database, Transaction } from '../../database' import { randomWords } from '../../util/random-words' import { generateIdWithinFamily } from '../../util/token' -export const createAddDeviceToken = async ({ familyId, database }: { +export const createAddDeviceToken = async ({ familyId, database, transaction }: { familyId: string database: Database + transaction: Transaction }) => { const token = randomWords(5) const deviceId = generateIdWithinFamily() @@ -29,7 +30,8 @@ export const createAddDeviceToken = async ({ familyId, database }: { await database.addDeviceToken.destroy({ where: { familyId - } + }, + transaction }) await database.addDeviceToken.create({ @@ -37,7 +39,7 @@ export const createAddDeviceToken = async ({ familyId, database }: { token: token.toLowerCase(), deviceId, createdAt: Date.now().toString() - }) + }, { transaction }) return { token, deviceId } } diff --git a/src/function/parent/create-family.ts b/src/function/parent/create-family.ts index 60a1a60..54262df 100644 --- a/src/function/parent/create-family.ts +++ b/src/function/parent/create-family.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -32,11 +32,12 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, timeZone: string, parentName: string, deviceName: string + // no transaction here because this is directly called from an API endpoint }) => { - const now = Date.now().toString(10) - const mail = await requireMailByAuthToken({ database, mailAuthToken }) - return database.transaction(async (transaction) => { + const now = Date.now().toString(10) + const mail = await requireMailByAuthToken({ database, mailAuthToken, transaction }) + // ensure that no family was created for this mail yet const exisitngUserEntry = await database.user.findOne({ where: { diff --git a/src/function/parent/get-status-by-mail-address.ts b/src/function/parent/get-status-by-mail-address.ts index 18e9ac7..bfa91c1 100644 --- a/src/function/parent/get-status-by-mail-address.ts +++ b/src/function/parent/get-status-by-mail-address.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -15,10 +15,12 @@ * along with this program. If not, see . */ -import { Database } from '../../database' +import { Database, Transaction } from '../../database' import { requireMailByAuthToken } from '../authentication' -const getStatusByMailAddress = async ({ mail, database }: {mail: string, database: Database}) => { +const getStatusByMailAddress = async ({ + mail, database, transaction +}: { mail: string, database: Database, transaction: Transaction }) => { if (!mail) { throw new Error('no mail address') } @@ -26,7 +28,8 @@ const getStatusByMailAddress = async ({ mail, database }: {mail: string, databas const entry = await database.user.findOne({ where: { mail - } + }, + transaction }) if (entry) { @@ -36,9 +39,11 @@ const getStatusByMailAddress = async ({ mail, database }: {mail: string, databas } } -export const getStatusByMailToken = async ({ mailAuthToken, database }: {mailAuthToken: string, database: Database}) => { - const mail = await requireMailByAuthToken({ mailAuthToken, database }) - const status = await getStatusByMailAddress({ mail, database }) +export const getStatusByMailToken = async ({ + mailAuthToken, database, transaction +}: { mailAuthToken: string, database: Database, transaction: Transaction }) => { + const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction }) + const status = await getStatusByMailAddress({ mail, database, transaction }) return { mail, status } } diff --git a/src/function/parent/link-mail-address.ts b/src/function/parent/link-mail-address.ts index 5924492..7c3ecb6 100644 --- a/src/function/parent/link-mail-address.ts +++ b/src/function/parent/link-mail-address.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -21,7 +21,7 @@ import { Database } from '../../database' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' import { requireMailByAuthToken } from '../authentication' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUserId, parentPasswordSecondHash, database, websocket }: { mailAuthToken: string @@ -30,32 +30,35 @@ export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUs parentPasswordSecondHash: string database: Database websocket: WebsocketApi + // no transaction here because this is directly called from an API endpoint }) => { - const deviceEntry = await database.device.findOne({ - where: { - deviceAuthToken - } - }) - - if (!deviceEntry) { - throw new Unauthorized() - } - - const familyId = deviceEntry.familyId - - const mailAddress = await requireMailByAuthToken({ mailAuthToken, database }) - - const exisitingUser = await database.user.findOne({ - where: { - mail: mailAddress - } - }) - - if (exisitingUser) { - throw new Conflict() - } - await database.transaction(async (transaction) => { + const deviceEntry = await database.device.findOne({ + where: { + deviceAuthToken + }, + transaction + }) + + if (!deviceEntry) { + throw new Unauthorized() + } + + const familyId = deviceEntry.familyId + + const mailAddress = await requireMailByAuthToken({ mailAuthToken, database, transaction }) + + const exisitingUser = await database.user.findOne({ + where: { + mail: mailAddress + }, + transaction + }) + + if (exisitingUser) { + throw new Conflict() + } + const parentEntry = await database.user.findOne({ where: { type: 'parent', @@ -95,13 +98,15 @@ export const linkMailAddress = async ({ mailAuthToken, deviceAuthToken, parentUs }, transaction }) - }) - await notifyClientsAboutChanges({ - familyId, - sourceDeviceId: null, - database, - websocket, - isImportant: true + // notify + await notifyClientsAboutChangesDelayed({ + familyId, + sourceDeviceId: null, + database, + websocket, + isImportant: true, + transaction + }) }) } diff --git a/src/function/parent/recover-parent-password.ts b/src/function/parent/recover-parent-password.ts index 426fb79..3040a99 100644 --- a/src/function/parent/recover-parent-password.ts +++ b/src/function/parent/recover-parent-password.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -16,34 +16,33 @@ */ import { Conflict } from 'http-errors' -import * as Sequelize from 'sequelize' import { ParentPassword } from '../../api/schema' import { Database } from '../../database' import { generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' import { requireMailByAuthToken } from '../authentication' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export const recoverParentPassword = async ({ database, websocket, password, mailAuthToken }: { database: Database websocket: WebsocketApi password: ParentPassword mailAuthToken: string + // no transaction here because this is directly called from an API endpoint }) => { - const mail = await requireMailByAuthToken({ mailAuthToken, database }) + await database.transaction(async (transaction) => { + const mail = await requireMailByAuthToken({ mailAuthToken, database, transaction }) - const { familyId } = await database.transaction(async (transaction) => { // update the user entry const userEntry = await database.user.findOne({ where: { mail }, - transaction, - lock: Sequelize.Transaction.LOCK.UPDATE + transaction }) if (!userEntry) { - return { familyId: null } + throw new Conflict() } userEntry.passwordHash = password.hash @@ -62,18 +61,13 @@ export const recoverParentPassword = async ({ database, websocket, password, mai transaction }) - return { familyId: userEntry.familyId } - }) - - if (familyId === null) { - throw new Conflict() - } - - await notifyClientsAboutChanges({ - database, - familyId, - websocket, - isImportant: true, - sourceDeviceId: null + await notifyClientsAboutChangesDelayed({ + database, + familyId: userEntry.familyId, + websocket, + isImportant: true, + sourceDeviceId: null, + transaction + }) }) } diff --git a/src/function/parent/sign-in-into-family.ts b/src/function/parent/sign-in-into-family.ts index 41bcad8..4da0527 100644 --- a/src/function/parent/sign-in-into-family.ts +++ b/src/function/parent/sign-in-into-family.ts @@ -22,7 +22,7 @@ import { generateAuthToken, generateIdWithinFamily, generateVersionId } from '.. import { WebsocketApi } from '../../websocket' import { requireMailByAuthToken } from '../authentication' import { prepareDeviceEntry } from '../device/prepare-device-entry' -import { notifyClientsAboutChanges } from '../websocket' +import { notifyClientsAboutChangesDelayed } from '../websocket' export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, deviceName, websocket }: { database: Database @@ -30,10 +30,11 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, newDeviceInfo: NewDeviceInfo deviceName: string websocket: WebsocketApi -}) => { - const mail = await requireMailByAuthToken({ database, mailAuthToken }) + // no transaction here because this is directly called from an API endpoint +}): Promise<{ deviceId: string; deviceAuthToken: string }> => { + return database.transaction(async (transaction) => { + const mail = await requireMailByAuthToken({ database, mailAuthToken, transaction }) - const { response, familyId, sourceDeviceId } = await database.transaction(async (transaction) => { const userEntryUnsafe = await database.user.findOne({ where: { mail @@ -73,23 +74,18 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, transaction }) - return { - response: { - deviceId, - deviceAuthToken - }, + await notifyClientsAboutChangesDelayed({ + familyId: userEntry.familyId, + websocket, + database, + isImportant: true, sourceDeviceId: deviceId, - familyId: userEntry.familyId + transaction + }) + + return { + deviceId, + deviceAuthToken } }) - - await notifyClientsAboutChanges({ - familyId, - websocket, - database, - isImportant: true, - sourceDeviceId - }) - - return response } diff --git a/src/function/purchase/add-purchase.ts b/src/function/purchase/add-purchase.ts index d0fc209..5078155 100644 --- a/src/function/purchase/add-purchase.ts +++ b/src/function/purchase/add-purchase.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -17,75 +17,75 @@ import { Conflict } from 'http-errors' import * as Sequelize from 'sequelize' -import { Database } from '../../database' -import { notifyClientsAboutChanges } from '../../function/websocket' +import { Database, Transaction } from '../../database' +import { notifyClientsAboutChangesDelayed } from '../../function/websocket' import { WebsocketApi } from '../../websocket' const day = 1000 * 60 * 60 * 24 const month = day * 31 const year = day * 366 -export const addPurchase = async ({ database, familyId, type, transactionId, websocket }: { +export const addPurchase = async ({ database, familyId, type, transactionId, websocket, transaction }: { database: Database familyId: string type: 'month' | 'year' transactionId: string websocket: WebsocketApi + transaction: Transaction }) => { const service = 'googleplay' - await database.transaction(async (transaction) => { - const oldPurchaseEntry = await database.purchase.findOne({ - where: { - service, - transactionId - }, - transaction - }) - - if (oldPurchaseEntry) { - return - } - - const familyEntry = await database.family.findOne({ - where: { - familyId - }, - transaction, - lock: Sequelize.Transaction.LOCK.UPDATE - }) - - if (!familyEntry) { - throw new Conflict() - } - - const previousFullVersionEndTime = familyEntry.fullVersionUntil - - const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + (type === 'year' ? year : month) - - familyEntry.fullVersionUntil = newFullVersionUntil.toString(10) - familyEntry.hasFullVersion = true - - await familyEntry.save({ transaction }) - - await database.purchase.create({ - familyId, + const oldPurchaseEntry = await database.purchase.findOne({ + where: { service, - transactionId, - type, - loggedAt: Date.now().toString(10), - previousFullVersionEndTime, - newFullVersionEndTime: newFullVersionUntil.toString(10) - }, { - transaction - }) + transactionId + }, + transaction + }) - await notifyClientsAboutChanges({ - familyId, - sourceDeviceId: null, - database, - websocket, - isImportant: true - }) + if (oldPurchaseEntry) { + return + } + + const familyEntry = await database.family.findOne({ + where: { + familyId + }, + transaction, + lock: Sequelize.Transaction.LOCK.UPDATE + }) + + if (!familyEntry) { + throw new Conflict() + } + + const previousFullVersionEndTime = familyEntry.fullVersionUntil + + const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + (type === 'year' ? year : month) + + familyEntry.fullVersionUntil = newFullVersionUntil.toString(10) + familyEntry.hasFullVersion = true + + await familyEntry.save({ transaction }) + + await database.purchase.create({ + familyId, + service, + transactionId, + type, + loggedAt: Date.now().toString(10), + previousFullVersionEndTime, + newFullVersionEndTime: newFullVersionUntil.toString(10) + }, { + transaction + }) + + await notifyClientsAboutChangesDelayed({ + familyId, + sourceDeviceId: null, + database, + websocket, + isImportant: true, + transaction }) } diff --git a/src/function/purchase/require-family-entry.ts b/src/function/purchase/require-family-entry.ts index 22458a6..2c4abb7 100644 --- a/src/function/purchase/require-family-entry.ts +++ b/src/function/purchase/require-family-entry.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -16,17 +16,19 @@ */ import { InternalServerError, Unauthorized } from 'http-errors' -import { Database } from '../../database' +import { Database, Transaction } from '../../database' -export const requireFamilyEntry = async ({ database, deviceAuthToken }: { +export const requireFamilyEntry = async ({ database, deviceAuthToken, transaction }: { database: Database deviceAuthToken: string + transaction: Transaction }) => { const deviceEntryUnsafe = await database.device.findOne({ where: { deviceAuthToken }, - attributes: ['familyId'] + attributes: ['familyId'], + transaction }) if (!deviceEntryUnsafe) { @@ -41,7 +43,8 @@ export const requireFamilyEntry = async ({ database, deviceAuthToken }: { where: { familyId: deviceEntry.familyId }, - attributes: ['fullVersionUntil'] + attributes: ['fullVersionUntil'], + transaction }) if (!familyEntryUnsafe) { diff --git a/src/function/statusmessage/index.ts b/src/function/statusmessage/index.ts index e66576f..e4ee70e 100644 --- a/src/function/statusmessage/index.ts +++ b/src/function/statusmessage/index.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -33,16 +33,19 @@ export const setStatusMessage = async ({ database, newStatusMessage }: { database: Database newStatusMessage: string }) => { - if (newStatusMessage === '') { - await database.config.destroy({ - where: { - id: configItemIds.statusMessage - } - }) - } else { - await database.config.upsert({ - id: configItemIds.statusMessage, - value: newStatusMessage - }) - } + await database.transaction(async (transaction) => { + if (newStatusMessage === '') { + await database.config.destroy({ + where: { + id: configItemIds.statusMessage + }, + transaction + }) + } else { + await database.config.upsert({ + id: configItemIds.statusMessage, + value: newStatusMessage + }, { transaction }) + } + }) } diff --git a/src/function/sync/apply-actions/cache.ts b/src/function/sync/apply-actions/cache.ts index cea35d7..58199d7 100644 --- a/src/function/sync/apply-actions/cache.ts +++ b/src/function/sync/apply-actions/cache.ts @@ -25,7 +25,7 @@ import { generateVersionId } from '../../../util/token' export class Cache { readonly familyId: string readonly hasFullVersion: boolean - readonly transaction: Sequelize.Transaction + transaction: Sequelize.Transaction readonly database: Database readonly connectedDevicesManager: VisibleConnectedDevicesManager private shouldTriggerFullSync = false @@ -56,6 +56,22 @@ export class Cache { this.connectedDevicesManager = connectedDevicesManager } + async subtransaction (callback: () => Promise): Promise { + const oldTransaction = this.transaction + + return this.database.transaction(async (newTransaction) => { + try { + this.transaction = newTransaction + + const result = await callback() + + return result + } finally { + this.transaction = oldTransaction + } + }, { transaction: oldTransaction }) + } + getSecondPasswordHashOfParent = memoize(async (parentId: string) => { const userEntryUnsafe = await this.database.user.findOne({ where: { diff --git a/src/function/sync/apply-actions/index.ts b/src/function/sync/apply-actions/index.ts index 66b9da2..7743e35 100644 --- a/src/function/sync/apply-actions/index.ts +++ b/src/function/sync/apply-actions/index.ts @@ -15,7 +15,6 @@ * along with this program. If not, see . */ -import { createHash } from 'crypto' import { BadRequest, Unauthorized } from 'http-errors' import { parseAppLogicAction, parseChildAction, parseParentAction } from '../../../action/serialization' import { ClientPushChangesRequest } from '../../../api/schema' @@ -25,11 +24,12 @@ import { Database } from '../../../database' import { UserFlags } from '../../../model/userflags' import { EventHandler } from '../../../monitoring/eventhandler' import { WebsocketApi } from '../../../websocket' -import { notifyClientsAboutChanges } from '../../websocket' +import { notifyClientsAboutChangesDelayed } from '../../websocket' import { Cache } from './cache' import { dispatchAppLogicAction } from './dispatch-app-logic-action' import { dispatchChildAction } from './dispatch-child-action' import { dispatchParentAction } from './dispatch-parent-action' +import { assertActionIntegrity } from './integrity' export const applyActionsFromDevice = async ({ database, request, websocket, connectedDevicesManager, eventHandler }: { database: Database @@ -37,7 +37,7 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con request: ClientPushChangesRequest connectedDevicesManager: VisibleConnectedDevicesManager eventHandler: EventHandler -}) => { +}): Promise<{ shouldDoFullSync: boolean }> => { eventHandler.countEvent('applyActionsFromDevice') if (request.actions.length > 50) { @@ -46,7 +46,7 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con throw new BadRequest() } - const { shouldDoFullSync, areChangesImportant, sourceDeviceId, familyId } = await database.transaction(async (transaction) => { + return database.transaction(async (transaction) => { const deviceEntryUnsafe = await database.device.findOne({ where: { deviceAuthToken: request.deviceAuthToken @@ -91,9 +91,7 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con let { nextSequenceNumber } = deviceEntry - for (let i = 0; i < request.actions.length; i++) { - const action = request.actions[i] - + for (const action of request.actions) { if (action.sequenceNumber < nextSequenceNumber) { // action was already received @@ -104,191 +102,141 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con } try { - // update the next sequence number - nextSequenceNumber = action.sequenceNumber + 1 + await cache.subtransaction(async () => { + // update the next sequence number + nextSequenceNumber = action.sequenceNumber + 1 - let isChildLimitAdding = false + const { isChildLimitAdding } = await assertActionIntegrity({ + deviceId: deviceEntry.deviceId, + cache, + eventHandler, + action + }) - if (action.type === 'parent') { - if (action.integrity === 'device') { - const deviceEntryUnsafe2 = await cache.database.device.findOne({ - attributes: ['currentUserId'], - where: { - familyId: cache.familyId, + const parsedSerializedAction = JSON.parse(action.encodedAction) + + if (action.type === 'appLogic') { + if (!isSerializedAppLogicAction(parsedSerializedAction)) { + eventHandler.countEvent('applyActionsFromDevice invalidAppLogicAction') + + throw new Error('invalid action: ' + action.encodedAction) + } + + eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type) + + const parsedAction = parseAppLogicAction(parsedSerializedAction) + + try { + await dispatchAppLogicAction({ + action: parsedAction, + cache, deviceId: deviceEntry.deviceId, - currentUserId: action.userId, - isUserKeptSignedIn: true - }, - transaction: cache.transaction - }) + eventHandler + }) + } catch (ex) { + eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type) - if (!deviceEntryUnsafe2) { - throw new Error('user is not signed in at this device') + throw ex + } + } else if (action.type === 'parent') { + if (!isSerializedParentAction(parsedSerializedAction)) { + eventHandler.countEvent('applyActionsFromDevice invalidParentAction') + + throw new Error('invalid action' + action.encodedAction) } - // this ensures that the parent exists - await cache.getSecondPasswordHashOfParent(action.userId) - } else if (action.integrity === 'childDevice') { - // will be checked later - isChildLimitAdding = true + eventHandler.countEvent('applyActionsFromDevice, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) + + const parsedAction = parseParentAction(parsedSerializedAction) + + try { + if (isChildLimitAdding) { + const deviceEntryUnsafe2 = await cache.database.device.findOne({ + attributes: ['currentUserId'], + where: { + familyId: cache.familyId, + deviceId: deviceEntry.deviceId, + currentUserId: action.userId + }, + transaction: cache.transaction + }) + + if (!deviceEntryUnsafe2) { + throw new Error('illegal state') + } + + const deviceUserId = deviceEntryUnsafe2.currentUserId + + if (!deviceUserId) { + throw new Error('no device user id set but child add self limit action requested') + } + + const deviceUserEntryUnsafe = await cache.database.user.findOne({ + attributes: ['flags'], + where: { + familyId: cache.familyId, + userId: deviceUserId, + type: 'child' + }, + transaction: cache.transaction + }) + + if (!deviceUserEntryUnsafe) { + throw new Error('no child user found for child limit adding action') + } + + if ((parseInt(deviceUserEntryUnsafe.flags, 10) & UserFlags.ALLOW_SELF_LIMIT_ADD) !== UserFlags.ALLOW_SELF_LIMIT_ADD) { + throw new Error('child add limit action found but not allowed') + } + + await dispatchParentAction({ + action: parsedAction, + cache, + parentUserId: action.userId, + sourceDeviceId: deviceEntry.deviceId, + fromChildSelfLimitAddChildUserId: deviceUserId + }) + } else { + await dispatchParentAction({ + action: parsedAction, + cache, + parentUserId: action.userId, + sourceDeviceId: deviceEntry.deviceId, + fromChildSelfLimitAddChildUserId: null + }) + } + } catch (ex) { + eventHandler.countEvent('applyActionsFromDeviceWithError, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) + + throw ex + } + } else if (action.type === 'child') { + if (!isSerializedChildAction(parsedSerializedAction)) { + eventHandler.countEvent('applyActionsFromDevice invalidChildAction') + + throw new Error('invalid action: ' + action.encodedAction) + } + + eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type) + + const parsedAction = parseChildAction(parsedSerializedAction) + + try { + await dispatchChildAction({ + action: parsedAction, + cache, + childUserId: action.userId, + deviceId: deviceEntry.deviceId + }) + } catch (ex) { + eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type) + + throw ex + } } else { - const parentSecondHash = await cache.getSecondPasswordHashOfParent(action.userId) - - const integrityData = action.sequenceNumber.toString(10) + - deviceEntry.deviceId + - parentSecondHash + - action.encodedAction - - const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex') - - if (action.integrity !== expectedIntegrityValue) { - eventHandler.countEvent('applyActionsFromDevice parentActionInvalidIntegrityValue') - - throw new Error('invalid integrity value') - } + throw new Error('illegal state') } - } - - if (action.type === 'child') { - const childSecondHash = await cache.getSecondPasswordHashOfChild(action.userId) - - const integrityData = action.sequenceNumber.toString(10) + - deviceEntry.deviceId + - childSecondHash + - action.encodedAction - - const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex') - - if (action.integrity !== expectedIntegrityValue) { - eventHandler.countEvent('applyActionsFromDevice childActionInvalidIntegrityValue') - - throw new Error('invalid integrity value') - } - } - - const parsedSerializedAction = JSON.parse(action.encodedAction) - - if (action.type === 'appLogic') { - if (!isSerializedAppLogicAction(parsedSerializedAction)) { - eventHandler.countEvent('applyActionsFromDevice invalidAppLogicAction') - - throw new Error('invalid action: ' + action.encodedAction) - } - - eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type) - - const parsedAction = parseAppLogicAction(parsedSerializedAction) - - try { - await dispatchAppLogicAction({ - action: parsedAction, - cache, - deviceId: deviceEntry.deviceId, - eventHandler - }) - } catch (ex) { - eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type) - - throw ex - } - } else if (action.type === 'parent') { - if (!isSerializedParentAction(parsedSerializedAction)) { - eventHandler.countEvent('applyActionsFromDevice invalidParentAction') - - throw new Error('invalid action' + action.encodedAction) - } - - eventHandler.countEvent('applyActionsFromDevice, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) - - const parsedAction = parseParentAction(parsedSerializedAction) - - try { - if (isChildLimitAdding) { - const deviceEntryUnsafe2 = await cache.database.device.findOne({ - attributes: ['currentUserId'], - where: { - familyId: cache.familyId, - deviceId: deviceEntry.deviceId, - currentUserId: action.userId - }, - transaction: cache.transaction - }) - - if (!deviceEntryUnsafe2) { - throw new Error('illegal state') - } - - const deviceUserId = deviceEntryUnsafe2.currentUserId - - if (!deviceUserId) { - throw new Error('no device user id set but child add self limit action requested') - } - - const deviceUserEntryUnsafe = await cache.database.user.findOne({ - attributes: ['flags'], - where: { - familyId: cache.familyId, - userId: deviceUserId, - type: 'child' - }, - transaction: cache.transaction - }) - - if (!deviceUserEntryUnsafe) { - throw new Error('no child user found for child limit adding action') - } - - if ((parseInt(deviceUserEntryUnsafe.flags, 10) & UserFlags.ALLOW_SELF_LIMIT_ADD) !== UserFlags.ALLOW_SELF_LIMIT_ADD) { - throw new Error('child add limit action found but not allowed') - } - - await dispatchParentAction({ - action: parsedAction, - cache, - parentUserId: action.userId, - sourceDeviceId: deviceEntry.deviceId, - fromChildSelfLimitAddChildUserId: deviceUserId - }) - } else { - await dispatchParentAction({ - action: parsedAction, - cache, - parentUserId: action.userId, - sourceDeviceId: deviceEntry.deviceId, - fromChildSelfLimitAddChildUserId: null - }) - } - } catch (ex) { - eventHandler.countEvent('applyActionsFromDeviceWithError, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) - - throw ex - } - } else if (action.type === 'child') { - if (!isSerializedChildAction(parsedSerializedAction)) { - eventHandler.countEvent('applyActionsFromDevice invalidChildAction') - - throw new Error('invalid action: ' + action.encodedAction) - } - - eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type) - - const parsedAction = parseChildAction(parsedSerializedAction) - - try { - await dispatchChildAction({ - action: parsedAction, - cache, - childUserId: action.userId, - deviceId: deviceEntry.deviceId - }) - } catch (ex) { - eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type) - - throw ex - } - } else { - throw new Error('illegal state') - } + }) } catch (ex) { eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction') @@ -313,25 +261,23 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con await cache.saveModifiedVersionNumbers() - return { - shouldDoFullSync: cache.shouldDoFullSync(), - areChangesImportant: cache.areChangesImportant, + await notifyClientsAboutChangesDelayed({ + familyId: deviceEntry.familyId, sourceDeviceId: deviceEntry.deviceId, - familyId: deviceEntry.familyId + isImportant: cache.areChangesImportant, + websocket, + database, + transaction + }) + + if (cache.areChangesImportant) { + transaction.afterCommit(() => { + eventHandler.countEvent('applyActionsFromDevice areChangesImportant') + }) + } + + return { + shouldDoFullSync: cache.shouldDoFullSync() } }) - - if (areChangesImportant) { - eventHandler.countEvent('applyActionsFromDevice areChangesImportant') - } - - await notifyClientsAboutChanges({ - familyId, - sourceDeviceId, - isImportant: areChangesImportant, - websocket, - database - }) - - return { shouldDoFullSync } } diff --git a/src/function/sync/apply-actions/integrity.ts b/src/function/sync/apply-actions/integrity.ts new file mode 100644 index 0000000..f10083a --- /dev/null +++ b/src/function/sync/apply-actions/integrity.ts @@ -0,0 +1,89 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 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 + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { createHash } from 'crypto' +import { ClientPushChangesRequestAction } from '../../../api/schema' +import { EventHandler } from '../../../monitoring/eventhandler' +import { Cache } from './cache' + +export async function assertActionIntegrity ({ action, cache, eventHandler, deviceId }: { + action: ClientPushChangesRequestAction + cache: Cache + eventHandler: EventHandler + deviceId: string +}): Promise<{ isChildLimitAdding: boolean }> { + let isChildLimitAdding = false + + if (action.type === 'parent') { + if (action.integrity === 'device') { + const deviceEntryUnsafe = await cache.database.device.findOne({ + attributes: ['currentUserId'], + where: { + familyId: cache.familyId, + deviceId, + currentUserId: action.userId, + isUserKeptSignedIn: true + }, + transaction: cache.transaction + }) + + if (!deviceEntryUnsafe) { + throw new Error('user is not signed in at this device') + } + + // this ensures that the parent exists + await cache.getSecondPasswordHashOfParent(action.userId) + } else if (action.integrity === 'childDevice') { + // will be checked later + isChildLimitAdding = true + } else { + const parentSecondHash = await cache.getSecondPasswordHashOfParent(action.userId) + + const integrityData = action.sequenceNumber.toString(10) + + deviceId + + parentSecondHash + + action.encodedAction + + const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex') + + if (action.integrity !== expectedIntegrityValue) { + eventHandler.countEvent('applyActionsFromDevice parentActionInvalidIntegrityValue') + + throw new Error('invalid integrity value') + } + } + } + + if (action.type === 'child') { + const childSecondHash = await cache.getSecondPasswordHashOfChild(action.userId) + + const integrityData = action.sequenceNumber.toString(10) + + deviceId + + childSecondHash + + action.encodedAction + + const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex') + + if (action.integrity !== expectedIntegrityValue) { + eventHandler.countEvent('applyActionsFromDevice childActionInvalidIntegrityValue') + + throw new Error('invalid integrity value') + } + } + + return { isChildLimitAdding } +} diff --git a/src/function/warningmail/manipulation.ts b/src/function/warningmail/manipulation.ts index aacb76c..7735ea9 100644 --- a/src/function/warningmail/manipulation.ts +++ b/src/function/warningmail/manipulation.ts @@ -1,5 +1,21 @@ -import * as Sequelize from 'sequelize' -import { Database } from '../../database' +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 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 + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Database, Transaction, warpPromiseReturner } from '../../database' import { sendManipulationWarningMail } from '../../util/mail' import { canSendWarningMail } from '../../util/ratelimit-warningmail' @@ -7,7 +23,7 @@ export const sendManipulationWarnings = async ({ database, familyId, deviceName, database: Database familyId: string deviceName: string - transaction: Sequelize.Transaction + transaction: Transaction }) => { const parentEntries = await database.user.findAll({ where: { @@ -22,11 +38,13 @@ export const sendManipulationWarnings = async ({ database, familyId, deviceName, .filter((item) => (item.mailNotificationFlags & 1) === 1) .map((item) => item.mail) - await Promise.all( - targetMailAddresses.map(async (receiver) => { - if (await canSendWarningMail(receiver)) { - await sendManipulationWarningMail({ receiver, deviceName }) - } - }) - ) + transaction.afterCommit(warpPromiseReturner(async () => { + await Promise.all( + targetMailAddresses.map(async (receiver) => { + if (await canSendWarningMail(receiver)) { + await sendManipulationWarningMail({ receiver, deviceName }) + } + }) + ) + })) } diff --git a/src/function/warningmail/uninstall.ts b/src/function/warningmail/uninstall.ts index 0699593..efd1545 100644 --- a/src/function/warningmail/uninstall.ts +++ b/src/function/warningmail/uninstall.ts @@ -1,17 +1,36 @@ -import { Database } from '../../database' +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 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 + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Database, Transaction, warpPromiseReturner } from '../../database' import { sendUninstallWarningMail } from '../../util/mail' import { canSendWarningMail } from '../../util/ratelimit-warningmail' -export const sendUninstallWarnings = async ({ database, familyId, deviceName }: { +export const sendUninstallWarnings = async ({ database, familyId, deviceName, transaction }: { database: Database familyId: string deviceName: string + transaction: Transaction }) => { const parentEntries = await database.user.findAll({ where: { familyId, type: 'parent' - } + }, + transaction }) const targetMailAddresses = parentEntries @@ -19,11 +38,13 @@ export const sendUninstallWarnings = async ({ database, familyId, deviceName }: .filter((item) => (item.mailNotificationFlags & 1) === 1) .map((item) => item.mail) - await Promise.all( - targetMailAddresses.map(async (receiver) => { - if (await canSendWarningMail(receiver)) { - await sendUninstallWarningMail({ receiver, deviceName }) - } - }) - ) + transaction.afterCommit(warpPromiseReturner(async () => { + await Promise.all( + targetMailAddresses.map(async (receiver) => { + if (await canSendWarningMail(receiver)) { + await sendUninstallWarningMail({ receiver, deviceName }) + } + }) + ) + })) } diff --git a/src/function/websocket/index.ts b/src/function/websocket/index.ts index 7932331..56626fd 100644 --- a/src/function/websocket/index.ts +++ b/src/function/websocket/index.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -16,16 +16,16 @@ */ import * as Sequelize from 'sequelize' -import { Database } from '../../database' +import { Database, Transaction } from '../../database' import { WebsocketApi } from '../../websocket' -// this should be called AFTER an transaction was commited -export const notifyClientsAboutChanges = async ({ familyId, sourceDeviceId, database, websocket, isImportant }: { +export const notifyClientsAboutChangesDelayed = async ({ familyId, sourceDeviceId, database, websocket, isImportant, transaction }: { familyId: string sourceDeviceId: string | null // this device will not get an push database: Database websocket: WebsocketApi isImportant: boolean + transaction: Transaction }) => { const relatedDeviceEntries = (await database.device.findAll({ where: sourceDeviceId ? { @@ -41,10 +41,12 @@ export const notifyClientsAboutChanges = async ({ familyId, sourceDeviceId, data deviceAuthToken: item.deviceAuthToken })) - relatedDeviceEntries.forEach((item) => { - websocket.triggerSyncByDeviceAuthToken({ - deviceAuthToken: item.deviceAuthToken, - isImportant + transaction.afterCommit(() => { + relatedDeviceEntries.forEach((item) => { + websocket.triggerSyncByDeviceAuthToken({ + deviceAuthToken: item.deviceAuthToken, + isImportant + }) }) }) } diff --git a/src/index.ts b/src/index.ts index 1afb5af..260df4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { Server } from 'http' import { createApi } from './api' import { config } from './config' import { VisibleConnectedDevicesManager } from './connected-devices' -import { defaultDatabase, defaultUmzug } from './database' +import { assertNestedTransactionsAreWorking, defaultDatabase, defaultUmzug } from './database' import { EventHandler } from './monitoring/eventhandler' import { InMemoryEventHandler } from './monitoring/inmemoryeventhandler' import { createWebsocketHandler } from './websocket' @@ -30,6 +30,8 @@ async function main () { const database = defaultDatabase const eventHandler: EventHandler = new InMemoryEventHandler() + await assertNestedTransactionsAreWorking(database) + const connectedDevicesManager = new VisibleConnectedDevicesManager({ database }) diff --git a/src/worker/delete-deprecated-purchases.ts b/src/worker/delete-deprecated-purchases.ts index 9d04ec2..8469999 100644 --- a/src/worker/delete-deprecated-purchases.ts +++ b/src/worker/delete-deprecated-purchases.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 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 @@ -17,7 +17,7 @@ import * as Sequelize from 'sequelize' import { Database } from '../database' -import { notifyClientsAboutChanges } from '../function/websocket' +import { notifyClientsAboutChangesDelayed } from '../function/websocket' import { WebsocketApi } from '../websocket' export function initDeleteDeprecatedPurchasesWorker ({ database, websocket }: { @@ -43,7 +43,7 @@ async function deleteDeprecatedPurchases ({ database, websocket }: { database: Database websocket: WebsocketApi }) { - const { affectedFamilyIds } = await database.transaction(async (transaction) => { + await database.transaction(async (transaction) => { const affectedFamilyIds = await database.family.findAll({ where: { hasFullVersion: true, @@ -68,18 +68,17 @@ async function deleteDeprecatedPurchases ({ database, websocket }: { transaction }) + for (const familyId of affectedFamilyIds) { + await notifyClientsAboutChangesDelayed({ + familyId, + sourceDeviceId: null, + database, + websocket, + isImportant: true, + transaction + }) + } + return { affectedFamilyIds } }) - - for (let i = 0; i < affectedFamilyIds.length; i++) { - const familyId = affectedFamilyIds[i] - - await notifyClientsAboutChanges({ - familyId, - sourceDeviceId: null, - database, - websocket, - isImportant: true - }) - } }