Compare commits

...
12 Commits
36 changed files with 945 additions and 708 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ COPY package.json package-lock.json tsconfig.json .eslintignore .eslintrc.js Rea
COPY src/ /usr/src/app/src/
COPY scripts/ /usr/src/app/scripts/
COPY other/ /usr/src/app/other/
RUN mkdir -p docs/schema && npm install --no-optional && npm run build && npm prune --production && rm -rf ./src
RUN mkdir -p docs/schema && npm install --exclude=optional && npm run build && npm prune --omit=dev && rm -rf ./src
# Start the App
EXPOSE 8080
+1 -2
View File
@@ -16,8 +16,7 @@ On a invalid request body: HTTP status code 400 Bad request
On a invalid add device token: HTTP status code 401 Unauthorized
On success: a JSON object with the properties ``deviceAuthToken`` and ``ownDeviceId``,
both of the type string
On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
## POST /child/update-primary-device
+2 -2
View File
@@ -44,7 +44,7 @@ If the mail auth token is invalid/ expired: HTTP status code 401 Unauthorized
If there is already a user with the mail address of the mail auth token: HTTP status code 409 Conflict
On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string)
On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
## POST /parent/sign-in-into-family
@@ -60,7 +60,7 @@ On a invalid request body: HTTP status code 400 Bad Request
If there is no user with the mail address of the mail auth token: HTTP status code 409 Conflict
On success: object with ``deviceAuthToken`` (string) and ``ownDeviceId`` (string)
On success: object with ``deviceAuthToken`` (string), ``ownDeviceId`` (string) and ``data`` (like a ``/sync/pull-status`` response)
## POST /parent/can-recover-password
@@ -18,6 +18,9 @@
},
"parentName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
@@ -9,6 +9,9 @@
},
"deviceName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
@@ -9,6 +9,9 @@
},
"deviceName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
@@ -0,0 +1,15 @@
# Untitled number in CreateFamilyByMailTokenRequest Schema
```txt
https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [CreateFamilyByMailTokenRequest.schema.json\*](CreateFamilyByMailTokenRequest.schema.json "open original schema") |
## clientLevel Type
`number`
@@ -24,6 +24,7 @@ https://timelimit.io/CreateFamilyByMailTokenRequest
| [deviceName](#devicename) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-devicename.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/deviceName") |
| [timeZone](#timezone) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-timezone.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/timeZone") |
| [parentName](#parentname) | `string` | Required | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-parentname.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/parentName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel") |
## mailAuthToken
@@ -133,6 +134,24 @@ https://timelimit.io/CreateFamilyByMailTokenRequest
`string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [CreateFamilyByMailTokenRequest](createfamilybymailtokenrequest-properties-clientlevel.md "https://timelimit.io/CreateFamilyByMailTokenRequest#/properties/clientLevel")
### clientLevel Type
`number`
# CreateFamilyByMailTokenRequest Definitions
## Definitions group PlaintextParentPassword
@@ -0,0 +1,15 @@
# Untitled number in RegisterChildDeviceRequest Schema
```txt
https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RegisterChildDeviceRequest.schema.json\*](RegisterChildDeviceRequest.schema.json "open original schema") |
## clientLevel Type
`number`
+19
View File
@@ -21,6 +21,7 @@ https://timelimit.io/RegisterChildDeviceRequest
| [registerToken](#registertoken) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-registertoken.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/registerToken") |
| [childDevice](#childdevice) | `object` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-definitions-newdeviceinfo.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/childDevice") |
| [deviceName](#devicename) | `string` | Required | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-devicename.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/deviceName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel") |
## registerToken
@@ -76,6 +77,24 @@ https://timelimit.io/RegisterChildDeviceRequest
`string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [RegisterChildDeviceRequest](registerchilddevicerequest-properties-clientlevel.md "https://timelimit.io/RegisterChildDeviceRequest#/properties/clientLevel")
### clientLevel Type
`number`
# RegisterChildDeviceRequest Definitions
## Definitions group NewDeviceInfo
@@ -0,0 +1,15 @@
# Untitled number in SignIntoFamilyRequest Schema
```txt
https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel
```
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------- |
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [SignIntoFamilyRequest.schema.json\*](SignIntoFamilyRequest.schema.json "open original schema") |
## clientLevel Type
`number`
+19
View File
@@ -21,6 +21,7 @@ https://timelimit.io/SignIntoFamilyRequest
| [mailAuthToken](#mailauthtoken) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-mailauthtoken.md "https://timelimit.io/SignIntoFamilyRequest#/properties/mailAuthToken") |
| [parentDevice](#parentdevice) | `object` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-definitions-newdeviceinfo.md "https://timelimit.io/SignIntoFamilyRequest#/properties/parentDevice") |
| [deviceName](#devicename) | `string` | Required | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-devicename.md "https://timelimit.io/SignIntoFamilyRequest#/properties/deviceName") |
| [clientLevel](#clientlevel) | `number` | Optional | cannot be null | [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel") |
## mailAuthToken
@@ -76,6 +77,24 @@ https://timelimit.io/SignIntoFamilyRequest
`string`
## clientLevel
`clientLevel`
* is optional
* Type: `number`
* cannot be null
* defined in: [SignIntoFamilyRequest](signintofamilyrequest-properties-clientlevel.md "https://timelimit.io/SignIntoFamilyRequest#/properties/clientLevel")
### clientLevel Type
`number`
# SignIntoFamilyRequest Definitions
## Definitions group NewDeviceInfo
+624 -270
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -61,7 +61,7 @@
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15",
"sequelize": "^6.11.0",
"sequelize": "^6.25.5",
"socket.io": "^4.0.1",
"sqlite3": "^4.0.0",
"umzug": "^2.3.0"
+9 -5
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -24,10 +24,12 @@ import { logoutAtPrimaryDevice } from '../function/child/logout-at-primary-devic
import { setPrimaryDevice } from '../function/child/set-primary-device'
import { WebsocketApi } from '../websocket'
import { isRegisterChildDeviceRequest, isRequestWithAuthToken, isUpdatePrimaryDeviceRequest } from './validator'
import { EventHandler } from '../monitoring/eventhandler'
export const createChildRouter = ({ database, websocket }: {
database: Database,
export const createChildRouter = ({ database, websocket, eventHandler }: {
database: Database
websocket: WebsocketApi
eventHandler: EventHandler
}) => {
const router = Router()
@@ -37,15 +39,17 @@ export const createChildRouter = ({ database, websocket }: {
throw new BadRequest()
}
const { deviceAuthToken, deviceId } = await addChildDevice({
const { deviceAuthToken, deviceId, data } = await addChildDevice({
request: req.body,
database,
eventHandler,
websocket
})
res.json({
deviceAuthToken,
ownDeviceId: deviceId
ownDeviceId: deviceId,
data
})
} catch (ex) {
next(ex)
+3 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -47,8 +47,8 @@ export const createApi = ({ database, websocket, connectedDevicesManager, eventH
})
app.use('/auth', createAuthRouter(database))
app.use('/child', createChildRouter({ database, websocket }))
app.use('/parent', createParentRouter({ database, websocket }))
app.use('/child', createChildRouter({ database, websocket, eventHandler }))
app.use('/parent', createParentRouter({ database, websocket, eventHandler }))
app.use('/purchase', createPurchaseRouter({ database, websocket }))
app.use('/sync', createSyncRouter({ database, websocket, connectedDevicesManager, eventHandler }))
+17 -4
View File
@@ -31,6 +31,7 @@ import { signInIntoFamily } from '../function/parent/sign-in-into-family'
import { validateU2fIntegrity, U2fValidationError } from '../function/u2f'
import { createIdentityToken, MissingSignSecretException } from '../util/identity-token'
import { WebsocketApi } from '../websocket'
import { EventHandler } from '../monitoring/eventhandler'
import {
isCreateFamilyByMailTokenRequest,
isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest,
@@ -38,7 +39,13 @@ import {
isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest
} from './validator'
export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => {
export const createParentRouter = ({
database, websocket, eventHandler
}: {
database: Database
websocket: WebsocketApi
eventHandler: EventHandler
}) => {
const router = Router()
router.post('/get-status-by-mail-address', json(), async (req, res, next) => {
@@ -75,17 +82,20 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
const result = await createFamily({
database,
eventHandler,
firstParentDevice: req.body.parentDevice,
mailAuthToken: req.body.mailAuthToken,
password: req.body.parentPassword,
deviceName: req.body.deviceName,
parentName: req.body.parentName,
timeZone: req.body.timeZone
timeZone: req.body.timeZone,
clientLevel: req.body.clientLevel || null
})
res.json({
deviceAuthToken: result.deviceAuthToken,
ownDeviceId: result.deviceId
ownDeviceId: result.deviceId,
data: result.data
})
} catch (ex) {
next(ex)
@@ -100,15 +110,18 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
const result = await signInIntoFamily({
database,
eventHandler,
newDeviceInfo: req.body.parentDevice,
mailAuthToken: req.body.mailAuthToken,
deviceName: req.body.deviceName,
clientLevel: req.body.clientLevel || null,
websocket
})
res.json({
deviceAuthToken: result.deviceAuthToken,
ownDeviceId: result.deviceId
ownDeviceId: result.deviceId,
data: result.data
})
} catch (ex) {
next(ex)
+3
View File
@@ -84,12 +84,14 @@ export interface CreateFamilyByMailTokenRequest {
deviceName: string
timeZone: string
parentName: string
clientLevel?: number
}
export interface SignIntoFamilyRequest {
mailAuthToken: string
parentDevice: NewDeviceInfo
deviceName: string
clientLevel?: number
}
export interface RecoverParentPasswordRequest {
@@ -101,6 +103,7 @@ export interface RegisterChildDeviceRequest {
registerToken: string
childDevice: NewDeviceInfo
deviceName: string
clientLevel?: number
}
export interface CreateRegisterDeviceTokenRequest {
+9
View File
@@ -2930,6 +2930,9 @@ export const isCreateFamilyByMailTokenRequest: (value: unknown) => value is Crea
},
"parentName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
@@ -2955,6 +2958,9 @@ export const isSignIntoFamilyRequest: (value: unknown) => value is SignIntoFamil
},
"deviceName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
@@ -2995,6 +3001,9 @@ export const isRegisterChildDeviceRequest: (value: unknown) => value is Register
},
"deviceName": {
"type": "string"
},
"clientLevel": {
"type": "number"
}
},
"additionalProperties": false,
+43 -11
View File
@@ -17,8 +17,6 @@
import * as Sequelize from 'sequelize'
import { AddDeviceTokenModelStatic, createAddDeviceTokenModel } from './adddevicetoken'
import { AppModelStatic, createAppModel } from './app'
import { AppActivityModelStatic, createAppActivityModel } from './appactivity'
import { AuthTokenModelStatic, createAuthtokenModel } from './authtoken'
import { CategoryModelStatic, createCategoryModel } from './category'
import { CategoryAppModelStatic, createCategoryAppModel } from './categoryapp'
@@ -42,14 +40,13 @@ import { createU2fKeyModel, U2fKeyModelStatic } from './u2fkey'
import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime'
import { createUserModel, UserModelStatic } from './user'
import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory'
import { shouldRetryWithException } from './utils/serialized'
export type Transaction = Sequelize.Transaction
export interface Database {
addDeviceToken: AddDeviceTokenModelStatic
authtoken: AuthTokenModelStatic
app: AppModelStatic
appActivity: AppActivityModelStatic
category: CategoryModelStatic
categoryApp: CategoryAppModelStatic
categoryNetworkId: CategoryNetworkIdModelStatic
@@ -71,15 +68,21 @@ export interface Database {
usedTime: UsedTimeModelStatic
user: UserModelStatic
userLimitLoginCategory: UserLimitLoginCategoryModelStatic
transaction: <T> (autoCallback: (t: Transaction) => Promise<T>, options?: { transaction: Transaction }) => Promise<T>
transaction: <T> (
autoCallback: (t: Transaction) => Promise<T>,
options?: TransactionOptions
) => Promise<T>
dialect: string
}
interface TransactionOptions {
transaction?: Transaction
disableRetry?: boolean
}
const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
addDeviceToken: createAddDeviceTokenModel(sequelize),
authtoken: createAuthtokenModel(sequelize),
app: createAppModel(sequelize),
appActivity: createAppActivityModel(sequelize),
category: createCategoryModel(sequelize),
categoryApp: createCategoryAppModel(sequelize),
childTask: createChildTaskModel(sequelize),
@@ -101,10 +104,39 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({
usedTime: createUsedTimeModel(sequelize),
user: createUserModel(sequelize),
userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize),
transaction: <T> (autoCallback: (transaction: Transaction) => Promise<T>, options?: { transaction: Transaction }) => (sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE,
transaction: options?.transaction
}, autoCallback)) as Promise<T>,
async transaction<T>(
autoCallback: (transaction: Transaction) => Promise<T>,
options?: TransactionOptions
): Promise<T> {
const runAttempt = () => sequelize.transaction({
isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE,
transaction: options?.transaction
}, autoCallback)
const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time))
try {
return await runAttempt()
} catch (ex) {
if (
options?.disableRetry ||
options?.transaction ||
!shouldRetryWithException(this, ex)
) throw ex
}
await delay(10 * (1 + Math.random()))
try {
return await runAttempt()
} catch (ex) {
if (!shouldRetryWithException(this, ex)) throw ex
}
await delay(100 * (1 + Math.random()))
return await runAttempt()
},
dialect: sequelize.getDialect()
})
@@ -15,26 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { RemoveInstalledAppsAction } from '../../../../action'
import { Cache } from '../cache'
import { QueryInterface, Sequelize, Transaction } from 'sequelize'
export async function dispatchRemoveInstalledApps ({ deviceId, action, cache }: {
deviceId: string
action: RemoveInstalledAppsAction
cache: Cache
}) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId,
packageName: {
[Sequelize.Op.in]: action.packageNames
}
},
transaction: cache.transaction
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.dropTable('Apps', { transaction })
await queryInterface.dropTable('AppActivities', { transaction })
})
}
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
export async function down() {
throw new Error('not possible')
}
+2 -2
View File
@@ -86,8 +86,8 @@ export async function assertSerializeableTransactionsAreWorking (database: Datab
await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo })
})()
])
})
})
}, { disableRetry: true })
}, { disableRetry: true })
throw new SerializationFeatureCheckException()
} catch (ex) {
+22 -3
View File
@@ -22,13 +22,22 @@ import { generateAuthToken, generateVersionId } from '../../util/token'
import { WebsocketApi } from '../../websocket'
import { prepareDeviceEntry } from '../device/prepare-device-entry'
import { notifyClientsAboutChangesDelayed } from '../websocket'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
export const addChildDevice = async ({ database, websocket, request }: {
export const addChildDevice = async ({ database, eventHandler, websocket, request }: {
database: Database
eventHandler: EventHandler
websocket: WebsocketApi
request: RegisterChildDeviceRequest
// no transaction here because this is directly called from an API endpoint
}) => {
}): Promise<{
deviceId: string
deviceAuthToken: string
data: ServerDataStatus
}> => {
return database.transaction(async (transaction) => {
const entry = await database.addDeviceToken.findOne({
where: {
@@ -75,9 +84,19 @@ export const addChildDevice = async ({ database, websocket, request }: {
transaction
})
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel: request.clientLevel || null }),
familyId: entry.familyId,
deviceId,
transaction,
eventHandler
})
return {
deviceId,
deviceAuthToken
deviceAuthToken,
data
}
})
}
+1 -21
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -29,26 +29,6 @@ export async function deleteFamilies ({ database, familiyIds }: {
}
await database.transaction(async (transaction) => {
// app
await database.app.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// app activity
await database.appActivity.destroy({
where: {
familyId: {
[Sequelize.Op.in]: familiyIds
}
},
transaction
})
// category
await database.category.destroy({
where: {
-17
View File
@@ -40,23 +40,6 @@ export async function removeDevice ({ database, familyId, deviceId, websocket, t
throw new Conflict()
}
// remove app entries
await database.app.destroy({
where: {
familyId,
deviceId
},
transaction
})
await database.appActivity.destroy({
where: {
familyId,
deviceId
},
transaction
})
// remove as current device
await database.user.update({
currentDevice: ''
+34 -11
View File
@@ -16,25 +16,38 @@
*/
import { Conflict } from 'http-errors'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { NewDeviceInfo, PlaintextParentPassword, assertPlaintextParentPasswordValid } from '../../api/schema'
import { Database } from '../../database'
import { maxMailNotificationFlags } from '../../database/user'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
import {
generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId
} from '../../util/token'
import { requireMailAndLocaleByAuthToken } from '../authentication'
import { prepareDeviceEntry } from '../device/prepare-device-entry'
export const createFamily = async ({ database, mailAuthToken, firstParentDevice, password, timeZone, parentName, deviceName }: {
database: Database,
mailAuthToken: string,
firstParentDevice: NewDeviceInfo,
password: PlaintextParentPassword,
timeZone: string,
parentName: string,
export async function createFamily ({
database, eventHandler, mailAuthToken, firstParentDevice,
password, timeZone, parentName, deviceName, clientLevel
}: {
database: Database
eventHandler: EventHandler
mailAuthToken: string
firstParentDevice: NewDeviceInfo
password: PlaintextParentPassword
timeZone: string
parentName: string
deviceName: string
clientLevel: number | null
// no transaction here because this is directly called from an API endpoint
}) => {
}): Promise<{
deviceAuthToken: string
deviceId: string
data: ServerDataStatus
}> {
assertPlaintextParentPasswordValid(password)
return database.transaction(async (transaction) => {
@@ -42,14 +55,14 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true })
// ensure that no family was created for this mail yet
const exisitngUserEntry = await database.user.findOne({
const existingUserEntry = await database.user.findOne({
where: {
mail: mailInfo.mail
},
transaction
})
if (exisitngUserEntry) {
if (existingUserEntry) {
throw new Conflict()
}
@@ -103,9 +116,19 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice,
isUserKeptSignedIn: true
}), { transaction })
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel }),
familyId,
deviceId,
transaction,
eventHandler
})
return {
deviceAuthToken,
deviceId
deviceId,
data
}
})
}
+19 -3
View File
@@ -24,15 +24,21 @@ import { WebsocketApi } from '../../websocket'
import { requireMailAndLocaleByAuthToken } from '../authentication'
import { prepareDeviceEntry } from '../device/prepare-device-entry'
import { notifyClientsAboutChangesDelayed } from '../websocket'
import { generateServerDataStatus } from '../sync/get-server-data-status'
import { EventHandler } from '../../monitoring/eventhandler'
import { ServerDataStatus } from '../../object/serverdatastatus'
import { createEmptyClientDataStatus } from '../../object/clientdatastatus'
export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo, deviceName, websocket }: {
export const signInIntoFamily = async ({ database, eventHandler, mailAuthToken, newDeviceInfo, deviceName, websocket, clientLevel }: {
database: Database
eventHandler: EventHandler
mailAuthToken: string
newDeviceInfo: NewDeviceInfo
deviceName: string
websocket: WebsocketApi
clientLevel: number | null
// no transaction here because this is directly called from an API endpoint
}): Promise<{ deviceId: string; deviceAuthToken: string }> => {
}): Promise<{ deviceId: string; deviceAuthToken: string; data: ServerDataStatus }> => {
return database.transaction(async (transaction) => {
const mailInfo = await requireMailAndLocaleByAuthToken({ database, mailAuthToken, transaction, invalidate: true })
@@ -94,9 +100,19 @@ export const signInIntoFamily = async ({ database, mailAuthToken, newDeviceInfo,
})
})
const data = await generateServerDataStatus({
database,
clientStatus: createEmptyClientDataStatus({ clientLevel }),
familyId: userEntry.familyId,
deviceId,
transaction,
eventHandler
})
return {
deviceId,
deviceAuthToken
deviceAuthToken,
data
}
})
}
-17
View File
@@ -40,7 +40,6 @@ export class Cache {
categoriesWithModifiedUsedTimes = new Set<string>()
categoriesWithModifiedTasks = new Set<string>()
devicesWithModifiedInstalledApps = new Set<string>()
devicesWithModifiedShowDeviceConnected = new Map<string, boolean>()
invalidiateUserList = false
@@ -241,22 +240,6 @@ export class Cache {
this.categoriesWithModifiedUsedTimes.clear()
}
if (this.devicesWithModifiedInstalledApps.size > 0) {
await database.device.update({
installedAppsVersion: generateVersionId()
}, {
where: {
familyId,
deviceId: {
[Sequelize.Op.in]: setToList(this.devicesWithModifiedInstalledApps)
}
},
transaction
})
this.devicesWithModifiedInstalledApps.clear()
}
if (this.invalidiateUserList) {
await database.family.update({
userListVersion: generateVersionId()
@@ -1,63 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { AddInstalledAppsAction } from '../../../../action'
import { AppAttributes, maxPackageNameLength } from '../../../../database/app'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception'
export async function dispatchAddInstalledApps ({ deviceId, action, cache }: {
deviceId: string
action: AddInstalledAppsAction
cache: Cache
}) {
action.apps.forEach((app) => {
if (app.packageName.length > maxPackageNameLength) {
throw new ApplyActionException({
staticMessage: 'package name too long',
dynamicMessage: 'package name too long: ' + app.packageName
})
}
})
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId,
packageName: {
[Sequelize.Op.in]: action.apps.map((app) => app.packageName)
}
},
transaction: cache.transaction
})
await cache.database.app.bulkCreate(
action.apps.map((app): AppAttributes => ({
familyId: cache.familyId,
deviceId,
packageName: app.packageName,
title: app.title,
isLaunchable: app.isLaunchable,
recommendation: app.recommendation
})),
{ transaction: cache.transaction }
)
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
}
@@ -36,18 +36,15 @@ import {
import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache'
import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { dispatchAddInstalledApps } from './addinstalledapps'
import { dispatchAddUsedTime } from './addusedtime'
import { dispatchAddUsedTimeVersion2 } from './addusedtime2'
import { dispatchFinishKeyRequestAction } from './finishkeyrequest'
import { dispatchForceSyncAction } from './forcesync'
import { dispatchMarkTaskPendingAction } from './marktaskpendingaction'
import { dispatchReplyToKeyRequestAction } from './replytokeyrequest'
import { dispatchRemoveInstalledApps } from './removeinstalledapps'
import { dispatchSendKeyRequestAction } from './sendkeyrequest'
import { dispatchSignOutAtDevice } from './signoutatdevice'
import { dispatchTriedDisablingDeviceAdmin } from './trieddisablingdeviceadmin'
import { dispatchUpdateAppActivities } from './updateappactivities'
import { dispatchUpdateDeviceStatus } from './updatedevicestatus'
import { dispatchUpdateInstalledApps } from './updateinstalledapps'
import { dispatchUploadDevicePublicKeyAction } from './uploaddevicepublickey'
@@ -59,7 +56,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
eventHandler: EventHandler
}) => {
if (action instanceof AddInstalledAppsAction) {
await dispatchAddInstalledApps({ deviceId, action, cache })
// do nothing
} else if (action instanceof AddUsedTimeAction) {
await dispatchAddUsedTime({ deviceId, action, cache })
} else if (action instanceof AddUsedTimeActionVersion2) {
@@ -73,7 +70,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof ReplyToKeyRequestAction) {
await dispatchReplyToKeyRequestAction({ deviceId, action, cache, eventHandler })
} else if (action instanceof RemoveInstalledAppsAction) {
await dispatchRemoveInstalledApps({ deviceId, action, cache })
// do nothing
} else if (action instanceof SendKeyRequestAction) {
await dispatchSendKeyRequestAction({ deviceId, action, cache })
} else if (action instanceof SignOutAtDeviceAction) {
@@ -81,7 +78,7 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof UpdateDeviceStatusAction) {
await dispatchUpdateDeviceStatus({ deviceId, action, cache })
} else if (action instanceof UpdateAppActivitiesAction) {
await dispatchUpdateAppActivities({ deviceId, action, cache })
// do nothing
} else if (action instanceof TriedDisablingDeviceAdminAction) {
await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache })
} else if (action instanceof UpdateInstalledAppsAction) {
@@ -1,99 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { chunk } from 'lodash'
import * as Sequelize from 'sequelize'
import { UpdateAppActivitiesAction } from '../../../../action'
import { AppActivityAttributes, maxActivityNameLength, maxPackageNameLength } from '../../../../database/appactivity'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception'
export async function dispatchUpdateAppActivities ({ deviceId, action, cache }: {
deviceId: string
action: UpdateAppActivitiesAction
cache: Cache
}) {
action.updatedOrAdded.forEach((app) => {
if (app.packageName.length > maxPackageNameLength) {
throw new ApplyActionException({
staticMessage: 'package name too long',
dynamicMessage: 'package name too long: ' + app.packageName
})
}
if (app.activityName.length > maxActivityNameLength) {
throw new ApplyActionException({
staticMessage: 'activity name too long',
dynamicMessage: 'activity name too long: ' + app.activityName
})
}
})
if (action.updatedOrAdded.length > 0) {
const chuncks = chunk(action.updatedOrAdded, 500)
for (const items of chuncks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
deviceId,
[Sequelize.Op.or]: (
items.map((item) => ({
packageName: item.packageName,
activityName: item.activityName
}))
)
},
transaction: cache.transaction
})
}
await cache.database.appActivity.bulkCreate(
action.updatedOrAdded.map((item): AppActivityAttributes => ({
familyId: cache.familyId,
deviceId,
packageName: item.packageName,
activityName: item.activityName,
title: item.title
})),
{ transaction: cache.transaction }
)
}
if (action.removed.length > 0) {
const chunks = chunk(action.removed, 500)
for (const items of chunks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
deviceId,
[Sequelize.Op.or]: (
items.map((item) => ({
packageName: item.packageName,
activityName: item.activityName
}))
)
},
transaction: cache.transaction
})
}
}
cache.devicesWithModifiedInstalledApps.add(deviceId)
cache.incrementTriggeredSyncLevel(1)
}
@@ -43,17 +43,5 @@ export async function dispatchUpdateInstalledApps ({ deviceId, action, cache }:
await upsert({ type: types.diff, data: action.diff })
}
if (action.wipe) {
await cache.database.app.destroy({
where: {
familyId: cache.familyId,
deviceId
},
transaction: cache.transaction
})
cache.devicesWithModifiedInstalledApps.add(deviceId)
}
cache.incrementTriggeredSyncLevel(1)
}
@@ -1,131 +0,0 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { difference, filter } from 'lodash'
import * as Sequelize from 'sequelize'
import { Database } from '../../../database'
import { ClientDataStatusApps } from '../../../object/clientdatastatus'
import { ServerInstalledAppsData } from '../../../object/serverdatastatus'
import { GetServerDataStatusIllegalStateException } from './exception'
import { FamilyEntry } from './family-entry'
export async function getAppList ({ database, transaction, familyEntry, appsStatus }: {
database: Database
transaction: Sequelize.Transaction
familyEntry: FamilyEntry
appsStatus: ClientDataStatusApps
}): Promise<Array<ServerInstalledAppsData> | null> {
const serverInstalledAppsVersions = (await database.device.findAll({
where: {
familyId: familyEntry.familyId
},
attributes: ['deviceId', 'installedAppsVersion'],
transaction
})).map((item) => ({
deviceId: item.deviceId,
installedAppsVersion: item.installedAppsVersion
}))
const getServerInstalledAppsVersionByDeviceId = (deviceId: string) => {
const entry = serverInstalledAppsVersions.find((item) => item.deviceId === deviceId)
if (!entry) {
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find device entry' })
}
return entry.installedAppsVersion
}
const serverDeviceIds = serverInstalledAppsVersions.map((item) => item.deviceId)
const clientDeviceIds = Object.keys(appsStatus)
const addedDeviceIds = difference(serverDeviceIds, clientDeviceIds)
const deviceIdsWhereInstalledAppsHaveChanged = filter(Object.keys(appsStatus), (deviceId) => {
const installedAppsVersion = appsStatus[deviceId]
const serverEntry = serverInstalledAppsVersions.find((item) => item.deviceId === deviceId)
return !!serverEntry && serverEntry.installedAppsVersion !== installedAppsVersion
})
const idsOfDevicesWhereInstalledAppsMustBeSynced = [...addedDeviceIds, ...deviceIdsWhereInstalledAppsHaveChanged]
if (idsOfDevicesWhereInstalledAppsMustBeSynced.length > 0) {
const [appsToSync, activitiesToSync] = await Promise.all([
(async () => {
return (await database.app.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: idsOfDevicesWhereInstalledAppsMustBeSynced
}
},
attributes: [
'deviceId',
'packageName',
'title',
'isLaunchable',
'recommendation'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
packageName: item.packageName,
title: item.title,
isLaunchable: item.isLaunchable,
recommendation: item.recommendation
}))
})(),
(async () => {
return (await database.appActivity.findAll({
where: {
familyId: familyEntry.familyId,
deviceId: {
[Sequelize.Op.in]: idsOfDevicesWhereInstalledAppsMustBeSynced
}
},
attributes: [
'deviceId',
'packageName',
'title',
'activityName'
],
transaction
})).map((item) => ({
deviceId: item.deviceId,
packageName: item.packageName,
activityName: item.activityName,
title: item.title
}))
})()
])
return idsOfDevicesWhereInstalledAppsMustBeSynced.map((deviceId): ServerInstalledAppsData => ({
deviceId,
apps: appsToSync.filter((item) => item.deviceId === deviceId).map((item) => ({
packageName: item.packageName,
title: item.title,
isLaunchable: item.isLaunchable,
recommendation: item.recommendation
})),
activities: activitiesToSync.filter((item) => item.deviceId === deviceId).map((item) => ({
p: item.packageName,
c: item.activityName,
t: item.title
})),
version: getServerInstalledAppsVersionByDeviceId(deviceId)
}))
} else return null // no changes
}
@@ -22,7 +22,6 @@ import { getStatusMessage } from '../../../function/statusmessage'
import { ClientDataStatus } from '../../../object/clientdatastatus'
import { ServerDataStatus } from '../../../object/serverdatastatus'
import { EventHandler } from '../../../monitoring/eventhandler'
import { getAppList } from './app-list'
import {
getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync,
getRules, getTasks, getUsedTimes
@@ -70,8 +69,6 @@ export const generateServerDataStatus = async ({
result.users = await getUserList({ database, transaction, familyEntry })
}
result.apps = await getAppList({ database, transaction, familyEntry, appsStatus: clientStatus.apps }) || undefined
const categoryDataToSync = await getCategoryDataToSync({ database, transaction, familyEntry, categoriesStatus: clientStatus.categories })
if (categoryDataToSync.removedCategoryIds.length > 0) {
+18 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2021 Jonas Lochmann
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -16,6 +16,7 @@
*/
import { Server } from 'http'
import { pid } from 'process'
import { createApi } from './api'
import { config } from './config'
import { VisibleConnectedDevicesManager } from './connected-devices'
@@ -61,7 +62,22 @@ async function main () {
pingInterval: config.pingInterval
})
server.listen(process.env.PORT || 8080)
const port = process.env.PORT || 8080
if (port === 'socketactivation') {
if (process.env.LISTEN_FDS !== '1') {
console.warn('expecting exactly one file descriptor for the socket activation')
process.exit(1)
} else if (process.env.LISTEN_PID !== pid.toString(10)) {
console.warn('expecting handover of file descriptors to this process for the socket activation')
process.exit(1)
}
// the sockets are passed using fd 3 + index (with index = 0 in this case)
server.listen({ fd: 3 })
} else {
server.listen(port)
}
console.log('ready')
}
+12
View File
@@ -28,6 +28,18 @@ export interface ClientDataStatus {
u2f?: string // last u2f list version
}
export function createEmptyClientDataStatus({ clientLevel }: {
clientLevel: number | null
}): ClientDataStatus {
return {
devices: '',
apps: {},
categories: {},
users: '',
clientLevel: clientLevel || undefined
}
}
export type ClientDataStatusApps = {[key: string]: string} // installedAppsVersionsByDeviceId
export type ClientDataStatusCategories = {[key: string]: CategoryDataStatus}
export type ClientDataStatusDevicesExtended = {[key: string]: DeviceDataStatus}