Refactor exception usage

This commit is contained in:
Jonas Lochmann
2020-09-28 02:00:00 +02:00
parent d68b425e0e
commit 8d65c5d777
148 changed files with 2061 additions and 769 deletions
@@ -0,0 +1,72 @@
/*
* 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 { Unauthorized } from 'http-errors'
import { Database, Transaction } from '../../../database'
import { SourceFamilyNotFoundException } from './exception/illegal-state'
export interface ApplyActionBaseInfo {
familyId: string
deviceId: string
nextSequenceNumber: number
hasFullVersion: boolean
}
export async function getApplyActionBaseInfo ({ database, transaction, deviceAuthToken }: {
database: Database
transaction: Transaction
deviceAuthToken: string
}): Promise<ApplyActionBaseInfo> {
const deviceEntryUnsafe = await database.device.findOne({
where: { deviceAuthToken },
attributes: ['familyId', 'deviceId', 'nextSequenceNumber'],
transaction
})
if (!deviceEntryUnsafe) {
throw new Unauthorized()
}
const deviceEntry = {
familyId: deviceEntryUnsafe.familyId,
deviceId: deviceEntryUnsafe.deviceId,
nextSequenceNumber: deviceEntryUnsafe.nextSequenceNumber
}
const familyEntryUnsafe = await database.family.findOne({
where: {
familyId: deviceEntry.familyId
},
transaction,
attributes: ['hasFullVersion']
})
if (!familyEntryUnsafe) {
throw new SourceFamilyNotFoundException()
}
const familyEntry = {
hasFullVersion: familyEntryUnsafe.hasFullVersion
}
return {
familyId: deviceEntry.familyId,
deviceId: deviceEntry.deviceId,
nextSequenceNumber: deviceEntry.nextSequenceNumber,
hasFullVersion: familyEntry.hasFullVersion
}
}
+5 -3
View File
@@ -21,6 +21,8 @@ import { config } from '../../../config'
import { VisibleConnectedDevicesManager } from '../../../connected-devices'
import { Database } from '../../../database'
import { generateVersionId } from '../../../util/token'
import { SourceUserNotFoundException } from './exception/illegal-state'
import { InvalidChildActionIntegrityValue } from './exception/integrity'
export class Cache {
readonly familyId: string
@@ -84,7 +86,7 @@ export class Cache {
})
if (!userEntryUnsafe) {
throw new Error('user not found')
throw new SourceUserNotFoundException()
}
return userEntryUnsafe.secondPasswordHash
@@ -102,11 +104,11 @@ export class Cache {
})
if (!userEntryUnsafe) {
throw new Error('user not found')
throw new SourceUserNotFoundException()
}
if (!userEntryUnsafe.secondPasswordHash) {
throw new Error('user does not have a password')
throw new InvalidChildActionIntegrityValue()
}
return userEntryUnsafe.secondPasswordHash
@@ -19,6 +19,7 @@ import * as Sequelize from 'sequelize'
import { AddUsedTimeAction } from '../../../../action'
import { MinuteOfDay } from '../../../../util/minuteofday'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export const getRoundedTimestamp = () => {
const now = Date.now()
@@ -29,7 +30,7 @@ export const getRoundedTimestamp = () => {
const dayLengthInMinutes = MinuteOfDay.LENGTH
const dayLengthInMs = dayLengthInMinutes * 1000 * 60
export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
export async function dispatchAddUsedTime ({ action, cache }: {
deviceId: string
action: AddUsedTimeAction
cache: Cache
@@ -50,7 +51,7 @@ export async function dispatchAddUsedTime ({ deviceId, action, cache }: {
})
// verify that the category exists
if (!categoryEntryUnsafe) {
throw new Error('invalid category id')
throw new MissingCategoryException()
}
const categoryEntry = {
@@ -20,6 +20,7 @@ import { AddUsedTimeActionVersion2 } from '../../../../action'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { MinuteOfDay } from '../../../../util/minuteofday'
import { Cache } from '../cache'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
import { getRoundedTimestamp as getRoundedTimestampForUsedTime } from './addusedtime'
export const getRoundedTimestampForSessionDuration = () => {
@@ -44,7 +45,7 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
})
if (!deviceEntryUnsafe) {
throw new Error('source device not found')
throw new SourceDeviceNotFoundException()
}
const deviceEntry = {
@@ -56,9 +57,7 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
let addUsedTimeForADifferentUserThanTheCurrentUserOfTheDevice = false
for (let i = 0; i < action.items.length; i++) {
const item = action.items[i]
for (const item of action.items) {
const categoryEntryUnsafe = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
@@ -73,9 +72,10 @@ export async function dispatchAddUsedTimeVersion2 ({ deviceId, action, cache, ev
// verify that the category exists
if (!categoryEntryUnsafe) {
eventHandler.countEvent('add used time category to add time for not found')
cache.requireFullSync()
return
continue
}
const categoryEntry = {
@@ -29,6 +29,7 @@ import {
} from '../../../../action'
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'
@@ -64,6 +65,6 @@ export const dispatchAppLogicAction = async ({ action, deviceId, cache, eventHan
} else if (action instanceof TriedDisablingDeviceAdminAction) {
await dispatchTriedDisablingDeviceAdmin({ deviceId, action, cache })
} else {
throw new Error('unsupported action type')
throw new ActionObjectTypeNotHandledException()
}
}
@@ -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,14 +18,16 @@
import { SetDeviceUserAction, SignOutAtDeviceAction } from '../../../../action'
import { Cache } from '../cache'
import { dispatchSetDeviceUser } from '../dispatch-parent-action/setdeviceuser'
import { IllegalStateException, SourceDeviceNotFoundException } from '../exception/illegal-state'
import { PremiumVersionMissingException } from '../exception/premium'
export async function dispatchSignOutAtDevice ({ deviceId, action, cache }: {
export async function dispatchSignOutAtDevice ({ deviceId, cache }: {
deviceId: string
action: SignOutAtDeviceAction
cache: Cache
}) {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
const deviceEntry = await cache.database.device.findOne({
@@ -37,11 +39,13 @@ export async function dispatchSignOutAtDevice ({ deviceId, action, cache }: {
})
if (!deviceEntry) {
throw new Error('illegal state: missing device which dispatched the action')
throw new SourceDeviceNotFoundException()
}
if (deviceEntry.defaultUserId === '') {
throw new Error('no default user available')
throw new IllegalStateException({
staticMessage: 'tried to switch to the default user where it does not exist'
})
}
if (deviceEntry.currentUserId !== deviceEntry.defaultUserId) {
@@ -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,8 +19,9 @@ import { TriedDisablingDeviceAdminAction } from '../../../../action'
import { hasDeviceManipulation } from '../../../../database/device'
import { sendManipulationWarnings } from '../../../warningmail/manipulation'
import { Cache } from '../cache'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
export async function dispatchTriedDisablingDeviceAdmin ({ deviceId, action, cache }: {
export async function dispatchTriedDisablingDeviceAdmin ({ deviceId, cache }: {
deviceId: string
action: TriedDisablingDeviceAdminAction
cache: Cache
@@ -34,7 +35,7 @@ export async function dispatchTriedDisablingDeviceAdmin ({ deviceId, action, cac
})
if (deviceEntry === null) {
throw new Error('illegal state: missing device which dispatched the action')
throw new SourceDeviceNotFoundException()
}
const hadManipulationBefore = hasDeviceManipulation(deviceEntry)
@@ -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
@@ -29,9 +29,7 @@ export async function dispatchUpdateAppActivities ({ deviceId, action, cache }:
if (action.updatedOrAdded.length > 0) {
const chuncks = chunk(action.updatedOrAdded, 500)
for (let i = 0; i < chuncks.length; i++) {
const items = chuncks[i]
for (const items of chuncks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
@@ -62,9 +60,7 @@ export async function dispatchUpdateAppActivities ({ deviceId, action, cache }:
if (action.removed.length > 0) {
const chunks = chunk(action.removed, 500)
for (let i = 0; i < chunks.length; i++) {
const items = chunks[i]
for (const items of chunks) {
await cache.database.appActivity.destroy({
where: {
familyId: cache.familyId,
@@ -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
@@ -23,6 +23,7 @@ import { runtimePermissionStatusValues } from '../../../../model/runtimepermissi
import { enumMax } from '../../../../util/enum'
import { sendManipulationWarnings } from '../../../warningmail/manipulation'
import { Cache } from '../cache'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
export async function dispatchUpdateDeviceStatus ({ deviceId, action, cache }: {
deviceId: string
@@ -38,7 +39,7 @@ export async function dispatchUpdateDeviceStatus ({ deviceId, action, cache }: {
})
if (!deviceEntry) {
throw new Error('device not found')
throw new SourceDeviceNotFoundException()
}
const hadManipulationBefore = hasDeviceManipulation(deviceEntry)
@@ -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,6 +18,7 @@
import * as Sequelize from 'sequelize'
import { ChildChangePasswordAction } from '../../../../action'
import { Cache } from '../cache'
import { SourceUserNotFoundException } from '../exception/illegal-state'
export const dispatchChildChangePassword = async ({ action, childUserId, cache }: {
action: ChildChangePasswordAction
@@ -35,7 +36,7 @@ export const dispatchChildChangePassword = async ({ action, childUserId, cache }
})
if (!childEntry) {
throw new Error('child entry not found')
throw new SourceUserNotFoundException()
}
childEntry.passwordHash = action.password.hash
@@ -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,15 +18,17 @@
import { ChildSignInAction, SetDeviceUserAction } from '../../../../action'
import { Cache } from '../cache'
import { dispatchSetDeviceUser } from '../dispatch-parent-action/setdeviceuser'
import { SourceUserNotFoundException } from '../exception/illegal-state'
import { PremiumVersionMissingException } from '../exception/premium'
export const dispatchChildSignIn = async ({ action, deviceId, childUserId, cache }: {
export const dispatchChildSignIn = async ({ deviceId, childUserId, cache }: {
action: ChildSignInAction
deviceId: string
childUserId: string
cache: Cache
}) => {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
await dispatchSetDeviceUser({
@@ -50,7 +52,7 @@ export const dispatchChildSignIn = async ({ action, deviceId, childUserId, cache
})
if (!userEntryUnsafe) {
throw new Error('illegal state')
throw new SourceUserNotFoundException()
}
if (userEntryUnsafe.currentDevice === deviceId) {
@@ -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
@@ -21,6 +21,7 @@ import {
ChildSignInAction
} from '../../../../action'
import { Cache } from '../cache'
import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { dispatchChildChangePassword } from './childchangepassword'
import { dispatchChildSignIn } from './childsignin'
@@ -35,6 +36,6 @@ export const dispatchChildAction = async ({ action, deviceId, childUserId, cache
} else if (action instanceof ChildSignInAction) {
await dispatchChildSignIn({ action, childUserId, deviceId, cache })
} else {
throw new Error('unsupported action type')
throw new ActionObjectTypeNotHandledException()
}
}
@@ -0,0 +1,42 @@
/*
* 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 { parseAppLogicAction } from '../../../../action/serialization'
import { ClientPushChangesRequestAction } from '../../../../api/schema'
import { isSerializedAppLogicAction } from '../../../../api/validator'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache'
import { dispatchAppLogicAction as dispatchAppLogicActionInternal } from '../dispatch-app-logic-action'
import { dispatch } from './helper'
export async function dispatchAppLogicAction ({ action, eventHandler, deviceId, cache }: {
action: ClientPushChangesRequestAction
deviceId: string
cache: Cache
eventHandler: EventHandler
}) {
return dispatch({
action,
eventHandler,
type: 'app logic',
validator: isSerializedAppLogicAction,
parser: parseAppLogicAction,
applier: async (action) => {
await dispatchAppLogicActionInternal({ action, cache, eventHandler, deviceId })
}
})
}
@@ -0,0 +1,43 @@
/*
* 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 { parseChildAction } from '../../../../action/serialization'
import { ClientPushChangesRequestAction } from '../../../../api/schema'
import { isSerializedChildAction } from '../../../../api/validator'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache'
import { dispatchChildAction as dispatchChildActionInternal } from '../dispatch-child-action'
import { dispatch } from './helper'
export async function dispatchChildAction ({ action, eventHandler, deviceId, cache, childUserId }: {
action: ClientPushChangesRequestAction
deviceId: string
cache: Cache
eventHandler: EventHandler
childUserId: string
}) {
return dispatch({
action,
eventHandler,
type: 'child',
validator: isSerializedChildAction,
parser: parseChildAction,
applier: async (action) => {
await dispatchChildActionInternal({ action, cache, deviceId, childUserId })
}
})
}
@@ -0,0 +1,54 @@
/*
* 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 { ClientPushChangesRequestAction } from '../../../../api/schema'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { ApplyActionException } from '../exception/index'
import { EncodedActionSchemaMismatchException } from '../exception/invalidaction'
import { parseEncodedAction } from '../parse-encoded-action'
export async function dispatch<T1 extends { type: string }, T2> ({ type, action, validator, parser, applier, eventHandler }: {
type: 'app logic' | 'parent' | 'child'
action: ClientPushChangesRequestAction
validator: (input: any) => input is T1
parser: (input: T1) => T2
applier: (input: T2) => Promise<void>
eventHandler: EventHandler
}) {
const parsedSerializedAction = parseEncodedAction(action)
if (!validator(parsedSerializedAction)) {
throw new EncodedActionSchemaMismatchException({ type, action: parsedSerializedAction })
}
const actionType = parsedSerializedAction.type
try {
const parsedAction = parser(parsedSerializedAction)
await applier(parsedAction)
eventHandler.countEvent('dispatched action:' + actionType)
} catch (ex) {
if (ex instanceof ApplyActionException) {
throw new ApplyActionException({
staticMessage: 'error during dispatching ' + actionType + ': ' + ex.staticMessage,
dynamicMessage: ex.dynamicMessage ? 'error during dispatching ' + actionType + ': ' + ex.dynamicMessage : undefined
})
} else throw ex
}
}
@@ -0,0 +1,20 @@
/*
* 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 { dispatchAppLogicAction } from './app-logic-action'
export { dispatchChildAction } from './child-action'
export { dispatchParentAction } from './parent-action'
@@ -0,0 +1,106 @@
/*
* 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 { parseParentAction } from '../../../../action/serialization'
import { ClientPushChangesRequestAction } from '../../../../api/schema'
import { isSerializedParentAction } from '../../../../api/validator'
import { UserFlags } from '../../../../model/userflags'
import { EventHandler } from '../../../../monitoring/eventhandler'
import { Cache } from '../cache'
import { dispatchParentAction as dispatchParentActionInternal } from '../dispatch-parent-action'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
import { SelfLimitNotPossibleException } from '../exception/self-limit'
import { dispatch } from './helper'
export async function dispatchParentAction ({ action, eventHandler, cache, isChildLimitAdding, deviceId }: {
action: ClientPushChangesRequestAction
cache: Cache
eventHandler: EventHandler
isChildLimitAdding: boolean
deviceId: string
}) {
return dispatch({
action,
eventHandler,
type: 'parent',
validator: isSerializedParentAction,
parser: parseParentAction,
applier: async (parsedAction) => {
if (isChildLimitAdding) {
const deviceEntryUnsafe = await cache.database.device.findOne({
attributes: ['currentUserId'],
where: {
familyId: cache.familyId,
deviceId,
currentUserId: action.userId
},
transaction: cache.transaction
})
if (!deviceEntryUnsafe) {
throw new SourceDeviceNotFoundException()
}
const deviceUserId = deviceEntryUnsafe.currentUserId
if (!deviceUserId) {
throw new SelfLimitNotPossibleException({
staticMessage: '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 SelfLimitNotPossibleException({
staticMessage: '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 SelfLimitNotPossibleException({
staticMessage: 'child add limit action found but not allowed'
})
}
await dispatchParentActionInternal({
action: parsedAction,
cache,
parentUserId: action.userId,
sourceDeviceId: deviceId,
fromChildSelfLimitAddChildUserId: deviceUserId
})
} else {
await dispatchParentActionInternal({
action: parsedAction,
cache,
parentUserId: action.userId,
sourceDeviceId: deviceId,
fromChildSelfLimitAddChildUserId: null
})
}
}
})
}
@@ -18,8 +18,11 @@
import * as Sequelize from 'sequelize'
import { AddCategoryAppsAction } from '../../../../action'
import { CategoryAppAttributes } from '../../../../database/categoryapp'
import { getCategoryWithParentCategories } from '../../../../util/category'
import { getCategoryWithParentCategories, GetParentCategoriesException } from '../../../../util/category'
import { Cache } from '../cache'
import { SourceUserNotFoundException } from '../exception/illegal-state'
import { MissingCategoryException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException, SelfLimitationException } from '../exception/self-limit'
export async function dispatchAddCategoryApps ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: AddCategoryAppsAction
@@ -36,14 +39,14 @@ export async function dispatchAddCategoryApps ({ action, cache, fromChildSelfLim
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id')
throw new MissingCategoryException()
}
const { childId } = categoryEntryUnsafe
if (fromChildSelfLimitAddChildUserId !== null) {
if (childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not add apps to other users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
}
@@ -77,64 +80,74 @@ export async function dispatchAddCategoryApps ({ action, cache, fromChildSelfLim
}).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' ],
try {
const parentCategoriesOfTargetCategory = getCategoryWithParentCategories(categoriesOfSameChild, action.categoryId)
const userEntryUnsafe = await cache.database.user.findOne({
attributes: [ 'categoryForNotAssignedApps' ],
where: {
familyId: cache.familyId,
categoryId: {
[Sequelize.Op.in]: userCategoryIds
},
packageName: packageName
userId: fromChildSelfLimitAddChildUserId
},
transaction: cache.transaction
})
const categoryAppEntry = categoryAppEntryUnsafe ? { categoryId: categoryAppEntryUnsafe.categoryId } : null
if (!userEntryUnsafe) {
throw new SourceUserNotFoundException()
}
if (categoryAppEntry === null) {
if ((isApp && allowUnassignedElements) || (!isApp)) {
// allow
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 SelfLimitationException({
staticMessage: 'can not assign apps without category as child'
})
}
} 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')
if (parentCategoriesOfTargetCategory.indexOf(categoryAppEntry.categoryId) !== -1) {
// allow
} else {
throw new SelfLimitationException({
staticMessage: 'can not add app which is not contained in the parent category as child'
})
}
}
}
}
for (let i = 0; i < action.packageNames.length; i++) {
const packageName = action.packageNames[i]
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 (packageName.indexOf(':') !== -1) {
await assertCanAddApp(packageName.substring(0, packageName.indexOf(':')), true)
await assertCanAddApp(packageName, false)
} else {
await assertCanAddApp(packageName, true)
}
}
} catch (ex) {
if (ex instanceof GetParentCategoriesException) {
throw new MissingCategoryException()
} else throw ex
}
}
@@ -18,6 +18,8 @@
import { AddCategoryNetworkIdAction } from '../../../../action'
import { maxNetworkIdsPerCategory } from '../../../../database/categorynetworkid'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchAddCategoryNetworkId ({ action, cache }: {
action: AddCategoryNetworkIdAction
@@ -33,7 +35,7 @@ export async function dispatchAddCategoryNetworkId ({ action, cache }: {
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
throw new MissingCategoryException()
}
const count = await cache.database.categoryNetworkId.count({
@@ -45,7 +47,9 @@ export async function dispatchAddCategoryNetworkId ({ action, cache }: {
})
if (count + 1 > maxNetworkIdsPerCategory) {
throw new Error('category network limit reached')
throw new ApplyActionException({
staticMessage: 'can not add a category network id because the category network limit reached'
})
}
const hasOldItem = (await cache.database.categoryNetworkId.count({
@@ -58,7 +62,9 @@ export async function dispatchAddCategoryNetworkId ({ action, cache }: {
})) !== 0
if (hasOldItem) {
throw new Error('id already used')
throw new ApplyActionException({
staticMessage: 'can not add a category network id because the id is already used'
})
}
await cache.database.categoryNetworkId.create({
@@ -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
@@ -16,8 +16,10 @@
*/
import * as Sequelize from 'sequelize'
import { ChangeParentPasswordAction } from '../../../../action'
import { ChangeParentPasswordAction, InvalidChangeParentPasswordIntegrityException } from '../../../../action/changeparentpassword'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchChangeParentPassword ({ action, cache }: {
action: ChangeParentPasswordAction
@@ -34,10 +36,17 @@ export async function dispatchChangeParentPassword ({ action, cache }: {
})
if (!parentEntry) {
throw new Error('parent entry not found')
throw new MissingUserException()
}
try {
action.assertIntegrityValid({ oldPasswordSecondHash: parentEntry.secondPasswordHash })
} catch (ex) {
if (ex instanceof InvalidChangeParentPasswordIntegrityException) {
throw new ApplyActionException({ staticMessage: 'invalid new password integrity' })
} else throw ex
}
action.assertIntegrityValid({ oldPasswordSecondHash: parentEntry.secondPasswordHash })
const newSecondPasswordHash = action.decryptSecondHash({ oldPasswordSecondHash: parentEntry.secondPasswordHash })
parentEntry.passwordHash = action.newPasswordFirstHash
@@ -18,6 +18,8 @@
import { CreateCategoryAction } from '../../../../action'
import { generateVersionId } from '../../../../util/token'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException } from '../exception/self-limit'
export async function dispatchCreateCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: CreateCategoryAction
@@ -26,7 +28,7 @@ export async function dispatchCreateCategory ({ action, cache, fromChildSelfLimi
}) {
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== action.childId) {
throw new Error('can not create categories for other child users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
}
@@ -41,7 +43,7 @@ export async function dispatchCreateCategory ({ action, cache, fromChildSelfLimi
})
if (!childEntry) {
throw new Error('missing child for new category')
throw new MissingUserException()
}
const oldMaxSort: number = await cache.database.category.max('sort', {
@@ -17,6 +17,8 @@
import { CreateTimeLimitRuleAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException } from '../exception/self-limit'
export async function dispatchCreateTimeLimitRule ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: CreateTimeLimitRuleAction
@@ -33,12 +35,12 @@ export async function dispatchCreateTimeLimitRule ({ action, cache, fromChildSel
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
throw new MissingCategoryException()
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) {
throw new Error('can not add rules for other users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
}
@@ -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,15 +17,25 @@
import { DeleteCategoryAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchDeleteCategory ({ action, cache }: {
action: DeleteCategoryAction
cache: Cache
}) {
// no version number needs to be updated
const { familyId, transaction } = cache
const { categoryId } = action
const categoryEntry = await cache.database.category.findOne({
where: {
familyId,
categoryId
},
transaction
})
if (!categoryEntry) { throw new MissingCategoryException() }
await cache.database.timelimitRule.destroy({
where: {
familyId,
@@ -75,4 +85,6 @@ export async function dispatchDeleteCategory ({ action, cache }: {
if (affectedUserRows !== 0) {
cache.invalidiateUserList = true
}
// no version number needs to be updated
}
@@ -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 { DeleteTimeLimitRuleAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingRuleException } from '../exception/missing-item'
export async function dispatchDeleteTimeLimitRule ({ action, cache }: {
action: DeleteTimeLimitRuleAction
@@ -30,10 +31,12 @@ export async function dispatchDeleteTimeLimitRule ({ action, cache }: {
transaction: cache.transaction
})
if (ruleEntry) {
await ruleEntry.destroy({ transaction: cache.transaction })
cache.categoriesWithModifiedTimeLimitRules.push(ruleEntry.categoryId)
cache.areChangesImportant = true
if (!ruleEntry) {
throw new MissingRuleException()
}
await ruleEntry.destroy({ transaction: cache.transaction })
cache.categoriesWithModifiedTimeLimitRules.push(ruleEntry.categoryId)
cache.areChangesImportant = true
}
@@ -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 { IgnoreManipulationAction } from '../../../../action'
import { Cache } from '../cache'
import { SourceDeviceNotFoundException } from '../exception/illegal-state'
export async function dispatchIgnoreManipulation ({ action, cache }: {
action: IgnoreManipulationAction
@@ -31,7 +32,7 @@ export async function dispatchIgnoreManipulation ({ action, cache }: {
})
if (deviceEntry === null) {
throw new Error('illegal state: missing device which dispatched the action')
throw new SourceDeviceNotFoundException()
}
if (action.ignoreDeviceAdminManipulation) {
@@ -18,13 +18,15 @@
import { IncrementCategoryExtraTimeAction } from '../../../../action'
import { CategoryModel } from '../../../../database/category'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { PremiumVersionMissingException } from '../exception/premium'
export async function dispatchIncrementCategoryExtraTime ({ action, cache }: {
action: IncrementCategoryExtraTimeAction
cache: Cache
}) {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
async function handleCategory (category: CategoryModel) {
@@ -51,7 +53,7 @@ export async function dispatchIncrementCategoryExtraTime ({ action, cache }: {
})
if (!categoryEntry) {
throw new Error(`tried to add extra time to ${action.categoryId} but it does not exist`)
throw new MissingCategoryException()
}
await handleCategory(categoryEntry)
@@ -62,6 +62,8 @@ import {
UpdateUserLimitLoginCategory
} from '../../../../action'
import { Cache } from '../cache'
import { ActionObjectTypeNotHandledException } from '../exception/illegal-state'
import { ActionNotSupportedBySelfLimitationException } from '../exception/self-limit'
import { dispatchAddCategoryApps } from './addcategoryapps'
import { dispatchAddCategoryNetworkId } from './addcategorynetworkid'
import { dispatchAddUser } from './adduser'
@@ -129,7 +131,9 @@ export const dispatchParentAction = async ({ action, cache, parentUserId, source
return dispatchUpdateCategoryBlockedTimes({ action, cache, fromChildSelfLimitAddChildUserId })
}
if (fromChildSelfLimitAddChildUserId === null) {
if (fromChildSelfLimitAddChildUserId !== null) {
throw new ActionNotSupportedBySelfLimitationException()
} else {
if (action instanceof AddCategoryNetworkIdAction) {
return dispatchAddCategoryNetworkId({ action, cache })
} else if (action instanceof AddUserAction) {
@@ -202,8 +206,8 @@ export const dispatchParentAction = async ({ action, cache, parentUserId, source
return dispatchUpdateUserFlagsAction({ action, cache })
} else if (action instanceof UpdateUserLimitLoginCategory) {
return dispatchUpdateUserLimitLoginCategoryAction({ action, cache, parentUserId })
} else {
throw new ActionObjectTypeNotHandledException()
}
} else {
throw new Error('unsupported action type')
}
}
@@ -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,6 +18,7 @@
import * as Sequelize from 'sequelize'
import { RemoveCategoryAppsAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingItemException } from '../exception/missing-item'
export async function dispatchRemoveCategoryApps ({ action, cache }: {
action: RemoveCategoryAppsAction
@@ -35,7 +36,9 @@ export async function dispatchRemoveCategoryApps ({ action, cache }: {
})
if (affectedRows !== action.packageNames.length) {
throw new Error('could not delete as much entries as requested')
throw new MissingItemException({
staticMessage: 'could not remove all requested category app items'
})
}
cache.categoriesWithModifiedApps.push(action.categoryId)
@@ -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
@@ -21,6 +21,9 @@ import { difference } from 'lodash'
import * as Sequelize from 'sequelize'
import { RemoveUserAction } from '../../../../action'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { ApplyActionIntegrityException } from '../exception/integrity'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
action: RemoveUserAction
@@ -36,7 +39,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
})
if (!user) {
throw new Error('invalid user id')
throw new MissingUserException()
}
if (user.type === 'parent') {
@@ -45,7 +48,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
}
if (parentUserId === action.userId) {
throw new Error('users can not delete themself')
throw new ApplyActionException({ staticMessage: 'users can not delete themself' })
}
const expectedIntegrityValue = createHash('sha512').update(
@@ -53,7 +56,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
).digest('hex').substring(0, 16)
if (expectedIntegrityValue !== action.authentication) {
throw new Error('invalid authentication value')
throw new ApplyActionIntegrityException({ staticMessage: 'invalid authentication value for removing a user' })
}
if (user.mail !== '') {
@@ -69,7 +72,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
})
if (usersWithLinkedMail <= 1) {
throw new Error('this user is the last one with a linked mail address')
throw new ApplyActionException({ staticMessage: 'this user is the last one with a linked mail address' })
}
}
@@ -93,7 +96,7 @@ export async function dispatchRemoveUser ({ action, cache, parentUserId }: {
const allOtherParentUserIds = allParentUserIds.filter((item) => item !== action.userId)
if (difference(allOtherParentUserIds, usersWithLimitLoginCategories).length === 0) {
throw new Error('can not delete the last user without limit login category')
throw new ApplyActionException({ staticMessage: 'can not delete the last user without limit login category' })
}
}
@@ -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,12 +17,26 @@
import { RenameChildAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchRenameChild ({ action, cache }: {
action: RenameChildAction
cache: Cache
}) {
const [affectedRows] = await cache.database.user.update({
const oldItem = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.childId,
type: 'child'
},
transaction: cache.transaction
})
if (!oldItem) {
throw new MissingUserException()
}
await cache.database.user.update({
name: action.newName
}, {
where: {
@@ -33,10 +47,6 @@ export async function dispatchRenameChild ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows !== 1) {
throw new Error('can not update child name if child does not exist')
}
cache.invalidiateUserList = true
cache.doesUserExist.cache.set(action.childId, false)
}
@@ -17,6 +17,7 @@
import { ResetCategoryNetworkIdsAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchResetCategoryNetworkIds ({ action, cache }: {
action: ResetCategoryNetworkIdsAction
@@ -32,7 +33,7 @@ export async function dispatchResetCategoryNetworkIds ({ action, cache }: {
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for new rule')
throw new MissingCategoryException()
}
await cache.database.categoryNetworkId.destroy({
@@ -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,12 +17,26 @@
import { ResetParentBlockedTimesAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchResetParentBlockedTimes ({ action, cache }: {
action: ResetParentBlockedTimesAction
cache: Cache
}) {
const [affectedRows] = await cache.database.user.update({
const oldItem = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.parentId,
type: 'parent'
},
transaction: cache.transaction
})
if (!oldItem) {
throw new MissingUserException()
}
await cache.database.user.update({
blockedTimes: ''
}, {
where: {
@@ -33,10 +47,6 @@ export async function dispatchResetParentBlockedTimes ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows === 0) {
throw new Error('invalid parent user id provided')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -17,16 +17,30 @@
import { SetCategoryExtraTimeAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { PremiumVersionMissingException } from '../exception/premium'
export async function dispatchSetCategoryExtraTime ({ action, cache }: {
action: SetCategoryExtraTimeAction
cache: Cache
}) {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
const [affectedRows] = await cache.database.category.update({
const oldItem = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction
})
if (!oldItem) {
throw new MissingCategoryException()
}
await cache.database.category.update({
extraTimeInMillis: action.newExtraTime,
extraTimeDay: action.day
}, {
@@ -37,8 +51,6 @@ export async function dispatchSetCategoryExtraTime ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows !== 0) {
cache.categoriesWithModifiedBaseData.push(action.categoryId)
cache.areChangesImportant = true
}
cache.categoriesWithModifiedBaseData.push(action.categoryId)
cache.areChangesImportant = true
}
@@ -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,11 +17,26 @@
import { SetCategoryForUnassignedAppsAction } from '../../../../action'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchSetCategoryForUnassignedApps ({ action, cache }: {
action: SetCategoryForUnassignedAppsAction
cache: Cache
}) {
const oldUserEntry = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.childId,
type: 'child'
},
transaction: cache.transaction
})
if (!oldUserEntry) {
throw new MissingUserException()
}
if (action.categoryId === '') {
// nothing to check
} else {
@@ -35,7 +50,9 @@ export async function dispatchSetCategoryForUnassignedApps ({ action, cache }: {
})
if (!categoryEntryUnsafe) {
throw new Error('can not set a category which does not exist as category for unassigned apps')
throw new ApplyActionException({
staticMessage: 'can not set a category which does not exist as category for unassigned apps'
})
}
const categoryEntry = {
@@ -43,11 +60,13 @@ export async function dispatchSetCategoryForUnassignedApps ({ action, cache }: {
}
if (categoryEntry.childId !== action.childId) {
throw new Error('can not set a category of one child as category for unassigned apps for an other child')
throw new ApplyActionException({
staticMessage: 'can not set a category of one child as category for unassigned apps for an other child'
})
}
}
const [affectedRows] = await cache.database.user.update({
await cache.database.user.update({
categoryForNotAssignedApps: action.categoryId
}, {
where: {
@@ -58,10 +77,6 @@ export async function dispatchSetCategoryForUnassignedApps ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows !== 1) {
throw new Error('could not find a child with matching id for setting the category for not assigned apps')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -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,6 +18,7 @@
import * as Sequelize from 'sequelize'
import { SetChildPasswordAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchSetChildPassword ({ action, cache }: {
action: SetChildPasswordAction
@@ -34,7 +35,7 @@ export async function dispatchSetChildPassword ({ action, cache }: {
})
if (!childEntry) {
throw new Error('parent entry not found')
throw new MissingUserException()
}
childEntry.passwordHash = action.newPassword.hash
@@ -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,12 +17,25 @@
import { SetConsiderRebootManipulationAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchSetConsiderRebootManipulation ({ action, cache }: {
action: SetConsiderRebootManipulationAction
cache: Cache
}) {
const [affectedRows] = await cache.database.device.update({
const oldDevice = await cache.database.device.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
deviceId: action.deviceId
}
})
if (!oldDevice) {
throw new MissingDeviceException()
}
await cache.database.device.update({
considerRebootManipulation: action.enable
}, {
transaction: cache.transaction,
@@ -32,10 +45,6 @@ export async function dispatchSetConsiderRebootManipulation ({ action, cache }:
}
})
if (affectedRows === 0) {
throw new Error('did not find device to update consider reboot manipulation')
}
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -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 { SetDeviceDefaultUserAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException, MissingUserException } from '../exception/missing-item'
export async function dispatchSetDeviceDefaultUser ({ action, cache }: {
action: SetDeviceDefaultUserAction
@@ -26,11 +27,23 @@ export async function dispatchSetDeviceDefaultUser ({ action, cache }: {
const doesUserExist = await cache.doesUserExist(action.defaultUserId)
if (!doesUserExist) {
throw new Error('can not set invalid user as default user')
throw new MissingUserException()
}
}
const [affectedRows] = await cache.database.device.update({
const oldDeviceItem = await cache.database.device.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
deviceId: action.deviceId
}
})
if (!oldDeviceItem) {
throw new MissingDeviceException()
}
await cache.database.device.update({
defaultUserId: action.defaultUserId
}, {
transaction: cache.transaction,
@@ -40,10 +53,6 @@ export async function dispatchSetDeviceDefaultUser ({ action, cache }: {
}
})
if (affectedRows === 0) {
throw new Error('did not find device to update default user')
}
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -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,12 +17,25 @@
import { SetDeviceDefaultUserTimeoutAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchSetDeviceDefaultUserTimeout ({ action, cache }: {
action: SetDeviceDefaultUserTimeoutAction
cache: Cache
}) {
const [affectedRows] = await cache.database.device.update({
const oldDeviceItem = await cache.database.device.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
deviceId: action.deviceId
}
})
if (!oldDeviceItem) {
throw new MissingDeviceException()
}
await cache.database.device.update({
defaultUserTimeout: action.timeout
}, {
transaction: cache.transaction,
@@ -32,10 +45,6 @@ export async function dispatchSetDeviceDefaultUserTimeout ({ action, cache }: {
}
})
if (affectedRows === 0) {
throw new Error('did not find device to update default user timeout')
}
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -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 { SetDeviceUserAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException, MissingUserException } from '../exception/missing-item'
export async function dispatchSetDeviceUser ({ action, cache }: {
action: SetDeviceUserAction
@@ -26,11 +27,23 @@ export async function dispatchSetDeviceUser ({ action, cache }: {
const doesUserExist = await cache.doesUserExist(action.userId)
if (!doesUserExist) {
throw new Error('invalid user id provided')
throw new MissingUserException()
}
}
const [affectedRows] = await cache.database.device.update({
const oldDeviceItem = await cache.database.device.findOne({
where: {
familyId: cache.familyId,
deviceId: action.deviceId
},
transaction: cache.transaction
})
if (!oldDeviceItem) {
throw new MissingDeviceException()
}
await cache.database.device.update({
currentUserId: action.userId,
isUserKeptSignedIn: false
}, {
@@ -41,8 +54,6 @@ export async function dispatchSetDeviceUser ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows !== 0) {
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -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,9 @@
import { SetKeepSignedInAction } from '../../../../action'
import { Cache } from '../cache'
import { SourceUserNotFoundException } from '../exception/illegal-state'
import { ApplyActionException } from '../exception/index'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchSetKeepSignedIn ({ action, cache, parentUserId }: {
action: SetKeepSignedInAction
@@ -26,7 +29,7 @@ export async function dispatchSetKeepSignedIn ({ action, cache, parentUserId }:
const doesUserExist = await cache.doesUserExist(parentUserId)
if (!doesUserExist) {
throw new Error('invalid user id provided')
throw new SourceUserNotFoundException()
}
const deviceEntry = await cache.database.device.findOne({
@@ -38,12 +41,12 @@ export async function dispatchSetKeepSignedIn ({ action, cache, parentUserId }:
})
if (!deviceEntry) {
throw new Error('device does not exist')
throw new MissingDeviceException()
}
if (deviceEntry.currentUserId !== parentUserId) {
if (action.keepSignedIn) {
throw new Error('only the user itself can disable asking for the password')
throw new ApplyActionException({ staticMessage: 'only the user itself can disable asking for the password' })
}
}
@@ -16,8 +16,11 @@
*/
import { SetParentCategoryAction } from '../../../../action'
import { getCategoryWithParentCategories } from '../../../../util/category'
import { getCategoryWithParentCategories, GetParentCategoriesException } from '../../../../util/category'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingCategoryException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException, SelfLimitationException } from '../exception/self-limit'
export async function dispatchSetParentCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: SetParentCategoryAction
@@ -33,12 +36,12 @@ export async function dispatchSetParentCategory ({ action, cache, fromChildSelfL
})
if (!categoryEntry) {
throw new Error('tried to set parent category of non existent category')
throw new MissingCategoryException()
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (categoryEntry.childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not set parent category for other user')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
}
@@ -56,7 +59,7 @@ export async function dispatchSetParentCategory ({ action, cache, fromChildSelfL
}))
if (categoriesByUserId.find((item) => item.categoryId === action.parentCategory) === undefined) {
throw new Error('selected parent category does not exist')
throw new MissingCategoryException()
}
const childCategoryIds = new Set<string>()
@@ -80,16 +83,26 @@ export async function dispatchSetParentCategory ({ action, cache, fromChildSelfL
}
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')
throw new ApplyActionException({
staticMessage: '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
try {
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')
if (!enableDueToLimitAddingWhenChild) {
throw new SelfLimitationException({
staticMessage: 'can not change parent categories in a way which reduces limits'
})
}
} catch (ex) {
if (ex instanceof GetParentCategoriesException) {
throw new MissingCategoryException()
} else throw ex
}
}
}
@@ -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,12 +17,26 @@
import { SetRelaxPrimaryDeviceAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchSetRelaxPrimaryDevice ({ action, cache }: {
action: SetRelaxPrimaryDeviceAction
cache: Cache
}) {
const [affectedRows] = await cache.database.user.update({
const oldUser = cache.database.user.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
userId: action.userId,
type: 'child'
}
})
if (!oldUser) {
throw new MissingUserException()
}
await cache.database.user.update({
relaxPrimaryDeviceRule: action.relax
}, {
transaction: cache.transaction,
@@ -33,10 +47,6 @@ export async function dispatchSetRelaxPrimaryDevice ({ action, cache }: {
}
})
if (affectedRows === 0) {
throw new Error('did not find user to update relax primary device')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -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 { SetSendDeviceConnected } from '../../../../action'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
export async function dispatchSetSendDeviceConnected ({ action, cache, sourceDeviceId }: {
action: SetSendDeviceConnected
@@ -24,10 +25,12 @@ export async function dispatchSetSendDeviceConnected ({ action, cache, sourceDev
sourceDeviceId: string | null
}) {
if (sourceDeviceId === null || action.deviceId !== sourceDeviceId) {
throw new Error('only can do that from the device itself')
throw new ApplyActionException({
staticMessage: 'only can do that from the device itself if the connection status should be sent'
})
}
const [affectedRows] = await cache.database.device.update({
await cache.database.device.update({
showDeviceConnected: action.enable
}, {
transaction: cache.transaction,
@@ -37,10 +40,6 @@ export async function dispatchSetSendDeviceConnected ({ action, cache, sourceDev
}
})
if (affectedRows === 0) {
throw new Error('did not find device to update send if connected')
}
cache.devicesWithModifiedShowDeviceConnected.set(action.deviceId, action.enable)
cache.invalidiateDeviceList = true
}
@@ -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,8 @@
import { SetUserDisableLimitsUntilAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
import { PremiumVersionMissingException } from '../exception/premium'
export async function dispatchUserSetDisableLimitsUntil ({ action, cache }: {
action: SetUserDisableLimitsUntilAction
@@ -24,11 +26,24 @@ export async function dispatchUserSetDisableLimitsUntil ({ action, cache }: {
}) {
if (action.timestamp !== 0) {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
}
const [affectedRows] = await cache.database.user.update({
const oldUser = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.childId,
type: 'child'
},
transaction: cache.transaction
})
if (!oldUser) {
throw new MissingUserException()
}
await cache.database.user.update({
disableTimelimitsUntil: action.timestamp.toString(10)
}, {
where: {
@@ -39,10 +54,6 @@ export async function dispatchUserSetDisableLimitsUntil ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows === 0) {
throw new Error('invalid user id provided')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -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,12 +17,25 @@
import { SetUserTimezoneAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchSetUserTimezone ({ action, cache }: {
action: SetUserTimezoneAction
cache: Cache
}) {
const [affectedRows] = await cache.database.user.update({
const oldUser = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.userId
},
transaction: cache.transaction
})
if (!oldUser) {
throw new MissingUserException()
}
await cache.database.user.update({
timeZone: action.timezone
}, {
transaction: cache.transaction,
@@ -32,10 +45,6 @@ export async function dispatchSetUserTimezone ({ action, cache }: {
}
})
if (affectedRows === 0) {
throw new Error('did not find user to update timezone')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -17,6 +17,7 @@
import { UpdateCategoryBatteryLimitAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchUpdateCategoryBatteryLimit ({ action, cache }: {
action: UpdateCategoryBatteryLimitAction
@@ -31,7 +32,7 @@ export async function dispatchUpdateCategoryBatteryLimit ({ action, cache }: {
})
if (!categoryEntry) {
throw new Error('invalid category id')
throw new MissingCategoryException()
}
if (action.chargeLimit !== undefined) {
@@ -17,6 +17,8 @@
import { UpdateCategoryBlockAllNotificationsAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException, SelfLimitationException } from '../exception/self-limit'
export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryBlockAllNotificationsAction
@@ -33,16 +35,16 @@ export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cac
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for updating notification blocking')
throw new MissingCategoryException()
}
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) {
throw new Error('can not add rules for other users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
if (!action.blocked) {
throw new Error('can not disable filter as child')
throw new SelfLimitationException({ staticMessage: 'can not disable notification filter as child' })
}
}
@@ -18,6 +18,8 @@
import { blockedTimesBitmaskLength, UpdateCategoryBlockedTimesAction } from '../../../../action/updatecategoryblockedtimes'
import { validateAndParseBitmask } from '../../../../util/bitmask'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { CanNotModifyOtherUsersBySelfLimitationException, SelfLimitationException } from '../exception/self-limit'
export async function dispatchUpdateCategoryBlockedTimes ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryBlockedTimesAction
@@ -34,7 +36,7 @@ export async function dispatchUpdateCategoryBlockedTimes ({ action, cache, fromC
})
if (!categoryEntryUnsafe) {
throw new Error('can not update blocked time areas for a category which does not exist')
throw new MissingCategoryException()
}
const categoryEntry = {
@@ -44,7 +46,7 @@ export async function dispatchUpdateCategoryBlockedTimes ({ action, cache, fromC
if (fromChildSelfLimitAddChildUserId !== null) {
if (categoryEntry.childId !== fromChildSelfLimitAddChildUserId) {
throw new Error('can not update blocked time areas for other child users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
const oldBlocked = validateAndParseBitmask(categoryEntry.blockedMinutesInWeek, blockedTimesBitmaskLength)
@@ -52,7 +54,7 @@ export async function dispatchUpdateCategoryBlockedTimes ({ action, cache, fromC
oldBlocked.forEach((value, index) => {
if (value && !newBlocked[index]) {
throw new Error('new blocked time areas are smaller')
throw new SelfLimitationException({ staticMessage: 'new blocked time areas are smaller' })
}
})
}
@@ -17,6 +17,9 @@
import { UpdateCategoryTemporarilyBlockedAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
import { PremiumVersionMissingException } from '../exception/premium'
import { CanNotModifyOtherUsersBySelfLimitationException, SelfLimitationException } from '../exception/self-limit'
export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache, fromChildSelfLimitAddChildUserId }: {
action: UpdateCategoryTemporarilyBlockedAction
@@ -25,7 +28,7 @@ export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache,
}) {
if (action.blocked === true) {
if (!cache.hasFullVersion) {
throw new Error('action requires full version')
throw new PremiumVersionMissingException()
}
}
@@ -39,7 +42,7 @@ export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache,
})
if (!categoryEntryUnsafe) {
throw new Error('invalid category id for updating temporarily blocking')
throw new MissingCategoryException()
}
const categoryEntry = {
@@ -50,16 +53,16 @@ export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache,
if (fromChildSelfLimitAddChildUserId !== null) {
if (fromChildSelfLimitAddChildUserId !== categoryEntry.childId) {
throw new Error('can not update temporarily blocking as child for other users')
throw new CanNotModifyOtherUsersBySelfLimitationException()
}
if (action.endTime === undefined || !action.blocked) {
throw new Error('the child may only enable a temporarily blocking')
throw new SelfLimitationException({ staticMessage: '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')
throw new SelfLimitationException({ staticMessage: 'the child may not reduce the temporarily blocking' })
}
}
}
@@ -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 { UpdateCategoryTimeWarningsAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchUpdateCategoryTimeWarnings ({ action, cache }: {
action: UpdateCategoryTimeWarningsAction
@@ -31,7 +32,7 @@ export async function dispatchUpdateCategoryTimeWarnings ({ action, cache }: {
})
if (!categoryEntry) {
throw new Error('invalid category id')
throw new MissingCategoryException()
}
if (action.enable) {
@@ -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,11 +17,24 @@
import { UpdateCategoryTitleAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingCategoryException } from '../exception/missing-item'
export async function dispatchUpdateCategoryTitle ({ action, cache }: {
action: UpdateCategoryTitleAction
cache: Cache
}) {
const oldCategory = await cache.database.category.findOne({
where: {
familyId: cache.familyId,
categoryId: action.categoryId
},
transaction: cache.transaction
})
if (!oldCategory) {
throw new MissingCategoryException()
}
const [affectedRows] = await cache.database.category.update({
title: action.newTitle
}, {
@@ -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,11 +17,24 @@
import { UpdateDeviceNameAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchUpdateDeviceName ({ action, cache }: {
action: UpdateDeviceNameAction
cache: Cache
}) {
const oldDevice = await cache.database.device.findOne({
where: {
familyId: cache.familyId,
deviceId: action.deviceId
},
transaction: cache.transaction
})
if (!oldDevice) {
throw new MissingDeviceException()
}
const [affectedRows] = await cache.database.device.update({
name: action.name
}, {
@@ -32,9 +45,7 @@ export async function dispatchUpdateDeviceName ({ action, cache }: {
transaction: cache.transaction
})
if (affectedRows === 0) {
throw new Error('invalid device id')
} else {
if (affectedRows !== 0) {
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -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,12 +17,25 @@
import { UpdateEnableActivityLevelBlockingAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchUpdateEnableActivityLevelBlocking ({ action, cache }: {
action: UpdateEnableActivityLevelBlockingAction
cache: Cache
}) {
const [affectedRows] = await cache.database.device.update({
const oldDevice = await cache.database.device.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
deviceId: action.deviceId
}
})
if (!oldDevice) {
throw new MissingDeviceException()
}
await cache.database.device.update({
activityLevelBlocking: action.enable
}, {
transaction: cache.transaction,
@@ -32,10 +45,6 @@ export async function dispatchUpdateEnableActivityLevelBlocking ({ action, cache
}
})
if (affectedRows === 0) {
throw new Error('did not find device to update activity level blocking')
}
cache.invalidiateDeviceList = true
cache.areChangesImportant = true
}
@@ -17,11 +17,24 @@
import { UpdateNetworkTimeVerificationAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingDeviceException } from '../exception/missing-item'
export async function dispatchUpdateNetworkTimeVerification ({ action, cache }: {
action: UpdateNetworkTimeVerificationAction
cache: Cache
}) {
const oldDevice = await cache.database.device.findOne({
transaction: cache.transaction,
where: {
familyId: cache.familyId,
deviceId: action.deviceId
}
})
if (!oldDevice) {
throw new MissingDeviceException()
}
await cache.database.device.update({
networkTime: action.mode
}, {
@@ -17,6 +17,8 @@
import { UpdateParentBlockedTimesAction } from '../../../../action'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchUpdateParentBlockedTimes ({ action, cache, parentUserId }: {
action: UpdateParentBlockedTimesAction
@@ -24,10 +26,23 @@ export async function dispatchUpdateParentBlockedTimes ({ action, cache, parentU
parentUserId: string
}) {
if (parentUserId !== action.parentId && action.blockedTimes !== '') {
throw new Error('only a parent itself can add limits')
throw new ApplyActionException({ staticMessage: 'only a parent itself can add limits' })
}
const [affectedRows] = await cache.database.user.update({
const oldUser = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.parentId,
type: 'parent'
},
transaction: cache.transaction
})
if (!oldUser) {
throw new MissingUserException()
}
await cache.database.user.update({
blockedTimes: action.blockedTimes
}, {
where: {
@@ -38,10 +53,6 @@ export async function dispatchUpdateParentBlockedTimes ({ action, cache, parentU
transaction: cache.transaction
})
if (affectedRows === 0) {
throw new Error('invalid parent user id provided')
}
cache.invalidiateUserList = true
cache.areChangesImportant = true
}
@@ -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 { UpdateParentNotificationFlagsAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchUpdateParentNotificationFlags ({ action, cache }: {
action: UpdateParentNotificationFlagsAction
@@ -32,7 +33,7 @@ export async function dispatchUpdateParentNotificationFlags ({ action, cache }:
})
if (!parentEntry) {
throw new Error('parent not found')
throw new MissingUserException()
}
if (action.set) {
@@ -17,6 +17,7 @@
import { UpdateTimelimitRuleAction } from '../../../../action'
import { Cache } from '../cache'
import { MissingRuleException } from '../exception/missing-item'
export async function dispatchUpdateTimelimitRule ({ action, cache }: {
action: UpdateTimelimitRuleAction
@@ -31,7 +32,7 @@ export async function dispatchUpdateTimelimitRule ({ action, cache }: {
})
if (!ruleEntry) {
throw new Error('invalid rule id provided')
throw new MissingRuleException()
}
ruleEntry.applyToExtraTimeUsage = action.applyToExtraTimeUsage
@@ -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,8 @@
import { UpdateUserFlagsAction } from '../../../../action'
import { Cache } from '../cache'
import { IllegalStateException } from '../exception/illegal-state'
import { MissingUserException } from '../exception/missing-item'
export async function dispatchUpdateUserFlagsAction ({ action, cache }: {
action: UpdateUserFlagsAction
@@ -31,13 +33,13 @@ export async function dispatchUpdateUserFlagsAction ({ action, cache }: {
})
if (!userEntry) {
throw new Error('user not found')
throw new MissingUserException()
}
const oldFlags = parseInt(userEntry.flags, 10)
if (!Number.isSafeInteger(oldFlags)) {
throw new Error()
throw new IllegalStateException({ staticMessage: 'oldFlags is not a safe integer' })
}
const newFlags = (oldFlags & ~action.modifiedBits) | action.newValues
@@ -17,6 +17,8 @@
import { UpdateUserLimitLoginCategory } from '../../../../action'
import { Cache } from '../cache'
import { ApplyActionException } from '../exception/index'
import { MissingCategoryException, MissingUserException } from '../exception/missing-item'
export async function dispatchUpdateUserLimitLoginCategoryAction ({ action, cache, parentUserId }: {
action: UpdateUserLimitLoginCategory
@@ -26,21 +28,20 @@ export async function dispatchUpdateUserLimitLoginCategoryAction ({ action, cach
const userEntry = await cache.database.user.findOne({
where: {
familyId: cache.familyId,
userId: action.userId
userId: action.userId,
type: 'parent'
},
transaction: cache.transaction
})
if (!userEntry) {
throw new Error('user not found')
}
if (userEntry.type !== 'parent') {
throw new Error('user must be a parent')
throw new MissingUserException()
}
if (action.categoryId !== undefined && parentUserId !== action.userId) {
throw new Error('only the user itself can add a limit')
throw new ApplyActionException({
staticMessage: 'only the parent user itself can add a limit login category'
})
}
await cache.database.userLimitLoginCategory.destroy({
@@ -61,7 +62,7 @@ export async function dispatchUpdateUserLimitLoginCategoryAction ({ action, cach
})
if (!categoryEntry) {
throw new Error('category must exist')
throw new MissingCategoryException()
}
await cache.database.userLimitLoginCategory.create({
@@ -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 { ApplyActionException } from './index'
export class IllegalStateException extends ApplyActionException {
constructor ({ staticMessage }: { staticMessage: string }) {
super({ staticMessage })
}
}
export class SourceDeviceNotFoundException extends IllegalStateException {
constructor () {
super({ staticMessage: 'source device not found' })
}
}
export class SourceUserNotFoundException extends IllegalStateException {
constructor () {
super({ staticMessage: 'source user not found' })
}
}
export class SourceFamilyNotFoundException extends IllegalStateException {
constructor () {
super({ staticMessage: 'family entry not found' })
}
}
export class ActionObjectTypeNotHandledException extends IllegalStateException {
constructor () {
super({ staticMessage: 'action object type not handled' })
}
}
@@ -0,0 +1,20 @@
/*
* 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 { StaticMessageException } from '../../../../exception'
export class ApplyActionException extends StaticMessageException {}
@@ -0,0 +1,32 @@
/*
* 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 { ApplyActionException } from './index'
export class ApplyActionIntegrityException extends ApplyActionException {}
export class ParentDeviceActionWithoutParentDeviceException extends ApplyActionIntegrityException {
constructor () { super({ staticMessage: 'parent device action but no parent device' }) }
}
export class InvalidParentActionIntegrityValue extends ApplyActionIntegrityException {
constructor () { super({ staticMessage: 'invalid parent action integrity value' }) }
}
export class InvalidChildActionIntegrityValue extends ApplyActionIntegrityException {
constructor () { super({ staticMessage: 'invalid child action integrity value' }) }
}
@@ -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 { InvalidActionParameterException } from '../../../../action/meta/exception'
import { ApplyActionException } from './index'
export class InvalidActionException extends ApplyActionException {}
export class InvalidActionParamterException extends InvalidActionException {
constructor (cause: InvalidActionParameterException) {
super({
staticMessage: cause.staticMessage,
dynamicMessage: cause.dynamicMessage
})
}
}
export class BadEncodedActionJsonException extends InvalidActionException {
constructor () { super({ staticMessage: 'bad encoded action JSON' }) }
}
export class BadEncodedActionContentException extends InvalidActionException {}
export class EncodedActionSchemaMismatchException extends BadEncodedActionContentException {
readonly action: object
constructor ({ type, action }: { type: 'parent' | 'child' | 'app logic', action: object }) {
super({
staticMessage: 'encoded ' + type + ' action does not match schema',
dynamicMessage: 'encoded action does not match schema: ' + JSON.stringify(action)
})
this.action = action
}
}
@@ -0,0 +1,44 @@
/*
* 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 { ApplyActionException } from './index'
export class MissingItemException extends ApplyActionException {}
export class MissingCategoryException extends MissingItemException {
constructor () {
super({ staticMessage: 'referenced category which does not exist' })
}
}
export class MissingUserException extends MissingItemException {
constructor () {
super({ staticMessage: 'referenced user which does not exist' })
}
}
export class MissingRuleException extends MissingItemException {
constructor () {
super({ staticMessage: 'referenced rule which does not exist' })
}
}
export class MissingDeviceException extends MissingItemException {
constructor () {
super({ staticMessage: 'referenced device which does not exist' })
}
}
@@ -0,0 +1,24 @@
/*
* 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 { ApplyActionException } from './index'
export class PremiumVersionMissingException extends ApplyActionException {
constructor () {
super({ staticMessage: 'premium version required but missing' })
}
}
@@ -0,0 +1,34 @@
/*
* 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 { ApplyActionException } from './index'
export class SelfLimitationException extends ApplyActionException {}
export class CanNotModifyOtherUsersBySelfLimitationException extends SelfLimitationException {
constructor () {
super({ staticMessage: 'can not modify other users with the self limitation' })
}
}
export class ActionNotSupportedBySelfLimitationException extends SelfLimitationException {
constructor () {
super({ staticMessage: 'can not dispatch this action with the self limitation' })
}
}
export class SelfLimitNotPossibleException extends SelfLimitationException {}
@@ -0,0 +1,22 @@
/*
* 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 { ApplyActionException } from './index'
export class SequenceNumberRepeatedException extends ApplyActionException {
constructor () { super({ staticMessage: 'sequence number repeated' }) }
}
+48 -178
View File
@@ -15,20 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BadRequest, Unauthorized } from 'http-errors'
import { parseAppLogicAction, parseChildAction, parseParentAction } from '../../../action/serialization'
import { BadRequest } from 'http-errors'
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 { notifyClientsAboutChangesDelayed } from '../../websocket'
import { getApplyActionBaseInfo } from './baseinfo'
import { Cache } from './cache'
import { dispatchAppLogicAction } from './dispatch-app-logic-action'
import { dispatchChildAction } from './dispatch-child-action'
import { dispatchParentAction } from './dispatch-parent-action'
import { dispatchAppLogicAction, dispatchChildAction, dispatchParentAction } from './dispatch-helper'
import { ApplyActionException } from './exception'
import { IllegalStateException } from './exception/illegal-state'
import { SequenceNumberRepeatedException } from './exception/sequence'
import { assertActionIntegrity } from './integrity'
export const applyActionsFromDevice = async ({ database, request, websocket, connectedDevicesManager, eventHandler }: {
@@ -37,6 +36,7 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
request: ClientPushChangesRequest
connectedDevicesManager: VisibleConnectedDevicesManager
eventHandler: EventHandler
// no transaction here because this is directly called from an API endpoint
}): Promise<{ shouldDoFullSync: boolean }> => {
eventHandler.countEvent('applyActionsFromDevice')
@@ -47,213 +47,83 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
}
return database.transaction(async (transaction) => {
const deviceEntryUnsafe = await database.device.findOne({
where: {
deviceAuthToken: request.deviceAuthToken
},
attributes: ['familyId', 'deviceId', 'nextSequenceNumber'],
transaction
})
if (!deviceEntryUnsafe) {
throw new Unauthorized()
}
const deviceEntry = {
familyId: deviceEntryUnsafe.familyId,
deviceId: deviceEntryUnsafe.deviceId,
nextSequenceNumber: deviceEntryUnsafe.nextSequenceNumber
}
const familyEntryUnsafe = await database.family.findOne({
where: {
familyId: deviceEntry.familyId
},
transaction,
attributes: ['hasFullVersion']
})
if (!familyEntryUnsafe) {
throw new Error('missing family entry')
}
const familyEntry = {
hasFullVersion: familyEntryUnsafe.hasFullVersion
}
const baseInfo = await getApplyActionBaseInfo({ database, transaction, deviceAuthToken: request.deviceAuthToken })
const cache = new Cache({
database,
hasFullVersion: familyEntry.hasFullVersion,
hasFullVersion: baseInfo.hasFullVersion,
transaction,
familyId: deviceEntry.familyId,
familyId: baseInfo.familyId,
connectedDevicesManager
})
let { nextSequenceNumber } = deviceEntry
let { nextSequenceNumber } = baseInfo
for (const action of request.actions) {
if (action.sequenceNumber < nextSequenceNumber) {
// action was already received
eventHandler.countEvent('applyActionsFromDevice sequenceNumberRepeated')
cache.requireFullSync()
continue
}
try {
if (action.sequenceNumber < nextSequenceNumber) {
// action was already received
throw new SequenceNumberRepeatedException()
}
await cache.subtransaction(async () => {
// update the next sequence number
nextSequenceNumber = action.sequenceNumber + 1
const { isChildLimitAdding } = await assertActionIntegrity({
deviceId: deviceEntry.deviceId,
deviceId: baseInfo.deviceId,
cache,
eventHandler,
action
})
const parsedSerializedAction = JSON.parse(action.encodedAction)
if (action.type === 'appLogic') {
if (!isSerializedAppLogicAction(parsedSerializedAction)) {
eventHandler.countEvent('applyActionsFromDevice invalidAppLogicAction')
throw new Error('invalid action: ' + action.encodedAction)
}
eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type)
const parsedAction = parseAppLogicAction(parsedSerializedAction)
try {
await dispatchAppLogicAction({
action: parsedAction,
cache,
deviceId: deviceEntry.deviceId,
eventHandler
})
} catch (ex) {
eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type)
throw ex
}
await dispatchAppLogicAction({
action,
cache,
deviceId: baseInfo.deviceId,
eventHandler
})
} else if (action.type === 'parent') {
if (!isSerializedParentAction(parsedSerializedAction)) {
eventHandler.countEvent('applyActionsFromDevice invalidParentAction')
throw new Error('invalid action' + action.encodedAction)
}
eventHandler.countEvent('applyActionsFromDevice, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type)
const parsedAction = parseParentAction(parsedSerializedAction)
try {
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('applyActionsFromDeviceWithError, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type)
throw ex
}
await dispatchParentAction({
action,
cache,
deviceId: baseInfo.deviceId,
eventHandler,
isChildLimitAdding
})
} else if (action.type === 'child') {
if (!isSerializedChildAction(parsedSerializedAction)) {
eventHandler.countEvent('applyActionsFromDevice invalidChildAction')
throw new Error('invalid action: ' + action.encodedAction)
}
eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type)
const parsedAction = parseChildAction(parsedSerializedAction)
try {
await dispatchChildAction({
action: parsedAction,
cache,
childUserId: action.userId,
deviceId: deviceEntry.deviceId
})
} catch (ex) {
eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type)
throw ex
}
await dispatchChildAction({
action,
cache,
childUserId: action.userId,
deviceId: baseInfo.deviceId,
eventHandler
})
} else {
throw new Error('illegal state')
throw new IllegalStateException({ staticMessage: 'not possible action.type value' })
}
})
} catch (ex) {
eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction')
if (ex instanceof ApplyActionException) {
eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction:' + ex.staticMessage)
} else {
eventHandler.countEvent('applyActionsFromDevice errorDispatchingAction:other')
}
cache.requireFullSync()
}
}
// save new next sequence number
if (nextSequenceNumber !== deviceEntry.nextSequenceNumber) {
if (nextSequenceNumber !== baseInfo.nextSequenceNumber) {
eventHandler.countEvent('applyActionsFromDevice updateSequenceNumber')
await database.device.update({
nextSequenceNumber
}, {
where: {
familyId: deviceEntry.familyId,
deviceId: deviceEntry.deviceId
familyId: baseInfo.familyId,
deviceId: baseInfo.deviceId
},
transaction
})
@@ -262,8 +132,8 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con
await cache.saveModifiedVersionNumbers()
await notifyClientsAboutChangesDelayed({
familyId: deviceEntry.familyId,
sourceDeviceId: deviceEntry.deviceId,
familyId: baseInfo.familyId,
sourceDeviceId: baseInfo.deviceId,
isImportant: cache.areChangesImportant,
websocket,
database,
+7 -10
View File
@@ -17,13 +17,14 @@
import { createHash } from 'crypto'
import { ClientPushChangesRequestAction } from '../../../api/schema'
import { EventHandler } from '../../../monitoring/eventhandler'
import { Cache } from './cache'
import {
InvalidChildActionIntegrityValue, InvalidParentActionIntegrityValue, ParentDeviceActionWithoutParentDeviceException
} from './exception/integrity'
export async function assertActionIntegrity ({ action, cache, eventHandler, deviceId }: {
export async function assertActionIntegrity ({ action, cache, deviceId }: {
action: ClientPushChangesRequestAction
cache: Cache
eventHandler: EventHandler
deviceId: string
}): Promise<{ isChildLimitAdding: boolean }> {
let isChildLimitAdding = false
@@ -42,7 +43,7 @@ export async function assertActionIntegrity ({ action, cache, eventHandler, devi
})
if (!deviceEntryUnsafe) {
throw new Error('user is not signed in at this device')
throw new ParentDeviceActionWithoutParentDeviceException()
}
// this ensures that the parent exists
@@ -61,9 +62,7 @@ export async function assertActionIntegrity ({ action, cache, eventHandler, devi
const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex')
if (action.integrity !== expectedIntegrityValue) {
eventHandler.countEvent('applyActionsFromDevice parentActionInvalidIntegrityValue')
throw new Error('invalid integrity value')
throw new InvalidParentActionIntegrityValue()
}
}
}
@@ -79,9 +78,7 @@ export async function assertActionIntegrity ({ action, cache, eventHandler, devi
const expectedIntegrityValue = createHash('sha512').update(integrityData).digest('hex')
if (action.integrity !== expectedIntegrityValue) {
eventHandler.countEvent('applyActionsFromDevice childActionInvalidIntegrityValue')
throw new Error('invalid integrity value')
throw new InvalidChildActionIntegrityValue()
}
}
@@ -0,0 +1,36 @@
/*
* 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 { InvalidActionParameterException } from '../../../action/meta/exception'
import { ClientPushChangesRequestAction } from '../../../api/schema'
import { BadEncodedActionJsonException, InvalidActionParamterException } from './exception/invalidaction'
export function parseEncodedAction (action: ClientPushChangesRequestAction): object {
try {
const result = JSON.parse(action.encodedAction)
if (typeof result !== 'object') throw new BadEncodedActionJsonException()
return result
} catch (ex) {
if (ex instanceof SyntaxError) {
throw new BadEncodedActionJsonException()
} else if (ex instanceof InvalidActionParameterException) {
throw new InvalidActionParamterException(ex)
} else throw ex
}
}
+13 -6
View File
@@ -19,6 +19,7 @@ import { difference, filter, intersection } from 'lodash'
import * as Sequelize from 'sequelize'
import { config } from '../../config'
import { Database } from '../../database'
import { StaticMessageException } from '../../exception'
import { getStatusMessage } from '../../function/statusmessage'
import { ClientDataStatus } from '../../object/clientdatastatus'
import {
@@ -48,7 +49,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
})
if (!familyEntryUnsafe) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find family entry' })
}
const familyEntry = {
@@ -210,7 +211,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
const entry = serverInstalledAppsVersions.find((item) => item.deviceId === deviceId)
if (!entry) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find device entry' })
}
return entry.installedAppsVersion
@@ -333,7 +334,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
const clientEntry = clientStatus.categories[categoryId]
if ((!serverEntry) || (!clientEntry)) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find category overview item again' })
}
if (serverEntry.baseVersion !== clientEntry.base) {
@@ -460,7 +461,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
const categoryEntry = serverCategoriesVersions.find((item) => item.categoryId === categoryId)
if (!categoryEntry) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find category entry' })
}
return categoryEntry.assignedAppsVersion
@@ -509,7 +510,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
const categoryEntry = serverCategoriesVersions.find((item) => item.categoryId === categoryId)
if (!categoryEntry) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find category entry' })
}
return categoryEntry.timeLimitRulesVersion
@@ -586,7 +587,7 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
const categoryEntry = serverCategoriesVersions.find((item) => item.categoryId === categoryId)
if (!categoryEntry) {
throw new Error('illegal state')
throw new GetServerDataStatusIllegalStateException({ staticMessage: 'could not find category entry' })
}
return categoryEntry.usedTimesVersion
@@ -614,3 +615,9 @@ export const generateServerDataStatus = async ({ database, clientStatus, familyI
return result
}
export class GetServerDataStatusIllegalStateException extends StaticMessageException {
constructor ({ staticMessage }: { staticMessage: string }) {
super({ staticMessage: 'GetServerDataStatusIllegalStateException: ' + staticMessage })
}
}