Compare commits

..
13 Commits
38 changed files with 763 additions and 546 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ COPY package.json package-lock.json tsconfig.json .eslintignore .eslintrc.js Rea
COPY src/ /usr/src/app/src/
COPY scripts/ /usr/src/app/scripts/
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
EXPOSE 8080
+38
View File
@@ -89,3 +89,41 @@ If there was nothing found for the mail address: HTTP status code 409 Conflict
### see
- [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
@@ -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"
}
+2
View File
@@ -16,6 +16,8 @@
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
* [IdentityTokenPayload](./identitytokenpayload.md) `https://timelimit.io/IdentityTokenPayload`
* [LinkParentMailAddressRequest](./linkparentmailaddressrequest.md) `https://timelimit.io/LinkParentMailAddressRequest`
* [MailAuthTokenRequestBody](./mailauthtokenrequestbody.md) `https://timelimit.io/MailAuthTokenRequestBody`
@@ -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`
+125
View File
@@ -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
+84 -112
View File
@@ -23,7 +23,7 @@
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15",
"sequelize": "^6.11.0",
"sequelize": "^6.25.5",
"socket.io": "^4.0.1",
"sqlite3": "^4.0.0",
"umzug": "^2.3.0"
@@ -237,13 +237,10 @@
"integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==",
"dev": true
},
"node_modules/@socket.io/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==",
"engines": {
"node": ">= 0.6.0"
}
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
@@ -294,11 +291,6 @@
"@types/node": "*"
}
},
"node_modules/@types/component-emitter": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -474,8 +466,7 @@
"node_modules/@types/validator": {
"version": "13.7.1",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.1.tgz",
"integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==",
"dev": true
"integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q=="
},
"node_modules/@types/webidl-conversions": {
"version": "6.1.1",
@@ -1100,11 +1091,6 @@
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
"dev": true
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1380,9 +1366,9 @@
}
},
"node_modules/engine.io": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -1392,7 +1378,7 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.0",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
},
"engines": {
@@ -1400,12 +1386,9 @@
}
},
"node_modules/engine.io-parser": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
"dependencies": {
"@socket.io/base64-arraybuffer": "~1.0.2"
},
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"engines": {
"node": ">=10.0.0"
}
@@ -2195,9 +2178,9 @@
}
},
"node_modules/inflection": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.1.tgz",
"integrity": "sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA==",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
"engines": [
"node >= 0.4.0"
]
@@ -3314,9 +3297,9 @@
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -4116,9 +4099,9 @@
}
},
"node_modules/retry-as-promised": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz",
"integrity": "sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA=="
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
"integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
},
"node_modules/reusify": {
"version": "1.0.4",
@@ -4290,9 +4273,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/sequelize": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.14.0.tgz",
"integrity": "sha512-rgfJFixBwnrxdouIlYS1G0nTo2xj+Gyn7EDn/miE6r5xCzN3EdjtbNrDXapLHNZxgKfu7hZHLzZvwl3DbfhM6g==",
"version": "6.25.5",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.25.5.tgz",
"integrity": "sha512-QwA2kL4lSNy4mYTv1Zm4DjvloZLaYajuYF5QMOv0O7FameMUEl8Q5zOWRD+FNKGCLzyOJ28gQCo3HX0N9mcssA==",
"funding": [
{
"type": "opencollective",
@@ -4301,14 +4284,15 @@
],
"dependencies": {
"@types/debug": "^4.1.7",
"@types/validator": "^13.7.1",
"debug": "^4.3.3",
"dottie": "^2.0.2",
"inflection": "^1.13.1",
"inflection": "^1.13.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"pg-connection-string": "^2.5.0",
"retry-as-promised": "^5.0.0",
"retry-as-promised": "^6.1.0",
"semver": "^7.3.5",
"sequelize-pool": "^7.1.0",
"toposort-class": "^1.0.1",
@@ -4329,6 +4313,9 @@
"mysql2": {
"optional": true
},
"oracledb": {
"optional": true
},
"pg": {
"optional": true
},
@@ -4426,33 +4413,32 @@
}
},
"node_modules/socket.io": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.1.0",
"socket.io-adapter": "~2.3.3",
"socket.io-parser": "~4.0.4"
"engine.io": "~6.2.1",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.2.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
},
"node_modules/socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
"dependencies": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
@@ -5481,10 +5467,10 @@
"integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==",
"dev": true
},
"@socket.io/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ=="
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@tsconfig/node10": {
"version": "1.0.8",
@@ -5535,11 +5521,6 @@
"@types/node": "*"
}
},
"@types/component-emitter": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz",
"integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -5715,8 +5696,7 @@
"@types/validator": {
"version": "13.7.1",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.1.tgz",
"integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==",
"dev": true
"integrity": "sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q=="
},
"@types/webidl-conversions": {
"version": "6.1.1",
@@ -6140,11 +6120,6 @@
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
"dev": true
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -6345,9 +6320,9 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"engine.io": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz",
"integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -6357,17 +6332,14 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.0",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
}
},
"engine.io-parser": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz",
"integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==",
"requires": {
"@socket.io/base64-arraybuffer": "~1.0.2"
}
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"es2015-i18n-tag": {
"version": "1.6.1",
@@ -6985,9 +6957,9 @@
"dev": true
},
"inflection": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.1.tgz",
"integrity": "sha512-dldYtl2WlN0QDkIDtg8+xFwOS2Tbmp12t1cHa5/YClU6ZQjTFm7B66UcVbh9NQB+HvT5BAd2t5+yKsBkw5pcqA=="
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw=="
},
"inflight": {
"version": "1.0.6",
@@ -7730,9 +7702,9 @@
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8346,9 +8318,9 @@
"dev": true
},
"retry-as-promised": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz",
"integrity": "sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA=="
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
"integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
},
"reusify": {
"version": "1.0.4",
@@ -8476,19 +8448,20 @@
}
},
"sequelize": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.14.0.tgz",
"integrity": "sha512-rgfJFixBwnrxdouIlYS1G0nTo2xj+Gyn7EDn/miE6r5xCzN3EdjtbNrDXapLHNZxgKfu7hZHLzZvwl3DbfhM6g==",
"version": "6.25.5",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.25.5.tgz",
"integrity": "sha512-QwA2kL4lSNy4mYTv1Zm4DjvloZLaYajuYF5QMOv0O7FameMUEl8Q5zOWRD+FNKGCLzyOJ28gQCo3HX0N9mcssA==",
"requires": {
"@types/debug": "^4.1.7",
"@types/validator": "^13.7.1",
"debug": "^4.3.3",
"dottie": "^2.0.2",
"inflection": "^1.13.1",
"inflection": "^1.13.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"pg-connection-string": "^2.5.0",
"retry-as-promised": "^5.0.0",
"retry-as-promised": "^6.1.0",
"semver": "^7.3.5",
"sequelize-pool": "^7.1.0",
"toposort-class": "^1.0.1",
@@ -8558,30 +8531,29 @@
"dev": true
},
"socket.io": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.1.0",
"socket.io-adapter": "~2.3.3",
"socket.io-parser": "~4.0.4"
"engine.io": "~6.2.1",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.2.1"
}
},
"socket.io-adapter": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz",
"integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
},
"socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
"integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
"requires": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
+1 -1
View File
@@ -61,7 +61,7 @@
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15",
"sequelize": "^6.11.0",
"sequelize": "^6.25.5",
"socket.io": "^4.0.1",
"sqlite3": "^4.0.0",
"umzug": "^2.3.0"
+2 -1
View File
@@ -42,7 +42,8 @@ const types = [
'RequestIdentityTokenRequest',
'RequestWithAuthToken',
'SendMailLoginCodeRequest',
'SignInByMailCodeRequest'
'SignInByMailCodeRequest',
'IdentityTokenPayload'
]
const docOnlyTypes = [
+150 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -18,11 +18,13 @@
import { json } from 'body-parser'
import { Router } from 'express'
import { BadRequest, Conflict } from 'http-errors'
import * as Sequelize from 'sequelize'
import { Database } from '../database'
import { addPurchase } from '../function/purchase'
import { addPurchase, canDoNextPurchase } from '../function/purchase'
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
import { EventHandler } from '../monitoring/eventhandler'
import { generatePurchaseId } from '../util/token'
import { verifyIdentitifyToken, TokenValidationException } from '../util/identity-token'
import { WebsocketApi } from '../websocket'
export const createAdminRouter = ({ database, websocket, eventHandler }: {
@@ -120,7 +122,8 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
database,
familyId: userEntry.familyId,
type,
transactionId: 'manual-' + type + '-' + generatePurchaseId(),
service: 'directpurchase',
transactionId: 'legacyunlock-' + type + '-' + generatePurchaseId(),
websocket,
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
}
+2 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -122,6 +122,7 @@ export const createPurchaseRouter = ({ database, websocket }: {
database,
familyId: deviceEntry.familyId,
type,
service: 'googleplay',
transactionId: orderId,
websocket,
transaction
+11
View File
@@ -162,5 +162,16 @@ export interface SignInByMailCodeRequest {
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 { ServerDataStatus } from '../object/serverdatastatus'
+34 -1
View File
@@ -1,5 +1,5 @@
// 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'
const ajv = new Ajv()
@@ -3451,3 +3451,36 @@ export const isSignInByMailCodeRequest: (value: unknown) => value is SignInByMai
"definitions": definitions,
"$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
View File
@@ -17,8 +17,6 @@
import * as Sequelize from 'sequelize'
import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken'
import { AppModelStatic, createAppModel } from './app'
import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
@@ -42,14 +40,13 @@ import { createU2fKeyModel, U2fKeyModelStatic } from './u2fkey'
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
import { createUserModel, UserModelStatic } from './user'
import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory'
import { shouldRetryWithException } from './utils/serialized'
export type Transaction = Sequelize.Transaction
export interface Database {
addDeviceToken: AddDeviceTokenModelStatic
authtoken: AuthTokenModelStatic
app: AppModelStatic
appActivity: AppActivityModelStatic
category: CategoryModelStatic
categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic
@@ -71,15 +68,21 @@ export interface Database {
usedTime: UsedTimeModelStatic
user: UserModelStatic
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
}
interface TransactionOptions {
transaction?: Transaction
disableRetry?: boolean
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
addDeviceToken: createAddDeviceTokenModel(sequelize),
authtoken: createAuthtokenModel(sequelize),
app: createAppModel(sequelize),
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize),
childTask: createChildTaskModel(sequelize),
@@ -101,10 +104,39 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
usedTime: createUsedTimeModel(sequelize),
user: createUserModel(sequelize),
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Transaction) => Promise<T>, options?: { transaction: Transaction }) => (sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE,
transaction: options?.transaction
}, autoCallback)) as Promise<T>,
async transaction<T>(
autoCallback: (transaction: Transaction) => Promise<T>,
options?: TransactionOptions
): 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()
})
@@ -15,26 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { RemoveInstalledAppsAction } from '../../../../action'
import { Cache } from '../cache'
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function dispatchRemoveInstalledApps ({ deviceId, action, cache }: {
deviceId: string
action: RemoveInstalledAppsAction
cache: Cache
}) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId,
packageName: {
[Sequelize.Op.in]: action.packageNames
}
},
transaction: cache.transaction
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('Apps', { transaction })
await queryInterface.dropTable('AppActivities', { transaction })
})
}
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
export async function down() {
throw new Error('not possible')
}
+3 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -21,7 +21,7 @@ import { SequelizeAttributes } from './types'
export interface PurchaseAttributes {
familyId: string
service: 'googleplay'
service: 'googleplay' | 'directpurchase'
transactionId: string
type: 'month' | 'year'
loggedAt: string
@@ -37,7 +37,7 @@ export type PurchaseModelStatic = typeof Sequelize.Model & {
export const attributes: SequelizeAttributes<PurchaseAttributes> = {
familyId: { ...familyIdColumn },
service: {
...createEnumColumn(['googleplay']),
...createEnumColumn(['googleplay', 'directpurchase']),
primaryKey: true
},
transactionId: {
+2 -2
View File
@@ -86,8 +86,8 @@ export async function assertSerializeableTransactionsAreWorking (database: Datab
await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo })
})()
])
})
})
}, { disableRetry: true })
}, { disableRetry: true })
throw new SerializationFeatureCheckException()
} catch (ex) {
+1 -21
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -29,26 +29,6 @@ export async function deleteFamilies ({ database, familiyIds }: {
}
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
await database.category.destroy({
where: {
-17
View File
@@ -40,23 +40,6 @@ export async function removeDevice ({ database, familyId, deviceId, websocket, t
throw new Conflict()
}
// remove app entries
await database.app.destroy({
where: {
familyId,
deviceId
},
transaction
})
await database.appActivity.destroy({
where: {
familyId,
deviceId
},
transaction
})
// remove as current device
await database.user.update({
currentDevice: ''
+1 -1
View File
@@ -69,7 +69,7 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
fullVersionUntil: (Date.now() + 1000 * 60 * 60 * 24 * 14).toString(10),
hasFullVersion: true,
nextServerKeyRequestSeq: '1',
u2fKeysVersion: generateIdWithinFamily()
u2fKeysVersion: generateVersionId()
}, { transaction })
// create parent user
+2 -3
View File
@@ -24,16 +24,15 @@ const day = 1000 * 60 * 60 * 24
const month = day * 31
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
familyId: string
type: 'month' | 'year'
service: 'googleplay' | 'directpurchase'
transactionId: string
websocket: WebsocketApi
transaction: Transaction
}) => {
const service = 'googleplay'
const oldPurchaseEntry = await database.purchase.findOne({
where: {
service,
-17
View File
@@ -40,7 +40,6 @@ export class Cache {
categoriesWithModifiedUsedTimes = new Set<string>()
categoriesWithModifiedTasks = new Set<string>()
devicesWithModifiedInstalledApps = new Set<string>()
devicesWithModifiedShowDeviceConnected = new Map<string, boolean>()
invalidiateUserList = false
@@ -241,22 +240,6 @@ export class Cache {
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) {
await database.family.update({
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 { Cache } from '../cache'
import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { dispatchAddInstalledApps } from './addinstalledapps'
import { dispatchAddUsedTime } from './addusedtime'
import { dispatchAddUsedTimeVersion2 } from './addusedtime2'
import { dispatchFinishKeyRequestAction } from './finishkeyrequest'
import { dispatchForceSyncAction } from './forcesync'
import { dispatchMarkTaskPendingAction } from './marktaskpendingaction'
import { dispatchReplyToKeyRequestAction } from './replytokeyrequest'
import { dispatchRemoveInstalledApps } from './removeinstalledapps'
import { dispatchSendKeyRequestAction } from './sendkeyrequest'
import { dispatchSignOutAtDevice } from './signoutatdevice'
import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin'
import { dispatchUpdateAppActivities } from './updateappactivities'
import { dispatchUpdateDeviceStatus } from './updatedevicestatus'
import { dispatchUpdateInstalledApps } from './updateinstalledapps'
import { dispatchUploadDevicePublicKeyAction } from './uploaddevicepublickey'
@@ -59,7 +56,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
eventHandler: EventHandler
}) => {
if (action instanceof AddInstalledAppsAction) {
await dispatchAddInstalledApps({ deviceId, action, cache })
// do nothing
} else if (action instanceof AddUsedTimeAction) {
await dispatchAddUsedTime({ deviceId, action, cache })
} else if (action instanceof AddUsedTimeActionVersion2) {
@@ -73,7 +70,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof ReplyToKeyRequestAction) {
await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler })
} else if (action instanceof RemoveInstalledAppsAction) {
await dispatchRemoveInstalledApps({ deviceId, action, cache })
// do nothing
} else if (action instanceof SendKeyRequestAction) {
await dispatchSendKeyRequestAction({ deviceId, action, cache })
} else if (action instanceof SignOutAtDeviceAction) {
@@ -81,7 +78,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof UpdateDeviceStatusAction) {
await dispatchUpdateDeviceStatus({ deviceId, action, cache })
} else if (action instanceof UpdateAppActivitiesAction) {
await dispatchUpdateAppActivities({ deviceId, action, cache })
// do nothing
} else if (action instanceof TriedDisablingDeviceAdminAction) {
await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache })
} 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 })
}
if (action.wipe) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
cache.devicesWithModifiedInstalledApps.add(deviceId)
}
cache.incrementTriggeredSyncLevel(1)
}
@@ -150,6 +150,8 @@ export const dispatchParentAction = async ({
return dispatchUpdateCategoryBlockedTimes({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof UpdateCategoryDisableLimitsAction) {
return dispatchUpdateCategoryDisableLimits({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof UpdateTimelimitRuleAction) {
return dispatchUpdateTimelimitRule({ action, cache, fromChildSelfLimitAddChildUserId })
}
if (fromChildSelfLimitAddChildUserId !== null) {
@@ -211,8 +213,6 @@ export const dispatchParentAction = async ({
return dispatchUpdateNetworkTimeVerification({ action, cache })
} else if (action instanceof UpdateParentNotificationFlagsAction) {
return dispatchUpdateParentNotificationFlags({ action, cache })
} else if (action instanceof UpdateTimelimitRuleAction) {
return dispatchUpdateTimelimitRule({ action, cache })
} else if (action instanceof RemoveUserAction) {
return dispatchRemoveUser({ action, cache, parentUserId })
} else if (action instanceof ReportU2fLoginAction) {
@@ -17,11 +17,17 @@
import { UpdateTimelimitRuleAction } from '../../../../action'
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
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const ruleEntry = await cache.database.timelimitRule.findOne({
where: {
@@ -35,6 +41,53 @@ export async function dispatchUpdateTimelimitRule ({ action, cache }: {
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.dayMaskAsBitmask = action.dayMask
ruleEntry.maximumTimeInMillis = action.maximumTimeInMillis
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -32,3 +32,9 @@ export class ActionNotSupportedBySelfLimitationException extends SelfLimitationE
}
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 { ServerDataStatus } from '../../../object/serverdatastatus'
import { EventHandler } from '../../../monitoring/eventhandler'
import { getAppList } from './app-list'
import {
getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync,
getRules, getTasks, getUsedTimes
@@ -59,7 +58,7 @@ export const generateServerDataStatus = async ({
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
),
message: await getStatusMessage({ database, transaction }) || undefined,
apiLevel: 6
apiLevel: 7
}
if (familyEntry.deviceListVersion !== clientStatus.devices) {
@@ -70,8 +69,6 @@ export const generateServerDataStatus = async ({
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 })
if (categoryDataToSync.removedCategoryIds.length > 0) {
+18 -2
View File
@@ -1,6 +1,6 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -16,6 +16,7 @@
*/
import { Server } from 'http'
import { pid } from 'process'
import { createApi } from './api'
import { config } from './config'
import { VisibleConnectedDevicesManager } from './connected-devices'
@@ -61,7 +62,22 @@ async function main () {
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')
}
+32 -10
View File
@@ -15,21 +15,16 @@
* 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 { IdentityTokenPayload, IdentityTokenCreatePayload } from '../api/schema'
import { isIdentityTokenPayload } from '../api/validator'
export async function createIdentityToken({ purpose, familyId, userId, mail }: {
purpose: string
familyId: string
userId: string
mail: string
}) {
if (config.signSecret === '') throw new MissingSignSecretException()
export async function createIdentityToken({ purpose, familyId, userId, mail }: IdentityTokenCreatePayload) {
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
.setExpirationTime('7d')
.setProtectedHeader({ alg: 'HS512' })
.sign(Buffer.from(config.signSecret, 'utf8'))
.sign(getSignSecret())
return Buffer.from(jwt, 'ascii')
.toString('base64')
@@ -38,4 +33,31 @@ export async function createIdentityToken({ purpose, familyId, userId, mail }: {
.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 TokenValidationException extends Error {}
class BadPayloadException extends TokenValidationException { constructor() { super('bad payload') } }