Compare commits

...
10 Commits
65 changed files with 2009 additions and 433 deletions
+1 -216
View File
@@ -2,219 +2,4 @@
This is the server for the connected mode in TimeLimit.
## Clustering
This application only supports running a single instance of it.
When clustering, push messages do not work anymore between devices
connected to different devices and the rate limiting is per instance.
## Running
Option 1: Build (run ``sudo docker build -t timelimit-server .`` in this directory) and use a docker image
Option 2: Install Node.JS (see below for detailed usage)
To test it, open ``http://server/time``, it should show a timestamp
## Commands
### npm start
This runs all pending migrations and starts the server.
### npm run build
This "compiles" the application.
### npm run lint:fix
This fixes the causes of lint warnings (where possible).
## Configuration (environment variables)
- DATABASE_URL
- this specifies the database to use
- default value: ``sqlite://test.db`` (sqlite database in the source code directory)
- supports mysql, postgresql and sqlite (sqlite in development builds only because it's declared as dev dependency)
- looks like ``postgres://user:pass@example.com:5432/dbname``
- no extra setup needed
- when starting the application, the database tables are created/ migrated
- this only works for upgrading; if you intend to eventually downgrade, make a backup first (you should make backups in all cases before an upgrade)
- PORT
- the port at which the server should listen
- default value: 8080
- NODE_ENV
- should be set to ``production`` in production
- when using ``development``, then mails are not sent; instead they are written to a html file which is opened
- GOOGLE_PLAY_PUBLIC_KEY
- key for validating purchases
- purchases using google play don't work without it/ when it is not set
- MAIL_SENDER
- sender (for the from-field) for sent mails
- MAIL_TRANSPORT
- a JSON encoded configuration for nodemailer
- supports setting a smtp server configuration, see <https://nodemailer.com/smtp/>
- allows easier configuration in case of a [well known services](https://nodemailer.com/smtp/well-known/)
- default value is ``null``
- examples
- ``{"host": "localhost", "port": 25}`` (using a local mail server which does not require any authentication)
- ``{"service": "1und1", "auth": {"user": "me@my.timelimit.server", "pass": "my password"}}`` (using a well known service)
- ``{"host": "my.mail.server", "secure": true, "auth": {"user": "me@my.timelimit.server", "pass": "my password"}}`` (using a external smtp server)
- in case of a docker-compose file, you should escape this, e.g. sourround it with ``'`` single quotes
- MAIL_IMPRINT
- a string which is added to the footer of the sent mails
- default value: ``not defined``
- ADMIN_TOKEN
- a password which allows to use some APIs
- admin APIs are disabled when this is not set
- MAIL_SERVER_BLACKLIST
- list of domains, separated by comma
- if the user tries to use such a mail service, then he will get the notification that this provider is not supported
- the blacklist is empty if this is not set
- MAIL_WHITELIST
- list of mail addresses (``someone@somewhere.com``) or domains (``mailbox.org``), separated by comma
- if a user requests signing in with a mail address which is not in this list, then the request is rejected
- if the list is empty/ the variable is not set, then any mail address (except with domains from the blacklist) is allowed
- note: this allows a third party who knows the server url to check if a certain mail address is allowed by trying to sign in with it
- DISABLE_SIGNUP
- ``yes`` or ``no`` (default: no)
- disables creating new families if ``yes`` is selected
- the default value is ``no``
- PING_INTERVAL_SEC
- ping interval at the websocket in seconds
- the default value is ``25``
## HTTPS
This server application itself does not support HTTPS. You have to use
an other tool to use HTTPS. One options for this is to use nginx with the
following site config:
```
# don't forget to update the port for your local configuration
#
# the max_fails is important - otherwise nginx
# marks the server sometimes as unreachable if it is restarted
# or starts after nginx
upstream timelimitbackend {
server localhost:8080 max_fails=0;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
# don't forget to update the domain
server_name my.domain;
# don't forget to update the paths
ssl_certificate /my/fullchain.pem;
ssl_certificate_key /my/privkey.pem;
# eventually configure the SSL parameters here
location / {
proxy_pass http://timelimitbackend/;
client_max_body_size 10m;
# the following is required for websocket support
#
# without websockets, the client will not detect
# that there is a connection and it will not sync
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
}
}
```
## Admin API
When the ``ADMIN_TOKEN`` environment variable was set, then there is a admin API available
at ``http(s)://server/admin``. It can be used by the [TimeLimit Server Admin UI](https://codeberg.org/timelimit/timelimit-server-ui).
## Purchases
To enable the automated purchase feature, set the ``GOOGLE_PLAY_PUBLIC_KEY`` environment variable.
The value for the official builds which are distributed using the Play Store can
be found at <https://codeberg.org/timelimit/timelimit-android/src/commit/3da677877f4dde0b1b01523daae33745f14e08ac/app/build.gradle#L49>.
Additionally, there is the admin API which allows one to unlock the
premium features.
## example docker-compose.yml with included database
(don't forget to build the docker image first)
```
version: '3'
services:
api:
image: 'timelimit-server:latest'
environment:
NODE_ENV: production
DATABASE_URL: mariadb://timelimit:timelimitpassword@database:3306/timelimit
PORT: 8080
MAIL_SENDER: me@my.timelimit.server
MAIL_TRANSPORT: '{"host": "localhost", "port": 25}'
# put additional config variables here
ports:
- "8080:8080"
restart: always
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
links:
- database
database:
image: 'mariadb:10'
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: timelimit
MYSQL_USER: timelimit
MYSQL_PASSWORD: timelimitpassword
volumes:
- ./database:/var/lib/mysql
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
```
The database files will be saved at the folder which contains the docker-compose.yml.
You should change the passwords.
Docker starts both (TimeLimit and the database) at the same time,
so the TimeLimit server will crash a few times due to the missing database
before it starts working.
## example docker-compose.yml with external databases
(don't forget to build the docker image first)
```
version: '2'
services:
api:
image: 'timelimit-server:latest'
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:pass@example.com:5432/dbname
PORT: 8080
MAIL_SENDER: me@my.timelimit.server
MAIL_TRANSPORT: '{"host": "localhost", "port": 25}'
# put additional config variables here
restart: always
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
# easy solution to use a database which does not run within docker
network_mode: "host"
# otherwise:
# redirect host port 9000 to guest port 8080 (to allow access to the API)
# ports:
# - "9000:8080"
# in case the database runs outside of docker and you don't want to use the host network mode, see
# https://forums.docker.com/t/accessing-host-machine-from-within-docker-container/14248
```
This Readme became too long. Due to that, it was split into [mutliple files](https://codeberg.org/timelimit/timelimit-server/src/branch/master/docs/usage).
+3 -2
View File
@@ -20,12 +20,13 @@ On a invalid request body: HTTP status code 400 Bad Request
If the mail auth token is invalid/ expired: HTTP status code 401 Unauthorized
On success: a object with the properties ``status`` (string), ``mail`` (string) and
``canCreateFamily`` (boolean)
On success: a object with the properties ``status`` (string), ``mail`` (string),
``canCreateFamily`` (boolean) and ``alwaysPro`` (boolean)
- ``status`` is ``with family`` or ``without family``
- ``mail`` is the mail address for which the auth token was created
- ``canCreateFamily`` is false if the sign up of new families was disabled and otherwise true
- ``alwaysPro`` is true if the premium version is always unlocked
## POST /parent/create-family
+1
View File
@@ -44,6 +44,7 @@ The integrity field of a action may have got one of the following values:
- an empty string when no user authentication is required/ for app logic actions (e.g. incrementing the used time)
- the string ``device`` in case of parent actions if a parent is assigned to the device and asking for the password was disabled
- ``sha512(sequence number as string with the base 10 + the device id as string + the hash of the user password using the second salt as string + the encoded action as string)`` for parent and child actions
- the string ``childDevice`` in case the child wants to add limits for itself using parent actions; this feature must be enabled for the child and this allows only some actions with some parameters
In case of a invalid integrity value, the action is ignored and the client is told to do a full sync
+69 -60
View File
@@ -39,98 +39,102 @@
- [NewDeviceInfo](./signintofamilyrequest-properties-newdeviceinfo.md) `https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice`
- [NewDeviceInfo](./createfamilybymailtokenrequest-definitions-newdeviceinfo.md) `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/NewDeviceInfo`
- [NewDeviceInfo](./createfamilybymailtokenrequest-properties-newdeviceinfo.md) `https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentDevice`
- [ParentPassword](./createfamilybymailtokenrequest-definitions-parentpassword.md) `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/ParentPassword`
- [ParentPassword](./createfamilybymailtokenrequest-properties-parentpassword.md) `https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentPassword`
- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password`
- [ParentPassword](./recoverparentpasswordrequest-properties-parentpassword.md) `https://timelimit.io/RecoverParentPasswordRequest#/properties/password`
- [ParentPassword](./recoverparentpasswordrequest-definitions-parentpassword.md) `https://timelimit.io/RecoverParentPasswordRequest#/definitions/ParentPassword`
- [ParentPassword](./serializedparentaction-definitions-serializedsetchildpasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction/properties/newPassword`
- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password`
- [ParentPassword](./serializedchildaction-definitions-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/ParentPassword`
- [ParentPassword](./serializedparentaction-definitions-parentpassword.md) `https://timelimit.io/SerializedParentAction#/definitions/ParentPassword`
- [ParentPassword](./serializedparentaction-definitions-serializedadduseraction-properties-parentpassword.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction/properties/password`
- [ParentPassword](./recoverparentpasswordrequest-properties-parentpassword.md) `https://timelimit.io/RecoverParentPasswordRequest#/properties/password`
- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password`
- [ParentPassword](./serializedparentaction-definitions-serializedsetchildpasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction/properties/newPassword`
- [ParentPassword](./serializedparentaction-definitions-serializedadduseraction-properties-parentpassword.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction/properties/password`
- [ParentPassword](./serializedchildaction-definitions-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/ParentPassword`
- [ParentPassword](./serializedchildaction-definitions-serializedchildchangepasswordaction-properties-parentpassword.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction/properties/password`
- [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-anyof-serialiezdtrieddisablingdeviceadminaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/5`
- [ParentPassword](./createfamilybymailtokenrequest-definitions-parentpassword.md) `https://timelimit.io/CreateFamilyByMailTokenRequest#/definitions/ParentPassword`
- [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-definitions-serialiezdtrieddisablingdeviceadminaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerialiezdTriedDisablingDeviceAdminAction`
- [SerialiezdTriedDisablingDeviceAdminAction](./serializedapplogicaction-anyof-serialiezdtrieddisablingdeviceadminaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/5`
- [SerialiizedUpdateNetworkTimeVerificationAction](./serializedparentaction-definitions-serialiizedupdatenetworktimeverificationaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerialiizedUpdateNetworkTimeVerificationAction`
- [SerialiizedUpdateNetworkTimeVerificationAction](./serializedparentaction-anyof-serialiizedupdatenetworktimeverificationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/35`
- [SerializedAddCategoryAppsAction](./serializedparentaction-anyof-serializedaddcategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/0`
- [SerialiizedUpdateNetworkTimeVerificationAction](./serializedparentaction-anyof-serialiizedupdatenetworktimeverificationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/37`
- [SerializeResetCategoryNetworkIdsAction](./serializedparentaction-definitions-serializeresetcategorynetworkidsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction`
- [SerializeResetCategoryNetworkIdsAction](./serializedparentaction-anyof-serializeresetcategorynetworkidsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/13`
- [SerializedAddCategoryAppsAction](./serializedparentaction-definitions-serializedaddcategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryAppsAction`
- [SerializedAddCategoryAppsAction](./serializedparentaction-anyof-serializedaddcategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/0`
- [SerializedAddCategoryNetworkIdAction](./serializedparentaction-definitions-serializedaddcategorynetworkidaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction`
- [SerializedAddCategoryNetworkIdAction](./serializedparentaction-anyof-serializedaddcategorynetworkidaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/1`
- [SerializedAddInstalledAppsAction](./serializedapplogicaction-definitions-serializedaddinstalledappsaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction`
- [SerializedAddInstalledAppsAction](./serializedapplogicaction-anyof-serializedaddinstalledappsaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/0`
- [SerializedAddUsedTimeAction](./serializedapplogicaction-anyof-serializedaddusedtimeaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/1`
- [SerializedAddUsedTimeAction](./serializedapplogicaction-definitions-serializedaddusedtimeaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeAction`
- [SerializedAddUsedTimeActionVersion2](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2`
- [SerializedAddUsedTimeActionVersion2](./serializedapplogicaction-anyof-serializedaddusedtimeactionversion2.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/2`
- [SerializedAddUserAction](./serializedparentaction-anyof-serializedadduseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/1`
- [SerializedAddUserAction](./serializedparentaction-definitions-serializedadduseraction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedAddUserAction`
- [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items`
- [SerializedAddUserAction](./serializedparentaction-anyof-serializedadduseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/2`
- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items`
- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items`
- [SerializedAppActivityItem](./serverdatastatus-definitions-serializedappactivityitem.md) `https://timelimit.io/ServerDataStatus#/definitions/SerializedAppActivityItem`
- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedappactivityitem.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAppActivityItem`
- [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items`
- [SerializedAppActivityItem](./serverdatastatus-definitions-serializedappactivityitem.md) `https://timelimit.io/ServerDataStatus#/definitions/SerializedAppActivityItem`
- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items`
- [SerializedAppActivityItem](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction-properties-updatedoradded-serializedappactivityitem.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction/properties/updatedOrAdded/items`
- [SerializedChangeParentPasswordAction](./serializedparentaction-anyof-serializedchangeparentpasswordaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/2`
- [SerializedAppActivityItem](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities-serializedappactivityitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities/items`
- [SerializedChangeParentPasswordAction](./serializedparentaction-definitions-serializedchangeparentpasswordaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedChangeParentPasswordAction`
- [SerializedChangeParentPasswordAction](./serializedparentaction-anyof-serializedchangeparentpasswordaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/3`
- [SerializedChildChangePasswordAction](./serializedchildaction-anyof-serializedchildchangepasswordaction.md) `https://timelimit.io/SerializedChildAction#/anyOf/0`
- [SerializedChildChangePasswordAction](./serializedchildaction-definitions-serializedchildchangepasswordaction.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildChangePasswordAction`
- [SerializedChildSignInAction](./serializedchildaction-definitions-serializedchildsigninaction.md) `https://timelimit.io/SerializedChildAction#/definitions/SerializedChildSignInAction`
- [SerializedChildSignInAction](./serializedchildaction-anyof-serializedchildsigninaction.md) `https://timelimit.io/SerializedChildAction#/anyOf/1`
- [SerializedCreateCategoryAction](./serializedparentaction-anyof-serializedcreatecategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/4`
- [SerializedCreateCategoryAction](./serializedparentaction-definitions-serializedcreatecategoryaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateCategoryAction`
- [SerializedCreateCategoryAction](./serializedparentaction-anyof-serializedcreatecategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/3`
- [SerializedCreateTimelimtRuleAction](./serializedparentaction-definitions-serializedcreatetimelimtruleaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction`
- [SerializedCreateTimelimtRuleAction](./serializedparentaction-anyof-serializedcreatetimelimtruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/4`
- [SerializedDeleteCategoryAction](./serializedparentaction-anyof-serializeddeletecategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/5`
- [SerializedCreateTimelimtRuleAction](./serializedparentaction-anyof-serializedcreatetimelimtruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/5`
- [SerializedDeleteCategoryAction](./serializedparentaction-definitions-serializeddeletecategoryaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedDeleteCategoryAction`
- [SerializedDeleteCategoryAction](./serializedparentaction-anyof-serializeddeletecategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/6`
- [SerializedDeleteTimeLimitRuleAction](./serializedparentaction-anyof-serializeddeletetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/7`
- [SerializedDeleteTimeLimitRuleAction](./serializedparentaction-definitions-serializeddeletetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedDeleteTimeLimitRuleAction`
- [SerializedDeleteTimeLimitRuleAction](./serializedparentaction-anyof-serializeddeletetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/6`
- [SerializedIgnoreManipulationAction](./serializedparentaction-definitions-serializedignoremanipulationaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedIgnoreManipulationAction`
- [SerializedIgnoreManipulationAction](./serializedparentaction-anyof-serializedignoremanipulationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/7`
- [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-anyof-serializedincrementcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/8`
- [SerializedIgnoreManipulationAction](./serializedparentaction-anyof-serializedignoremanipulationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/8`
- [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-definitions-serializedincrementcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedIncrementCategoryExtraTimeAction`
- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items`
- [SerializedIncrementCategoryExtraTimeAction](./serializedparentaction-anyof-serializedincrementcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/9`
- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items`
- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedinstalledapp.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedInstalledApp`
- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items`
- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items`
- [SerializedInstalledApp](./serverdatastatus-definitions-serializedinstalledapp.md) `https://timelimit.io/ServerDataStatus#/definitions/SerializedInstalledApp`
- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedinstalledapp.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedInstalledApp`
- [SerializedInstalledApp](./serializedapplogicaction-definitions-serializedaddinstalledappsaction-properties-apps-serializedinstalledapp.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddInstalledAppsAction/properties/apps/items`
- [SerializedInstalledApp](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps-serializedinstalledapp.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps/items`
- [SerializedRemoveCategoryAppsAction](./serializedparentaction-definitions-serializedremovecategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedRemoveCategoryAppsAction`
- [SerializedRemoveCategoryAppsAction](./serializedparentaction-anyof-serializedremovecategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/9`
- [SerializedRemoveCategoryAppsAction](./serializedparentaction-anyof-serializedremovecategoryappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/10`
- [SerializedRemoveInstalledAppsAction](./serializedapplogicaction-anyof-serializedremoveinstalledappsaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/3`
- [SerializedRemoveInstalledAppsAction](./serializedapplogicaction-definitions-serializedremoveinstalledappsaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedRemoveInstalledAppsAction`
- [SerializedRemoveUserAction](./serializedparentaction-anyof-serializedremoveuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/11`
- [SerializedRemoveUserAction](./serializedparentaction-definitions-serializedremoveuseraction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedRemoveUserAction`
- [SerializedRemoveUserAction](./serializedparentaction-anyof-serializedremoveuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/10`
- [SerializedRenameChildAction](./serializedparentaction-anyof-serializedrenamechildaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/11`
- [SerializedRenameChildAction](./serializedparentaction-definitions-serializedrenamechildaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedRenameChildAction`
- [SerializedResetParentBlockedTimesAction](./serializedparentaction-anyof-serializedresetparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/12`
- [SerializedRenameChildAction](./serializedparentaction-anyof-serializedrenamechildaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/12`
- [SerializedResetParentBlockedTimesAction](./serializedparentaction-definitions-serializedresetparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedResetParentBlockedTimesAction`
- [SerializedSetCategoryExtraTimeAction](./serializedparentaction-anyof-serializedsetcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/13`
- [SerializedResetParentBlockedTimesAction](./serializedparentaction-anyof-serializedresetparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/14`
- [SerializedSetCategoryExtraTimeAction](./serializedparentaction-anyof-serializedsetcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/15`
- [SerializedSetCategoryExtraTimeAction](./serializedparentaction-definitions-serializedsetcategoryextratimeaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetCategoryExtraTimeAction`
- [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-anyof-serializedsetcategoryforunassignedappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/14`
- [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-definitions-serializedsetcategoryforunassignedappsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetCategoryForUnassignedAppsAction`
- [SerializedSetChildPasswordAction](./serializedparentaction-anyof-serializedsetchildpasswordaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/15`
- [SerializedSetCategoryForUnassignedAppsAction](./serializedparentaction-anyof-serializedsetcategoryforunassignedappsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/16`
- [SerializedSetChildPasswordAction](./serializedparentaction-definitions-serializedsetchildpasswordaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetChildPasswordAction`
- [SerializedSetChildPasswordAction](./serializedparentaction-anyof-serializedsetchildpasswordaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/17`
- [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-anyof-serializedsetconsiderrebootmanipulationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/18`
- [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-definitions-serializedsetconsiderrebootmanipulationaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetConsiderRebootManipulationAction`
- [SerializedSetConsiderRebootManipulationAction](./serializedparentaction-anyof-serializedsetconsiderrebootmanipulationaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/16`
- [SerializedSetDeviceDefaultUserAction](./serializedparentaction-anyof-serializedsetdevicedefaultuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/17`
- [SerializedSetDeviceDefaultUserAction](./serializedparentaction-anyof-serializedsetdevicedefaultuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/19`
- [SerializedSetDeviceDefaultUserAction](./serializedparentaction-definitions-serializedsetdevicedefaultuseraction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceDefaultUserAction`
- [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-anyof-serializedsetdevicedefaultusertimeoutaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/18`
- [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-anyof-serializedsetdevicedefaultusertimeoutaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/20`
- [SerializedSetDeviceDefaultUserTimeoutAction](./serializedparentaction-definitions-serializedsetdevicedefaultusertimeoutaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceDefaultUserTimeoutAction`
- [SerializedSetDeviceUserAction](./serializedparentaction-anyof-serializedsetdeviceuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/19`
- [SerializedSetDeviceUserAction](./serializedparentaction-anyof-serializedsetdeviceuseraction.md) `https://timelimit.io/SerializedParentAction#/anyOf/21`
- [SerializedSetDeviceUserAction](./serializedparentaction-definitions-serializedsetdeviceuseraction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetDeviceUserAction`
- [SerializedSetKeepSignedInAction](./serializedparentaction-anyof-serializedsetkeepsignedinaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/20`
- [SerializedSetKeepSignedInAction](./serializedparentaction-anyof-serializedsetkeepsignedinaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/22`
- [SerializedSetKeepSignedInAction](./serializedparentaction-definitions-serializedsetkeepsignedinaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetKeepSignedInAction`
- [SerializedSetParentCategoryAction](./serializedparentaction-definitions-serializedsetparentcategoryaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetParentCategoryAction`
- [SerializedSetParentCategoryAction](./serializedparentaction-anyof-serializedsetparentcategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/21`
- [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-anyof-serializedsetrelaxprimarydeviceaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/22`
- [SerializedSetParentCategoryAction](./serializedparentaction-anyof-serializedsetparentcategoryaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/23`
- [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-definitions-serializedsetrelaxprimarydeviceaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetRelaxPrimaryDeviceAction`
- [SerializedSetSendDeviceConnected](./serializedparentaction-anyof-serializedsetsenddeviceconnected.md) `https://timelimit.io/SerializedParentAction#/anyOf/23`
- [SerializedSetRelaxPrimaryDeviceAction](./serializedparentaction-anyof-serializedsetrelaxprimarydeviceaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/24`
- [SerializedSetSendDeviceConnected](./serializedparentaction-definitions-serializedsetsenddeviceconnected.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetSendDeviceConnected`
- [SerializedSetSendDeviceConnected](./serializedparentaction-anyof-serializedsetsenddeviceconnected.md) `https://timelimit.io/SerializedParentAction#/anyOf/25`
- [SerializedSetUserDisableLimitsUntilAction](./serializedparentaction-definitions-serializedsetuserdisablelimitsuntilaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetUserDisableLimitsUntilAction`
- [SerializedSetUserDisableLimitsUntilAction](./serializedparentaction-anyof-serializedsetuserdisablelimitsuntilaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/24`
- [SerializedSetUserDisableLimitsUntilAction](./serializedparentaction-anyof-serializedsetuserdisablelimitsuntilaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/26`
- [SerializedSetUserTimezoneAction](./serializedparentaction-definitions-serializedsetusertimezoneaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedSetUserTimezoneAction`
- [SerializedSetUserTimezoneAction](./serializedparentaction-anyof-serializedsetusertimezoneaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/25`
- [SerializedSetUserTimezoneAction](./serializedparentaction-anyof-serializedsetusertimezoneaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/27`
- [SerializedSignOutAtDeviceAction](./serializedapplogicaction-anyof-serializedsignoutatdeviceaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/4`
- [SerializedSignOutAtDeviceAction](./serializedapplogicaction-definitions-serializedsignoutatdeviceaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedSignOutAtDeviceAction`
- [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedcreatetimelimtruleaction-properties-serializedtimelimitrule.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedCreateTimelimtRuleAction/properties/rule`
@@ -138,36 +142,39 @@
- [SerializedTimeLimitRule](./serializedparentaction-definitions-serializedtimelimitrule.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedTimeLimitRule`
- [SerializedUpdateAppActivitiesAction](./serializedapplogicaction-anyof-serializedupdateappactivitiesaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/6`
- [SerializedUpdateAppActivitiesAction](./serializedapplogicaction-definitions-serializedupdateappactivitiesaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateAppActivitiesAction`
- [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-anyof-serializedupdatecategorybatterylimitaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/26`
- [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-definitions-serializedupdatecategorybatterylimitaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBatteryLimitAction`
- [SerializedUpdateCategoryBlockAllNotificationsAction](./serializedparentaction-anyof-serializedupdatecategoryblockallnotificationsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/27`
- [SerializedUpdateCategoryBatteryLimitAction](./serializedparentaction-anyof-serializedupdatecategorybatterylimitaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/28`
- [SerializedUpdateCategoryBlockAllNotificationsAction](./serializedparentaction-definitions-serializedupdatecategoryblockallnotificationsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBlockAllNotificationsAction`
- [SerializedUpdateCategoryBlockedTimesAction](./serializedparentaction-anyof-serializedupdatecategoryblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/28`
- [SerializedUpdateCategoryBlockAllNotificationsAction](./serializedparentaction-anyof-serializedupdatecategoryblockallnotificationsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/29`
- [SerializedUpdateCategoryBlockedTimesAction](./serializedparentaction-definitions-serializedupdatecategoryblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryBlockedTimesAction`
- [SerializedUpdateCategoryBlockedTimesAction](./serializedparentaction-anyof-serializedupdatecategoryblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/30`
- [SerializedUpdateCategorySortingAction](./serializedparentaction-anyof-serializedupdatecategorysortingaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/31`
- [SerializedUpdateCategorySortingAction](./serializedparentaction-definitions-serializedupdatecategorysortingaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategorySortingAction`
- [SerializedUpdateCategorySortingAction](./serializedparentaction-anyof-serializedupdatecategorysortingaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/29`
- [SerializedUpdateCategoryTemporarilyBlockedAction](./serializedparentaction-definitions-serializedupdatecategorytemporarilyblockedaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTemporarilyBlockedAction`
- [SerializedUpdateCategoryTemporarilyBlockedAction](./serializedparentaction-anyof-serializedupdatecategorytemporarilyblockedaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/30`
- [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-anyof-serializedupdatecategorytimewarningsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/31`
- [SerializedUpdateCategoryTemporarilyBlockedAction](./serializedparentaction-anyof-serializedupdatecategorytemporarilyblockedaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/32`
- [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-anyof-serializedupdatecategorytimewarningsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/33`
- [SerializedUpdateCategoryTimeWarningsAction](./serializedparentaction-definitions-serializedupdatecategorytimewarningsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTimeWarningsAction`
- [SerializedUpdateCategoryTitleAction](./serializedparentaction-anyof-serializedupdatecategorytitleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/32`
- [SerializedUpdateCategoryTitleAction](./serializedparentaction-definitions-serializedupdatecategorytitleaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateCategoryTitleAction`
- [SerializedUpdateDeviceNameAction](./serializedparentaction-anyof-serializedupdatedevicenameaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/33`
- [SerializedUpdateCategoryTitleAction](./serializedparentaction-anyof-serializedupdatecategorytitleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/34`
- [SerializedUpdateDeviceNameAction](./serializedparentaction-anyof-serializedupdatedevicenameaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/35`
- [SerializedUpdateDeviceNameAction](./serializedparentaction-definitions-serializedupdatedevicenameaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateDeviceNameAction`
- [SerializedUpdateDeviceStatusAction](./serializedapplogicaction-anyof-serializedupdatedevicestatusaction.md) `https://timelimit.io/SerializedAppLogicAction#/anyOf/7`
- [SerializedUpdateDeviceStatusAction](./serializedapplogicaction-definitions-serializedupdatedevicestatusaction.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedUpdateDeviceStatusAction`
- [SerializedUpdateEnableActivityLevelBlockingAction](./serializedparentaction-anyof-serializedupdateenableactivitylevelblockingaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/34`
- [SerializedUpdateEnableActivityLevelBlockingAction](./serializedparentaction-anyof-serializedupdateenableactivitylevelblockingaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/36`
- [SerializedUpdateEnableActivityLevelBlockingAction](./serializedparentaction-definitions-serializedupdateenableactivitylevelblockingaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateEnableActivityLevelBlockingAction`
- [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-anyof-serializedupdateparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/38`
- [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-definitions-serializedupdateparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateParentBlockedTimesAction`
- [SerializedUpdateParentBlockedTimesAction](./serializedparentaction-anyof-serializedupdateparentblockedtimesaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/36`
- [SerializedUpdateParentNotificationFlagsAction](./serializedparentaction-anyof-serializedupdateparentnotificationflagsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/37`
- [SerializedUpdateParentNotificationFlagsAction](./serializedparentaction-anyof-serializedupdateparentnotificationflagsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/39`
- [SerializedUpdateParentNotificationFlagsAction](./serializedparentaction-definitions-serializedupdateparentnotificationflagsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateParentNotificationFlagsAction`
- [SerializedUpdateTimelimitRuleAction](./serializedparentaction-anyof-serializedupdatetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/38`
- [SerializedUpdateTimelimitRuleAction](./serializedparentaction-anyof-serializedupdatetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/40`
- [SerializedUpdateTimelimitRuleAction](./serializedparentaction-definitions-serializedupdatetimelimitruleaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateTimelimitRuleAction`
- [SerializedUpdateUserFlagsAction](./serializedparentaction-definitions-serializedupdateuserflagsaction.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateUserFlagsAction`
- [SerializedUpdateUserFlagsAction](./serializedparentaction-anyof-serializedupdateuserflagsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/39`
- [SerializedUpdateUserFlagsAction](./serializedparentaction-anyof-serializedupdateuserflagsaction.md) `https://timelimit.io/SerializedParentAction#/anyOf/41`
- [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-definitions-serializedupdateuserlimitlogincategory.md) `https://timelimit.io/SerializedParentAction#/definitions/SerializedUpdateUserLimitLoginCategory`
- [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-anyof-serializedupdateuserlimitlogincategory.md) `https://timelimit.io/SerializedParentAction#/anyOf/40`
- [SerializedUpdateUserLimitLoginCategory](./serializedparentaction-anyof-serializedupdateuserlimitlogincategory.md) `https://timelimit.io/SerializedParentAction#/anyOf/42`
- [ServerCategoryNetworkId](./serverdatastatus-definitions-servercategorynetworkid.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId`
- [ServerCategoryNetworkId](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks-servercategorynetworkid.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks/items`
- [ServerCategoryNetworkId](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks-servercategorynetworkid.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks/items`
- [ServerDeviceData](./serverdatastatus-definitions-serverdevicelist-properties-data-serverdevicedata.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList/properties/data/items`
- [ServerDeviceData](./serverdatastatus-definitions-serverdevicedata.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceData`
- [ServerDeviceData](./serverdatastatus-definitions-serverdevicelist-properties-data-serverdevicedata.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList/properties/data/items`
@@ -175,9 +182,9 @@
- [ServerDeviceList](./serverdatastatus-definitions-serverdevicelist.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList`
- [ServerInstalledAppsData](./serverdatastatus-properties-apps-serverinstalledappsdata.md) `https://timelimit.io/ServerDataStatus#/properties/apps/items`
- [ServerInstalledAppsData](./serverdatastatus-definitions-serverinstalledappsdata.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData`
- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items`
- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items`
- [ServerSessionDurationItem](./serverdatastatus-definitions-serversessiondurationitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerSessionDurationItem`
- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items`
- [ServerSessionDurationItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations-serversessiondurationitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations/items`
- [ServerTimeLimitRule](./serverdatastatus-definitions-serverupdatedtimelimitrules-properties-rules-servertimelimitrule.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules/properties/rules/items`
- [ServerTimeLimitRule](./serverdatastatus-definitions-serverupdatedtimelimitrules-properties-rules-servertimelimitrule.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules/properties/rules/items`
- [ServerTimeLimitRule](./serverdatastatus-definitions-servertimelimitrule.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerTimeLimitRule`
@@ -187,20 +194,20 @@
- [ServerUpdatedCategoryBaseData](./serverdatastatus-definitions-serverupdatedcategorybasedata.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData`
- [ServerUpdatedCategoryUsedTimes](./serverdatastatus-definitions-serverupdatedcategoryusedtimes.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes`
- [ServerUpdatedCategoryUsedTimes](./serverdatastatus-properties-usedtimes-serverupdatedcategoryusedtimes.md) `https://timelimit.io/ServerDataStatus#/properties/usedTimes/items`
- [ServerUpdatedTimeLimitRules](./serverdatastatus-properties-rules-serverupdatedtimelimitrules.md) `https://timelimit.io/ServerDataStatus#/properties/rules/items`
- [ServerUpdatedTimeLimitRules](./serverdatastatus-definitions-serverupdatedtimelimitrules.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedTimeLimitRules`
- [ServerUpdatedTimeLimitRules](./serverdatastatus-properties-rules-serverupdatedtimelimitrules.md) `https://timelimit.io/ServerDataStatus#/properties/rules/items`
- [ServerUsedTimeItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times-serverusedtimeitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times/items`
- [ServerUsedTimeItem](./serverdatastatus-definitions-serverusedtimeitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUsedTimeItem`
- [ServerUsedTimeItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times-serverusedtimeitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times/items`
- [ServerUsedTimeItem](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times-serverusedtimeitem.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times/items`
- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items`
- [ServerUserEntry](./serverdatastatus-definitions-serveruserentry.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUserEntry`
- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items`
- [ServerUserEntry](./serverdatastatus-definitions-serveruserlist-properties-data-serveruserentry.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList/properties/data/items`
- [ServerUserList](./serverdatastatus-properties-serveruserlist.md) `https://timelimit.io/ServerDataStatus#/properties/users`
- [ServerUserList](./serverdatastatus-definitions-serveruserlist.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUserList`
- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories`
- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-apps.md) `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/apps`
- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories`
- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-apps.md) `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/apps`
- [Untitled object in ClientPullChangesRequest](./clientpullchangesrequest-definitions-clientdatastatus-properties-categories.md) `https://timelimit.io/ClientPullChangesRequest#/definitions/ClientDataStatus/properties/categories`
- [Untitled object in ClientPushChangesRequest](./clientpushchangesrequest-properties-actions-items.md) `https://timelimit.io/ClientPushChangesRequest#/properties/actions/items`
- [Untitled object in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i-items.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i/items`
- [Untitled object in SerializedAppLogicAction](./serializedapplogicaction-definitions-serializedaddusedtimeactionversion2-properties-i-items.md) `https://timelimit.io/SerializedAppLogicAction#/definitions/SerializedAddUsedTimeActionVersion2/properties/i/items`
@@ -240,6 +247,7 @@
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities`
- [Untitled array in ServerDataStatus](./serverdatastatus-properties-rmcategories.md) `https://timelimit.io/ServerDataStatus#/properties/rmCategories`
- [Untitled array in ServerDataStatus](./serverdatastatus-properties-categorybase.md) `https://timelimit.io/ServerDataStatus#/properties/categoryBase`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks`
- [Untitled array in ServerDataStatus](./serverdatastatus-properties-categoryapp.md) `https://timelimit.io/ServerDataStatus#/properties/categoryApp`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategoryassignedapps-properties-apps.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryAssignedApps/properties/apps`
- [Untitled array in ServerDataStatus](./serverdatastatus-properties-usedtimes.md) `https://timelimit.io/ServerDataStatus#/properties/usedTimes`
@@ -251,6 +259,7 @@
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverdevicelist-properties-data.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerDeviceList/properties/data`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverinstalledappsdata-properties-apps.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/apps`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverinstalledappsdata-properties-activities.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerInstalledAppsData/properties/activities`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategoryassignedapps-properties-apps.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryAssignedApps/properties/apps`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-times.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/times`
- [Untitled array in ServerDataStatus](./serverdatastatus-definitions-serverupdatedcategoryusedtimes-properties-sessiondurations.md) `https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryUsedTimes/properties/sessionDurations`
@@ -3,6 +3,9 @@
{
"$ref": "#/definitions/SerializedAddCategoryAppsAction"
},
{
"$ref": "#/definitions/SerializedAddCategoryNetworkIdAction"
},
{
"$ref": "#/definitions/SerializedAddUserAction"
},
@@ -36,6 +39,9 @@
{
"$ref": "#/definitions/SerializedRenameChildAction"
},
{
"$ref": "#/definitions/SerializeResetCategoryNetworkIdsAction"
},
{
"$ref": "#/definitions/SerializedResetParentBlockedTimesAction"
},
@@ -152,6 +158,34 @@
],
"title": "SerializedAddCategoryAppsAction"
},
"SerializedAddCategoryNetworkIdAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"ADD_CATEGORY_NETWORK_ID"
]
},
"categoryId": {
"type": "string"
},
"itemId": {
"type": "string"
},
"hashedNetworkId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"categoryId",
"hashedNetworkId",
"itemId",
"type"
],
"title": "SerializedAddCategoryNetworkIdAction"
},
"SerializedAddUserAction": {
"type": "object",
"properties": {
@@ -534,6 +568,26 @@
],
"title": "SerializedRenameChildAction"
},
"SerializeResetCategoryNetworkIdsAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"RESET_CATEGORY_NETWORK_IDS"
]
},
"categoryId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"categoryId",
"type"
],
"title": "SerializeResetCategoryNetworkIdsAction"
},
"SerializedResetParentBlockedTimesAction": {
"type": "object",
"properties": {
+24
View File
@@ -370,6 +370,12 @@
},
"sort": {
"type": "number"
},
"networks": {
"type": "array",
"items": {
"$ref": "#/definitions/ServerCategoryNetworkId"
}
}
},
"additionalProperties": false,
@@ -382,6 +388,7 @@
"extraTimeDay",
"mblCharging",
"mblMobile",
"networks",
"parentCategoryId",
"sort",
"tempBlockTime",
@@ -392,6 +399,23 @@
],
"title": "ServerUpdatedCategoryBaseData"
},
"ServerCategoryNetworkId": {
"type": "object",
"properties": {
"itemId": {
"type": "string"
},
"hashedNetworkId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"hashedNetworkId",
"itemId"
],
"title": "ServerCategoryNetworkId"
},
"ServerUpdatedCategoryAssignedApps": {
"type": "object",
"properties": {
@@ -0,0 +1,16 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/categoryId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## categoryId Type
`string`
@@ -0,0 +1,16 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/hashedNetworkId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## hashedNetworkId Type
`string`
@@ -0,0 +1,16 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/itemId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## itemId Type
`string`
@@ -0,0 +1,24 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/type
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## type Type
`string`
## type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :-------------------------- | ----------- |
| `"ADD_CATEGORY_NETWORK_ID"` | |
@@ -0,0 +1,16 @@
# Untitled undefined type in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## properties Type
unknown
@@ -0,0 +1,97 @@
# SerializedAddCategoryNetworkIdAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ------------ | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | No | Forbidden | Forbidden | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## SerializedAddCategoryNetworkIdAction Type
`object` ([SerializedAddCategoryNetworkIdAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction.md))
# SerializedAddCategoryNetworkIdAction Properties
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | -------- | -------- | -------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [type](#type) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/type") |
| [categoryId](#categoryId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/categoryId") |
| [itemId](#itemId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-itemid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/itemId") |
| [hashedNetworkId](#hashedNetworkId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-hashednetworkid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/hashedNetworkId") |
## type
`type`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/type")
### type Type
`string`
### type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :-------------------------- | ----------- |
| `"ADD_CATEGORY_NETWORK_ID"` | |
## categoryId
`categoryId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/categoryId")
### categoryId Type
`string`
## itemId
`itemId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-itemid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/itemId")
### itemId Type
`string`
## hashedNetworkId
`hashedNetworkId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-hashednetworkid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/hashedNetworkId")
### hashedNetworkId Type
`string`
@@ -0,0 +1,16 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/categoryId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## categoryId Type
`string`
@@ -0,0 +1,24 @@
# Untitled string in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/type
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## type Type
`string`
## type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :----------------------------- | ----------- |
| `"RESET_CATEGORY_NETWORK_IDS"` | |
@@ -0,0 +1,16 @@
# Untitled undefined type in SerializedParentAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## properties Type
unknown
@@ -0,0 +1,63 @@
# SerializeResetCategoryNetworkIdsAction Schema
```txt
https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ------------ | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | No | Forbidden | Forbidden | none | [SerializedParentAction.schema.json\*](SerializedParentAction.schema.json "open original schema") |
## SerializeResetCategoryNetworkIdsAction Type
`object` ([SerializeResetCategoryNetworkIdsAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction.md))
# SerializeResetCategoryNetworkIdsAction Properties
| Property | Type | Required | Nullable | Defined by |
| :------------------------ | -------- | -------- | -------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [type](#type) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/type") |
| [categoryId](#categoryId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/categoryId") |
## type
`type`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/type")
### type Type
`string`
### type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :----------------------------- | ----------- |
| `"RESET_CATEGORY_NETWORK_IDS"` | |
## categoryId
`categoryId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/categoryId")
### categoryId Type
`string`
+142
View File
@@ -18,6 +18,7 @@ merged type ([SerializedParentAction](serializedparentaction.md))
any of
- [SerializedAddCategoryAppsAction](serializedparentaction-definitions-serializedaddcategoryappsaction.md "check type definition")
- [SerializedAddCategoryNetworkIdAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction.md "check type definition")
- [SerializedAddUserAction](serializedparentaction-definitions-serializedadduseraction.md "check type definition")
- [SerializedChangeParentPasswordAction](serializedparentaction-definitions-serializedchangeparentpasswordaction.md "check type definition")
- [SerializedCreateCategoryAction](serializedparentaction-definitions-serializedcreatecategoryaction.md "check type definition")
@@ -29,6 +30,7 @@ any of
- [SerializedRemoveCategoryAppsAction](serializedparentaction-definitions-serializedremovecategoryappsaction.md "check type definition")
- [SerializedRemoveUserAction](serializedparentaction-definitions-serializedremoveuseraction.md "check type definition")
- [SerializedRenameChildAction](serializedparentaction-definitions-serializedrenamechildaction.md "check type definition")
- [SerializeResetCategoryNetworkIdsAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction.md "check type definition")
- [SerializedResetParentBlockedTimesAction](serializedparentaction-definitions-serializedresetparentblockedtimesaction.md "check type definition")
- [SerializedSetCategoryExtraTimeAction](serializedparentaction-definitions-serializedsetcategoryextratimeaction.md "check type definition")
- [SerializedSetCategoryForUnassignedAppsAction](serializedparentaction-definitions-serializedsetcategoryforunassignedappsaction.md "check type definition")
@@ -131,6 +133,93 @@ Reference this group by using
`string[]`
## Definitions group SerializedAddCategoryNetworkIdAction
Reference this group by using
```json
{"$ref":"https://timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction"}
```
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | -------- | -------- | -------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [type](#type) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/type") |
| [categoryId](#categoryId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/categoryId") |
| [itemId](#itemId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-itemid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/itemId") |
| [hashedNetworkId](#hashedNetworkId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-hashednetworkid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/hashedNetworkId") |
### type
`type`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/type")
#### type Type
`string`
#### type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :-------------------------- | ----------- |
| `"ADD_CATEGORY_NETWORK_ID"` | |
### categoryId
`categoryId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/categoryId")
#### categoryId Type
`string`
### itemId
`itemId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-itemid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/itemId")
#### itemId Type
`string`
### hashedNetworkId
`hashedNetworkId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializedaddcategorynetworkidaction-properties-hashednetworkid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializedAddCategoryNetworkIdAction/properties/hashedNetworkId")
#### hashedNetworkId Type
`string`
## Definitions group SerializedAddUserAction
Reference this group by using
@@ -1374,6 +1463,59 @@ Reference this group by using
`string`
## Definitions group SerializeResetCategoryNetworkIdsAction
Reference this group by using
```json
{"$ref":"https://timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction"}
```
| Property | Type | Required | Nullable | Defined by |
| :------------------------ | -------- | -------- | -------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [type](#type) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/type") |
| [categoryId](#categoryId) | `string` | Required | cannot be null | [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/categoryId") |
### type
`type`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-type.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/type")
#### type Type
`string`
#### type Constraints
**enum**: the value of this property must be equal to one of the following values:
| Value | Explanation |
| :----------------------------- | ----------- |
| `"RESET_CATEGORY_NETWORK_IDS"` | |
### categoryId
`categoryId`
- is required
- Type: `string`
- cannot be null
- defined in: [SerializedParentAction](serializedparentaction-definitions-serializeresetcategorynetworkidsaction-properties-categoryid.md "https&#x3A;//timelimit.io/SerializedParentAction#/definitions/SerializeResetCategoryNetworkIdsAction/properties/categoryId")
#### categoryId Type
`string`
## Definitions group SerializedResetParentBlockedTimesAction
Reference this group by using
@@ -0,0 +1,16 @@
# Untitled string in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/hashedNetworkId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## hashedNetworkId Type
`string`
@@ -0,0 +1,16 @@
# Untitled string in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/itemId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## itemId Type
`string`
@@ -0,0 +1,16 @@
# Untitled undefined type in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## properties Type
unknown
@@ -0,0 +1,55 @@
# ServerCategoryNetworkId Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ------------ | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | No | Forbidden | Forbidden | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## ServerCategoryNetworkId Type
`object` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
# ServerCategoryNetworkId Properties
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | -------- | -------- | -------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [itemId](#itemId) | `string` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-itemid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/itemId") |
| [hashedNetworkId](#hashedNetworkId) | `string` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-hashednetworkid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/hashedNetworkId") |
## itemId
`itemId`
- is required
- Type: `string`
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-itemid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/itemId")
### itemId Type
`string`
## hashedNetworkId
`hashedNetworkId`
- is required
- Type: `string`
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-hashednetworkid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/hashedNetworkId")
### hashedNetworkId Type
`string`
@@ -0,0 +1,16 @@
# Untitled array in ServerDataStatus Schema
```txt
https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | ---------- | -------------- | ----------------------- | :---------------- | --------------------- | ------------------- | ------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [ServerDataStatus.schema.json\*](ServerDataStatus.schema.json "open original schema") |
## networks Type
`object[]` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
@@ -34,6 +34,7 @@ https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData
| [mblCharging](#mblCharging) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-mblcharging.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/mblCharging") |
| [mblMobile](#mblMobile) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-mblmobile.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/mblMobile") |
| [sort](#sort) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-sort.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/sort") |
| [networks](#networks) | `array` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks") |
## categoryId
@@ -274,3 +275,19 @@ https://timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData
### sort Type
`number`
## networks
`networks`
- is required
- Type: `object[]` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks")
### networks Type
`object[]` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
+62
View File
@@ -974,6 +974,7 @@ Reference this group by using
| [mblCharging](#mblCharging) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-mblcharging.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/mblCharging") |
| [mblMobile](#mblMobile) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-mblmobile.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/mblMobile") |
| [sort](#sort) | `number` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-sort.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/sort") |
| [networks](#networks) | `array` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks") |
### categoryId
@@ -1215,6 +1216,67 @@ Reference this group by using
`number`
### networks
`networks`
- is required
- Type: `object[]` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-serverupdatedcategorybasedata-properties-networks.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerUpdatedCategoryBaseData/properties/networks")
#### networks Type
`object[]` ([ServerCategoryNetworkId](serverdatastatus-definitions-servercategorynetworkid.md))
## Definitions group ServerCategoryNetworkId
Reference this group by using
```json
{"$ref":"https://timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId"}
```
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | -------- | -------- | -------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [itemId](#itemId) | `string` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-itemid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/itemId") |
| [hashedNetworkId](#hashedNetworkId) | `string` | Required | cannot be null | [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-hashednetworkid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/hashedNetworkId") |
### itemId
`itemId`
- is required
- Type: `string`
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-itemid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/itemId")
#### itemId Type
`string`
### hashedNetworkId
`hashedNetworkId`
- is required
- Type: `string`
- cannot be null
- defined in: [ServerDataStatus](serverdatastatus-definitions-servercategorynetworkid-properties-hashednetworkid.md "https&#x3A;//timelimit.io/ServerDataStatus#/definitions/ServerCategoryNetworkId/properties/hashedNetworkId")
#### hashedNetworkId Type
`string`
## Definitions group ServerUpdatedCategoryAssignedApps
Reference this group by using
+4
View File
@@ -0,0 +1,4 @@
# Admin API
When the ``ADMIN_TOKEN`` environment variable was set, then there is a admin API available
at ``http(s)://server/admin``. It can be used by the [TimeLimit Server Admin UI](https://codeberg.org/timelimit/timelimit-server-ui).
+7
View File
@@ -0,0 +1,7 @@
# Clustering
This application only supports running one instance per database.
Otherwise, when doing clustering, push messages do not work anymore between devices
connected to different instances and the rate limiting is per instance and thus not
strictly enforced.
+56
View File
@@ -0,0 +1,56 @@
# Configuration/ environment variables
- DATABASE_URL
- this specifies the database to use
- default value: ``sqlite://test.db`` (sqlite database in the source code directory)
- supports mysql, postgresql and sqlite (sqlite in development builds only because it's declared as dev dependency)
- looks like ``postgres://user:pass@example.com:5432/dbname``
- no extra setup needed
- when starting the application, the database tables are created/ migrated
- this only works for upgrading; if you intend to eventually downgrade, make a backup first (you should make backups in all cases before an upgrade)
- PORT
- the port at which the server should listen
- default value: 8080
- NODE_ENV
- should be set to ``production`` in production
- when using ``development``, then mails are not sent; instead they are written to a html file which is opened
- GOOGLE_PLAY_PUBLIC_KEY
- key for validating purchases
- purchases using google play don't work without it/ when it is not set
- MAIL_SENDER
- sender (for the from-field) for sent mails
- the sent mails contain the info that one can reply to it in case of questions ... so you should have someone who looks into the inbox and send replies (or you know that there is nobody and don't ask anything)
- MAIL_TRANSPORT
- a JSON encoded configuration for nodemailer
- supports setting a smtp server configuration, see <https://nodemailer.com/smtp/>
- allows easier configuration in case of a [well known services](https://nodemailer.com/smtp/well-known/)
- default value is ``null``
- examples
- ``{"host": "localhost", "port": 25}`` (using a local mail server which does not require any authentication)
- ``{"service": "1und1", "auth": {"user": "me@my.timelimit.server", "pass": "my password"}}`` (using a well known service)
- ``{"host": "my.mail.server", "secure": true, "auth": {"user": "me@my.timelimit.server", "pass": "my password"}}`` (using a external smtp server)
- in case of a docker-compose file, you should escape this, e.g. sourround it with ``'`` single quotes
- MAIL_IMPRINT
- a string which is added to the footer of the sent mails
- default value: ``not defined``
- ADMIN_TOKEN
- a password which allows to use some APIs
- admin APIs are disabled when this is not set
- MAIL_SERVER_BLACKLIST
- list of domains, separated by comma
- if the user tries to use such a mail service, then he will get the notification that this provider is not supported
- the blacklist is empty if this is not set
- MAIL_WHITELIST
- list of mail addresses (``someone@somewhere.com``) or domains (``mailbox.org``), separated by comma
- if a user requests signing in with a mail address which is not in this list, then the request is rejected
- if the list is empty/ the variable is not set, then any mail address (except with domains from the blacklist) is allowed
- note: this allows a third party who knows the server url to check if a certain mail address is allowed by trying to sign in with it
- DISABLE_SIGNUP
- ``yes`` or ``no`` (default: no)
- disables creating new families if ``yes`` is selected
- ALWAYS_PRO
- ``yes`` or ``no`` (default: ``no``)
- if ``yes``, then the features of the premium version are unlocked for all users
- PING_INTERVAL_SEC
- ping interval at the websocket in seconds
- the default value is ``25``
+90
View File
@@ -0,0 +1,90 @@
# Docker
You can run the timelimit server with docker. Here are two example configuration files:
## Important
The ``image: 'timelimit-server:latest'`` will not work out of the box.
You have to build this image yourself (using ``sudo docker build -t timelimit-server .``
within the root directory of this git repository) or you can replace it by
``image: docker.timelimit.io/timelimit-server`` which will use prebuilt docker
images.
In case of self building the image, don't forget to run ``docker-compose up`` again
to make docker use the new image.
## example docker-compose.yml with included database
```
version: '3'
services:
api:
image: 'timelimit-server:latest'
environment:
NODE_ENV: production
DATABASE_URL: mariadb://timelimit:timelimitpassword@database:3306/timelimit
PORT: 8080
MAIL_SENDER: me@my.timelimit.server
MAIL_TRANSPORT: '{"host": "localhost", "port": 25}'
ALWAYS_PRO: yes
# put additional config variables here
ports:
- "8080:8080"
restart: always
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
links:
- database
database:
image: 'mariadb:10'
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: timelimit
MYSQL_USER: timelimit
MYSQL_PASSWORD: timelimitpassword
volumes:
- ./database:/var/lib/mysql
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
```
The database files will be saved at the folder which contains the docker-compose.yml.
You should change the passwords.
Docker starts both (TimeLimit and the database) at the same time,
so the TimeLimit server will crash a few times due to the missing database
before it starts working.
## example docker-compose.yml with external databases
```
version: '2'
services:
api:
image: 'timelimit-server:latest'
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:pass@example.com:5432/dbname
PORT: 8080
MAIL_SENDER: me@my.timelimit.server
MAIL_TRANSPORT: '{"host": "localhost", "port": 25}'
ALWAYS_PRO: yes
# put additional config variables here
restart: always
# you can enable logging during testing by commenting this out,
# but logging is not needed when everything works
logging:
driver: none
# easy solution to use a database which does not run within docker
network_mode: "host"
# otherwise:
# redirect host port 9000 to guest port 8080 (to allow access to the API)
# ports:
# - "9000:8080"
# in case the database runs outside of docker and you don't want to use the host network mode, see
# https://forums.docker.com/t/accessing-host-machine-from-within-docker-container/14248
```
+43
View File
@@ -0,0 +1,43 @@
# HTTPS
This server application itself does not support HTTPS. You have to use
an other tool to use HTTPS. One options for this is to use nginx with the
following site config:
```
# don't forget to update the port for your local configuration
#
# the max_fails is important - otherwise nginx
# marks the server sometimes as unreachable if it is restarted
# or starts after nginx
upstream timelimitbackend {
server localhost:8080 max_fails=0;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
# don't forget to update the domain
server_name my.domain;
# don't forget to update the paths
ssl_certificate /my/fullchain.pem;
ssl_certificate_key /my/privkey.pem;
# You can add custom SSL parameters here
location / {
proxy_pass http://timelimitbackend/;
client_max_body_size 10m;
# the following is required for websocket support
#
# without websockets, the client will not detect
# that there is a connection and it will not sync
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
}
}
```
+27
View File
@@ -0,0 +1,27 @@
# npm Commands
This requires that you've installed Node.JS and NPM.
You need this for the development, but you don't need it
when deploying using Docker.
## Warning
You have to run ``npm install`` and ``npm run build`` and restart the server again
after running ``git pull``. otherwise you will keep using the old version.
## npm install
This install all dependencies.
## npm run build
This "compiles" the application.
## npm start
This runs all pending migrations and starts the server.
## npm run lint:fix
This fixes the causes of lint warnings (where possible). This is only needed
during the development.
+8
View File
@@ -0,0 +1,8 @@
# Purchases
To enable the automated purchase feature, set the ``GOOGLE_PLAY_PUBLIC_KEY`` environment variable.
The value for the official builds which are distributed using the Play Store can
be found at <https://codeberg.org/timelimit/timelimit-android/src/commit/3da677877f4dde0b1b01523daae33745f14e08ac/app/build.gradle#L49>.
Additionally, there is the admin API which allows one to unlock the
premium features.
+12
View File
@@ -0,0 +1,12 @@
# Running
There are 3 options for running this application:
Option 1: Build a docker image and use it
Option 2: Install Node.JS (see below for detailed usage)
Option 3: Use a prebuilt docker image
After starting it, you can open ``http://server/time`` to test it,
it should show a timestamp
+46 -29
View File
@@ -213,27 +213,27 @@
"dev": true
},
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"dev": true,
"requires": {
"@babel/highlight": "7.9.0"
"@babel/highlight": "7.10.4"
}
},
"@babel/helper-validator-identifier": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz",
"integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==",
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
"dev": true
},
"@babel/highlight": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
"integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "7.9.0",
"@babel/helper-validator-identifier": "7.10.4",
"chalk": "2.4.2",
"js-tokens": "4.0.0"
}
@@ -270,7 +270,7 @@
"debug": "4.1.1",
"i18n": "0.8.6",
"i18n-locales": "0.0.4",
"lodash": "4.17.15",
"lodash": "4.17.19",
"moment": "2.24.0",
"multimatch": "4.0.0",
"qs": "6.9.3",
@@ -755,7 +755,7 @@
"requires": {
"babel-runtime": "6.26.0",
"esutils": "2.0.3",
"lodash": "4.17.15",
"lodash": "4.17.19",
"to-fast-properties": "1.0.3"
}
},
@@ -1456,7 +1456,7 @@
"get-paths": "0.0.7",
"html-to-text": "5.1.1",
"juice": "6.0.0",
"lodash": "4.17.15",
"lodash": "4.17.19",
"nodemailer": "6.4.6",
"pify": "5.0.0",
"preview-email": "2.0.1"
@@ -1959,7 +1959,7 @@
"requires": {
"he": "1.2.0",
"htmlparser2": "3.10.1",
"lodash": "4.17.15",
"lodash": "4.17.19",
"minimist": "1.2.5"
},
"dependencies": {
@@ -2404,9 +2404,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.assignin": {
"version": "4.2.0",
@@ -3641,7 +3641,7 @@
"debug": "4.1.1",
"dottie": "2.0.2",
"inflection": "1.12.0",
"lodash": "4.17.15",
"lodash": "4.17.19",
"moment": "2.24.0",
"moment-timezone": "0.5.28",
"retry-as-promised": "3.2.0",
@@ -4088,18 +4088,18 @@
}
},
"tslib": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
"dev": true
},
"tslint": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.0.tgz",
"integrity": "sha512-fXjYd/61vU6da04E505OZQGb2VCN2Mq3doeWcOIryuG+eqdmFUXTYVwdhnbEu2k46LNLgUYt9bI5icQze/j0bQ==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
"integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
"dev": true,
"requires": {
"@babel/code-frame": "7.8.3",
"@babel/code-frame": "7.10.4",
"builtin-modules": "1.1.1",
"chalk": "2.4.2",
"commander": "2.20.0",
@@ -4107,11 +4107,28 @@
"glob": "7.1.6",
"js-yaml": "3.13.1",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"mkdirp": "0.5.5",
"resolve": "1.12.0",
"semver": "5.5.0",
"tslib": "1.11.1",
"tslib": "1.13.0",
"tsutils": "2.29.0"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "1.2.5"
}
}
}
},
"tslint-config-standard": {
@@ -4157,7 +4174,7 @@
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
"requires": {
"tslib": "1.11.1"
"tslib": "1.13.0"
}
},
"tunnel-agent": {
+2 -2
View File
@@ -39,7 +39,7 @@
"@types/socket.io": "^2.1.4",
"@types/tokgen": "^1.0.0",
"@types/umzug": "^2.2.3",
"tslint": "^6.1.0",
"tslint": "^6.1.3",
"tslint-config-standard": "^9.0.0",
"typescript": "^3.8.3",
"typescript-json-schema": "^0.42.0"
@@ -54,7 +54,7 @@
"express": "^4.17.1",
"http-errors": "^1.7.3",
"iab_verifier": "^0.1.2",
"lodash": "^4.17.15",
"lodash": "^4.17.19",
"mariadb": "^2.3.1",
"pg": "^7.18.2",
"pg-hstore": "^2.3.3",
+66
View File
@@ -0,0 +1,66 @@
/*
* 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 { anonymizedNetworkIdLength } from '../database/categorynetworkid'
import { assertIsHexString } from '../util/hexstring'
import { assertIdWithinFamily } from '../util/token'
import { ParentAction } from './basetypes'
export class AddCategoryNetworkIdAction extends ParentAction {
readonly categoryId: string
readonly itemId: string
readonly hashedNetworkId: string
constructor ({ categoryId, itemId, hashedNetworkId }: {
categoryId: string
itemId: string
hashedNetworkId: string
}) {
super()
assertIdWithinFamily(categoryId)
assertIdWithinFamily(itemId)
assertIsHexString(hashedNetworkId)
if (hashedNetworkId.length !== anonymizedNetworkIdLength) throw new Error('wrong network id length')
this.categoryId = categoryId
this.itemId = itemId
this.hashedNetworkId = hashedNetworkId
}
serialize = (): SerializedAddCategoryNetworkIdAction => ({
type: 'ADD_CATEGORY_NETWORK_ID',
categoryId: this.categoryId,
itemId: this.itemId,
hashedNetworkId: this.hashedNetworkId
})
static parse = ({ categoryId, itemId, hashedNetworkId }: SerializedAddCategoryNetworkIdAction) => (
new AddCategoryNetworkIdAction({
categoryId,
itemId,
hashedNetworkId
})
)
}
export interface SerializedAddCategoryNetworkIdAction {
type: 'ADD_CATEGORY_NETWORK_ID'
categoryId: string
itemId: string
hashedNetworkId: string
}
+2
View File
@@ -18,6 +18,7 @@
export { AppLogicAction, ChildAction, ParentAction } from './basetypes'
export { AddCategoryAppsAction } from './addcategoryapps'
export { AddCategoryNetworkIdAction } from './addcategorynetworkid'
export { AddUserAction } from './adduser'
export { AddInstalledAppsAction } from './addinstalledapps'
export { AddUsedTimeAction } from './addusedtime'
@@ -34,6 +35,7 @@ export { IncrementCategoryExtraTimeAction } from './incrementcategoryextratime'
export { RemoveCategoryAppsAction } from './removecategoryapps'
export { RemoveInstalledAppsAction } from './removeinstalledapps'
export { RemoveUserAction } from './removeuser'
export { ResetCategoryNetworkIdsAction } from './resetcategorynetworkids'
export { RenameChildAction } from './renamechild'
export { ResetParentBlockedTimesAction } from './resetparentblockedtimes'
export { SetCategoryExtraTimeAction } from './setcategoryextratime'
+49
View File
@@ -0,0 +1,49 @@
/*
* 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 { assertIdWithinFamily } from '../util/token'
import { ParentAction } from './basetypes'
export class ResetCategoryNetworkIdsAction extends ParentAction {
readonly categoryId: string
constructor ({ categoryId }: {
categoryId: string
}) {
super()
assertIdWithinFamily(categoryId)
this.categoryId = categoryId
}
serialize = (): SerializeResetCategoryNetworkIdsAction => ({
type: 'RESET_CATEGORY_NETWORK_IDS',
categoryId: this.categoryId
})
static parse = ({ categoryId }: SerializeResetCategoryNetworkIdsAction) => (
new ResetCategoryNetworkIdsAction({
categoryId
})
)
}
export interface SerializeResetCategoryNetworkIdsAction {
type: 'RESET_CATEGORY_NETWORK_IDS'
categoryId: string
}
+8
View File
@@ -16,6 +16,7 @@
*/
import { AddCategoryAppsAction, SerializedAddCategoryAppsAction } from '../addcategoryapps'
import { AddCategoryNetworkIdAction, SerializedAddCategoryNetworkIdAction } from '../addcategorynetworkid'
import { AddUserAction, SerializedAddUserAction } from '../adduser'
import { ParentAction } from '../basetypes'
import { ChangeParentPasswordAction, SerializedChangeParentPasswordAction } from '../changeparentpassword'
@@ -28,6 +29,7 @@ import { IncrementCategoryExtraTimeAction, SerializedIncrementCategoryExtraTimeA
import { RemoveCategoryAppsAction, SerializedRemoveCategoryAppsAction } from '../removecategoryapps'
import { RemoveUserAction, SerializedRemoveUserAction } from '../removeuser'
import { RenameChildAction, SerializedRenameChildAction } from '../renamechild'
import { ResetCategoryNetworkIdsAction, SerializeResetCategoryNetworkIdsAction } from '../resetcategorynetworkids'
import { ResetParentBlockedTimesAction, SerializedResetParentBlockedTimesAction } from '../resetparentblockedtimes'
import { SerializedSetCategoryExtraTimeAction, SetCategoryExtraTimeAction } from '../setcategoryextratime'
import { SerializedSetCategoryForUnassignedAppsAction, SetCategoryForUnassignedAppsAction } from '../setcategoryforunassignedapps'
@@ -60,6 +62,7 @@ import { SerializedUpdateUserLimitLoginCategory, UpdateUserLimitLoginCategory }
export type SerializedParentAction =
SerializedAddCategoryAppsAction |
SerializedAddCategoryNetworkIdAction |
SerializedAddUserAction |
SerializedChangeParentPasswordAction |
SerializedCreateCategoryAction |
@@ -71,6 +74,7 @@ export type SerializedParentAction =
SerializedRemoveCategoryAppsAction |
SerializedRemoveUserAction |
SerializedRenameChildAction |
SerializeResetCategoryNetworkIdsAction |
SerializedResetParentBlockedTimesAction |
SerializedSetCategoryForUnassignedAppsAction |
SerializedSetChildPasswordAction |
@@ -104,6 +108,8 @@ export type SerializedParentAction =
export const parseParentAction = (action: SerializedParentAction): ParentAction => {
if (action.type === 'ADD_CATEGORY_APPS') {
return AddCategoryAppsAction.parse(action)
} else if (action.type === 'ADD_CATEGORY_NETWORK_ID') {
return AddCategoryNetworkIdAction.parse(action)
} else if (action.type === 'ADD_USER') {
return AddUserAction.parse(action)
} else if (action.type === 'CHANGE_PARENT_PASSWORD') {
@@ -126,6 +132,8 @@ export const parseParentAction = (action: SerializedParentAction): ParentAction
return RemoveUserAction.parse(action)
} else if (action.type === 'RENAME_CHILD') {
return RenameChildAction.parse(action)
} else if (action.type === 'RESET_CATEGORY_NETWORK_IDS') {
return ResetCategoryNetworkIdsAction.parse(action)
} else if (action.type === 'RESET_PARENT_BLOCKED_TIMES') {
return ResetParentBlockedTimesAction.parse(action)
} else if (action.type === 'SET_CATEGORY_EXTRA_TIME') {
+4 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -19,6 +19,8 @@ import { validateBitmask } from '../util/bitmask'
import { assertIdWithinFamily } from '../util/token'
import { ParentAction } from './basetypes'
export const blockedTimesBitmaskLength = 60 * 24 * 7 /* number of minutes per week */
export class UpdateCategoryBlockedTimesAction extends ParentAction {
readonly categoryId: string
readonly blockedTimes: string
@@ -30,7 +32,7 @@ export class UpdateCategoryBlockedTimesAction extends ParentAction {
super()
assertIdWithinFamily(categoryId)
validateBitmask(blockedTimes, 60 * 24 * 7 /* number of minutes per week */)
validateBitmask(blockedTimes, blockedTimesBitmaskLength)
this.categoryId = categoryId
this.blockedTimes = blockedTimes
+2 -1
View File
@@ -51,7 +51,8 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
res.json({
status,
mail,
canCreateFamily: !config.disableSignup
canCreateFamily: !config.disableSignup,
alwaysPro: config.alwaysPro
})
} catch (ex) {
next(ex)
+75
View File
@@ -119,6 +119,33 @@ const definitions = {
"type"
]
},
"SerializedAddCategoryNetworkIdAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"ADD_CATEGORY_NETWORK_ID"
]
},
"categoryId": {
"type": "string"
},
"itemId": {
"type": "string"
},
"hashedNetworkId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"categoryId",
"hashedNetworkId",
"itemId",
"type"
]
},
"SerializedAddUserAction": {
"type": "object",
"properties": {
@@ -468,6 +495,25 @@ const definitions = {
"type"
]
},
"SerializeResetCategoryNetworkIdsAction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"RESET_CATEGORY_NETWORK_IDS"
]
},
"categoryId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"categoryId",
"type"
]
},
"SerializedResetParentBlockedTimesAction": {
"type": "object",
"properties": {
@@ -1835,6 +1881,12 @@ const definitions = {
},
"sort": {
"type": "number"
},
"networks": {
"type": "array",
"items": {
"$ref": "#/definitions/ServerCategoryNetworkId"
}
}
},
"additionalProperties": false,
@@ -1847,6 +1899,7 @@ const definitions = {
"extraTimeDay",
"mblCharging",
"mblMobile",
"networks",
"parentCategoryId",
"sort",
"tempBlockTime",
@@ -1856,6 +1909,22 @@ const definitions = {
"version"
]
},
"ServerCategoryNetworkId": {
"type": "object",
"properties": {
"itemId": {
"type": "string"
},
"hashedNetworkId": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"hashedNetworkId",
"itemId"
]
},
"ServerUpdatedCategoryAssignedApps": {
"type": "object",
"properties": {
@@ -2327,6 +2396,9 @@ export const isSerializedParentAction: (value: object) => value is SerializedPar
{
"$ref": "#/definitions/SerializedAddCategoryAppsAction"
},
{
"$ref": "#/definitions/SerializedAddCategoryNetworkIdAction"
},
{
"$ref": "#/definitions/SerializedAddUserAction"
},
@@ -2360,6 +2432,9 @@ export const isSerializedParentAction: (value: object) => value is SerializedPar
{
"$ref": "#/definitions/SerializedRenameChildAction"
},
{
"$ref": "#/definitions/SerializeResetCategoryNetworkIdsAction"
},
{
"$ref": "#/definitions/SerializedResetParentBlockedTimesAction"
},
+4 -2
View File
@@ -20,6 +20,7 @@ interface Config {
mailWhitelist: Array<string>
disableSignup: boolean
pingInterval: number
alwaysPro: boolean
}
function parseYesNo (value: string) {
@@ -28,12 +29,13 @@ function parseYesNo (value: string) {
} else if (value === 'no') {
return false
} else {
throw new Error('invalid value "' + value + '", expected "" or "no"')
throw new Error('invalid value "' + value + '", expected "yes" or "no"')
}
}
export const config: Config = {
mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0),
disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'),
pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000
pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000,
alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false
}
+60
View File
@@ -0,0 +1,60 @@
/*
* 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 * as Sequelize from 'sequelize'
import { familyIdColumn, idWithinFamilyColumn } from './columns'
import { SequelizeAttributes } from './types'
export interface CategoryNetworkIdAttributes {
familyId: string
categoryId: string
networkItemId: string
hashedNetworkId: string
}
export type CategoryNetworkIdModel = Sequelize.Model & CategoryNetworkIdAttributes
export type CategoryNetworkIdModelStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): CategoryNetworkIdModel;
}
export const anonymizedNetworkIdLength = 8
export const maxNetworkIdsPerCategory = 8
export const attributes: SequelizeAttributes<CategoryNetworkIdAttributes> = {
familyId: {
...familyIdColumn,
primaryKey: true
},
categoryId: {
...idWithinFamilyColumn,
primaryKey: true
},
networkItemId: {
...idWithinFamilyColumn,
primaryKey: true
},
hashedNetworkId: {
type: Sequelize.STRING(anonymizedNetworkIdLength),
allowNull: false,
validate: {
notEmpty: true,
is: /^([0-9a-f]{8})?$/
}
}
}
export const createCategoryNetworkIdModel = (sequelize: Sequelize.Sequelize): CategoryNetworkIdModelStatic => sequelize.define('CategoryNetworkId', attributes) as CategoryNetworkIdModelStatic
+6 -1
View File
@@ -22,6 +22,7 @@ import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
import { CategoryNetworkIdModelStatic, createCategoryNetworkIdModel } from './categorynetworkid'
import { ConfigModelStatic, createConfigModel } from './config'
import { createDeviceModel, DeviceModelStatic } from './device'
import { createFamilyModel, FamilyModelStatic } from './family'
@@ -42,6 +43,7 @@ export interface Database {
appActivity: AppActivityModelStatic
category: CategoryModelStatic
categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic
config: ConfigModelStatic
device: DeviceModelStatic
family: FamilyModelStatic
@@ -54,6 +56,7 @@ export interface Database {
user: UserModelStatic
userLimitLoginCategory: UserLimitLoginCategoryModelStatic
transaction: <T> (autoCallback: (t: Sequelize.Transaction) => Promise<T>) => Promise<T>
dialect: string
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
@@ -63,6 +66,7 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize),
categoryNetworkId: createCategoryNetworkIdModel(sequelize),
config: createConfigModel(sequelize),
device: createDeviceModel(sequelize),
family: createFamilyModel(sequelize),
@@ -76,7 +80,8 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Sequelize.Transaction) => Promise<T>) => (sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
}, autoCallback) as any) as Promise<T>
}, autoCallback) as any) as Promise<T>,
dialect: sequelize.getDialect()
})
export const sequelize = new Sequelize.Sequelize(process.env.DATABASE_URL || 'sqlite://test.db', {
@@ -0,0 +1,41 @@
/*
* 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 { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function up (_: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await sequelize.query(
'CREATE TABLE `CategoryNetworkIds` ' +
'(`familyId` VARCHAR(10) NOT NULL, `categoryId` VARCHAR(6) NOT NULL,' +
'`networkItemId` VARCHAR(6) NOT NULL, `hashedNetworkId` VARCHAR(8) NOT NULL,' +
'PRIMARY KEY(`familyId`, `categoryId`, `networkItemId`), FOREIGN KEY(`familyId`, `categoryId`)' +
'REFERENCES `Categories`(`familyId`, `categoryId`) ON UPDATE CASCADE ON DELETE CASCADE )',
{ transaction }
)
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('CategoryNetworkIds', { transaction })
})
}
+3 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -17,6 +17,7 @@
import { Conflict, Unauthorized } from 'http-errors'
import * as Sequelize from 'sequelize'
import { config } from '../../config'
import { Database } from '../../database'
import { generateVersionId } from '../../util/token'
import { WebsocketApi } from '../../websocket'
@@ -104,7 +105,7 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c
hasFullVersion: familyEntryUnsafe.hasFullVersion
}
if (!familyEntry.hasFullVersion) {
if (!(familyEntry.hasFullVersion || config.alwaysPro)) {
return {
response: 'requires full version',
sourceDeviceId: deviceEntry.deviceId,
+3 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -17,6 +17,7 @@
import { memoize, uniq } from 'lodash'
import * as Sequelize from 'sequelize'
import { config } from '../../../config'
import { VisibleConnectedDevicesManager } from '../../../connected-devices'
import { Database } from '../../../database'
import { generateVersionId } from '../../../util/token'
@@ -49,7 +50,7 @@ export class Cache {
connectedDevicesManager: VisibleConnectedDevicesManager
}) {
this.familyId = familyId
this.hasFullVersion = hasFullVersion
this.hasFullVersion = hasFullVersion || config.alwaysPro
this.database = database
this.transaction = transaction
this.connectedDevicesManager = connectedDevicesManager
@@ -26,6 +26,9 @@ export const getRoundedTimestamp = () => {
return now - (now % (1000 * 60 * 60 * 24 * 2 /* 2 days */))
}
const dayLengthInMinutes = MinuteOfDay.LENGTH
const dayLengthInMs = dayLengthInMinutes * 1000 * 60
export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
deviceId: string
action: AddUsedTimeAction
@@ -61,9 +64,12 @@ export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
currentExtraTime: number
}) => {
if (action.timeToAdd !== 0) {
const maxOperator = cache.database.dialect === 'sqlite' ? 'MAX' : 'GREATEST'
const minOperator = cache.database.dialect === 'sqlite' ? 'MIN' : 'LEAST'
// try to update first
const [updatedRows] = await cache.database.usedTime.update({
usedTime: Sequelize.literal(`usedTime + ${action.timeToAdd}`) as any,
usedTime: Sequelize.literal(`${maxOperator}(0, ${minOperator}(usedTime + ${action.timeToAdd}, ${dayLengthInMs}))`) as any,
lastUpdate: roundedTimestamp
}, {
where: {
@@ -82,7 +88,7 @@ export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
familyId: cache.familyId,
categoryId: categoryId,
dayOfEpoch: action.dayOfEpoch,
usedTime: action.timeToAdd,
usedTime: Math.min(action.timeToAdd, dayLengthInMs),
lastUpdate: roundedTimestamp,
startMinuteOfDay: MinuteOfDay.MIN,
endMinuteOfDay: MinuteOfDay.MAX
@@ -64,9 +64,15 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }:
// tslint:disable-next-line:no-inner-declarations
async function handle (start: number, end: number) {
const lengthInMinutes = (end - start) + 1
const lengthInMs = lengthInMinutes * 1000 * 60
const maxOperator = cache.database.dialect === 'sqlite' ? 'MAX' : 'GREATEST'
const minOperator = cache.database.dialect === 'sqlite' ? 'MIN' : 'LEAST'
// try to update first
const [updatedRows] = await cache.database.usedTime.update({
usedTime: Sequelize.literal(`usedTime + ${item.timeToAdd}`) as any,
usedTime: Sequelize.literal(`${maxOperator}(0, ${minOperator}(usedTime + ${item.timeToAdd}, ${lengthInMs}))`) as any,
lastUpdate: roundedTimestampForUsedTime
}, {
where: {
@@ -85,7 +91,7 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache }:
familyId: cache.familyId,
categoryId: item.categoryId,
dayOfEpoch: action.dayOfEpoch,
usedTime: item.timeToAdd,
usedTime: Math.min(item.timeToAdd, lengthInMs),
lastUpdate: roundedTimestampForUsedTime,
startMinuteOfDay: start,
endMinuteOfDay: end
@@ -18,11 +18,13 @@
import * as Sequelize from 'sequelize'
import { AddCategoryAppsAction } from '../../../../action'
import { CategoryAppAttributes } from '../../../../database/categoryapp'
import { getCategoryWithParentCategories } from '../../../../util/category'
import { Cache } from '../cache'
export async function dispatchAddCategoryApps ({ action, cache }: {
export async function dispatchAddCategoryApps ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: AddCategoryAppsAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
@@ -39,14 +41,25 @@ export async function dispatchAddCategoryApps ({ action, cache }: {
const { childId } = categoryEntryUnsafe
if (fromChildSelfLimitAddChildUserId !== null) {
if (childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not add apps to other users')
}
}
const categoriesOfSameChild = await cache.database.category.findAll({
where: {
familyId: cache.familyId,
childId
},
attributes: ['categoryId'],
attributes: ['categoryId', 'parentCategoryId'],
transaction: cache.transaction
}).map((item) => ({ categoryId: item.categoryId }))
}).map((item) => ({
categoryId: item.categoryId,
parentCategoryId: item.parentCategoryId
}))
const userCategoryIds = categoriesOfSameChild.map((item) => item.categoryId)
const oldCategories = await cache.database.categoryApp.findAll({
attributes: [ 'categoryId' ],
@@ -54,7 +67,7 @@ export async function dispatchAddCategoryApps ({ action, cache }: {
where: {
familyId: cache.familyId,
categoryId: {
[Sequelize.Op.in]: categoriesOfSameChild.map((item) => item.categoryId)
[Sequelize.Op.in]: userCategoryIds
},
packageName: {
[Sequelize.Op.in]: action.packageNames
@@ -63,6 +76,68 @@ export async function dispatchAddCategoryApps ({ action, cache }: {
transaction: cache.transaction
}).map((item) => item.categoryId)
if (fromChildSelfLimitAddChildUserId !== null) {
const parentCategoriesOfTargetCategory = getCategoryWithParentCategories(categoriesOfSameChild, action.categoryId)
const userEntryUnsafe = await cache.database.user.findOne({
attributes: [ 'categoryForNotAssignedApps' ],
where: {
familyId: cache.familyId,
userId: fromChildSelfLimitAddChildUserId
},
transaction: cache.transaction
})
if (!userEntryUnsafe) {
throw new Error('illegal state')
}
const userEntry = { categoryForNotAssignedApps: userEntryUnsafe.categoryForNotAssignedApps }
const validatedDefaultCategoryId = categoriesOfSameChild.find((item) => item.categoryId === userEntry.categoryForNotAssignedApps)?.categoryId
const allowUnassignedElements = validatedDefaultCategoryId !== undefined &&
parentCategoriesOfTargetCategory.indexOf(validatedDefaultCategoryId) !== -1
const assertCanAddApp = async (packageName: string, isApp: boolean) => {
const categoryAppEntryUnsafe = await cache.database.categoryApp.findOne({
attributes: [ 'categoryId' ],
where: {
familyId: cache.familyId,
categoryId: {
[Sequelize.Op.in]: userCategoryIds
},
packageName: packageName
},
transaction: cache.transaction
})
const categoryAppEntry = categoryAppEntryUnsafe ? { categoryId: categoryAppEntryUnsafe.categoryId } : null
if (categoryAppEntry === null) {
if ((isApp && allowUnassignedElements) || (!isApp)) {
// allow
} else {
throw new Error('can not assign apps without category as child')
}
} else {
if (parentCategoriesOfTargetCategory.indexOf(categoryAppEntry.categoryId) !== -1) {
// allow
} else {
throw new Error('can not add app which is not contained in the parent category')
}
}
}
for (let i = 0; i < action.packageNames.length; i++) {
const packageName = action.packageNames[i]
if (packageName.indexOf(':') !== -1) {
await assertCanAddApp(packageName.substring(0, packageName.indexOf(':')), true)
await assertCanAddApp(packageName, false)
} else {
await assertCanAddApp(packageName, true)
}
}
}
if (oldCategories.length > 0) {
await cache.database.categoryApp.destroy({
where: {
@@ -0,0 +1,73 @@
/*
* 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 { AddCategoryNetworkIdAction } from '../../../../action'
import { maxNetworkIdsPerCategory } from '../../../../database/categorynetworkid'
import { Cache } from '../cache'
export async function dispatchAddCategoryNetworkId ({ action, cache }: {
action: AddCategoryNetworkIdAction
cache: Cache
}) {
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction,
attributes: ['childId']
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
}
const count = await cache.database.categoryNetworkId.count({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction
})
if (count + 1 > maxNetworkIdsPerCategory) {
throw new Error('category network limit reached')
}
const hasOldItem = (await cache.database.categoryNetworkId.count({
where: {
familyId: cache.familyId,
categoryId: action.categoryId,
networkItemId: action.itemId
},
transaction: cache.transaction
})) !== 0
if (hasOldItem) {
throw new Error('id already used')
}
await cache.database.categoryNetworkId.create({
familyId: cache.familyId,
categoryId: action.categoryId,
networkItemId: action.itemId,
hashedNetworkId: action.hashedNetworkId
}, { transaction: cache.transaction })
cache.categoriesWithModifiedBaseData.push(action.categoryId)
cache.areChangesImportant = true
}
@@ -19,10 +19,17 @@ import { CreateCategoryAction } from '../../../../action'
import { generateVersionId } from '../../../../util/token'
import { Cache } from '../cache'
export async function dispatchCreateCategory ({ action, cache }: {
export async function dispatchCreateCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: CreateCategoryAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== action.childId) {
throw new Error('can not create categories for other child users')
}
}
// check that the child exists
const childEntry = await cache.database.user.findOne({
where: {
@@ -18,16 +18,30 @@
import { CreateTimeLimitRuleAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchCreateTimeLimitRule ({ action, cache }: {
export async function dispatchCreateTimeLimitRule ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: CreateTimeLimitRuleAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const doesCategoryExist = await cache.doesCategoryExist(action.rule.categoryId)
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.rule.categoryId
},
transaction: cache.transaction,
attributes: ['childId']
})
if (!doesCategoryExist) {
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) {
throw new Error('can not add rules for other users')
}
}
await cache.database.timelimitRule.create({
familyId: cache.familyId,
ruleId: action.rule.ruleId,
@@ -17,6 +17,7 @@
import {
AddCategoryAppsAction,
AddCategoryNetworkIdAction,
AddUserAction,
ChangeParentPasswordAction,
CreateCategoryAction,
@@ -29,6 +30,7 @@ import {
RemoveCategoryAppsAction,
RemoveUserAction,
RenameChildAction,
ResetCategoryNetworkIdsAction,
ResetParentBlockedTimesAction,
SetCategoryExtraTimeAction,
SetCategoryForUnassignedAppsAction,
@@ -61,6 +63,7 @@ import {
} from '../../../../action'
import { Cache } from '../cache'
import { dispatchAddCategoryApps } from './addcategoryapps'
import { dispatchAddCategoryNetworkId } from './addcategorynetworkid'
import { dispatchAddUser } from './adduser'
import { dispatchChangeParentPassword } from './changeparentpassword'
import { dispatchCreateCategory } from './createcategory'
@@ -72,6 +75,7 @@ import { dispatchIncrementCategoryExtraTime } from './incrementcategoryextratime
import { dispatchRemoveCategoryApps } from './removecategoryapps'
import { dispatchRemoveUser } from './removeuser'
import { dispatchRenameChild } from './renamechild'
import { dispatchResetCategoryNetworkIds } from './resetcategorynetworkids'
import { dispatchResetParentBlockedTimes } from './resetparentblockedtimes'
import { dispatchSetCategoryExtraTime } from './setcategoryextratime'
import { dispatchSetCategoryForUnassignedApps } from './setcategoryforunassignedapps'
@@ -102,94 +106,103 @@ import { dispatchUpdateTimelimitRule } from './updatetimelimitrule'
import { dispatchUpdateUserFlagsAction } from './updateuserflags'
import { dispatchUpdateUserLimitLoginCategoryAction } from './updateuserlimitlogincategory'
export const dispatchParentAction = async ({ action, cache, parentUserId, sourceDeviceId }: {
export const dispatchParentAction = async ({ action, cache, parentUserId, sourceDeviceId, fromChildSelfLimitAddChildUserId }: {
action: ParentAction
cache: Cache
parentUserId: string
sourceDeviceId: string | null
fromChildSelfLimitAddChildUserId: string | null
}) => {
if (action instanceof AddCategoryAppsAction) {
await dispatchAddCategoryApps({ action, cache })
} else if (action instanceof AddUserAction) {
await dispatchAddUser({ action, cache })
} else if (action instanceof RemoveCategoryAppsAction) {
await dispatchRemoveCategoryApps({ action, cache })
return dispatchAddCategoryApps({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof CreateCategoryAction) {
await dispatchCreateCategory({ action, cache })
return dispatchCreateCategory({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof CreateTimeLimitRuleAction) {
await dispatchCreateTimeLimitRule({ action, cache })
} else if (action instanceof DeleteCategoryAction) {
await dispatchDeleteCategory({ action, cache })
} else if (action instanceof UpdateCategoryTitleAction) {
await dispatchUpdateCategoryTitle({ action, cache })
} else if (action instanceof SetCategoryExtraTimeAction) {
await dispatchSetCategoryExtraTime({ action, cache })
} else if (action instanceof SetCategoryForUnassignedAppsAction) {
await dispatchSetCategoryForUnassignedApps({ action, cache })
} else if (action instanceof SetChildPasswordAction) {
await dispatchSetChildPassword({ action, cache })
} else if (action instanceof SetConsiderRebootManipulationAction) {
await dispatchSetConsiderRebootManipulation({ action, cache })
} else if (action instanceof SetDeviceDefaultUserAction) {
await dispatchSetDeviceDefaultUser({ action, cache })
} else if (action instanceof SetDeviceDefaultUserTimeoutAction) {
await dispatchSetDeviceDefaultUserTimeout({ action, cache })
} else if (action instanceof SetDeviceUserAction) {
await dispatchSetDeviceUser({ action, cache })
} else if (action instanceof SetKeepSignedInAction) {
await dispatchSetKeepSignedIn({ action, cache, parentUserId })
} else if (action instanceof SetParentCategoryAction) {
await dispatchSetParentCategory({ action, cache })
} else if (action instanceof SetRelaxPrimaryDeviceAction) {
await dispatchSetRelaxPrimaryDevice({ action, cache })
} else if (action instanceof SetSendDeviceConnected) {
await dispatchSetSendDeviceConnected({ action, cache, sourceDeviceId })
} else if (action instanceof SetUserDisableLimitsUntilAction) {
await dispatchUserSetDisableLimitsUntil({ action, cache })
} else if (action instanceof SetUserTimezoneAction) {
await dispatchSetUserTimezone({ action, cache })
} else if (action instanceof UpdateCategoryBatteryLimitAction) {
await dispatchUpdateCategoryBatteryLimit({ action, cache })
return dispatchCreateTimeLimitRule({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof UpdateCategoryBlockAllNotificationsAction) {
await dispatchUpdateCategoryBlockAllNotifications({ action, cache })
} else if (action instanceof UpdateCategoryBlockedTimesAction) {
await dispatchUpdateCategoryBlockedTimes({ action, cache })
} else if (action instanceof UpdateCategorySortingAction) {
await dispatchUpdateCategorySorting({ action, cache })
} else if (action instanceof IncrementCategoryExtraTimeAction) {
await dispatchIncrementCategoryExtraTime({ action, cache })
return dispatchUpdateCategoryBlockAllNotifications({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof SetParentCategoryAction) {
return dispatchSetParentCategory({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof UpdateCategoryTemporarilyBlockedAction) {
await dispatchUpdateCategoryTemporarilyBlocked({ action, cache })
} else if (action instanceof DeleteTimeLimitRuleAction) {
await dispatchDeleteTimeLimitRule({ action, cache })
} else if (action instanceof UpdateDeviceNameAction) {
await dispatchUpdateDeviceName({ action, cache })
} else if (action instanceof UpdateEnableActivityLevelBlockingAction) {
await dispatchUpdateEnableActivityLevelBlocking({ action, cache })
} else if (action instanceof UpdateNetworkTimeVerificationAction) {
await dispatchUpdateNetworkTimeVerification({ action, cache })
} else if (action instanceof UpdateParentNotificationFlagsAction) {
await dispatchUpdateParentNotificationFlags({ action, cache })
} else if (action instanceof UpdateTimelimitRuleAction) {
await dispatchUpdateTimelimitRule({ action, cache })
} else if (action instanceof RemoveUserAction) {
await dispatchRemoveUser({ action, cache, parentUserId })
} else if (action instanceof RenameChildAction) {
await dispatchRenameChild({ action, cache })
} else if (action instanceof ChangeParentPasswordAction) {
await dispatchChangeParentPassword({ action, cache })
} else if (action instanceof IgnoreManipulationAction) {
await dispatchIgnoreManipulation({ action, cache })
} else if (action instanceof UpdateCategoryTimeWarningsAction) {
await dispatchUpdateCategoryTimeWarnings({ action, cache })
} else if (action instanceof ResetParentBlockedTimesAction) {
await dispatchResetParentBlockedTimes({ action, cache })
} else if (action instanceof UpdateParentBlockedTimesAction) {
await dispatchUpdateParentBlockedTimes({ action, cache, parentUserId })
} else if (action instanceof UpdateUserFlagsAction) {
await dispatchUpdateUserFlagsAction({ action, cache })
} else if (action instanceof UpdateUserLimitLoginCategory) {
await dispatchUpdateUserLimitLoginCategoryAction({ action, cache, parentUserId })
return dispatchUpdateCategoryTemporarilyBlocked({ action, cache, fromChildSelfLimitAddChildUserId })
} else if (action instanceof UpdateCategoryBlockedTimesAction) {
return dispatchUpdateCategoryBlockedTimes({ action, cache, fromChildSelfLimitAddChildUserId })
}
if (fromChildSelfLimitAddChildUserId === null) {
if (action instanceof AddCategoryNetworkIdAction) {
return dispatchAddCategoryNetworkId({ action, cache })
} else if (action instanceof AddUserAction) {
return dispatchAddUser({ action, cache })
} else if (action instanceof RemoveCategoryAppsAction) {
return dispatchRemoveCategoryApps({ action, cache })
} else if (action instanceof DeleteCategoryAction) {
return dispatchDeleteCategory({ action, cache })
} else if (action instanceof UpdateCategoryTitleAction) {
return dispatchUpdateCategoryTitle({ action, cache })
} else if (action instanceof SetCategoryExtraTimeAction) {
return dispatchSetCategoryExtraTime({ action, cache })
} else if (action instanceof SetCategoryForUnassignedAppsAction) {
return dispatchSetCategoryForUnassignedApps({ action, cache })
} else if (action instanceof SetChildPasswordAction) {
return dispatchSetChildPassword({ action, cache })
} else if (action instanceof SetConsiderRebootManipulationAction) {
return dispatchSetConsiderRebootManipulation({ action, cache })
} else if (action instanceof SetDeviceDefaultUserAction) {
return dispatchSetDeviceDefaultUser({ action, cache })
} else if (action instanceof SetDeviceDefaultUserTimeoutAction) {
return dispatchSetDeviceDefaultUserTimeout({ action, cache })
} else if (action instanceof SetDeviceUserAction) {
return dispatchSetDeviceUser({ action, cache })
} else if (action instanceof SetKeepSignedInAction) {
return dispatchSetKeepSignedIn({ action, cache, parentUserId })
} else if (action instanceof SetRelaxPrimaryDeviceAction) {
return dispatchSetRelaxPrimaryDevice({ action, cache })
} else if (action instanceof SetSendDeviceConnected) {
return dispatchSetSendDeviceConnected({ action, cache, sourceDeviceId })
} else if (action instanceof SetUserDisableLimitsUntilAction) {
return dispatchUserSetDisableLimitsUntil({ action, cache })
} else if (action instanceof SetUserTimezoneAction) {
return dispatchSetUserTimezone({ action, cache })
} else if (action instanceof UpdateCategoryBatteryLimitAction) {
return dispatchUpdateCategoryBatteryLimit({ action, cache })
} else if (action instanceof UpdateCategorySortingAction) {
return dispatchUpdateCategorySorting({ action, cache })
} else if (action instanceof IncrementCategoryExtraTimeAction) {
return dispatchIncrementCategoryExtraTime({ action, cache })
} else if (action instanceof DeleteTimeLimitRuleAction) {
return dispatchDeleteTimeLimitRule({ action, cache })
} else if (action instanceof UpdateDeviceNameAction) {
return dispatchUpdateDeviceName({ action, cache })
} else if (action instanceof UpdateEnableActivityLevelBlockingAction) {
return dispatchUpdateEnableActivityLevelBlocking({ action, cache })
} else if (action instanceof UpdateNetworkTimeVerificationAction) {
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 ResetCategoryNetworkIdsAction) {
return dispatchResetCategoryNetworkIds({ action, cache })
} else if (action instanceof RenameChildAction) {
return dispatchRenameChild({ action, cache })
} else if (action instanceof ChangeParentPasswordAction) {
return dispatchChangeParentPassword({ action, cache })
} else if (action instanceof IgnoreManipulationAction) {
return dispatchIgnoreManipulation({ action, cache })
} else if (action instanceof UpdateCategoryTimeWarningsAction) {
return dispatchUpdateCategoryTimeWarnings({ action, cache })
} else if (action instanceof ResetParentBlockedTimesAction) {
return dispatchResetParentBlockedTimes({ action, cache })
} else if (action instanceof UpdateParentBlockedTimesAction) {
return dispatchUpdateParentBlockedTimes({ action, cache, parentUserId })
} else if (action instanceof UpdateUserFlagsAction) {
return dispatchUpdateUserFlagsAction({ action, cache })
} else if (action instanceof UpdateUserLimitLoginCategory) {
return dispatchUpdateUserLimitLoginCategoryAction({ action, cache, parentUserId })
}
} else {
throw new Error('unsupported action type')
}
@@ -0,0 +1,48 @@
/*
* 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 { ResetCategoryNetworkIdsAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchResetCategoryNetworkIds ({ action, cache }: {
action: ResetCategoryNetworkIdsAction
cache: Cache
}) {
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction,
attributes: ['childId']
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
}
await cache.database.categoryNetworkId.destroy({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction
})
cache.categoriesWithModifiedBaseData.push(action.categoryId)
cache.areChangesImportant = true
}
@@ -16,11 +16,13 @@
*/
import { SetParentCategoryAction } from '../../../../action'
import { getCategoryWithParentCategories } from '../../../../util/category'
import { Cache } from '../cache'
export async function dispatchSetParentCategory ({ action, cache }: {
export async function dispatchSetParentCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: SetParentCategoryAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const categoryEntry = await cache.database.category.findOne({
where: {
@@ -34,6 +36,12 @@ export async function dispatchSetParentCategory ({ action, cache }: {
throw new Error('tried to set parent category of non existent category')
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (categoryEntry.childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not set parent category for other user')
}
}
if (action.parentCategory !== '') {
const categoriesByUserId = (await cache.database.category.findAll({
where: {
@@ -74,6 +82,16 @@ export async function dispatchSetParentCategory ({ action, cache }: {
if (childCategoryIds.has(action.parentCategory) || action.parentCategory === action.categoryId) {
throw new Error('can not set a category as parent which is a child of the category')
}
if (fromChildSelfLimitAddChildUserId !== null) {
const ownParentCategory = categoriesByUserId.find((item) => item.categoryId === categoryEntry.parentCategoryId)
const enableDueToLimitAddingWhenChild = ownParentCategory === undefined ||
getCategoryWithParentCategories(categoriesByUserId, action.parentCategory).indexOf(ownParentCategory.categoryId) !== -1
if (!enableDueToLimitAddingWhenChild) {
throw new Error('can not change parent categories in a way which reduces limits')
}
}
}
await cache.database.category.update({
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -18,10 +18,34 @@
import { UpdateCategoryBlockAllNotificationsAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cache }: {
export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryBlockAllNotificationsAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction,
attributes: ['childId']
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for updating notification blocking')
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) {
throw new Error('can not add rules for other users')
}
if (!action.blocked) {
throw new Error('can not disable filter as child')
}
}
const [affectedRows] = await cache.database.category.update({
blockAllNotifications: action.blocked
}, {
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -15,14 +15,49 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { UpdateCategoryBlockedTimesAction } from '../../../../action'
import { blockedTimesBitmaskLength, UpdateCategoryBlockedTimesAction } from '../../../../action/updatecategoryblockedtimes'
import { validateAndParseBitmask } from '../../../../util/bitmask'
import { Cache } from '../cache'
export async function dispatchUpdateCategoryBlockedTimes ({ action, cache }: {
export async function dispatchUpdateCategoryBlockedTimes ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryBlockedTimesAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
const [affectedRows] = await cache.database.category.update({
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction,
attributes: ['childId', 'blockedMinutesInWeek']
})
if (!categoryEntryUnsafe) {
throw new Error('can not update blocked time areas for a category which does not exist')
}
const categoryEntry = {
childId: categoryEntryUnsafe.childId,
blockedMinutesInWeek: categoryEntryUnsafe.blockedMinutesInWeek
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (categoryEntry.childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not update blocked time areas for other child users')
}
const oldBlocked = validateAndParseBitmask(categoryEntry.blockedMinutesInWeek, blockedTimesBitmaskLength)
const newBlocked = validateAndParseBitmask(action.blockedTimes, blockedTimesBitmaskLength)
oldBlocked.forEach((value, index) => {
if (value && !newBlocked[index]) {
throw new Error('new blocked time areas are smaller')
}
})
}
await cache.database.category.update({
blockedMinutesInWeek: action.blockedTimes
}, {
where: {
@@ -32,10 +67,6 @@ export async function dispatchUpdateCategoryBlockedTimes ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows === 0) {
throw new Error('invalid category id provided')
}
cache.categoriesWithModifiedBaseData.push(action.categoryId)
cache.areChangesImportant = true
}
@@ -18,9 +18,10 @@
import { UpdateCategoryTemporarilyBlockedAction } from '../../../../action'
import { Cache } from '../cache'
export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache }: {
export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryTemporarilyBlockedAction
cache: Cache
fromChildSelfLimitAddChildUserId: string | null
}) {
if (action.blocked === true) {
if (!cache.hasFullVersion) {
@@ -28,6 +29,41 @@ export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache
}
}
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction,
attributes: ['childId', 'temporarilyBlocked', 'temporarilyBlockedEndTime']
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for updating temporarily blocking')
}
const categoryEntry = {
childId: categoryEntryUnsafe.childId,
temporarilyBlocked: categoryEntryUnsafe.temporarilyBlocked,
temporarilyBlockedEndTime: categoryEntryUnsafe.temporarilyBlockedEndTime
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntry.childId) {
throw new Error('can not update temporarily blocking as child for other users')
}
if (action.endTime === undefined || !action.blocked) {
throw new Error('the child may only enable a temporarily blocking')
}
if (categoryEntry.temporarilyBlocked) {
if (action.endTime < categoryEntry.temporarilyBlockedEndTime || categoryEntry.temporarilyBlockedEndTime === 0) {
throw new Error('the child may not reduce the temporarily blocking')
}
}
}
const [affectedRows] = await cache.database.category.update({
temporarilyBlocked: action.blocked,
temporarilyBlockedEndTime: action.blocked ? (action.endTime ?? 0) : 0
+63 -8
View File
@@ -22,6 +22,7 @@ import { ClientPushChangesRequest } from '../../../api/schema'
import { isSerializedAppLogicAction, isSerializedChildAction, isSerializedParentAction } from '../../../api/validator'
import { VisibleConnectedDevicesManager } from '../../../connected-devices'
import { Database } from '../../../database'
import { UserFlags } from '../../../model/userflags'
import { EventHandler } from '../../../monitoring/eventhandler'
import { WebsocketApi } from '../../../websocket'
import { notifyClientsAboutChanges } from '../../websocket'
@@ -106,6 +107,8 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
// update the next sequence number
nextSequenceNumber = action.sequenceNumber + 1
let isChildLimitAdding = false
if (action.type === 'parent') {
if (action.integrity === 'device') {
const deviceEntryUnsafe2 = await cache.database.device.findOne({
@@ -125,6 +128,9 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
// this ensures that the parent exists
await cache.getSecondPasswordHashOfParent(action.userId)
} else if (action.integrity === 'childDevice') {
// will be checked later
isChildLimitAdding = true
} else {
const parentSecondHash = await cache.getSecondPasswordHashOfParent(action.userId)
@@ -191,19 +197,68 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
throw new Error('invalid action' + action.encodedAction)
}
eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type)
eventHandler.countEvent('applyActionsFromDevice, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type)
const parsedAction = parseParentAction(parsedSerializedAction)
try {
await dispatchParentAction({
action: parsedAction,
cache,
parentUserId: action.userId,
sourceDeviceId: deviceEntry.deviceId
})
if (isChildLimitAdding) {
const deviceEntryUnsafe2 = await cache.database.device.findOne({
attributes: ['currentUserId'],
where: {
familyId: cache.familyId,
deviceId: deviceEntry.deviceId,
currentUserId: action.userId
},
transaction: cache.transaction
})
if (!deviceEntryUnsafe2) {
throw new Error('illegal state')
}
const deviceUserId = deviceEntryUnsafe2.currentUserId
if (!deviceUserId) {
throw new Error('no device user id set but child add self limit action requested')
}
const deviceUserEntryUnsafe = await cache.database.user.findOne({
attributes: ['flags'],
where: {
familyId: cache.familyId,
userId: deviceUserId,
type: 'child'
},
transaction: cache.transaction
})
if (!deviceUserEntryUnsafe) {
throw new Error('no child user found for child limit adding action')
}
if ((parseInt(deviceUserEntryUnsafe.flags, 10) & UserFlags.ALLOW_SELF_LIMIT_ADD) !== UserFlags.ALLOW_SELF_LIMIT_ADD) {
throw new Error('child add limit action found but not allowed')
}
await dispatchParentAction({
action: parsedAction,
cache,
parentUserId: action.userId,
sourceDeviceId: deviceEntry.deviceId,
fromChildSelfLimitAddChildUserId: deviceUserId
})
} else {
await dispatchParentAction({
action: parsedAction,
cache,
parentUserId: action.userId,
sourceDeviceId: deviceEntry.deviceId,
fromChildSelfLimitAddChildUserId: null
})
}
} catch (ex) {
eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type)
eventHandler.countEvent('applyActionsFromDeviceWithError, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type)
throw ex
}
+30 -2
View File
@@ -17,6 +17,7 @@
import { difference, filter, intersection } from 'lodash'
import * as Sequelize from 'sequelize'
import { config } from '../../config'
import { Database } from '../../database'
import { getStatusMessage } from '../../function/statusmessage'
import { ClientDataStatus } from '../../object/clientdatastatus'
@@ -58,7 +59,9 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
}
let result: ServerDataStatus = {
fullVersion: familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0,
fullVersion: config.alwaysPro ? 1 : (
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
),
message: await getStatusMessage({ database, transaction }) || undefined
}
@@ -394,6 +397,25 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
sort: item.sort
}))
const networkIdsForSyncing = (await database.categoryNetworkId.findAll({
where: {
familyId,
categoryId: {
[Sequelize.Op.in]: categoryIdsToSyncBaseData
}
},
attributes: [
'categoryId',
'networkItemId',
'hashedNetworkId'
],
transaction
})).map((item) => ({
categoryId: item.categoryId,
networkItemId: item.networkItemId,
hashedNetworkId: item.hashedNetworkId
}))
result.categoryBase = dataForSyncing.map((item): ServerUpdatedCategoryBaseData => ({
categoryId: item.categoryId,
childId: item.childId,
@@ -409,7 +431,13 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
mblMobile: item.minBatteryMobile,
mblCharging: item.minBatteryCharging,
tempBlockTime: item.temporarilyBlockedEndTime,
sort: item.sort
sort: item.sort,
networks: networkIdsForSyncing
.filter((network) => network.categoryId === item.categoryId)
.map((network) => ({
itemId: network.networkItemId,
hashedNetworkId: network.hashedNetworkId
}))
}))
}
+2 -1
View File
@@ -17,5 +17,6 @@
export const UserFlags = {
RESTRICT_VIEWING_TO_PARENTS: 1,
ALL_FLAGS: 1
ALLOW_SELF_LIMIT_ADD: 2,
ALL_FLAGS: 1 | 2
}
+6
View File
@@ -112,6 +112,12 @@ export interface ServerUpdatedCategoryBaseData {
mblCharging: number
mblMobile: number
sort: number
networks: Array<ServerCategoryNetworkId>
}
export interface ServerCategoryNetworkId {
itemId: string
hashedNetworkId: string
}
export interface ServerUpdatedCategoryAssignedApps {
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
+40
View File
@@ -0,0 +1,40 @@
/*
* 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/>.
*/
export function getCategoryWithParentCategories (categories: Array<{ categoryId: string, parentCategoryId: string }>, startCategoryId: string): Array<string> {
const categoryById = new Map<string, { categoryId: string, parentCategoryId: string }>()
categories.forEach((category) => categoryById.set(category.categoryId, category))
const startCategory = categoryById.get(startCategoryId)
if (!startCategory) {
throw new Error('start category not found')
}
const categoryIds = [ startCategoryId ]
let currentCategory = categoryById.get(startCategory.parentCategoryId)
while (currentCategory !== undefined && categoryIds.indexOf(currentCategory.categoryId) === -1) {
categoryIds.push(currentCategory.categoryId)
currentCategory = categoryById.get(currentCategory.parentCategoryId)
}
return categoryIds
}