Compare commits

...

44 commits

Author SHA1 Message Date
Jonas Lochmann
6189601459
Update dependencies 2025-06-14 19:08:56 +02:00
Jonas Lochmann
569e5ce62d
Update dependencies 2024-12-02 01:00:00 +01:00
Jonas Lochmann
6220cc6bb9
Update dependencies 2024-11-18 01:00:00 +01:00
Jonas Lochmann
33d9fd732f
Update dependencies 2024-10-07 02:00:00 +02:00
Jonas Lochmann
764f240707
Update dependencies 2024-09-09 02:00:00 +02:00
Jonas Lochmann
b392ca295a
Update dependencies 2024-09-09 02:00:00 +02:00
Jonas Lochmann
f5fc8e6cd6
Update dependencies 2024-08-19 02:00:00 +02:00
Jonas Lochmann
9c2048af64
Fix lint warnings 2024-07-29 02:00:00 +02:00
Jonas Lochmann
b69271f7df
Update umzug 2024-07-29 02:00:00 +02:00
Jonas Lochmann
97d2730b20
Update dependencies 2024-06-17 02:00:00 +02:00
Jonas Lochmann
0346197c23
Update dependencies 2024-06-10 02:00:00 +02:00
Jonas Lochmann
a7ed01af74
Update dependencies 2024-06-03 02:00:00 +02:00
Jonas Lochmann
f77d91ff56
Update dependencies 2024-04-29 02:00:00 +02:00
Jonas Lochmann
2d035da0da
Update dependencies 2024-04-08 02:00:00 +02:00
Jonas Lochmann
2d73cba90e
Update dependencies 2024-04-08 02:00:00 +02:00
Jonas Lochmann
1918c74277
Update dependencies 2024-03-25 01:00:00 +01:00
Jonas Lochmann
e55d1fd1a9
Update dependencies 2024-03-18 01:00:00 +01:00
Jonas Lochmann
f10b79a023
Add skipLibCheck to tsconfig.json 2024-03-18 01:00:00 +01:00
Jonas Lochmann
2c401288a3
Adjust json schema generation parameters 2024-03-18 01:00:00 +01:00
Jonas Lochmann
89f3325a18
Update dependencies 2024-03-04 01:00:00 +01:00
Jonas Lochmann
7aaad00881
Update dependencies 2024-02-05 01:00:00 +01:00
Jonas Lochmann
c7e4cfc9f9
Add workaround for session duration change logic bug 2024-01-01 01:00:00 +01:00
Jonas Lochmann
12ed5d73cd
Update dependencies 2023-09-18 02:00:00 +02:00
Jonas Lochmann
4df809a306
Update dependencies 2023-07-10 02:00:00 +02:00
Jonas Lochmann
8ec0781859
Add platformLevel and platformType 2023-06-13 16:17:53 +02:00
Jonas Lochmann
376a2cc624
Update dependencies 2023-06-13 16:17:46 +02:00
Jonas Lochmann
e14237be7d
Update dependencies 2023-05-29 02:00:00 +02:00
Jonas Lochmann
4183ea615a
Update dependencies 2023-05-08 02:00:00 +02:00
Jonas Lochmann
e46f5bea3f
Add account deletion API 2023-04-09 14:30:26 +02:00
Jonas Lochmann
05fac79849
Refactor signInByMailCode 2023-04-03 02:00:00 +02:00
Jonas Lochmann
1e5da1b95e
Improve logging of mails during development mode 2023-04-03 02:00:00 +02:00
Jonas Lochmann
dc5e2baebd
Add deleting session durations when deleting families
For the automatic deletion, they were always already deleted.
However, for manual deletion, this is required.
2023-04-03 02:00:00 +02:00
Jonas Lochmann
73465ebe6e
Update sequelize 2023-02-27 01:00:00 +01:00
Jonas Lochmann
98ee0fe94e
Fix ReferenceError instead of ParseYesNoException 2022-12-12 01:00:00 +01:00
Jonas Lochmann
d9a032823e
Update dependencies 2022-12-12 01:00:00 +01:00
Jonas Lochmann
ae044c19d6
Send ClientDataStatus after registering new device 2022-11-28 01:00:00 +01:00
Jonas Lochmann
41758c32f2
Update dependencies 2022-11-21 01:00:00 +01:00
Jonas Lochmann
38b113c9e8
Remove files with empty functions 2022-11-25 07:25:57 +01:00
Jonas Lochmann
83619eb98e
Fix saving app list in the new format 2022-11-25 07:25:54 +01:00
Jonas Lochmann
89e9b85bda
Remove saving Apps in the legacy format 2022-11-14 01:00:00 +01:00
Jonas Lochmann
f5198c7c0b
Retry transactions 2022-11-07 01:00:00 +01:00
Jonas Lochmann
9b8caccfbd
Update sequelize 2022-11-07 01:00:00 +01:00
Jonas Lochmann
747be7cf7d
Remove sending the unencrypted app list 2022-11-07 01:00:00 +01:00
Jonas Lochmann
fe1ce74ff3
Add socket activation support 2022-11-07 01:00:00 +01:00
73 changed files with 5007 additions and 2852 deletions

View file

@ -16,8 +16,7 @@ On a invalid request body: HTTP status code 400 Bad request
On a invalid add device token: HTTP status code 401 Unauthorized On a invalid add device token: HTTP status code 401 Unauthorized
On success: a JSON object with the properties ``deviceAuthToken`` and ``ownDeviceId``, On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
both of the type string
## POST /child/update-primary-device ## POST /child/update-primary-device

View file

@ -44,7 +44,7 @@ If the mail auth token is invalid/ expired: HTTP status code 401 Unauthorized
If there is already a user with the mail address of the mail auth token: HTTP status code 409 Conflict If there is already a user with the mail address of the mail auth token: HTTP status code 409 Conflict
On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string) On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
## POST /parent/sign-in-into-family ## POST /parent/sign-in-into-family
@ -60,7 +60,7 @@ On a invalid request body: HTTP status code 400 Bad Request
If there is no user with the mail address of the mail auth token: HTTP status code 409 Conflict If there is no user with the mail address of the mail auth token: HTTP status code 409 Conflict
On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string) On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
## POST /parent/can-recover-password ## POST /parent/can-recover-password
@ -194,3 +194,27 @@ If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized
If the server does not support this request: HTTP status code 404 If the server does not support this request: HTTP status code 404
On success: ``{"token": "some string"}``; you should not make any assumptions about the token string On success: ``{"token": "some string"}``; you should not make any assumptions about the token string
## POST /parent/delete-account
Use this to delete an account. This includes the complete family registration
with users and devices. Due to that, all parents with a linked mail address
have to authenticate this action.
## request
see [this JSON schema](../schema/deleteaccountpayload.md)
## response
On success: HTTP status code 200
On a invalid request body: HTTP status code 400 Bad Request
On unknown device auth token: HTTP status code 401 Unauthorized
On missing parent authentication: HTTP status code 401 Unauthorized
On unrelated parent authentication: HTTP status code 401 Unauthorized
If a newer endpoint must be used/the client is too old: HTTP status code 410 Gone

View file

@ -18,6 +18,9 @@
}, },
"parentName": { "parentName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View file

@ -0,0 +1,23 @@
{
"type": "object",
"properties": {
"deviceAuthToken": {
"type": "string"
},
"mailAuthTokens": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"deviceAuthToken",
"mailAuthTokens"
],
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DeleteAccountPayload",
"$id": "https://timelimit.io/DeleteAccountPayload"
}

View file

@ -14,6 +14,8 @@
* [CreateRegisterDeviceTokenRequest](./createregisterdevicetokenrequest.md) `https://timelimit.io/CreateRegisterDeviceTokenRequest` * [CreateRegisterDeviceTokenRequest](./createregisterdevicetokenrequest.md) `https://timelimit.io/CreateRegisterDeviceTokenRequest`
* [DeleteAccountPayload](./deleteaccountpayload.md) `https://timelimit.io/DeleteAccountPayload`
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) `https://timelimit.io/FinishPurchaseByGooglePlayRequest` * [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
* [IdentityTokenPayload](./identitytokenpayload.md) `https://timelimit.io/IdentityTokenPayload` * [IdentityTokenPayload](./identitytokenpayload.md) `https://timelimit.io/IdentityTokenPayload`
@ -282,6 +284,8 @@
* [Untitled array in ClientPushChangesRequest](./clientpushchangesrequest-properties-actions.md) `https://timelimit.io/ClientPushChangesRequest#/properties/actions` * [Untitled array in ClientPushChangesRequest](./clientpushchangesrequest-properties-actions.md) `https://timelimit.io/ClientPushChangesRequest#/properties/actions`
* [Untitled array in DeleteAccountPayload](./deleteaccountpayload-properties-mailauthtokens.md) `https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens`
* [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps` * [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps`
* [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i` * [Untitled array in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i`

View file

@ -9,6 +9,9 @@
}, },
"deviceName": { "deviceName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View file

@ -545,6 +545,12 @@
}, },
"addedManipulationFlags": { "addedManipulationFlags": {
"type": "number" "type": "number"
},
"platformType": {
"type": "string"
},
"platformLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View file

@ -212,6 +212,12 @@
}, },
"pk": { "pk": {
"type": "string" "type": "string"
},
"pType": {
"type": "string"
},
"pLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -240,6 +246,7 @@
"model", "model",
"name", "name",
"networkTime", "networkTime",
"pLevel",
"qOrLater", "qOrLater",
"reboot", "reboot",
"rebootIsManipulation", "rebootIsManipulation",

View file

@ -9,6 +9,9 @@
}, },
"deviceName": { "deviceName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View file

@ -0,0 +1,15 @@
# Untitled number in CreateFamilyByMailTokenRequest Schema
```txt
https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [CreateFamilyByMailTokenRequest.schema.json\*](CreateFamilyByMailTokenRequest.schema.json "open original schema") |
## clientLevel Type
`number`

View file

@ -24,6 +24,7 @@ https://timelimit.io/CreateFamilyByMailTokenRequest
| [deviceName](#devicename) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-devicename.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/deviceName") | | [deviceName](#devicename) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-devicename.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/deviceName") |
| [timeZone](#timezone) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-timezone.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/timeZone") | | [timeZone](#timezone) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-timezone.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/timeZone") |
| [parentName](#parentname) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-parentname.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentName") | | [parentName](#parentname) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-parentname.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel") |
## mailAuthToken ## mailAuthToken
@ -133,6 +134,24 @@ https://timelimit.io/CreateFamilyByMailTokenRequest
`string` `string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel")
### clientLevel Type
`number`
# CreateFamilyByMailTokenRequest Definitions # CreateFamilyByMailTokenRequest Definitions
## Definitions group PlaintextParentPassword ## Definitions group PlaintextParentPassword

View file

@ -0,0 +1,15 @@
# Untitled undefined type in DeleteAccountPayload Schema
```txt
https://timelimit.io/DeleteAccountPayload#/definitions
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") |
## definitions Type
unknown

View file

@ -0,0 +1,15 @@
# Untitled string in DeleteAccountPayload Schema
```txt
https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") |
## deviceAuthToken Type
`string`

View file

@ -0,0 +1,15 @@
# Untitled string in DeleteAccountPayload Schema
```txt
https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens/items
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") |
## items Type
`string`

View file

@ -0,0 +1,15 @@
# Untitled array in DeleteAccountPayload Schema
```txt
https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [DeleteAccountPayload.schema.json\*](DeleteAccountPayload.schema.json "open original schema") |
## mailAuthTokens Type
`string[]`

View file

@ -0,0 +1,60 @@
# DeleteAccountPayload Schema
```txt
https://timelimit.io/DeleteAccountPayload
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------------ |
| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [DeleteAccountPayload.schema.json](DeleteAccountPayload.schema.json "open original schema") |
## DeleteAccountPayload Type
`object` ([DeleteAccountPayload](deleteaccountpayload.md))
# DeleteAccountPayload Properties
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| [deviceAuthToken](#deviceauthtoken) | `string` | Required | cannot be null | [DeleteAccountPayload](deleteaccountpayload-properties-deviceauthtoken.md "https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken") |
| [mailAuthTokens](#mailauthtokens) | `array` | Required | cannot be null | [DeleteAccountPayload](deleteaccountpayload-properties-mailauthtokens.md "https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens") |
## deviceAuthToken
`deviceAuthToken`
* is required
* Type: `string`
* cannot be null
* defined in: [DeleteAccountPayload](deleteaccountpayload-properties-deviceauthtoken.md "https://timelimit.io/DeleteAccountPayload#/properties/deviceAuthToken")
### deviceAuthToken Type
`string`
## mailAuthTokens
`mailAuthTokens`
* is required
* Type: `string[]`
* cannot be null
* defined in: [DeleteAccountPayload](deleteaccountpayload-properties-mailauthtokens.md "https://timelimit.io/DeleteAccountPayload#/properties/mailAuthTokens")
### mailAuthTokens Type
`string[]`
# DeleteAccountPayload Definitions

View file

@ -0,0 +1,15 @@
# Untitled number in RegisterChildDeviceRequest Schema
```txt
https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RegisterChildDeviceRequest.schema.json\*](RegisterChildDeviceRequest.schema.json "open original schema") |
## clientLevel Type
`number`

View file

@ -21,6 +21,7 @@ https://timelimit.io/RegisterChildDeviceRequest
| [registerToken](#registertoken) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-registertoken.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/registerToken") | | [registerToken](#registertoken) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-registertoken.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/registerToken") |
| [childDevice](#childdevice) | `object` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-definitions-newdeviceinfo.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice") | | [childDevice](#childdevice) | `object` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-definitions-newdeviceinfo.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice") |
| [deviceName](#devicename) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-devicename.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/deviceName") | | [deviceName](#devicename) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-devicename.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/deviceName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel") |
## registerToken ## registerToken
@ -76,6 +77,24 @@ https://timelimit.io/RegisterChildDeviceRequest
`string` `string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel")
### clientLevel Type
`number`
# RegisterChildDeviceRequest Definitions # RegisterChildDeviceRequest Definitions
## Definitions group NewDeviceInfo ## Definitions group NewDeviceInfo

View file

@ -0,0 +1,15 @@
# Untitled number in SerializedAppLogicAction Schema
```txt
https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedAppLogicAction.schema.json\*](SerializedAppLogicAction.schema.json "open original schema") |
## platformLevel Type
`number`

View file

@ -0,0 +1,15 @@
# Untitled string in SerializedAppLogicAction Schema
```txt
https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformType
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedAppLogicAction.schema.json\*](SerializedAppLogicAction.schema.json "open original schema") |
## platformType Type
`string`

View file

@ -28,6 +28,8 @@ https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDevic
| [didReboot](#didreboot) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-didreboot.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/didReboot") | | [didReboot](#didreboot) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-didreboot.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/didReboot") |
| [isQOrLaterNow](#isqorlaternow) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-isqorlaternow.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/isQOrLaterNow") | | [isQOrLaterNow](#isqorlaternow) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-isqorlaternow.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/isQOrLaterNow") |
| [addedManipulationFlags](#addedmanipulationflags) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-addedmanipulationflags.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/addedManipulationFlags") | | [addedManipulationFlags](#addedmanipulationflags) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-addedmanipulationflags.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/addedManipulationFlags") |
| [platformType](#platformtype) | `string` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformtype.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformType") |
| [platformLevel](#platformlevel) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformlevel.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformLevel") |
## type ## type
@ -257,3 +259,39 @@ https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDevic
### addedManipulationFlags Type ### addedManipulationFlags Type
`number` `number`
## platformType
`platformType`
* is optional
* Type: `string`
* cannot be null
* defined in: [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformtype.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformType")
### platformType Type
`string`
## platformLevel
`platformLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformlevel.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformLevel")
### platformLevel Type
`number`

View file

@ -1240,6 +1240,8 @@ Reference this group by using
| [didReboot](#didreboot) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-didreboot.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/didReboot") | | [didReboot](#didreboot) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-didreboot.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/didReboot") |
| [isQOrLaterNow](#isqorlaternow) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-isqorlaternow.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/isQOrLaterNow") | | [isQOrLaterNow](#isqorlaternow) | `boolean` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-isqorlaternow.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/isQOrLaterNow") |
| [addedManipulationFlags](#addedmanipulationflags) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-addedmanipulationflags.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/addedManipulationFlags") | | [addedManipulationFlags](#addedmanipulationflags) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-addedmanipulationflags.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/addedManipulationFlags") |
| [platformType](#platformtype) | `string` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformtype.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformType") |
| [platformLevel](#platformlevel) | `number` | Optional | cannot be null | [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformlevel.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformLevel") |
### type ### type
@ -1470,6 +1472,42 @@ Reference this group by using
`number` `number`
### platformType
`platformType`
* is optional
* Type: `string`
* cannot be null
* defined in: [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformtype.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformType")
#### platformType Type
`string`
### platformLevel
`platformLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [SerializedAppLogicAction](serializedapplogicaction-definitions-serializedupdatedevicestatusaction-properties-platformlevel.md "https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction/properties/platformLevel")
#### platformLevel Type
`number`
## Definitions group SerializedUploadDevicePublicKeyAction ## Definitions group SerializedUploadDevicePublicKeyAction
Reference this group by using Reference this group by using

View file

@ -0,0 +1,15 @@
# Untitled number in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------ |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## pLevel Type
`number`

View file

@ -0,0 +1,15 @@
# Untitled string in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pType
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------ |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## pType Type
`string`

View file

@ -50,6 +50,8 @@ https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData
| [qOrLater](#qorlater) | `boolean` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-qorlater.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/qOrLater") | | [qOrLater](#qorlater) | `boolean` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-qorlater.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/qOrLater") |
| [mFlags](#mflags) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-mflags.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/mFlags") | | [mFlags](#mflags) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-mflags.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/mFlags") |
| [pk](#pk) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-pk.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pk") | | [pk](#pk) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-pk.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pk") |
| [pType](#ptype) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-ptype.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pType") |
| [pLevel](#plevel) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-plevel.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pLevel") |
## deviceId ## deviceId
@ -718,3 +720,39 @@ https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData
### pk Type ### pk Type
`string` `string`
## pType
`pType`
* is optional
* Type: `string`
* cannot be null
* defined in: [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-ptype.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pType")
### pType Type
`string`
## pLevel
`pLevel`
* is required
* Type: `number`
* cannot be null
* defined in: [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-plevel.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pLevel")
### pLevel Type
`number`

View file

@ -435,6 +435,8 @@ Reference this group by using
| [qOrLater](#qorlater) | `boolean` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-qorlater.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/qOrLater") | | [qOrLater](#qorlater) | `boolean` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-qorlater.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/qOrLater") |
| [mFlags](#mflags) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-mflags.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/mFlags") | | [mFlags](#mflags) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-mflags.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/mFlags") |
| [pk](#pk) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-pk.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pk") | | [pk](#pk) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-pk.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pk") |
| [pType](#ptype) | `string` | Optional | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-ptype.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pType") |
| [pLevel](#plevel) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-plevel.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pLevel") |
### deviceId ### deviceId
@ -1104,6 +1106,42 @@ Reference this group by using
`string` `string`
### pType
`pType`
* is optional
* Type: `string`
* cannot be null
* defined in: [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-ptype.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pType")
#### pType Type
`string`
### pLevel
`pLevel`
* is required
* Type: `number`
* cannot be null
* defined in: [ServerDataStatus](serverdatastatus-definitions-serverdevicedata-properties-plevel.md "https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData/properties/pLevel")
#### pLevel Type
`number`
## Definitions group ProtectionLevel ## Definitions group ProtectionLevel
Reference this group by using Reference this group by using

View file

@ -0,0 +1,15 @@
# Untitled number in SignIntoFamilyRequest Schema
```txt
https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SignIntoFamilyRequest.schema.json\*](SignIntoFamilyRequest.schema.json "open original schema") |
## clientLevel Type
`number`

View file

@ -21,6 +21,7 @@ https://timelimit.io/SignIntoFamilyRequest
| [mailAuthToken](#mailauthtoken) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-mailauthtoken.md "https://timelimit.io/SignIntoFamilyRequest#/properties/mailAuthToken") | | [mailAuthToken](#mailauthtoken) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-mailauthtoken.md "https://timelimit.io/SignIntoFamilyRequest#/properties/mailAuthToken") |
| [parentDevice](#parentdevice) | `object` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-definitions-newdeviceinfo.md "https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice") | | [parentDevice](#parentdevice) | `object` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-definitions-newdeviceinfo.md "https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice") |
| [deviceName](#devicename) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-devicename.md "https://timelimit.io/SignIntoFamilyRequest#/properties/deviceName") | | [deviceName](#devicename) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-devicename.md "https://timelimit.io/SignIntoFamilyRequest#/properties/deviceName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel") |
## mailAuthToken ## mailAuthToken
@ -76,6 +77,24 @@ https://timelimit.io/SignIntoFamilyRequest
`string` `string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel")
### clientLevel Type
`number`
# SignIntoFamilyRequest Definitions # SignIntoFamilyRequest Definitions
## Definitions group NewDeviceInfo ## Definitions group NewDeviceInfo

View file

@ -0,0 +1,192 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;">
<div style="">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#009688" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#009688;background-color:#009688;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#009688;background-color:#009688;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#ffffff;">TimeLimit</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt. </p>
<p> You requested the deletion of your account. This deletion is finished now. </p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> Sie erhalten diese Nachricht aufgrund Ihrer Anfrage. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. </p>
<p> You got this mail due to your request. If you have got any questions, then you can reply to this message. </p>
<p> &copy; <%= mailimprint %> </p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
<mjml>
<mj-body>
<mj-section background-color="#009688">
<mj-column>
<mj-text font-size="20px" color="#ffffff">TimeLimit</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text>
<p>
Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt.
</p>
<p>
You requested the deletion of your account. This deletion is finished now.
</p>
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-divider border-width="1px" border-style="dashed" border-color="lightgrey" />
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text>
<p>
Sie erhalten diese Nachricht aufgrund Ihrer Anfrage.
Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.
</p>
<p>
You got this mail due to your request.
If you have got any questions, then you can reply to this message.
</p>
<p>
&copy; <%= mailimprint %>
</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

View file

@ -0,0 +1 @@
Konto gelöscht/Account deleted

View file

@ -0,0 +1,13 @@
Sie haben eine Löschung Ihres Benutzerkontos angefordert. Diese wurde soeben durchgeführt.
You requested the deletion of your account. This deletion is finished now.
----------------------
Sie erhalten diese Nachricht aufgrund Ihrer Anfrage.
Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.
You got this mail due to your request.
If you have got any questions, then you can reply to this message.
<C> <%- mailimprint %>

5762
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -38,12 +38,11 @@
"@types/lodash": "^4.14.166", "@types/lodash": "^4.14.166",
"@types/node": "^16.11.59", "@types/node": "^16.11.59",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/umzug": "^2.3.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/parser": "^5.10.0",
"eslint": "^8.7.0", "eslint": "^8.7.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"typescript": "^4.4.4", "typescript": "^5.5.4",
"typescript-json-schema": "^0.52.0" "typescript-json-schema": "^0.52.0"
}, },
"dependencies": { "dependencies": {
@ -61,12 +60,11 @@
"pg": "^8.5.1", "pg": "^8.5.1",
"pg-hstore": "^2.3.3", "pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15", "rate-limiter-flexible": "^2.1.15",
"sequelize": "^6.11.0", "sequelize": "^6.25.5",
"socket.io": "^4.0.1", "socket.io": "^4.0.1",
"sqlite3": "^4.0.0", "umzug": "^3.8.1"
"umzug": "^2.3.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"sqlite3": "^4.0.0" "sqlite3": "^5.0.0"
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -43,7 +43,8 @@ const types = [
'RequestWithAuthToken', 'RequestWithAuthToken',
'SendMailLoginCodeRequest', 'SendMailLoginCodeRequest',
'SignInByMailCodeRequest', 'SignInByMailCodeRequest',
'IdentityTokenPayload' 'IdentityTokenPayload',
'DeleteAccountPayload',
] ]
const docOnlyTypes = [ const docOnlyTypes = [
@ -57,7 +58,9 @@ const allTypes = [
const settings = { const settings = {
required: true, required: true,
noExtraProps: true noExtraProps: true,
// otherwise it finds errors in dependencies that we don't care about
ignoreErrors: true
}; };
const compilerOptions = { const compilerOptions = {

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { minPlatformLevel, maxPlatformLevel, minPlatformTypeLength, maxPlatformTypeLength } from '../database/device'
import { NewPermissionStatus } from '../model/newpermissionstatus' import { NewPermissionStatus } from '../model/newpermissionstatus'
import { ProtectionLevel } from '../model/protectionlevel' import { ProtectionLevel } from '../model/protectionlevel'
import { RuntimePermissionStatus } from '../model/runtimepermissionstatus' import { RuntimePermissionStatus } from '../model/runtimepermissionstatus'
@ -33,6 +34,8 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
readonly didReboot: boolean readonly didReboot: boolean
readonly isQOrLaterNow: boolean readonly isQOrLaterNow: boolean
readonly addedManipulationFlags: number readonly addedManipulationFlags: number
readonly platformType?: string
readonly platformLevel?: number
constructor ({ constructor ({
newProtetionLevel, newProtetionLevel,
@ -43,7 +46,9 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
newAppVersion, newAppVersion,
didReboot, didReboot,
isQOrLaterNow, isQOrLaterNow,
addedManipulationFlags addedManipulationFlags,
platformType,
platformLevel
}: { }: {
newProtetionLevel?: ProtectionLevel newProtetionLevel?: ProtectionLevel
newUsageStatsPermissionStatus?: RuntimePermissionStatus newUsageStatsPermissionStatus?: RuntimePermissionStatus
@ -54,6 +59,8 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
didReboot: boolean didReboot: boolean
isQOrLaterNow: boolean isQOrLaterNow: boolean
addedManipulationFlags: number addedManipulationFlags: number
platformType?: string
platformLevel?: number
}) { }) {
super() super()
@ -67,6 +74,20 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
assertSafeInteger({ actionType, field: 'addedManipulationFlags', value: addedManipulationFlags }) assertSafeInteger({ actionType, field: 'addedManipulationFlags', value: addedManipulationFlags })
if (platformType !== undefined) {
if (platformType.length < minPlatformTypeLength || platformType.length > maxPlatformTypeLength) {
throwOutOfRange({ actionType, field: 'platformType.length', value: platformType.length })
}
}
if (platformLevel !== undefined) {
assertSafeInteger({ actionType, field: 'platformLevel', value: platformLevel })
if (platformLevel < minPlatformLevel || platformLevel > maxPlatformLevel) {
throwOutOfRange({ actionType, field: 'platformLevel', value: platformLevel })
}
}
this.newProtetionLevel = newProtetionLevel this.newProtetionLevel = newProtetionLevel
this.newUsageStatsPermissionStatus = newUsageStatsPermissionStatus this.newUsageStatsPermissionStatus = newUsageStatsPermissionStatus
this.newNotificationAccessPermission = newNotificationAccessPermission this.newNotificationAccessPermission = newNotificationAccessPermission
@ -76,6 +97,8 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
this.didReboot = didReboot this.didReboot = didReboot
this.isQOrLaterNow = isQOrLaterNow this.isQOrLaterNow = isQOrLaterNow
this.addedManipulationFlags = addedManipulationFlags this.addedManipulationFlags = addedManipulationFlags
this.platformType = platformType
this.platformLevel = platformLevel
} }
static parse = ({ static parse = ({
@ -87,7 +110,9 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
appVersion, appVersion,
didReboot, didReboot,
isQOrLaterNow, isQOrLaterNow,
addedManipulationFlags addedManipulationFlags,
platformType,
platformLevel
}: SerializedUpdateDeviceStatusAction) => ( }: SerializedUpdateDeviceStatusAction) => (
new UpdateDeviceStatusAction({ new UpdateDeviceStatusAction({
newProtetionLevel: protectionLevel, newProtetionLevel: protectionLevel,
@ -98,7 +123,9 @@ export class UpdateDeviceStatusAction extends AppLogicAction {
newAppVersion: appVersion, newAppVersion: appVersion,
didReboot: !!didReboot, didReboot: !!didReboot,
isQOrLaterNow: !!isQOrLaterNow, isQOrLaterNow: !!isQOrLaterNow,
addedManipulationFlags: addedManipulationFlags || 0 addedManipulationFlags: addedManipulationFlags || 0,
platformType: platformType,
platformLevel: platformLevel
}) })
) )
} }
@ -114,4 +141,6 @@ export interface SerializedUpdateDeviceStatusAction {
didReboot?: boolean didReboot?: boolean
isQOrLaterNow?: boolean isQOrLaterNow?: boolean
addedManipulationFlags?: number addedManipulationFlags?: number
platformType?: string
platformLevel?: number
} }

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann * Copyright (C) 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -24,10 +24,12 @@ import { logoutAtPrimaryDevice } from '../function/child/logout-at-primary-devic
import { setPrimaryDevice } from '../function/child/set-primary-device' import { setPrimaryDevice } from '../function/child/set-primary-device'
import { WebsocketApi } from '../websocket' import { WebsocketApi } from '../websocket'
import { isRegisterChildDeviceRequest, isRequestWithAuthToken, isUpdatePrimaryDeviceRequest } from './validator' import { isRegisterChildDeviceRequest, isRequestWithAuthToken, isUpdatePrimaryDeviceRequest } from './validator'
import { EventHandler } from '../monitoring/eventhandler'
export const createChildRouter = ({ database, websocket }: { export const createChildRouter = ({ database, websocket, eventHandler }: {
database: Database, database: Database
websocket: WebsocketApi websocket: WebsocketApi
eventHandler: EventHandler
}) => { }) => {
const router = Router() const router = Router()
@ -37,15 +39,17 @@ export const createChildRouter = ({ database, websocket }: {
throw new BadRequest() throw new BadRequest()
} }
const { deviceAuthToken, deviceId } = await addChildDevice({ const { deviceAuthToken, deviceId, data } = await addChildDevice({
request: req.body, request: req.body,
database, database,
eventHandler,
websocket websocket
}) })
res.json({ res.json({
deviceAuthToken, deviceAuthToken,
ownDeviceId: deviceId ownDeviceId: deviceId,
data
}) })
} catch (ex) { } catch (ex) {
next(ex) next(ex)

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -47,8 +47,8 @@ export const createApi = ({ database, websocket, connectedDevicesManager, eventH
}) })
app.use('/auth', createAuthRouter(database)) app.use('/auth', createAuthRouter(database))
app.use('/child', createChildRouter({ database, websocket })) app.use('/child', createChildRouter({ database, websocket, eventHandler }))
app.use('/parent', createParentRouter({ database, websocket })) app.use('/parent', createParentRouter({ database, websocket, eventHandler }))
app.use('/purchase', createPurchaseRouter({ database, websocket })) app.use('/purchase', createPurchaseRouter({ database, websocket }))
app.use('/sync', createSyncRouter({ database, websocket, connectedDevicesManager, eventHandler })) app.use('/sync', createSyncRouter({ database, websocket, connectedDevicesManager, eventHandler }))

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -21,6 +21,7 @@ import { Router } from 'express'
import { BadRequest, Forbidden, Unauthorized } from 'http-errors' import { BadRequest, Forbidden, Unauthorized } from 'http-errors'
import { config } from '../config' import { config } from '../config'
import { Database, Transaction } from '../database' import { Database, Transaction } from '../database'
import { deleteAccount } from '../function/cleanup/account-deletion'
import { removeDevice } from '../function/device/remove-device' import { removeDevice } from '../function/device/remove-device'
import { createAddDeviceToken } from '../function/parent/create-add-device-token' import { createAddDeviceToken } from '../function/parent/create-add-device-token'
import { createFamily } from '../function/parent/create-family' import { createFamily } from '../function/parent/create-family'
@ -31,14 +32,22 @@ import { signInIntoFamily } from '../function/parent/sign-in-into-family'
import { validateU2fIntegrity, U2fValidationError } from '../function/u2f' import { validateU2fIntegrity, U2fValidationError } from '../function/u2f'
import { createIdentityToken, MissingSignSecretException } from '../util/identity-token' import { createIdentityToken, MissingSignSecretException } from '../util/identity-token'
import { WebsocketApi } from '../websocket' import { WebsocketApi } from '../websocket'
import { EventHandler } from '../monitoring/eventhandler'
import { import {
isCreateFamilyByMailTokenRequest, isCreateFamilyByMailTokenRequest,
isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest, isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest,
isMailAuthTokenRequestBody, isRecoverParentPasswordRequest, isMailAuthTokenRequestBody, isRecoverParentPasswordRequest,
isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest,
isDeleteAccountPayload
} from './validator' } from './validator'
export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => { export const createParentRouter = ({
database, websocket, eventHandler
}: {
database: Database
websocket: WebsocketApi
eventHandler: EventHandler
}) => {
const router = Router() const router = Router()
router.post('/get-status-by-mail-address', json(), async (req, res, next) => { router.post('/get-status-by-mail-address', json(), async (req, res, next) => {
@ -75,17 +84,20 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
const result = await createFamily({ const result = await createFamily({
database, database,
eventHandler,
firstParentDevice: req.body.parentDevice, firstParentDevice: req.body.parentDevice,
mailAuthToken: req.body.mailAuthToken, mailAuthToken: req.body.mailAuthToken,
password: req.body.parentPassword, password: req.body.parentPassword,
deviceName: req.body.deviceName, deviceName: req.body.deviceName,
parentName: req.body.parentName, parentName: req.body.parentName,
timeZone: req.body.timeZone timeZone: req.body.timeZone,
clientLevel: req.body.clientLevel || null
}) })
res.json({ res.json({
deviceAuthToken: result.deviceAuthToken, deviceAuthToken: result.deviceAuthToken,
ownDeviceId: result.deviceId ownDeviceId: result.deviceId,
data: result.data
}) })
} catch (ex) { } catch (ex) {
next(ex) next(ex)
@ -100,15 +112,18 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
const result = await signInIntoFamily({ const result = await signInIntoFamily({
database, database,
eventHandler,
newDeviceInfo: req.body.parentDevice, newDeviceInfo: req.body.parentDevice,
mailAuthToken: req.body.mailAuthToken, mailAuthToken: req.body.mailAuthToken,
deviceName: req.body.deviceName, deviceName: req.body.deviceName,
clientLevel: req.body.clientLevel || null,
websocket websocket
}) })
res.json({ res.json({
deviceAuthToken: result.deviceAuthToken, deviceAuthToken: result.deviceAuthToken,
ownDeviceId: result.deviceId ownDeviceId: result.deviceId,
data: result.data
}) })
} catch (ex) { } catch (ex) {
next(ex) next(ex)
@ -343,5 +358,19 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
} }
}) })
router.post('/delete-account', json(), async (req, res, next) => {
try {
if (!isDeleteAccountPayload(req.body)) {
throw new BadRequest()
}
await deleteAccount({ database, request: req.body, websocket })
res.sendStatus(200)
} catch (ex) {
next(ex)
}
})
return router return router
} }

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -84,12 +84,14 @@ export interface CreateFamilyByMailTokenRequest {
deviceName: string deviceName: string
timeZone: string timeZone: string
parentName: string parentName: string
clientLevel?: number
} }
export interface SignIntoFamilyRequest { export interface SignIntoFamilyRequest {
mailAuthToken: string mailAuthToken: string
parentDevice: NewDeviceInfo parentDevice: NewDeviceInfo
deviceName: string deviceName: string
clientLevel?: number
} }
export interface RecoverParentPasswordRequest { export interface RecoverParentPasswordRequest {
@ -101,6 +103,7 @@ export interface RegisterChildDeviceRequest {
registerToken: string registerToken: string
childDevice: NewDeviceInfo childDevice: NewDeviceInfo
deviceName: string deviceName: string
clientLevel?: number
} }
export interface CreateRegisterDeviceTokenRequest { export interface CreateRegisterDeviceTokenRequest {
@ -173,5 +176,10 @@ export type IdentityTokenPayload = IdentityTokenCreatePayload & {
exp: number exp: number
} }
export interface DeleteAccountPayload {
deviceAuthToken: string
mailAuthTokens: Array<string>
}
export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization' export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization'
export { ServerDataStatus } from '../object/serverdatastatus' export { ServerDataStatus } from '../object/serverdatastatus'

View file

@ -1,5 +1,5 @@
// tslint:disable // tslint:disable
import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest, IdentityTokenPayload } from './schema' import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest, IdentityTokenPayload, DeleteAccountPayload } from './schema'
import Ajv from 'ajv' import Ajv from 'ajv'
const ajv = new Ajv() const ajv = new Ajv()
@ -1982,6 +1982,12 @@ const definitions = {
}, },
"addedManipulationFlags": { "addedManipulationFlags": {
"type": "number" "type": "number"
},
"platformType": {
"type": "string"
},
"platformLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -2164,6 +2170,12 @@ const definitions = {
}, },
"pk": { "pk": {
"type": "string" "type": "string"
},
"pType": {
"type": "string"
},
"pLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -2192,6 +2204,7 @@ const definitions = {
"model", "model",
"name", "name",
"networkTime", "networkTime",
"pLevel",
"qOrLater", "qOrLater",
"reboot", "reboot",
"rebootIsManipulation", "rebootIsManipulation",
@ -2930,6 +2943,9 @@ export const isCreateFamilyByMailTokenRequest: (value: unknown) => value is Crea
}, },
"parentName": { "parentName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -2955,6 +2971,9 @@ export const isSignIntoFamilyRequest: (value: unknown) => value is SignIntoFamil
}, },
"deviceName": { "deviceName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -2995,6 +3014,9 @@ export const isRegisterChildDeviceRequest: (value: unknown) => value is Register
}, },
"deviceName": { "deviceName": {
"type": "string" "type": "string"
},
"clientLevel": {
"type": "number"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -3484,3 +3506,24 @@ export const isIdentityTokenPayload: (value: unknown) => value is IdentityTokenP
"definitions": definitions, "definitions": definitions,
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
}) })
export const isDeleteAccountPayload: (value: unknown) => value is DeleteAccountPayload = ajv.compile({
"type": "object",
"properties": {
"deviceAuthToken": {
"type": "string"
},
"mailAuthTokens": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"deviceAuthToken",
"mailAuthTokens"
],
"definitions": definitions,
"$schema": "http://json-schema.org/draft-07/schema#"
})

View file

@ -34,6 +34,8 @@ function parseYesNo (value: string) {
} }
} }
class ParseYesNoException extends Error {}
export const config: Config = { export const config: Config = {
mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0), mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0),
disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'), disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'),
@ -41,5 +43,3 @@ export const config: Config = {
alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false, alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false,
signSecret: process.env.SIGN_SECRET || '' signSecret: process.env.SIGN_SECRET || ''
} }
class ParseYesNoException extends Error {}

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -37,6 +37,12 @@ export const DeviceManipulationFlags = {
ALL: 1 ALL: 1
} }
export const minPlatformTypeLength = 1
export const maxPlatformTypeLength = 8
export const minPlatformLevel = 0
export const maxPlatformLevel = 128
export interface DeviceAttributesVersion1 { export interface DeviceAttributesVersion1 {
familyId: string familyId: string
deviceId: string deviceId: string
@ -119,11 +125,17 @@ export interface DeviceAttributesVersion14 {
nextKeyReplySequenceNumber: string nextKeyReplySequenceNumber: string
} }
export interface DeviceAttributesVersion15 {
platformType: string | null
platformLevel: number
}
export type DeviceAttributes = DeviceAttributesVersion1 & DeviceAttributesVersion2 & export type DeviceAttributes = DeviceAttributesVersion1 & DeviceAttributesVersion2 &
DeviceAttributesVersion3 & DeviceAttributesVersion4 & DeviceAttributesVersion5 & DeviceAttributesVersion3 & DeviceAttributesVersion4 & DeviceAttributesVersion5 &
DeviceAttributesVersion6 & DeviceAttributesVersion7 & DeviceAttributesVersion8 & DeviceAttributesVersion6 & DeviceAttributesVersion7 & DeviceAttributesVersion8 &
DeviceAttributesVersion9 & DeviceAttributesVersion10 & DeviceAttributesVersion11 & DeviceAttributesVersion9 & DeviceAttributesVersion10 & DeviceAttributesVersion11 &
DeviceAttributesVersion12 & DeviceAttributesVersion13 & DeviceAttributesVersion14 DeviceAttributesVersion12 & DeviceAttributesVersion13 & DeviceAttributesVersion14 &
DeviceAttributesVersion15
export type DeviceModel = Sequelize.Model<DeviceAttributes> & DeviceAttributes export type DeviceModel = Sequelize.Model<DeviceAttributes> & DeviceAttributes
export type DeviceModelStatic = typeof Sequelize.Model & { export type DeviceModelStatic = typeof Sequelize.Model & {
@ -305,6 +317,26 @@ export const attributesVersion14: SequelizeAttributes<DeviceAttributesVersion14>
} }
} }
export const attributesVersion15: SequelizeAttributes<DeviceAttributesVersion15> = {
platformType: {
type: Sequelize.STRING(maxPlatformTypeLength),
allowNull: true,
defaultValue: null,
validate: {
len: [minPlatformTypeLength, maxPlatformTypeLength]
}
},
platformLevel: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: minPlatformLevel,
validate: {
min: minPlatformLevel,
max: maxPlatformLevel
}
}
}
export const attributes: SequelizeAttributes<DeviceAttributes> = { export const attributes: SequelizeAttributes<DeviceAttributes> = {
...attributesVersion1, ...attributesVersion1,
...attributesVersion2, ...attributesVersion2,
@ -319,7 +351,8 @@ export const attributes: SequelizeAttributes<DeviceAttributes> = {
...attributesVersion11, ...attributesVersion11,
...attributesVersion12, ...attributesVersion12,
...attributesVersion13, ...attributesVersion13,
...attributesVersion14 ...attributesVersion14,
...attributesVersion15
} }
export const createDeviceModel = (sequelize: Sequelize.Sequelize): DeviceModelStatic => sequelize.define('Device', attributes) as DeviceModelStatic export const createDeviceModel = (sequelize: Sequelize.Sequelize): DeviceModelStatic => sequelize.define('Device', attributes) as DeviceModelStatic

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -26,7 +26,7 @@ export const config = {
expireTimeRounding: 1000 * 60 * 15 expireTimeRounding: 1000 * 60 * 15
} }
export function calculateExpireTime(now: bigint): BigInt { export function calculateExpireTime(now: bigint): bigint {
const expireBaseTime = now + BigInt(config.expireDelay) const expireBaseTime = now + BigInt(config.expireDelay)
const expireTime = expireBaseTime - expireBaseTime % BigInt(config.expireTimeRounding) + BigInt(config.expireTimeRounding) const expireTime = expireBaseTime - expireBaseTime % BigInt(config.expireTimeRounding) + BigInt(config.expireTimeRounding)

View file

@ -17,8 +17,6 @@
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken' import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken'
import { AppModelStatic, createAppModel } from './app'
import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken' import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category' import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp' import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
@ -42,14 +40,13 @@ import { createU2fKeyModel, U2fKeyModelStatic } from './u2fkey'
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime' import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
import { createUserModel, UserModelStatic } from './user' import { createUserModel, UserModelStatic } from './user'
import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory' import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory'
import { shouldRetryWithException } from './utils/serialized'
export type Transaction = Sequelize.Transaction export type Transaction = Sequelize.Transaction
export interface Database { export interface Database {
addDeviceToken: AddDeviceTokenModelStatic addDeviceToken: AddDeviceTokenModelStatic
authtoken: AuthTokenModelStatic authtoken: AuthTokenModelStatic
app: AppModelStatic
appActivity: AppActivityModelStatic
category: CategoryModelStatic category: CategoryModelStatic
categoryApp: CategoryAppModelStatic categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic categoryNetworkId: CategoryNetworkIdModelStatic
@ -71,15 +68,21 @@ export interface Database {
usedTime: UsedTimeModelStatic usedTime: UsedTimeModelStatic
user: UserModelStatic user: UserModelStatic
userLimitLoginCategory: UserLimitLoginCategoryModelStatic userLimitLoginCategory: UserLimitLoginCategoryModelStatic
transaction: <T> (autoCallback: (t: Transaction) => Promise<T>, options?: { transaction: Transaction }) => Promise<T> transaction: <T> (
autoCallback: (t: Transaction) => Promise<T>,
options?: TransactionOptions
) => Promise<T>
dialect: string dialect: string
} }
interface TransactionOptions {
transaction?: Transaction
disableRetry?: boolean
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
addDeviceToken: createAddDeviceTokenModel(sequelize), addDeviceToken: createAddDeviceTokenModel(sequelize),
authtoken: createAuthtokenModel(sequelize), authtoken: createAuthtokenModel(sequelize),
app: createAppModel(sequelize),
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize), category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize), categoryApp: createCategoryAppModel(sequelize),
childTask: createChildTaskModel(sequelize), childTask: createChildTaskModel(sequelize),
@ -101,10 +104,39 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
usedTime: createUsedTimeModel(sequelize), usedTime: createUsedTimeModel(sequelize),
user: createUserModel(sequelize), user: createUserModel(sequelize),
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize), userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Transaction) => Promise<T>, options?: { transaction: Transaction }) => (sequelize.transaction({ async transaction<T>(
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE, autoCallback: (transaction: Transaction) => Promise<T>,
transaction: options?.transaction options?: TransactionOptions
}, autoCallback)) as Promise<T>, ): Promise<T> {
const runAttempt = () => sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE,
transaction: options?.transaction
}, autoCallback)
const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time))
try {
return await runAttempt()
} catch (ex) {
if (
options?.disableRetry ||
options?.transaction ||
!shouldRetryWithException(this, ex)
) throw ex
}
await delay(10 * (1 + Math.random()))
try {
return await runAttempt()
} catch (ex) {
if (!shouldRetryWithException(this, ex)) throw ex
}
await delay(100 * (1 + Math.random()))
return await runAttempt()
},
dialect: sequelize.getDialect() dialect: sequelize.getDialect()
}) })

View file

@ -15,26 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize' import { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { RemoveInstalledAppsAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchRemoveInstalledApps ({ deviceId, action, cache }: { export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
deviceId: string await sequelize.transaction({
action: RemoveInstalledAppsAction type: Transaction.TYPES.EXCLUSIVE
cache: Cache }, async (transaction) => {
}) { await queryInterface.dropTable('Apps', { transaction })
await cache.database.app.destroy({ await queryInterface.dropTable('AppActivities', { transaction })
where: {
familyId: cache.familyId,
deviceId,
packageName: {
[Sequelize.Op.in]: action.packageNames
}
},
transaction: cache.transaction
}) })
}
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1) export async function down() {
throw new Error('not possible')
} }

View file

@ -0,0 +1,46 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2023 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 <https://www.gnu.org/licenses/>.
*/
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { attributesVersion15 as deviceAttributes } from '../../device'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.addColumn('Devices', 'platformType', {
...deviceAttributes.platformType
}, {
transaction
})
await queryInterface.addColumn('Devices', 'platformLevel', {
...deviceAttributes.platformLevel
}, {
transaction
})
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.removeColumn('Devices', 'platformLevel', { transaction })
await queryInterface.removeColumn('Devices', 'platformType', { transaction })
})
}

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann * Copyright (C) 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -17,18 +17,26 @@
import { resolve } from 'path' import { resolve } from 'path'
import { Sequelize } from 'sequelize' import { Sequelize } from 'sequelize'
import * as Umzug from 'umzug' import { Umzug, SequelizeStorage } from 'umzug'
export const createUmzug = (sequelize: Sequelize) => ( export const createUmzug = (sequelize: Sequelize) => (
new Umzug({ new Umzug({
storage: 'sequelize', storage: new SequelizeStorage({ sequelize }),
storageOptions: {
sequelize
},
migrations: { migrations: {
params: [sequelize.getQueryInterface(), sequelize], glob: resolve(__dirname, '../../../build/database/migration/migrations/*.js'),
path: resolve(__dirname, '../../../build/database/migration/migrations'), resolve: ({ name, path }) => {
pattern: /^\d+[\w-]+\.js$/ if (!path) throw new Error()
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const migration = require(path)
return {
name,
up: async () => migration.up(sequelize.getQueryInterface(), sequelize),
down: async () => migration.down(sequelize.getQueryInterface(), sequelize),
}
},
},
logger: console
}) })
) )

View file

@ -86,8 +86,8 @@ export async function assertSerializeableTransactionsAreWorking (database: Datab
await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo }) await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo })
})() })()
]) ])
}) }, { disableRetry: true })
}) }, { disableRetry: true })
throw new SerializationFeatureCheckException() throw new SerializationFeatureCheckException()
} catch (ex) { } catch (ex) {

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -109,7 +109,7 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database
database: Database database: Database
// no transaction here because this is directly called from an API endpoint // no transaction here because this is directly called from an API endpoint
}): Promise<{ mailAuthToken: string }> => { }): Promise<{ mailAuthToken: string }> => {
return database.transaction(async (transaction) => { const result = await database.transaction(async (transaction) => {
const entry = await database.mailLoginToken.findOne({ const entry = await database.mailLoginToken.findOne({
where: { where: {
mailLoginToken mailLoginToken
@ -127,9 +127,9 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database
await entry.save({ transaction }) await entry.save({ transaction })
if (entry.remainingAttempts === 0) { if (entry.remainingAttempts === 0) {
throw new Gone() return () => { throw new Gone() }
} else { } else {
throw new Forbidden() return () => { throw new Forbidden() }
} }
} }
@ -151,6 +151,8 @@ export const signInByMailCode = async ({ mailLoginToken, receivedCode, database
transaction transaction
}) })
return { mailAuthToken } return () => ({ mailAuthToken })
}) })
return result()
} }

View file

@ -22,13 +22,22 @@ import { generateAuthToken, generateVersionId } from '../../util/token'
import { WebsocketApi } from '../../websocket' import { WebsocketApi } from '../../websocket'
import { prepareDeviceEntry } from '../device/prepare-device-entry' import { prepareDeviceEntry } from '../device/prepare-device-entry'
import { notifyClientsAboutChangesDelayed } from '../websocket' import { notifyClientsAboutChangesDelayed } from '../websocket'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
export const addChildDevice = async ({ database, websocket, request }: { export const addChildDevice = async ({ database, eventHandler, websocket, request }: {
database: Database database: Database
eventHandler: EventHandler
websocket: WebsocketApi websocket: WebsocketApi
request: RegisterChildDeviceRequest request: RegisterChildDeviceRequest
// no transaction here because this is directly called from an API endpoint // no transaction here because this is directly called from an API endpoint
}) => { }): Promise<{
deviceId: string
deviceAuthToken: string
data: ServerDataStatus
}> => {
return database.transaction(async (transaction) => { return database.transaction(async (transaction) => {
const entry = await database.addDeviceToken.findOne({ const entry = await database.addDeviceToken.findOne({
where: { where: {
@ -75,9 +84,19 @@ export const addChildDevice = async ({ database, websocket, request }: {
transaction transaction
}) })
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel: request.clientLevel || null }),
familyId: entry.familyId,
deviceId,
transaction,
eventHandler
})
return { return {
deviceId, deviceId,
deviceAuthToken deviceAuthToken,
data
} }
}) })
} }

View file

@ -0,0 +1,107 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2023 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 <https://www.gnu.org/licenses/>.
*/
import { Unauthorized } from 'http-errors'
import { DeleteAccountPayload } from '../../api/schema'
import { Database } from '../../database'
import { sendAccountDeletedMail } from '../../util/mail'
import { WebsocketApi } from '../../websocket'
import { requireMailAndLocaleByAuthToken } from '../authentication'
import { deleteFamilies } from './delete-families'
export async function deleteAccount({ request, database, websocket }: {
request: DeleteAccountPayload
database: Database
websocket: WebsocketApi
}) {
await database.transaction(async (transaction) => {
const deviceEntryUnsafe = await database.device.findOne({
where: { deviceAuthToken: request.deviceAuthToken },
attributes: ['familyId'],
transaction
})
if (!deviceEntryUnsafe) {
throw new Unauthorized()
}
const deviceEntry = {
familyId: deviceEntryUnsafe.familyId
}
const userEntries = (await database.user.findAll({
where: {
familyId: deviceEntry.familyId,
type: 'parent'
},
attributes: ['mail'],
transaction
})).map((item) => ({ mail: item.mail }))
const registeredMailAddresses = new Set<string>()
userEntries.forEach((item) => {
if (item.mail !== '') registeredMailAddresses.add(item.mail)
})
const authenticatedMailAddresses = new Set<string>()
for (const mailAuthToken of request.mailAuthTokens) {
const info = await requireMailAndLocaleByAuthToken({
mailAuthToken,
database,
transaction,
invalidate: true
})
if (!registeredMailAddresses.has(info.mail)) throw new Unauthorized()
authenticatedMailAddresses.add(info.mail)
}
if (registeredMailAddresses.size !== authenticatedMailAddresses.size) throw new Unauthorized()
registeredMailAddresses.forEach((mail) => {
if (!authenticatedMailAddresses.has(mail)) throw new Unauthorized()
})
const deviceEntries = (await database.device.findAll({
where: {
familyId: deviceEntry.familyId
},
transaction,
attributes: ['deviceAuthToken']
})).map((item) => ({ deviceAuthToken: item.deviceAuthToken }))
await deleteFamilies({ database, transaction, familiyIds: [deviceEntry.familyId] })
transaction.afterCommit(() => {
for (const device of deviceEntries) {
websocket.triggerSyncByDeviceAuthToken({
deviceAuthToken: device.deviceAuthToken,
isImportant: true
})
}
registeredMailAddresses.forEach((receiver) => {
sendAccountDeletedMail({ receiver }).catch((ex) => {
console.warn('failure while sending account deletion confirmation', ex)
})
})
})
})
}

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -17,10 +17,11 @@
import { difference } from 'lodash' import { difference } from 'lodash'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { Database } from '../../database' import { Database, Transaction } from '../../database'
export async function deleteFamilies ({ database, familiyIds }: { export async function deleteFamilies ({ database, transaction, familiyIds }: {
database: Database database: Database
transaction: Transaction
familiyIds: Array<string> familiyIds: Array<string>
// no transaction here because this should run isolated // no transaction here because this should run isolated
}) { }) {
@ -28,138 +29,126 @@ export async function deleteFamilies ({ database, familiyIds }: {
return return
} }
await database.transaction(async (transaction) => { // category
// app await database.category.destroy({
await database.app.destroy({ where: {
where: { familyId: {
familyId: { [Sequelize.Op.in]: familiyIds
[Sequelize.Op.in]: familiyIds }
} },
}, transaction
transaction })
})
// app activity // categoryapp
await database.appActivity.destroy({ await database.categoryApp.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// category // purchase
await database.category.destroy({ await database.purchase.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// categoryapp // timelimitrule
await database.categoryApp.destroy({ await database.timelimitRule.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// purchase // usedtime
await database.purchase.destroy({ await database.usedTime.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// timelimitrule // session durations
await database.timelimitRule.destroy({ await database.sessionDuration.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// usedtime // user
await database.usedTime.destroy({ await database.user.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
}) })
// user // device
await database.user.destroy({ const oldDeviceAuthTokens = (await database.device.findAll({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction attributes: ['deviceAuthToken'],
}) transaction
})).map((item) => item.deviceAuthToken)
// device await database.device.destroy({
const oldDeviceAuthTokens = (await database.device.findAll({ where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// olddevice
if (oldDeviceAuthTokens.length > 0) {
const knownOldDeviceAuthTokens = (await database.oldDevice.findAll({
where: { where: {
familyId: { deviceAuthToken: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: oldDeviceAuthTokens
} }
}, },
attributes: ['deviceAuthToken'],
transaction transaction
})).map((item) => item.deviceAuthToken) })).map((item) => item.deviceAuthToken)
await database.device.destroy({ const oldDeviceAuthTokensToAdd = difference(oldDeviceAuthTokens, knownOldDeviceAuthTokens)
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// olddevice if (oldDeviceAuthTokensToAdd.length > 0) {
if (oldDeviceAuthTokens.length > 0) { await database.oldDevice.bulkCreate(
const knownOldDeviceAuthTokens = (await database.oldDevice.findAll({ oldDeviceAuthTokensToAdd.map((item) => ({
where: { deviceAuthToken: item
deviceAuthToken: { })),
[Sequelize.Op.in]: oldDeviceAuthTokens { transaction }
} )
},
transaction
})).map((item) => item.deviceAuthToken)
const oldDeviceAuthTokensToAdd = difference(oldDeviceAuthTokens, knownOldDeviceAuthTokens)
if (oldDeviceAuthTokensToAdd.length > 0) {
await database.oldDevice.bulkCreate(
oldDeviceAuthTokensToAdd.map((item) => ({
deviceAuthToken: item
})),
{ transaction }
)
}
} }
}
// family // family
await database.family.destroy({ await database.family.destroy({
where: { where: {
familyId: { familyId: {
[Sequelize.Op.in]: familiyIds [Sequelize.Op.in]: familiyIds
} }
}, },
transaction transaction
})
}) })
} }

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -26,9 +26,12 @@ export async function deleteOldFamilies (database: Database) {
if (oldFamilyIds.length > 0) { if (oldFamilyIds.length > 0) {
const familyIdsToDelete = oldFamilyIds.slice(0, 256) /* limit to 256 families per execution */ const familyIdsToDelete = oldFamilyIds.slice(0, 256) /* limit to 256 families per execution */
await deleteFamilies({ await database.transaction(async (transaction) => {
database, await deleteFamilies({
familiyIds: familyIdsToDelete database,
transaction,
familiyIds: familyIdsToDelete
})
}) })
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -66,5 +66,7 @@ export const prepareDeviceEntry = ({ familyId, userId, deviceAuthToken, deviceId
isQorLater: false, isQorLater: false,
manipulationFlags: 0, manipulationFlags: 0,
publicKey: null, publicKey: null,
nextKeyReplySequenceNumber: '1' nextKeyReplySequenceNumber: '1',
platformType: null,
platformLevel: 0
}) })

View file

@ -40,23 +40,6 @@ export async function removeDevice ({ database, familyId, deviceId, websocket, t
throw new Conflict() 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 // remove as current device
await database.user.update({ await database.user.update({
currentDevice: '' currentDevice: ''

View file

@ -16,25 +16,38 @@
*/ */
import { Conflict } from 'http-errors' import { Conflict } from 'http-errors'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { NewDeviceInfo, PlaintextParentPassword, assertPlaintextParentPasswordValid } from '../../api/schema' import { NewDeviceInfo, PlaintextParentPassword, assertPlaintextParentPasswordValid } from '../../api/schema'
import { Database } from '../../database' import { Database } from '../../database'
import { maxMailNotificationFlags } from '../../database/user' import { maxMailNotificationFlags } from '../../database/user'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
import { import {
generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId
} from '../../util/token' } from '../../util/token'
import { requireMailAndLocaleByAuthToken } from '../authentication' import { requireMailAndLocaleByAuthToken } from '../authentication'
import { prepareDeviceEntry } from '../device/prepare-device-entry' import { prepareDeviceEntry } from '../device/prepare-device-entry'
export const createFamily = async ({ database, mailAuthToken, firstParentDevice, password, timeZone, parentName, deviceName }: { export async function createFamily ({
database: Database, database, eventHandler, mailAuthToken, firstParentDevice,
mailAuthToken: string, password, timeZone, parentName, deviceName, clientLevel
firstParentDevice: NewDeviceInfo, }: {
password: PlaintextParentPassword, database: Database
timeZone: string, eventHandler: EventHandler
parentName: string, mailAuthToken: string
firstParentDevice: NewDeviceInfo
password: PlaintextParentPassword
timeZone: string
parentName: string
deviceName: string deviceName: string
clientLevel: number | null
// no transaction here because this is directly called from an API endpoint // no transaction here because this is directly called from an API endpoint
}) => { }): Promise<{
deviceAuthToken: string
deviceId: string
data: ServerDataStatus
}> {
assertPlaintextParentPasswordValid(password) assertPlaintextParentPasswordValid(password)
return database.transaction(async (transaction) => { return database.transaction(async (transaction) => {
@ -42,14 +55,14 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true })
// ensure that no family was created for this mail yet // ensure that no family was created for this mail yet
const exisitngUserEntry = await database.user.findOne({ const existingUserEntry = await database.user.findOne({
where: { where: {
mail: mailInfo.mail mail: mailInfo.mail
}, },
transaction transaction
}) })
if (exisitngUserEntry) { if (existingUserEntry) {
throw new Conflict() throw new Conflict()
} }
@ -103,9 +116,19 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
isUserKeptSignedIn: true isUserKeptSignedIn: true
}), { transaction }) }), { transaction })
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel }),
familyId,
deviceId,
transaction,
eventHandler
})
return { return {
deviceAuthToken, deviceAuthToken,
deviceId deviceId,
data
} }
}) })
} }

View file

@ -24,15 +24,21 @@ import { WebsocketApi } from '../../websocket'
import { requireMailAndLocaleByAuthToken } from '../authentication' import { requireMailAndLocaleByAuthToken } from '../authentication'
import { prepareDeviceEntry } from '../device/prepare-device-entry' import { prepareDeviceEntry } from '../device/prepare-device-entry'
import { notifyClientsAboutChangesDelayed } from '../websocket' import { notifyClientsAboutChangesDelayed } from '../websocket'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, deviceName, websocket }: { export const signInIntoFamily = async ({ database, eventHandler, mailAuthToken, newDeviceInfo, deviceName, websocket, clientLevel }: {
database: Database database: Database
eventHandler: EventHandler
mailAuthToken: string mailAuthToken: string
newDeviceInfo: NewDeviceInfo newDeviceInfo: NewDeviceInfo
deviceName: string deviceName: string
websocket: WebsocketApi websocket: WebsocketApi
clientLevel: number | null
// no transaction here because this is directly called from an API endpoint // no transaction here because this is directly called from an API endpoint
}): Promise<{ deviceId: string; deviceAuthToken: string }> => { }): Promise<{ deviceId: string; deviceAuthToken: string; data: ServerDataStatus }> => {
return database.transaction(async (transaction) => { return database.transaction(async (transaction) => {
const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true }) const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true })
@ -94,9 +100,19 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo,
}) })
}) })
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel }),
familyId: userEntry.familyId,
deviceId,
transaction,
eventHandler
})
return { return {
deviceId, deviceId,
deviceAuthToken deviceAuthToken,
data
} }
}) })
} }

View file

@ -40,7 +40,6 @@ export class Cache {
categoriesWithModifiedUsedTimes = new Set<string>() categoriesWithModifiedUsedTimes = new Set<string>()
categoriesWithModifiedTasks = new Set<string>() categoriesWithModifiedTasks = new Set<string>()
devicesWithModifiedInstalledApps = new Set<string>()
devicesWithModifiedShowDeviceConnected = new Map<string, boolean>() devicesWithModifiedShowDeviceConnected = new Map<string, boolean>()
invalidiateUserList = false invalidiateUserList = false
@ -241,22 +240,6 @@ export class Cache {
this.categoriesWithModifiedUsedTimes.clear() this.categoriesWithModifiedUsedTimes.clear()
} }
if (this.devicesWithModifiedInstalledApps.size > 0) {
await database.device.update({
installedAppsVersion: generateVersionId()
}, {
where: {
familyId,
deviceId: {
[Sequelize.Op.in]: setToList(this.devicesWithModifiedInstalledApps)
}
},
transaction
})
this.devicesWithModifiedInstalledApps.clear()
}
if (this.invalidiateUserList) { if (this.invalidiateUserList) {
await database.family.update({ await database.family.update({
userListVersion: generateVersionId() userListVersion: generateVersionId()

View file

@ -1,63 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* 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 <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { AddInstalledAppsAction } from '../../../../action'
import { AppAttributes, maxPackageNameLength } from '../../../../database/app'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception'
export async function dispatchAddInstalledApps ({ deviceId, action, cache }: {
deviceId: string
action: AddInstalledAppsAction
cache: Cache
}) {
action.apps.forEach((app) => {
if (app.packageName.length > maxPackageNameLength) {
throw new ApplyActionException({
staticMessage: 'package name too long',
dynamicMessage: 'package name too long: ' + app.packageName
})
}
})
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId,
packageName: {
[Sequelize.Op.in]: action.apps.map((app) => app.packageName)
}
},
transaction: cache.transaction
})
await cache.database.app.bulkCreate(
action.apps.map((app): AppAttributes => ({
familyId: cache.familyId,
deviceId,
packageName: app.packageName,
title: app.title,
isLaunchable: app.isLaunchable,
recommendation: app.recommendation
})),
{ transaction: cache.transaction }
)
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
}

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sequelize from 'sequelize'
import { AddUsedTimeActionVersion2 } from '../../../../action' import { AddUsedTimeActionVersion2 } from '../../../../action'
import { EventHandler } from '../../../../monitoring/eventhandler' import { EventHandler } from '../../../../monitoring/eventhandler'
import { MinuteOfDay } from '../../../../util/minuteofday' import { MinuteOfDay } from '../../../../util/minuteofday'
@ -22,6 +23,8 @@ import { Cache } from '../cache'
import { IllegalStateException, SourceDeviceNotFoundException } from '../exception/illegal-state' import { IllegalStateException, SourceDeviceNotFoundException } from '../exception/illegal-state'
import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime' import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime'
const tolerance = 5 * 1000 // 5 seconds
export const getRoundedTimestampForSessionDuration = () => { export const getRoundedTimestampForSessionDuration = () => {
const now = Date.now() const now = Date.now()
@ -129,11 +132,32 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
} }
} }
} else { } else {
const oldTime: number | null = await cache.database.usedTime.aggregate(
'usedTime',
'MAX',
{
where: {
familyId: cache.familyId,
categoryId: item.categoryId,
dayOfEpoch: action.dayOfEpoch,
startMinuteOfDay: {
[Sequelize.Op.gte]: start
},
endMinuteOfDay: {
[Sequelize.Op.lte]: end
}
},
transaction: cache.transaction
}
) || 0
if (oldTime !== null && typeof oldTime !== 'number') throw new Error()
await cache.database.usedTime.create({ await cache.database.usedTime.create({
familyId: cache.familyId, familyId: cache.familyId,
categoryId: item.categoryId, categoryId: item.categoryId,
dayOfEpoch: action.dayOfEpoch, dayOfEpoch: action.dayOfEpoch,
usedTime: Math.max(0, Math.min(item.timeToAdd, lengthInMs)), usedTime: Math.max(0, Math.min(oldTime + item.timeToAdd, lengthInMs)),
lastUpdate: roundedTimestampForUsedTime, lastUpdate: roundedTimestampForUsedTime,
startMinuteOfDay: start, startMinuteOfDay: start,
endMinuteOfDay: end endMinuteOfDay: end
@ -168,6 +192,39 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
transaction: cache.transaction transaction: cache.transaction
}) })
const oldDuration: () => Promise<number> = async () => {
const fittingDurationItems = await cache.database.sessionDuration.findAll({
where: {
familyId: cache.familyId,
categoryId: item.categoryId,
startMinuteOfDay: {
[Sequelize.Op.gte]: limit.start
},
endMinuteOfDay: {
[Sequelize.Op.lte]: limit.end
},
maxSessionDuration: {
[Sequelize.Op.gte]: limit.duration
},
sessionPauseDuration: {
[Sequelize.Op.lte]: limit.pause
}
},
transaction: cache.transaction
})
const fittingDurationItemsLastUsageFiltered =
hasTrustedTimestamp ?
fittingDurationItems.filter((it) => {
action.trustedTimestamp - item.timeToAdd <=
parseInt(it.lastUsage, 10) + it.sessionPauseDuration - tolerance
}) : fittingDurationItems
return fittingDurationItemsLastUsageFiltered
.map((it) => it.lastSessionDuration)
.reduce((a, b) => Math.max(a, b), 0)
}
if (oldItem) { if (oldItem) {
let extendSession: boolean let extendSession: boolean
@ -188,14 +245,13 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
* Due to this, a session is reset if it would be over in a few seconds, too. * Due to this, a session is reset if it would be over in a few seconds, too.
*/ */
const tolerance = 5 * 1000 // 5 seconds
const timeWhenStartingCurrentUsage = action.trustedTimestamp - item.timeToAdd const timeWhenStartingCurrentUsage = action.trustedTimestamp - item.timeToAdd
const nextSessionStart = parseInt(oldItem.lastUsage, 10) + oldItem.sessionPauseDuration - tolerance const nextSessionStart = parseInt(oldItem.lastUsage, 10) + oldItem.sessionPauseDuration - tolerance
extendSession = timeWhenStartingCurrentUsage <= nextSessionStart extendSession = timeWhenStartingCurrentUsage <= nextSessionStart
} }
oldItem.lastSessionDuration = extendSession ? oldItem.lastSessionDuration + item.timeToAdd : item.timeToAdd oldItem.lastSessionDuration = extendSession ? oldItem.lastSessionDuration + item.timeToAdd : await oldDuration() + item.timeToAdd
oldItem.roundedLastUpdate = roundedTimestampForSessionDuration oldItem.roundedLastUpdate = roundedTimestampForSessionDuration
if (hasTrustedTimestamp) { if (hasTrustedTimestamp) {
@ -215,7 +271,7 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
endMinuteOfDay: limit.end, endMinuteOfDay: limit.end,
// end of primary key // end of primary key
lastUsage: action.trustedTimestamp.toString(10), lastUsage: action.trustedTimestamp.toString(10),
lastSessionDuration: item.timeToAdd, lastSessionDuration: await oldDuration() + item.timeToAdd,
roundedLastUpdate: roundedTimestampForSessionDuration roundedLastUpdate: roundedTimestampForSessionDuration
}, { transaction: cache.transaction }) }, { transaction: cache.transaction })
} }

View file

@ -36,18 +36,15 @@ import {
import { EventHandler } from '../../../../monitoring/eventhandler' import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache' import { Cache } from '../cache'
import { ActionObjectTypeNotHandledException } from '../exception/illegal-state' import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { dispatchAddInstalledApps } from './addinstalledapps'
import { dispatchAddUsedTime } from './addusedtime' import { dispatchAddUsedTime } from './addusedtime'
import { dispatchAddUsedTimeVersion2 } from './addusedtime2' import { dispatchAddUsedTimeVersion2 } from './addusedtime2'
import { dispatchFinishKeyRequestAction } from './finishkeyrequest' import { dispatchFinishKeyRequestAction } from './finishkeyrequest'
import { dispatchForceSyncAction } from './forcesync' import { dispatchForceSyncAction } from './forcesync'
import { dispatchMarkTaskPendingAction } from './marktaskpendingaction' import { dispatchMarkTaskPendingAction } from './marktaskpendingaction'
import { dispatchReplyToKeyRequestAction } from './replytokeyrequest' import { dispatchReplyToKeyRequestAction } from './replytokeyrequest'
import { dispatchRemoveInstalledApps } from './removeinstalledapps'
import { dispatchSendKeyRequestAction } from './sendkeyrequest' import { dispatchSendKeyRequestAction } from './sendkeyrequest'
import { dispatchSignOutAtDevice } from './signoutatdevice' import { dispatchSignOutAtDevice } from './signoutatdevice'
import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin' import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin'
import { dispatchUpdateAppActivities } from './updateappactivities'
import { dispatchUpdateDeviceStatus } from './updatedevicestatus' import { dispatchUpdateDeviceStatus } from './updatedevicestatus'
import { dispatchUpdateInstalledApps } from './updateinstalledapps' import { dispatchUpdateInstalledApps } from './updateinstalledapps'
import { dispatchUploadDevicePublicKeyAction } from './uploaddevicepublickey' import { dispatchUploadDevicePublicKeyAction } from './uploaddevicepublickey'
@ -59,7 +56,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
eventHandler: EventHandler eventHandler: EventHandler
}) => { }) => {
if (action instanceof AddInstalledAppsAction) { if (action instanceof AddInstalledAppsAction) {
await dispatchAddInstalledApps({ deviceId, action, cache }) // do nothing
} else if (action instanceof AddUsedTimeAction) { } else if (action instanceof AddUsedTimeAction) {
await dispatchAddUsedTime({ deviceId, action, cache }) await dispatchAddUsedTime({ deviceId, action, cache })
} else if (action instanceof AddUsedTimeActionVersion2) { } else if (action instanceof AddUsedTimeActionVersion2) {
@ -73,7 +70,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof ReplyToKeyRequestAction) { } else if (action instanceof ReplyToKeyRequestAction) {
await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler }) await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler })
} else if (action instanceof RemoveInstalledAppsAction) { } else if (action instanceof RemoveInstalledAppsAction) {
await dispatchRemoveInstalledApps({ deviceId, action, cache }) // do nothing
} else if (action instanceof SendKeyRequestAction) { } else if (action instanceof SendKeyRequestAction) {
await dispatchSendKeyRequestAction({ deviceId, action, cache }) await dispatchSendKeyRequestAction({ deviceId, action, cache })
} else if (action instanceof SignOutAtDeviceAction) { } else if (action instanceof SignOutAtDeviceAction) {
@ -81,7 +78,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof UpdateDeviceStatusAction) { } else if (action instanceof UpdateDeviceStatusAction) {
await dispatchUpdateDeviceStatus({ deviceId, action, cache }) await dispatchUpdateDeviceStatus({ deviceId, action, cache })
} else if (action instanceof UpdateAppActivitiesAction) { } else if (action instanceof UpdateAppActivitiesAction) {
await dispatchUpdateAppActivities({ deviceId, action, cache }) // do nothing
} else if (action instanceof TriedDisablingDeviceAdminAction) { } else if (action instanceof TriedDisablingDeviceAdminAction) {
await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache }) await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache })
} else if (action instanceof UpdateInstalledAppsAction) { } else if (action instanceof UpdateInstalledAppsAction) {

View file

@ -1,99 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* 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 <https://www.gnu.org/licenses/>.
*/
import { chunk } from 'lodash'
import * as Sequelize from 'sequelize'
import { UpdateAppActivitiesAction } from '../../../../action'
import { AppActivityAttributes, maxActivityNameLength, maxPackageNameLength } from '../../../../database/appactivity'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception'
export async function dispatchUpdateAppActivities ({ deviceId, action, cache }: {
deviceId: string
action: UpdateAppActivitiesAction
cache: Cache
}) {
action.updatedOrAdded.forEach((app) => {
if (app.packageName.length > maxPackageNameLength) {
throw new ApplyActionException({
staticMessage: 'package name too long',
dynamicMessage: 'package name too long: ' + app.packageName
})
}
if (app.activityName.length > maxActivityNameLength) {
throw new ApplyActionException({
staticMessage: 'activity name too long',
dynamicMessage: 'activity name too long: ' + app.activityName
})
}
})
if (action.updatedOrAdded.length > 0) {
const chuncks = chunk(action.updatedOrAdded, 500)
for (const items of chuncks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
deviceId,
[Sequelize.Op.or]: (
items.map((item) => ({
packageName: item.packageName,
activityName: item.activityName
}))
)
},
transaction: cache.transaction
})
}
await cache.database.appActivity.bulkCreate(
action.updatedOrAdded.map((item): AppActivityAttributes => ({
familyId: cache.familyId,
deviceId,
packageName: item.packageName,
activityName: item.activityName,
title: item.title
})),
{ transaction: cache.transaction }
)
}
if (action.removed.length > 0) {
const chunks = chunk(action.removed, 500)
for (const items of chunks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
deviceId,
[Sequelize.Op.or]: (
items.map((item) => ({
packageName: item.packageName,
activityName: item.activityName
}))
)
},
transaction: cache.transaction
})
}
}
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
}

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -155,6 +155,18 @@ export async function dispatchUpdateDeviceStatus ({ deviceId, action, cache }: {
} }
} }
if (action.platformType !== undefined) {
if (action.platformType !== deviceEntry.platformType) {
deviceEntry.platformType = action.platformType
}
}
if (action.platformLevel !== undefined) {
if (action.platformLevel !== deviceEntry.platformLevel) {
deviceEntry.platformLevel = action.platformLevel
}
}
{ {
const effectiveManipulationFlags = action.addedManipulationFlags & DeviceManipulationFlags.ALL const effectiveManipulationFlags = action.addedManipulationFlags & DeviceManipulationFlags.ALL

View file

@ -43,17 +43,5 @@ export async function dispatchUpdateInstalledApps ({ deviceId, action, cache }:
await upsert({ type: types.diff, data: action.diff }) await upsert({ type: types.diff, data: action.diff })
} }
if (action.wipe) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
cache.devicesWithModifiedInstalledApps.add(deviceId)
}
cache.incrementTriggeredSyncLevel(1) cache.incrementTriggeredSyncLevel(1)
} }

View file

@ -1,131 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { difference, filter } from 'lodash'
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { ClientDataStatusApps } from '../../../object/clientdatastatus'
import { ServerInstalledAppsData } from '../../../object/serverdatastatus'
import { GetServerDataStatusIllegalStateException } from './exception'
import { FamilyEntry } from './family-entry'
export async function getAppList ({ database, transaction, familyEntry, appsStatus }: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
appsStatus: ClientDataStatusApps
}): Promise<Array<ServerInstalledAppsData> | null> {
const serverInstalledAppsVersions = (await database.device.findAll({
where: {
familyId: familyEntry.familyId
},
attributes: ['deviceId', 'installedAppsVersion'],
transaction
})).map((item) => ({
deviceId: item.deviceId,
installedAppsVersion: item.installedAppsVersion
}))
const getServerInstalledAppsVersionByDeviceId = (deviceId: string) => {
const entry = serverInstalledAppsVersions.find((item) => item.deviceId === deviceId)
if (!entry) {
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find device entry' })
}
return entry.installedAppsVersion
}
const serverDeviceIds = serverInstalledAppsVersions.map((item) => item.deviceId)
const clientDeviceIds = Object.keys(appsStatus)
const addedDeviceIds = difference(serverDeviceIds, clientDeviceIds)
const deviceIdsWhereInstalledAppsHaveChanged = filter(Object.keys(appsStatus), (deviceId) => {
const installedAppsVersion = appsStatus[deviceId]
const serverEntry = serverInstalledAppsVersions.find((item) => item.deviceId === deviceId)
return !!serverEntry && serverEntry.installedAppsVersion !== installedAppsVersion
})
const idsOfDevicesWhereInstalledAppsMustBeSynced = [...addedDeviceIds, ...deviceIdsWhereInstalledAppsHaveChanged]
if (idsOfDevicesWhereInstalledAppsMustBeSynced.length > 0) {
const [appsToSync, activitiesToSync] = await Promise.all([
(async () => {
return (await database.app.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: idsOfDevicesWhereInstalledAppsMustBeSynced
}
},
attributes: [
'deviceId',
'packageName',
'title',
'isLaunchable',
'recommendation'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
packageName: item.packageName,
title: item.title,
isLaunchable: item.isLaunchable,
recommendation: item.recommendation
}))
})(),
(async () => {
return (await database.appActivity.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: idsOfDevicesWhereInstalledAppsMustBeSynced
}
},
attributes: [
'deviceId',
'packageName',
'title',
'activityName'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
packageName: item.packageName,
activityName: item.activityName,
title: item.title
}))
})()
])
return idsOfDevicesWhereInstalledAppsMustBeSynced.map((deviceId): ServerInstalledAppsData => ({
deviceId,
apps: appsToSync.filter((item) => item.deviceId === deviceId).map((item) => ({
packageName: item.packageName,
title: item.title,
isLaunchable: item.isLaunchable,
recommendation: item.recommendation
})),
activities: activitiesToSync.filter((item) => item.deviceId === deviceId).map((item) => ({
p: item.packageName,
c: item.activityName,
t: item.title
})),
version: getServerInstalledAppsVersionByDeviceId(deviceId)
}))
} else return null // no changes
}

View file

@ -66,7 +66,9 @@ export async function getDeviceList ({ database, transaction, familyEntry }: {
activityLevelBlocking: item.activityLevelBlocking, activityLevelBlocking: item.activityLevelBlocking,
qOrLater: item.isQorLater, qOrLater: item.isQorLater,
mFlags: item.manipulationFlags, mFlags: item.manipulationFlags,
pk: item.publicKey ? item.publicKey.toString('base64') : undefined pk: item.publicKey ? item.publicKey.toString('base64') : undefined,
pType: item.platformType || undefined,
pLevel: item.platformLevel
})) }))
} }
} }

View file

@ -22,7 +22,6 @@ import { getStatusMessage } from '../../../function/statusmessage'
import { ClientDataStatus } from '../../../object/clientdatastatus' import { ClientDataStatus } from '../../../object/clientdatastatus'
import { ServerDataStatus } from '../../../object/serverdatastatus' import { ServerDataStatus } from '../../../object/serverdatastatus'
import { EventHandler } from '../../../monitoring/eventhandler' import { EventHandler } from '../../../monitoring/eventhandler'
import { getAppList } from './app-list'
import { import {
getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync, getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync,
getRules, getTasks, getUsedTimes getRules, getTasks, getUsedTimes
@ -70,8 +69,6 @@ export const generateServerDataStatus = async ({
result.users = await getUserList({ database, transaction, familyEntry }) result.users = await getUserList({ database, transaction, familyEntry })
} }
result.apps = await getAppList({ database, transaction, familyEntry, appsStatus: clientStatus.apps }) || undefined
const categoryDataToSync = await getCategoryDataToSync({ database, transaction, familyEntry, categoriesStatus: clientStatus.categories }) const categoryDataToSync = await getCategoryDataToSync({ database, transaction, familyEntry, categoriesStatus: clientStatus.categories })
if (categoryDataToSync.removedCategoryIds.length > 0) { if (categoryDataToSync.removedCategoryIds.length > 0) {

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann * Copyright (C) 2019 - 2022 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -16,6 +16,7 @@
*/ */
import { Server } from 'http' import { Server } from 'http'
import { pid } from 'process'
import { createApi } from './api' import { createApi } from './api'
import { config } from './config' import { config } from './config'
import { VisibleConnectedDevicesManager } from './connected-devices' import { VisibleConnectedDevicesManager } from './connected-devices'
@ -61,7 +62,22 @@ async function main () {
pingInterval: config.pingInterval pingInterval: config.pingInterval
}) })
server.listen(process.env.PORT || 8080) const port = process.env.PORT || 8080
if (port === 'socketactivation') {
if (process.env.LISTEN_FDS !== '1') {
console.warn('expecting exactly one file descriptor for the socket activation')
process.exit(1)
} else if (process.env.LISTEN_PID !== pid.toString(10)) {
console.warn('expecting handover of file descriptors to this process for the socket activation')
process.exit(1)
}
// the sockets are passed using fd 3 + index (with index = 0 in this case)
server.listen({ fd: 3 })
} else {
server.listen(port)
}
console.log('ready') console.log('ready')
} }

View file

@ -28,6 +28,18 @@ export interface ClientDataStatus {
u2f?: string // last u2f list version u2f?: string // last u2f list version
} }
export function createEmptyClientDataStatus({ clientLevel }: {
clientLevel: number | null
}): ClientDataStatus {
return {
devices: '',
apps: {},
categories: {},
users: '',
clientLevel: clientLevel || undefined
}
}
export type ClientDataStatusApps = {[key: string]: string} // installedAppsVersionsByDeviceId export type ClientDataStatusApps = {[key: string]: string} // installedAppsVersionsByDeviceId
export type ClientDataStatusCategories = {[key: string]: CategoryDataStatus} export type ClientDataStatusCategories = {[key: string]: CategoryDataStatus}
export type ClientDataStatusDevicesExtended = {[key: string]: DeviceDataStatus} export type ClientDataStatusDevicesExtended = {[key: string]: DeviceDataStatus}

View file

@ -103,6 +103,8 @@ export interface ServerDeviceData {
qOrLater: boolean qOrLater: boolean
mFlags: number // manipulation flags mFlags: number // manipulation flags
pk?: string // public key pk?: string // public key
pType?: string
pLevel: number
} }
export interface ServerUpdatedCategoryBaseData { export interface ServerUpdatedCategoryBaseData {

View file

@ -1,6 +1,6 @@
/* /*
* server component for the TimeLimit App * server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann * Copyright (C) 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -72,7 +72,10 @@ function createMailTemplateSender (templateName: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = (info as any).message const data = (info as any).message
console.log(JSON.stringify(JSON.parse(data), null, 2)) console.log(JSON.stringify({
...JSON.parse(data),
params
}, null, 2))
} }
resolve() resolve()
@ -190,6 +193,19 @@ export const sendPasswordRecoveryUsedMail = async ({ receiver, locale }: {
}) })
} }
const accountDeletedMailSender = createMailTemplateSender('account-deleted')
export const sendAccountDeletedMail = async ({ receiver }: {
receiver: string
}) => {
await accountDeletedMailSender.sendMail({
receiver,
params: {
mailimprint
}
})
}
function getMailSecurityText (locale: string) { function getMailSecurityText (locale: string) {
if (locale === 'de') { if (locale === 'de') {
return 'Achten Sie darauf, dass Ihr Kind/Ihre Kinder keinen Zugang zu der E-Mail-Adresse hat/haben, die Sie bei TimeLimit angegeben haben.' return 'Achten Sie darauf, dass Ihr Kind/Ihre Kinder keinen Zugang zu der E-Mail-Adresse hat/haben, die Sie bei TimeLimit angegeben haben.'

View file

@ -11,7 +11,8 @@
"noUnusedParameters": false, "noUnusedParameters": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"sourceMap": true "sourceMap": true,
"skipLibCheck": true
}, },
"include": [ "include": [
"./src/**/*" "./src/**/*"