mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2026-08-31 19:03:45 +02:00
Compare commits
14
Commits
2022-09-23
...
2022-12-19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9a032823e | ||
|
|
ae044c19d6 | ||
|
|
41758c32f2 | ||
|
|
38b113c9e8 | ||
|
|
83619eb98e | ||
|
|
89e9b85bda | ||
|
|
f5198c7c0b | ||
|
|
9b8caccfbd | ||
|
|
747be7cf7d | ||
|
|
fe1ce74ff3 | ||
|
|
4ab97d7bf3 | ||
|
|
d1cc48a208 | ||
|
|
58a6359a3e | ||
|
|
35acd05d08 |
+1
-1
@@ -9,7 +9,7 @@ COPY package.json package-lock.json tsconfig.json .eslintignore .eslintrc.js Rea
|
|||||||
COPY src/ /usr/src/app/src/
|
COPY src/ /usr/src/app/src/
|
||||||
COPY scripts/ /usr/src/app/scripts/
|
COPY scripts/ /usr/src/app/scripts/
|
||||||
COPY other/ /usr/src/app/other/
|
COPY other/ /usr/src/app/other/
|
||||||
RUN mkdir -p docs/schema && npm install --no-optional && npm run build && npm prune --production && rm -rf ./src
|
RUN mkdir -p docs/schema && npm install --exclude=optional && npm run build && npm prune --omit=dev && rm -rf ./src
|
||||||
|
|
||||||
# Start the App
|
# Start the App
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -89,3 +89,41 @@ If there was nothing found for the mail address: HTTP status code 409 Conflict
|
|||||||
### see
|
### see
|
||||||
|
|
||||||
- [premium concept](../concept/premium.md)
|
- [premium concept](../concept/premium.md)
|
||||||
|
|
||||||
|
## POST /admin/unlock-premium-v2
|
||||||
|
|
||||||
|
Use this to unlock all features for one user for a specified duration.
|
||||||
|
|
||||||
|
### request
|
||||||
|
|
||||||
|
request properties: ``purchaseToken`` and ``purchaseId``
|
||||||
|
|
||||||
|
- ``purchasetoken`` is a string which the client shows at the purchase screen
|
||||||
|
- ``purchaseId`` is the ID that is used at the bill
|
||||||
|
|
||||||
|
### response
|
||||||
|
|
||||||
|
The response contains the following properties:
|
||||||
|
|
||||||
|
- ``ok`` (boolean)
|
||||||
|
- ``error``
|
||||||
|
- string
|
||||||
|
- set if and only if ``ok`` is false
|
||||||
|
- possible values
|
||||||
|
- ``token invalid``
|
||||||
|
- ``illegal state``
|
||||||
|
- ``purchase id already used``
|
||||||
|
- ``detail``
|
||||||
|
- optional string
|
||||||
|
- should be shown to the support
|
||||||
|
- ``lastPurchase``
|
||||||
|
- optional object
|
||||||
|
- should be shown to the support
|
||||||
|
- ``wasAlreadyExecuted`` (boolean, set if and only if ``ok`` is true)
|
||||||
|
|
||||||
|
If the request was malformed: HTTP status code 400 Bad Request
|
||||||
|
|
||||||
|
Using the same ``purchaseId`` twice results in:
|
||||||
|
|
||||||
|
- ``wasAlreadyExecuted`` if the familyId is unchanged
|
||||||
|
- ``error`` = ``purchase id already used`` otherwise
|
||||||
|
|||||||
+1
-2
@@ -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
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
},
|
},
|
||||||
"parentName": {
|
"parentName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"familyId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"exp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"exp",
|
||||||
|
"familyId",
|
||||||
|
"mail",
|
||||||
|
"purpose",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"definitions": {},
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "IdentityTokenPayload",
|
||||||
|
"$id": "https://timelimit.io/IdentityTokenPayload"
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) – `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
|
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) – `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
|
||||||
|
|
||||||
|
* [IdentityTokenPayload](./identitytokenpayload.md) – `https://timelimit.io/IdentityTokenPayload`
|
||||||
|
|
||||||
* [LinkParentMailAddressRequest](./linkparentmailaddressrequest.md) – `https://timelimit.io/LinkParentMailAddressRequest`
|
* [LinkParentMailAddressRequest](./linkparentmailaddressrequest.md) – `https://timelimit.io/LinkParentMailAddressRequest`
|
||||||
|
|
||||||
* [MailAuthTokenRequestBody](./mailauthtokenrequestbody.md) – `https://timelimit.io/MailAuthTokenRequestBody`
|
* [MailAuthTokenRequestBody](./mailauthtokenrequestbody.md) – `https://timelimit.io/MailAuthTokenRequestBody`
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
},
|
},
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
},
|
},
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -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`
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Untitled undefined type in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## definitions Type
|
||||||
|
|
||||||
|
unknown
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Untitled number in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/exp
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## exp Type
|
||||||
|
|
||||||
|
`number`
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/familyId
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## familyId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/mail
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## mail Type
|
||||||
|
|
||||||
|
`string`
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/purpose
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## purpose Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## purpose Constraints
|
||||||
|
|
||||||
|
**enum**: the value of this property must be equal to one of the following values:
|
||||||
|
|
||||||
|
| Value | Explanation |
|
||||||
|
| :----------- | :---------- |
|
||||||
|
| `"purchase"` | |
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/userId
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## userId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------------ |
|
||||||
|
| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [IdentityTokenPayload.schema.json](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## IdentityTokenPayload Type
|
||||||
|
|
||||||
|
`object` ([IdentityTokenPayload](identitytokenpayload.md))
|
||||||
|
|
||||||
|
# IdentityTokenPayload Properties
|
||||||
|
|
||||||
|
| Property | Type | Required | Nullable | Defined by |
|
||||||
|
| :-------------------- | :------- | :------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [purpose](#purpose) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-purpose.md "https://timelimit.io/IdentityTokenPayload#/properties/purpose") |
|
||||||
|
| [familyId](#familyid) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-familyid.md "https://timelimit.io/IdentityTokenPayload#/properties/familyId") |
|
||||||
|
| [userId](#userid) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-userid.md "https://timelimit.io/IdentityTokenPayload#/properties/userId") |
|
||||||
|
| [mail](#mail) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-mail.md "https://timelimit.io/IdentityTokenPayload#/properties/mail") |
|
||||||
|
| [exp](#exp) | `number` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-exp.md "https://timelimit.io/IdentityTokenPayload#/properties/exp") |
|
||||||
|
|
||||||
|
## purpose
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`purpose`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-purpose.md "https://timelimit.io/IdentityTokenPayload#/properties/purpose")
|
||||||
|
|
||||||
|
### purpose Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
### purpose Constraints
|
||||||
|
|
||||||
|
**enum**: the value of this property must be equal to one of the following values:
|
||||||
|
|
||||||
|
| Value | Explanation |
|
||||||
|
| :----------- | :---------- |
|
||||||
|
| `"purchase"` | |
|
||||||
|
|
||||||
|
## familyId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`familyId`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-familyid.md "https://timelimit.io/IdentityTokenPayload#/properties/familyId")
|
||||||
|
|
||||||
|
### familyId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## userId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`userId`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-userid.md "https://timelimit.io/IdentityTokenPayload#/properties/userId")
|
||||||
|
|
||||||
|
### userId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## mail
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`mail`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-mail.md "https://timelimit.io/IdentityTokenPayload#/properties/mail")
|
||||||
|
|
||||||
|
### mail Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## exp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`exp`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `number`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-exp.md "https://timelimit.io/IdentityTokenPayload#/properties/exp")
|
||||||
|
|
||||||
|
### exp Type
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
# IdentityTokenPayload Definitions
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
@@ -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
|
||||||
|
|||||||
Generated
+624
-270
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -61,7 +61,7 @@
|
|||||||
"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",
|
"sqlite3": "^4.0.0",
|
||||||
"umzug": "^2.3.0"
|
"umzug": "^2.3.0"
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ const types = [
|
|||||||
'RequestIdentityTokenRequest',
|
'RequestIdentityTokenRequest',
|
||||||
'RequestWithAuthToken',
|
'RequestWithAuthToken',
|
||||||
'SendMailLoginCodeRequest',
|
'SendMailLoginCodeRequest',
|
||||||
'SignInByMailCodeRequest'
|
'SignInByMailCodeRequest',
|
||||||
|
'IdentityTokenPayload'
|
||||||
]
|
]
|
||||||
|
|
||||||
const docOnlyTypes = [
|
const docOnlyTypes = [
|
||||||
|
|||||||
+150
-3
@@ -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
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
import { json } from 'body-parser'
|
import { json } from 'body-parser'
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { BadRequest, Conflict } from 'http-errors'
|
import { BadRequest, Conflict } from 'http-errors'
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
import { Database } from '../database'
|
import { Database } from '../database'
|
||||||
import { addPurchase } from '../function/purchase'
|
import { addPurchase, canDoNextPurchase } from '../function/purchase'
|
||||||
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
|
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
|
||||||
import { EventHandler } from '../monitoring/eventhandler'
|
import { EventHandler } from '../monitoring/eventhandler'
|
||||||
import { generatePurchaseId } from '../util/token'
|
import { generatePurchaseId } from '../util/token'
|
||||||
|
import { verifyIdentitifyToken, TokenValidationException } from '../util/identity-token'
|
||||||
import { WebsocketApi } from '../websocket'
|
import { WebsocketApi } from '../websocket'
|
||||||
|
|
||||||
export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
||||||
@@ -120,7 +122,8 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
|||||||
database,
|
database,
|
||||||
familyId: userEntry.familyId,
|
familyId: userEntry.familyId,
|
||||||
type,
|
type,
|
||||||
transactionId: 'manual-' + type + '-' + generatePurchaseId(),
|
service: 'directpurchase',
|
||||||
|
transactionId: 'legacyunlock-' + type + '-' + generatePurchaseId(),
|
||||||
websocket,
|
websocket,
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
@@ -132,5 +135,149 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/unlock-premium-v2', json(), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof req.body !== 'object' ||
|
||||||
|
typeof req.body.purchaseToken !== 'string' ||
|
||||||
|
typeof req.body.purchaseId !== 'string'
|
||||||
|
) {
|
||||||
|
throw new BadRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
const purchaseToken: string = req.body.purchaseToken
|
||||||
|
const purchaseId: string = req.body.purchaseId
|
||||||
|
|
||||||
|
const tokenContent = await verifyIdentitifyToken(purchaseToken)
|
||||||
|
|
||||||
|
if (tokenContent.purpose !== 'purchase') {
|
||||||
|
res.json({ ok: false, error: 'token invalid', detail: 'wrong purpose' })
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await database.transaction(async (transaction) => {
|
||||||
|
const userValid = await database.user.count({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
userId: tokenContent.userId,
|
||||||
|
mail: tokenContent.mail,
|
||||||
|
type: 'parent'
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userValid) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'token invalid',
|
||||||
|
detail: 'user not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
let mailToReturn: string
|
||||||
|
|
||||||
|
if (tokenContent.mail !== '') mailToReturn = tokenContent.mail
|
||||||
|
else {
|
||||||
|
const userEntryWithMail = await database.user.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
mail: {
|
||||||
|
[Sequelize.Op.ne]: ''
|
||||||
|
},
|
||||||
|
type: 'parent'
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userEntryWithMail) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'illegal state',
|
||||||
|
detail: 'no user with mail found'
|
||||||
|
}
|
||||||
|
|
||||||
|
mailToReturn = userEntryWithMail.mail
|
||||||
|
}
|
||||||
|
|
||||||
|
let wasAlreadyExecuted: boolean
|
||||||
|
|
||||||
|
const oldPurchaseByPurchaseId = await database.purchase.findOne({
|
||||||
|
where: {
|
||||||
|
service: 'directpurchase',
|
||||||
|
transactionId: purchaseId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (oldPurchaseByPurchaseId === null) wasAlreadyExecuted = false
|
||||||
|
else if (oldPurchaseByPurchaseId.familyId === tokenContent.familyId) wasAlreadyExecuted = true
|
||||||
|
else return {
|
||||||
|
ok: false,
|
||||||
|
error: 'purchase id already used'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasAlreadyExecuted) {
|
||||||
|
const familyEntry = await database.family.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!familyEntry) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'family not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDoPurchase = canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil) })
|
||||||
|
|
||||||
|
if (!canDoPurchase) {
|
||||||
|
const lastPurchase = await database.purchase.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
order: [['loggedAt', 'DESC']],
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'can not renew now',
|
||||||
|
lastPurchase: lastPurchase ? {
|
||||||
|
service: lastPurchase.service,
|
||||||
|
transactionId: lastPurchase.transactionId,
|
||||||
|
timestamp: parseInt(lastPurchase.loggedAt),
|
||||||
|
timestring: new Date(parseInt(lastPurchase.loggedAt)).toISOString()
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await addPurchase({
|
||||||
|
database,
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
type: 'year',
|
||||||
|
service: 'directpurchase',
|
||||||
|
transactionId: purchaseId,
|
||||||
|
websocket,
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
mail: mailToReturn,
|
||||||
|
wasAlreadyExecuted
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(response)
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof TokenValidationException) res.json({
|
||||||
|
ok: false,
|
||||||
|
error: 'token invalid',
|
||||||
|
detail: ex.message
|
||||||
|
})
|
||||||
|
else next(ex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -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)
|
||||||
|
|||||||
+3
-3
@@ -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 }))
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -31,6 +31,7 @@ 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,
|
||||||
@@ -38,7 +39,13 @@ import {
|
|||||||
isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest
|
isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest
|
||||||
} 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 +82,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 +110,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)
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
@@ -122,6 +122,7 @@ export const createPurchaseRouter = ({ database, websocket }: {
|
|||||||
database,
|
database,
|
||||||
familyId: deviceEntry.familyId,
|
familyId: deviceEntry.familyId,
|
||||||
type,
|
type,
|
||||||
|
service: 'googleplay',
|
||||||
transactionId: orderId,
|
transactionId: orderId,
|
||||||
websocket,
|
websocket,
|
||||||
transaction
|
transaction
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -162,5 +165,16 @@ export interface SignInByMailCodeRequest {
|
|||||||
receivedCode: string
|
receivedCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IdentityTokenCreatePayload {
|
||||||
|
purpose: 'purchase'
|
||||||
|
familyId: string
|
||||||
|
userId: string
|
||||||
|
mail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IdentityTokenPayload = IdentityTokenCreatePayload & {
|
||||||
|
exp: number
|
||||||
|
}
|
||||||
|
|
||||||
export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization'
|
export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization'
|
||||||
export { ServerDataStatus } from '../object/serverdatastatus'
|
export { ServerDataStatus } from '../object/serverdatastatus'
|
||||||
|
|||||||
+43
-1
@@ -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 } from './schema'
|
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 Ajv from 'ajv'
|
import Ajv from 'ajv'
|
||||||
const ajv = new Ajv()
|
const ajv = new Ajv()
|
||||||
|
|
||||||
@@ -2930,6 +2930,9 @@ export const isCreateFamilyByMailTokenRequest: (value: unknown) => value is Crea
|
|||||||
},
|
},
|
||||||
"parentName": {
|
"parentName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -2955,6 +2958,9 @@ export const isSignIntoFamilyRequest: (value: unknown) => value is SignIntoFamil
|
|||||||
},
|
},
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -2995,6 +3001,9 @@ export const isRegisterChildDeviceRequest: (value: unknown) => value is Register
|
|||||||
},
|
},
|
||||||
"deviceName": {
|
"deviceName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientLevel": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -3451,3 +3460,36 @@ export const isSignInByMailCodeRequest: (value: unknown) => value is SignInByMai
|
|||||||
"definitions": definitions,
|
"definitions": definitions,
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
})
|
})
|
||||||
|
export const isIdentityTokenPayload: (value: unknown) => value is IdentityTokenPayload = ajv.compile({
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"familyId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"exp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"exp",
|
||||||
|
"familyId",
|
||||||
|
"mail",
|
||||||
|
"purpose",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"definitions": definitions,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
})
|
||||||
|
|||||||
+43
-11
@@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+10
-19
@@ -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)
|
export async function down() {
|
||||||
cache.incrementTriggeredSyncLevel(1)
|
throw new Error('not possible')
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -21,7 +21,7 @@ import { SequelizeAttributes } from './types'
|
|||||||
|
|
||||||
export interface PurchaseAttributes {
|
export interface PurchaseAttributes {
|
||||||
familyId: string
|
familyId: string
|
||||||
service: 'googleplay'
|
service: 'googleplay' | 'directpurchase'
|
||||||
transactionId: string
|
transactionId: string
|
||||||
type: 'month' | 'year'
|
type: 'month' | 'year'
|
||||||
loggedAt: string
|
loggedAt: string
|
||||||
@@ -37,7 +37,7 @@ export type PurchaseModelStatic = typeof Sequelize.Model & {
|
|||||||
export const attributes: SequelizeAttributes<PurchaseAttributes> = {
|
export const attributes: SequelizeAttributes<PurchaseAttributes> = {
|
||||||
familyId: { ...familyIdColumn },
|
familyId: { ...familyIdColumn },
|
||||||
service: {
|
service: {
|
||||||
...createEnumColumn(['googleplay']),
|
...createEnumColumn(['googleplay', 'directpurchase']),
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
},
|
},
|
||||||
transactionId: {
|
transactionId: {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,26 +29,6 @@ export async function deleteFamilies ({ database, familiyIds }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await database.transaction(async (transaction) => {
|
await database.transaction(async (transaction) => {
|
||||||
// app
|
|
||||||
await database.app.destroy({
|
|
||||||
where: {
|
|
||||||
familyId: {
|
|
||||||
[Sequelize.Op.in]: familiyIds
|
|
||||||
}
|
|
||||||
},
|
|
||||||
transaction
|
|
||||||
})
|
|
||||||
|
|
||||||
// app activity
|
|
||||||
await database.appActivity.destroy({
|
|
||||||
where: {
|
|
||||||
familyId: {
|
|
||||||
[Sequelize.Op.in]: familiyIds
|
|
||||||
}
|
|
||||||
},
|
|
||||||
transaction
|
|
||||||
})
|
|
||||||
|
|
||||||
// category
|
// category
|
||||||
await database.category.destroy({
|
await database.category.destroy({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -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: ''
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,15 @@ const day = 1000 * 60 * 60 * 24
|
|||||||
const month = day * 31
|
const month = day * 31
|
||||||
const year = day * 366
|
const year = day * 366
|
||||||
|
|
||||||
export const addPurchase = async ({ database, familyId, type, transactionId, websocket, transaction }: {
|
export const addPurchase = async ({ database, familyId, type, service, transactionId, websocket, transaction }: {
|
||||||
database: Database
|
database: Database
|
||||||
familyId: string
|
familyId: string
|
||||||
type: 'month' | 'year'
|
type: 'month' | 'year'
|
||||||
|
service: 'googleplay' | 'directpurchase'
|
||||||
transactionId: string
|
transactionId: string
|
||||||
websocket: WebsocketApi
|
websocket: WebsocketApi
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
}) => {
|
}) => {
|
||||||
const service = 'googleplay'
|
|
||||||
|
|
||||||
const oldPurchaseEntry = await database.purchase.findOne({
|
const oldPurchaseEntry = await database.purchase.findOne({
|
||||||
where: {
|
where: {
|
||||||
service,
|
service,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ export const dispatchParentAction = async ({
|
|||||||
return dispatchUpdateCategoryBlockedTimes({ action, cache, fromChildSelfLimitAddChildUserId })
|
return dispatchUpdateCategoryBlockedTimes({ action, cache, fromChildSelfLimitAddChildUserId })
|
||||||
} else if (action instanceof UpdateCategoryDisableLimitsAction) {
|
} else if (action instanceof UpdateCategoryDisableLimitsAction) {
|
||||||
return dispatchUpdateCategoryDisableLimits({ action, cache, fromChildSelfLimitAddChildUserId })
|
return dispatchUpdateCategoryDisableLimits({ action, cache, fromChildSelfLimitAddChildUserId })
|
||||||
|
} else if (action instanceof UpdateTimelimitRuleAction) {
|
||||||
|
return dispatchUpdateTimelimitRule({ action, cache, fromChildSelfLimitAddChildUserId })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromChildSelfLimitAddChildUserId !== null) {
|
if (fromChildSelfLimitAddChildUserId !== null) {
|
||||||
@@ -211,8 +213,6 @@ export const dispatchParentAction = async ({
|
|||||||
return dispatchUpdateNetworkTimeVerification({ action, cache })
|
return dispatchUpdateNetworkTimeVerification({ action, cache })
|
||||||
} else if (action instanceof UpdateParentNotificationFlagsAction) {
|
} else if (action instanceof UpdateParentNotificationFlagsAction) {
|
||||||
return dispatchUpdateParentNotificationFlags({ action, cache })
|
return dispatchUpdateParentNotificationFlags({ action, cache })
|
||||||
} else if (action instanceof UpdateTimelimitRuleAction) {
|
|
||||||
return dispatchUpdateTimelimitRule({ action, cache })
|
|
||||||
} else if (action instanceof RemoveUserAction) {
|
} else if (action instanceof RemoveUserAction) {
|
||||||
return dispatchRemoveUser({ action, cache, parentUserId })
|
return dispatchRemoveUser({ action, cache, parentUserId })
|
||||||
} else if (action instanceof ReportU2fLoginAction) {
|
} else if (action instanceof ReportU2fLoginAction) {
|
||||||
|
|||||||
@@ -17,11 +17,17 @@
|
|||||||
|
|
||||||
import { UpdateTimelimitRuleAction } from '../../../../action'
|
import { UpdateTimelimitRuleAction } from '../../../../action'
|
||||||
import { Cache } from '../cache'
|
import { Cache } from '../cache'
|
||||||
import { MissingRuleException } from '../exception/missing-item'
|
import { MissingRuleException, MissingCategoryException } from '../exception/missing-item'
|
||||||
|
import {
|
||||||
|
CanNotModifyOtherUsersBySelfLimitationException, CanNotRelaxRestrictionsSelfLimitException
|
||||||
|
} from '../exception/self-limit'
|
||||||
|
|
||||||
export async function dispatchUpdateTimelimitRule ({ action, cache }: {
|
export async function dispatchUpdateTimelimitRule ({
|
||||||
|
action, cache, fromChildSelfLimitAddChildUserId
|
||||||
|
}: {
|
||||||
action: UpdateTimelimitRuleAction
|
action: UpdateTimelimitRuleAction
|
||||||
cache: Cache
|
cache: Cache
|
||||||
|
fromChildSelfLimitAddChildUserId: string | null
|
||||||
}) {
|
}) {
|
||||||
const ruleEntry = await cache.database.timelimitRule.findOne({
|
const ruleEntry = await cache.database.timelimitRule.findOne({
|
||||||
where: {
|
where: {
|
||||||
@@ -35,6 +41,53 @@ export async function dispatchUpdateTimelimitRule ({ action, cache }: {
|
|||||||
throw new MissingRuleException()
|
throw new MissingRuleException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fromChildSelfLimitAddChildUserId != null) {
|
||||||
|
const categoryEntryUnsafe = await cache.database.category.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: cache.familyId,
|
||||||
|
categoryId: ruleEntry.categoryId
|
||||||
|
},
|
||||||
|
transaction: cache.transaction,
|
||||||
|
attributes: ['childId']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!categoryEntryUnsafe) {
|
||||||
|
throw new MissingCategoryException()
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryEntry = {
|
||||||
|
childId: categoryEntryUnsafe.childId
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (fromChildSelfLimitAddChildUserId !== categoryEntry.childId) {
|
||||||
|
throw new CanNotModifyOtherUsersBySelfLimitationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasSessionDurationLimitationEnabled =
|
||||||
|
ruleEntry.sessionPauseMilliseconds > 0 && ruleEntry.sessionDurationMilliseconds > 0
|
||||||
|
|
||||||
|
const countOldAffectedDays = Array(7)
|
||||||
|
.fill(0)
|
||||||
|
.reduce((sum, _, index) => sum + ((ruleEntry.dayMaskAsBitmask >> index) & 1), 0)
|
||||||
|
|
||||||
|
const isAtLeastAsStrictAsPreviously =
|
||||||
|
action.maximumTimeInMillis <= ruleEntry.maximumTimeInMillis &&
|
||||||
|
(action.dayMask & ruleEntry.dayMaskAsBitmask) === ruleEntry.dayMaskAsBitmask &&
|
||||||
|
(action.applyToExtraTimeUsage || !ruleEntry.applyToExtraTimeUsage) &&
|
||||||
|
action.start <= ruleEntry.startMinuteOfDay &&
|
||||||
|
action.end >= ruleEntry.endMinuteOfDay &&
|
||||||
|
(!wasSessionDurationLimitationEnabled || (
|
||||||
|
action.sessionDurationMilliseconds <= ruleEntry.sessionDurationMilliseconds &&
|
||||||
|
action.sessionPauseMilliseconds >= ruleEntry.sessionPauseMilliseconds
|
||||||
|
)) &&
|
||||||
|
(!action.perDay || ruleEntry.perDay || countOldAffectedDays <= 1)
|
||||||
|
|
||||||
|
if (!isAtLeastAsStrictAsPreviously) {
|
||||||
|
throw new CanNotRelaxRestrictionsSelfLimitException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ruleEntry.applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
ruleEntry.applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
||||||
ruleEntry.dayMaskAsBitmask = action.dayMask
|
ruleEntry.dayMaskAsBitmask = action.dayMask
|
||||||
ruleEntry.maximumTimeInMillis = action.maximumTimeInMillis
|
ruleEntry.maximumTimeInMillis = action.maximumTimeInMillis
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -32,3 +32,9 @@ export class ActionNotSupportedBySelfLimitationException extends SelfLimitationE
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SelfLimitNotPossibleException extends SelfLimitationException {}
|
export class SelfLimitNotPossibleException extends SelfLimitationException {}
|
||||||
|
|
||||||
|
export class CanNotRelaxRestrictionsSelfLimitException extends SelfLimitationException {
|
||||||
|
constructor () {
|
||||||
|
super({ staticMessage: 'can not relax restrictions with the self limitation' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -59,7 +58,7 @@ export const generateServerDataStatus = async ({
|
|||||||
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
|
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
|
||||||
),
|
),
|
||||||
message: await getStatusMessage({ database, transaction }) || undefined,
|
message: await getStatusMessage({ database, transaction }) || undefined,
|
||||||
apiLevel: 6
|
apiLevel: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
if (familyEntry.deviceListVersion !== clientStatus.devices) {
|
if (familyEntry.deviceListVersion !== clientStatus.devices) {
|
||||||
@@ -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) {
|
||||||
|
|||||||
+18
-2
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
+32
-10
@@ -15,21 +15,16 @@
|
|||||||
* 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 { SignJWT } from 'jose'
|
import { SignJWT, jwtVerify } from 'jose'
|
||||||
import { config } from '../config'
|
import { config } from '../config'
|
||||||
|
import { IdentityTokenPayload, IdentityTokenCreatePayload } from '../api/schema'
|
||||||
|
import { isIdentityTokenPayload } from '../api/validator'
|
||||||
|
|
||||||
export async function createIdentityToken({ purpose, familyId, userId, mail }: {
|
export async function createIdentityToken({ purpose, familyId, userId, mail }: IdentityTokenCreatePayload) {
|
||||||
purpose: string
|
|
||||||
familyId: string
|
|
||||||
userId: string
|
|
||||||
mail: string
|
|
||||||
}) {
|
|
||||||
if (config.signSecret === '') throw new MissingSignSecretException()
|
|
||||||
|
|
||||||
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
|
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
|
||||||
.setExpirationTime('7d')
|
.setExpirationTime('7d')
|
||||||
.setProtectedHeader({ alg: 'HS512' })
|
.setProtectedHeader({ alg: 'HS512' })
|
||||||
.sign(Buffer.from(config.signSecret, 'utf8'))
|
.sign(getSignSecret())
|
||||||
|
|
||||||
return Buffer.from(jwt, 'ascii')
|
return Buffer.from(jwt, 'ascii')
|
||||||
.toString('base64')
|
.toString('base64')
|
||||||
@@ -38,4 +33,31 @@ export async function createIdentityToken({ purpose, familyId, userId, mail }: {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyIdentitifyToken(token: string): Promise<IdentityTokenPayload> {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(
|
||||||
|
Buffer.from(token, 'base64').toString('ascii'),
|
||||||
|
getSignSecret(),
|
||||||
|
{ algorithms: ['HS512'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isIdentityTokenPayload(payload)) throw new BadPayloadException()
|
||||||
|
|
||||||
|
return payload
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof TokenValidationException) throw ex
|
||||||
|
else if (ex instanceof Error) throw new TokenValidationException(ex.message)
|
||||||
|
else throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignSecret(): Buffer {
|
||||||
|
if (config.signSecret === '') throw new MissingSignSecretException()
|
||||||
|
|
||||||
|
return Buffer.from(config.signSecret, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
export class MissingSignSecretException extends Error {}
|
export class MissingSignSecretException extends Error {}
|
||||||
|
|
||||||
|
export class TokenValidationException extends Error {}
|
||||||
|
class BadPayloadException extends TokenValidationException { constructor() { super('bad payload') } }
|
||||||
|
|||||||
Reference in New Issue
Block a user