diff --git a/src/action/addcategoryapps.ts b/src/action/addcategoryapps.ts index 5c212b8..28c04ba 100644 --- a/src/action/addcategoryapps.ts +++ b/src/action/addcategoryapps.ts @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -import { assertNonEmptyListWithoutDuplicates } from '../util/list' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertNonEmptyListWithoutDuplicates } from './meta/util' + +const actionType = 'AddCategoryApps' export class AddCategoryAppsAction extends ParentAction { readonly categoryId: string @@ -26,8 +27,8 @@ export class AddCategoryAppsAction extends ParentAction { constructor ({ categoryId, packageNames }: {categoryId: string, packageNames: Array}) { super() - assertIdWithinFamily(categoryId) - assertNonEmptyListWithoutDuplicates(packageNames) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + assertNonEmptyListWithoutDuplicates({ actionType, field: 'packageNames', list: packageNames }) this.categoryId = categoryId this.packageNames = packageNames diff --git a/src/action/addcategorynetworkid.ts b/src/action/addcategorynetworkid.ts index 9d6c87e..6592b51 100644 --- a/src/action/addcategorynetworkid.ts +++ b/src/action/addcategorynetworkid.ts @@ -16,9 +16,11 @@ */ import { anonymizedNetworkIdLength } from '../database/categorynetworkid' -import { assertIsHexString } from '../util/hexstring' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertHexString, assertIdWithinFamily } from './meta/util' + +const actionType = 'AddCategoryNetworkIdAction' export class AddCategoryNetworkIdAction extends ParentAction { readonly categoryId: string @@ -32,10 +34,16 @@ export class AddCategoryNetworkIdAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) - assertIdWithinFamily(itemId) - assertIsHexString(hashedNetworkId) - if (hashedNetworkId.length !== anonymizedNetworkIdLength) throw new Error('wrong network id length') + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + assertIdWithinFamily({ actionType, field: 'itemId', value: itemId }) + assertHexString({ actionType, field: 'hashedNetworkId', value: hashedNetworkId }) + + if (hashedNetworkId.length !== anonymizedNetworkIdLength) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'wrong network id length' + }) + } this.categoryId = categoryId this.itemId = itemId diff --git a/src/action/addinstalledapps.ts b/src/action/addinstalledapps.ts index 48f731b..666113f 100644 --- a/src/action/addinstalledapps.ts +++ b/src/action/addinstalledapps.ts @@ -16,8 +16,10 @@ */ import { InstalledApp, SerializedInstalledApp } from '../model/installedapp' -import { assertNonEmptyListWithoutDuplicates } from '../util/list' import { AppLogicAction } from './basetypes' +import { assertNonEmptyListWithoutDuplicates } from './meta/util' + +const actionType = 'AddInstalledAppsAction' export class AddInstalledAppsAction extends AppLogicAction { readonly apps: Array @@ -25,7 +27,7 @@ export class AddInstalledAppsAction extends AppLogicAction { constructor ({ apps }: {apps: Array}) { super() - assertNonEmptyListWithoutDuplicates(apps.map((app) => app.packageName)) + assertNonEmptyListWithoutDuplicates({ actionType, field: 'apps', list: apps.map((app) => app.packageName) }) this.apps = apps } diff --git a/src/action/addusedtime.ts b/src/action/addusedtime.ts index 650db00..59da4b2 100644 --- a/src/action/addusedtime.ts +++ b/src/action/addusedtime.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { AppLogicAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'AddUsedTimeAction' export class AddUsedTimeAction extends AppLogicAction { readonly categoryId: string @@ -32,18 +35,30 @@ export class AddUsedTimeAction extends AppLogicAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) if (dayOfEpoch < 0 || (!Number.isSafeInteger(dayOfEpoch))) { - throw new Error('illegal dayOfEpoch') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid dayOfEpoch', + dynamicMessage: 'invalid dayOfEpoch: ' + dayOfEpoch + }) } if (timeToAdd < 0 || (!Number.isSafeInteger(timeToAdd))) { - throw new Error('illegal timeToAdd') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'illegal timeToAdd', + dynamicMessage: 'illegal timeToAdd: ' + timeToAdd + }) } if (extraTimeToSubtract < 0 || (!Number.isSafeInteger(extraTimeToSubtract))) { - throw new Error('illegal extra time to subtract') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'illegal extra time to subtract', + dynamicMessage: 'illegal extra time to subtract: ' + extraTimeToSubtract + }) } this.categoryId = categoryId diff --git a/src/action/addusedtime2.ts b/src/action/addusedtime2.ts index f7a4387..1bd9701 100644 --- a/src/action/addusedtime2.ts +++ b/src/action/addusedtime2.ts @@ -15,10 +15,12 @@ * along with this program. If not, see . */ -import { uniq } from 'lodash' import { MinuteOfDay } from '../util/minuteofday' -import { assertIdWithinFamily } from '../util/token' import { AppLogicAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertListWithoutDuplicates, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'AddUsedTimeActionVersion2' export class AddUsedTimeActionVersion2 extends AppLogicAction { readonly dayOfEpoch: number @@ -44,46 +46,54 @@ export class AddUsedTimeActionVersion2 extends AppLogicAction { }) { super() - if (dayOfEpoch < 0 || (!Number.isSafeInteger(dayOfEpoch))) { - throw new Error('illegal dayOfEpoch') + assertSafeInteger({ actionType, field: 'dayOfEpoch', value: dayOfEpoch }) + + if (dayOfEpoch < 0) { + throwOutOfRange({ actionType, field: 'dayOfEpoch', value: dayOfEpoch }) } - if (trustedTimestamp < 0 || (!Number.isSafeInteger(trustedTimestamp))) { - throw new Error('illegal trustedTimestamp') + assertSafeInteger({ actionType, field: 'trustedTimestamp', value: trustedTimestamp }) + + if (trustedTimestamp < 0) { + throwOutOfRange({ actionType, field: 'trustedTimestamp', value: trustedTimestamp }) } if (items.length === 0) { - throw new Error('missing items') + throw new InvalidActionParameterException({ actionType, staticMessage: 'no items' }) } - if (items.length !== uniq(items.map((item) => item.categoryId)).length) { - throw new Error('duplicate category ids') - } + assertListWithoutDuplicates({ + actionType, + field: 'categoryIds', + list: items.map((item) => item.categoryId) + }) items.forEach((item) => { - assertIdWithinFamily(item.categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: item.categoryId }) - if (item.timeToAdd < 0 || (!Number.isSafeInteger(item.timeToAdd))) { - throw new Error('illegal timeToAdd') + assertSafeInteger({ actionType, field: 'timeToAdd', value: item.timeToAdd }) + + if (item.timeToAdd < 0) { + throwOutOfRange({ actionType, field: 'timeToAdd', value: item.timeToAdd }) } - if (item.extraTimeToSubtract < 0 || (!Number.isSafeInteger(item.extraTimeToSubtract))) { - throw new Error('illegal extra time to subtract') + assertSafeInteger({ actionType, field: 'extraTimeToSubtract', value: item.extraTimeToSubtract }) + + if (item.extraTimeToSubtract < 0) { + throwOutOfRange({ actionType, field: 'extraTimeToSubtract', value: item.extraTimeToSubtract }) } - if ( - uniq(item.additionalCountingSlots.map((item) => JSON.stringify(item.serialize()))).length !== - item.additionalCountingSlots.length - ) { - throw new Error() - } + assertListWithoutDuplicates({ + actionType, + field: 'additionalCountingSlots', + list: item.additionalCountingSlots.map((item) => JSON.stringify(item.serialize())) + }) - if ( - uniq(item.sessionDurationLimits.map((item) => JSON.stringify(item.serialize()))).length !== - item.sessionDurationLimits.length - ) { - throw new Error() - } + assertListWithoutDuplicates({ + actionType, + field: 'sessionDurationLimits', + list: item.sessionDurationLimits.map((item) => JSON.stringify(item.serialize())) + }) }) this.dayOfEpoch = dayOfEpoch @@ -111,16 +121,15 @@ class AddUsedTimeActionItemAdditionalCountingSlot { readonly end: number constructor ({ start, end }: { start: number, end: number }) { - if ((!Number.isSafeInteger(start)) || (!Number.isSafeInteger(end))) { - throw new Error() - } + assertSafeInteger({ actionType, field: 'start', value: start }) + assertSafeInteger({ actionType, field: 'end', value: end }) if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) { - throw new Error() + throw new InvalidActionParameterException({ actionType, staticMessage: 'start or end out of range' }) } if (start === MinuteOfDay.MIN && end === MinuteOfDay.MAX) { - throw new Error() + throw new InvalidActionParameterException({ actionType, staticMessage: 'couting slot can not fill the whole day' }) } this.start = start @@ -139,19 +148,17 @@ class AddUsedTimeActionItemSessionDurationLimitSlot { readonly pause: number constructor ({ start, end, duration, pause }: { start: number, end: number, duration: number, pause: number }) { - if ( - (!Number.isSafeInteger(start)) || (!Number.isSafeInteger(end)) || - (!Number.isSafeInteger(duration)) || (!Number.isSafeInteger(pause)) - ) { - throw new Error() - } + assertSafeInteger({ actionType, field: 'start', value: start }) + assertSafeInteger({ actionType, field: 'end', value: end }) + assertSafeInteger({ actionType, field: 'duration', value: duration }) + assertSafeInteger({ actionType, field: 'pause', value: pause }) if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) { - throw new Error() + throw new InvalidActionParameterException({ actionType, staticMessage: 'start or end out of range' }) } if (duration <= 0 || pause <= 0) { - throw new Error() + throw new InvalidActionParameterException({ actionType, staticMessage: 'duration and pause must not be zero or smaller' }) } this.start = start diff --git a/src/action/adduser.ts b/src/action/adduser.ts index 28deca6..2339b1b 100644 --- a/src/action/adduser.ts +++ b/src/action/adduser.ts @@ -15,9 +15,12 @@ * along with this program. If not, see . */ -import { assertParentPasswordValid, ParentPassword } from '../api/schema' -import { assertIdWithinFamily } from '../util/token' +import { assertParentPasswordValid, ParentPassword, ParentPasswordValidationException } from '../api/schema' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'AddUserAction' export class AddUserAction extends ParentAction { readonly userId: string @@ -35,7 +38,7 @@ export class AddUserAction extends ParentAction { }) { super() - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) this.userId = userId this.name = name @@ -45,12 +48,24 @@ export class AddUserAction extends ParentAction { if (userType === 'parent') { if (!password) { - throw new Error('parent users must have got an password') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'parent users must have got an password' + }) } } if (password) { - assertParentPasswordValid(password) + try { + assertParentPasswordValid(password) + } catch (ex) { + if (ex instanceof ParentPasswordValidationException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid password data' + }) + } else throw ex + } } } diff --git a/src/action/changeparentpassword.ts b/src/action/changeparentpassword.ts index 4ef63ea..875ab6d 100644 --- a/src/action/changeparentpassword.ts +++ b/src/action/changeparentpassword.ts @@ -16,9 +16,11 @@ */ import { createDecipheriv, createHash } from 'crypto' -import { assertIsHexString } from '../util/hexstring' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertHexString, assertIdWithinFamily } from './meta/util' + +const actionType = 'ChangeParentPasswordAction' export class ChangeParentPasswordAction extends ParentAction { readonly parentUserId: string @@ -36,7 +38,7 @@ export class ChangeParentPasswordAction extends ParentAction { }) { super() - assertIdWithinFamily(parentUserId) + assertIdWithinFamily({ actionType, field: 'parentUserId', value: parentUserId }) if ( (!parentUserId) || @@ -45,15 +47,25 @@ export class ChangeParentPasswordAction extends ParentAction { (!newPasswordSecondHashEncrypted) || (!integrity) ) { - throw new Error('missing required parameter for change parent password') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'missing required parameter for change parent password' + }) } if (integrity.length !== 128) { - throw new Error('wrong length of integrity data') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'wrong length of integrity data' + }) } - assertIsHexString(newPasswordSecondHashEncrypted) - assertIsHexString(integrity) + assertHexString({ actionType, field: 'newPasswordSecondHashEncrypted', value: newPasswordSecondHashEncrypted }) + assertHexString({ actionType, field: 'integrity', value: integrity }) + + if (newPasswordSecondHashEncrypted.length <= 70) { + throw new InvalidActionParameterException({ actionType, staticMessage: 'wrong length of the new password' }) + } this.parentUserId = parentUserId this.newPasswordFirstHash = newPasswordFirstHash @@ -82,15 +94,11 @@ export class ChangeParentPasswordAction extends ParentAction { const expected = createHash('sha512').update(integrityData).digest('hex') if (expected !== this.integrity) { - throw new Error('invalid integrity for change parent password action') + throw new InvalidChangeParentPasswordIntegrityException() } } - decryptSecondHash ({ oldPasswordSecondHash }: {oldPasswordSecondHash: string}) { - if (this.newPasswordSecondHashEncrypted.length <= 70) { - throw new Error('wrong length of the new password') - } - + decryptSecondHash ({ oldPasswordSecondHash }: { oldPasswordSecondHash: string }) { const ivHex = this.newPasswordSecondHashEncrypted.substring(0, 32) const salt = this.newPasswordSecondHashEncrypted.substring(32, 64) const encryptedData = this.newPasswordSecondHashEncrypted.substring(64) @@ -115,3 +123,7 @@ export interface SerializedChangeParentPasswordAction { secondHashEncrypted: string integrity: string } + +export class InvalidChangeParentPasswordIntegrityException extends Error { + constructor () { super('invalid integrity for change parent password action') } +} diff --git a/src/action/childchangepassword.ts b/src/action/childchangepassword.ts index e5156c7..cb36999 100644 --- a/src/action/childchangepassword.ts +++ b/src/action/childchangepassword.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertParentPasswordValid, ParentPassword } from '../api/schema' +import { assertParentPasswordValid, ParentPassword, ParentPasswordValidationException } from '../api/schema' import { ChildAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' + +const actionType = 'ChildChangePasswordAction' export class ChildChangePasswordAction extends ChildAction { readonly password: ParentPassword @@ -26,7 +29,16 @@ export class ChildChangePasswordAction extends ChildAction { }) { super() - assertParentPasswordValid(password) + try { + assertParentPasswordValid(password) + } catch (ex) { + if (ex instanceof ParentPasswordValidationException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid password' + }) + } else throw ex + } this.password = password } diff --git a/src/action/childsignin.ts b/src/action/childsignin.ts index 4b1538a..5fe208f 100644 --- a/src/action/childsignin.ts +++ b/src/action/childsignin.ts @@ -22,7 +22,7 @@ export class ChildSignInAction extends ChildAction { super() } - static parse = (action: SerializedChildSignInAction) => ( + static parse = (_: SerializedChildSignInAction) => ( new ChildSignInAction() ) } diff --git a/src/action/createcategory.ts b/src/action/createcategory.ts index aa57e79..6db72ce 100644 --- a/src/action/createcategory.ts +++ b/src/action/createcategory.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'CreateCategoryAction' export class CreateCategoryAction extends ParentAction { readonly categoryId: string @@ -26,8 +28,8 @@ export class CreateCategoryAction extends ParentAction { constructor ({ categoryId, childId, title }: {categoryId: string, childId: string, title: string}) { super() - assertIdWithinFamily(categoryId) - assertIdWithinFamily(childId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + assertIdWithinFamily({ actionType, field: 'childId', value: childId }) this.categoryId = categoryId this.childId = childId diff --git a/src/action/createtimelimitrule.ts b/src/action/createtimelimitrule.ts index 0a6514d..9b91a94 100644 --- a/src/action/createtimelimitrule.ts +++ b/src/action/createtimelimitrule.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { SerializedTimeLimitRule, TimelimitRule } from '../model/timelimitrule' +import { ParseTimeLimitRuleException, SerializedTimeLimitRule, TimelimitRule } from '../model/timelimitrule' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' + +const actionType = 'CreateTimeLimitRuleAction' export class CreateTimeLimitRuleAction extends ParentAction { rule: TimelimitRule @@ -27,11 +30,21 @@ export class CreateTimeLimitRuleAction extends ParentAction { this.rule = rule } - static parse = ({ rule }: SerializedCreateTimelimtRuleAction) => ( - new CreateTimeLimitRuleAction({ - rule: TimelimitRule.parse(rule) - }) - ) + static parse = ({ rule }: SerializedCreateTimelimtRuleAction) => { + try { + return new CreateTimeLimitRuleAction({ + rule: TimelimitRule.parse(rule) + }) + } catch (ex) { + if (ex instanceof ParseTimeLimitRuleException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid time limit rule', + dynamicMessage: 'invalid time limit rule: ' + ex.toString() + }) + } else throw ex + } + } } export interface SerializedCreateTimelimtRuleAction { diff --git a/src/action/deletecategory.ts b/src/action/deletecategory.ts index 32ac423..a32a27f 100644 --- a/src/action/deletecategory.ts +++ b/src/action/deletecategory.ts @@ -15,16 +15,18 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'DeleteCategoryAction' export class DeleteCategoryAction extends ParentAction { readonly categoryId: string - constructor ({ categoryId }: {categoryId: string}) { + constructor ({ categoryId }: { categoryId: string }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) this.categoryId = categoryId } diff --git a/src/action/deletetimelimitrule.ts b/src/action/deletetimelimitrule.ts index 8bb03c3..8638c3a 100644 --- a/src/action/deletetimelimitrule.ts +++ b/src/action/deletetimelimitrule.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'DeleteTimeLimitRuleAction' export class DeleteTimeLimitRuleAction extends ParentAction { readonly ruleId: string @@ -24,7 +26,7 @@ export class DeleteTimeLimitRuleAction extends ParentAction { constructor ({ ruleId }: {ruleId: string}) { super() - assertIdWithinFamily(ruleId) + assertIdWithinFamily({ actionType, field: 'ruleId', value: ruleId }) this.ruleId = ruleId } diff --git a/src/action/ignoremanipulation.ts b/src/action/ignoremanipulation.ts index bd84330..ce7d79b 100644 --- a/src/action/ignoremanipulation.ts +++ b/src/action/ignoremanipulation.ts @@ -16,8 +16,10 @@ */ import { DeviceHadManipulationFlags } from '../database/device' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'IgnoreManipulationAction' export class IgnoreManipulationAction extends ParentAction { readonly deviceId: string @@ -51,13 +53,14 @@ export class IgnoreManipulationAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) + + assertSafeInteger({ actionType, field: 'ignoreHadManipulationFlags', value: ignoreHadManipulationFlags }) if ( - (!Number.isSafeInteger(ignoreHadManipulationFlags)) || ignoreHadManipulationFlags < 0 || ignoreHadManipulationFlags > DeviceHadManipulationFlags.ALL ) { - throw new Error('invalid ignoreHadManipulationFlags') + throwOutOfRange({ actionType, field: 'ignoreHadManipulationFlags', value: ignoreHadManipulationFlags }) } this.deviceId = deviceId diff --git a/src/action/incrementcategoryextratime.ts b/src/action/incrementcategoryextratime.ts index 91ade10..0c24dd2 100644 --- a/src/action/incrementcategoryextratime.ts +++ b/src/action/incrementcategoryextratime.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'IncrementCategoryExtraTimeAction' export class IncrementCategoryExtraTimeAction extends ParentAction { readonly categoryId: string @@ -26,14 +28,18 @@ export class IncrementCategoryExtraTimeAction extends ParentAction { constructor ({ categoryId, addedExtraTime, day }: {categoryId: string, addedExtraTime: number, day: number}) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) - if (addedExtraTime <= 0 || (!Number.isSafeInteger(addedExtraTime))) { - throw new Error('must add some extra time with IncrementCategoryExtraTimeAction') + assertSafeInteger({ actionType, field: 'addedExtraTime', value: addedExtraTime }) + + if (addedExtraTime < 0) { + throwOutOfRange({ actionType, field: 'addedExtraTime', value: addedExtraTime }) } - if (day < -1 || (!Number.isSafeInteger(day))) { - throw Error('day must be valid') + assertSafeInteger({ actionType, field: 'day', value: day }) + + if (day < -1) { + throwOutOfRange({ actionType, field: 'day', value: day }) } this.categoryId = categoryId diff --git a/src/action/meta/exception.ts b/src/action/meta/exception.ts new file mode 100644 index 0000000..d56f651 --- /dev/null +++ b/src/action/meta/exception.ts @@ -0,0 +1,40 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { StaticMessageException } from '../../exception' + +export class InvalidActionParameterException extends StaticMessageException { + constructor ({ actionType, staticMessage, dynamicMessage }: { + actionType: string + staticMessage: string + dynamicMessage?: string + }) { + super({ + staticMessage: 'invalid action paramters:' + actionType + ':' + staticMessage, + dynamicMessage: dynamicMessage ? 'invalid action paramters:' + actionType + ':' + dynamicMessage : undefined + }) + } +} + +export class UnknownActionTypeException extends InvalidActionParameterException { + constructor ({ group }: { group: string }) { + super({ + actionType: group, + staticMessage: 'unknown action type' + }) + } +} diff --git a/src/action/meta/util.ts b/src/action/meta/util.ts new file mode 100644 index 0000000..ec5fb6e --- /dev/null +++ b/src/action/meta/util.ts @@ -0,0 +1,104 @@ +/* + * 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 . + */ + +import { checkIfHexString } from '../../util/hexstring' +import { hasDuplicates } from '../../util/list' +import { isIdWithinFamily } from '../../util/token' +import { InvalidActionParameterException } from './exception' + +export const assertIdWithinFamily = ({ value, actionType, field }: { + value: string + actionType: string + field: string +}) => { + if (!isIdWithinFamily(value)) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid id within family for ' + field, + dynamicMessage: 'invalid id within family for ' + field + ': ' + value + }) + } +} + +export const assertHexString = ({ value, actionType, field }: { + value: string + actionType: string + field: string +}) => { + if (!checkIfHexString(value)) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid hex string for ' + field, + dynamicMessage: 'invalid hex string for ' + field + ': ' + value + }) + } +} + +export const assertSafeInteger = ({ value, actionType, field }: { + value: number + actionType: string + field: string +}) => { + if (!Number.isSafeInteger(value)) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'require number for ' + field, + dynamicMessage: 'require number for ' + field + ': ' + value + }) + } +} + +export const throwOutOfRange = ({ value, actionType, field }: { + value: number + actionType: string + field: string +}) => { + throw new InvalidActionParameterException({ + actionType, + staticMessage: field + ' out of range', + dynamicMessage: field + ' out of range: ' + value + }) +} + +export function assertNonEmptyListWithoutDuplicates ({ list, actionType, field }: { + list: Array + actionType: string + field: string +}) { + assertListWithoutDuplicates({ list, actionType, field }) + + if (hasDuplicates(list)) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'empty list for ' + field + }) + } +} + +export function assertListWithoutDuplicates ({ list, actionType, field }: { + list: Array + actionType: string + field: string +}) { + if (hasDuplicates(list)) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'list has duplicates for ' + field, + dynamicMessage: 'list has duplicates for ' + field + ': ' + list.join(';') + }) + } +} diff --git a/src/action/removecategoryapps.ts b/src/action/removecategoryapps.ts index ea6e2a5..b4f5835 100644 --- a/src/action/removecategoryapps.ts +++ b/src/action/removecategoryapps.ts @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -import { assertNonEmptyListWithoutDuplicates } from '../util/list' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertNonEmptyListWithoutDuplicates } from './meta/util' + +const actionType = 'RemoveCategoryAppsAction' export class RemoveCategoryAppsAction extends ParentAction { readonly categoryId: string @@ -26,8 +27,8 @@ export class RemoveCategoryAppsAction extends ParentAction { constructor ({ categoryId, packageNames }: {categoryId: string, packageNames: Array}) { super() - assertIdWithinFamily(categoryId) - assertNonEmptyListWithoutDuplicates(packageNames) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + assertNonEmptyListWithoutDuplicates({ actionType, field: 'packageNames', list: packageNames }) this.categoryId = categoryId this.packageNames = packageNames diff --git a/src/action/removeinstalledapps.ts b/src/action/removeinstalledapps.ts index 84c924e..0e45b5c 100644 --- a/src/action/removeinstalledapps.ts +++ b/src/action/removeinstalledapps.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertNonEmptyListWithoutDuplicates } from '../util/list' import { AppLogicAction } from './basetypes' +import { assertNonEmptyListWithoutDuplicates } from './meta/util' + +const actionType = 'RemoveInstalledAppsAction' export class RemoveInstalledAppsAction extends AppLogicAction { readonly packageNames: Array @@ -24,7 +26,7 @@ export class RemoveInstalledAppsAction extends AppLogicAction { constructor ({ packageNames }: {packageNames: Array}) { super() - assertNonEmptyListWithoutDuplicates(packageNames) + assertNonEmptyListWithoutDuplicates({ actionType, field: 'packageNames', list: packageNames }) this.packageNames = packageNames } diff --git a/src/action/removeuser.ts b/src/action/removeuser.ts index b64871d..449fb52 100644 --- a/src/action/removeuser.ts +++ b/src/action/removeuser.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'RemoveUserAction' export class RemoveUserAction extends ParentAction { readonly userId: string @@ -31,7 +33,7 @@ export class RemoveUserAction extends ParentAction { }) { super() - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) this.userId = userId this.authentication = authentication diff --git a/src/action/renamechild.ts b/src/action/renamechild.ts index 88ec04b..5b1be60 100644 --- a/src/action/renamechild.ts +++ b/src/action/renamechild.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'RenameChildAction' export class RenameChildAction extends ParentAction { readonly childId: string @@ -28,10 +31,13 @@ export class RenameChildAction extends ParentAction { }) { super() - assertIdWithinFamily(childId) + assertIdWithinFamily({ actionType, field: 'childId', value: childId }) if (newName === '') { - throw new Error('new name must not be empty') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'new name must not be empty' + }) } this.childId = childId diff --git a/src/action/resetcategorynetworkids.ts b/src/action/resetcategorynetworkids.ts index 6c88c2d..cc6c8de 100644 --- a/src/action/resetcategorynetworkids.ts +++ b/src/action/resetcategorynetworkids.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'ResetCategoryNetworkIdsAction' export class ResetCategoryNetworkIdsAction extends ParentAction { readonly categoryId: string @@ -26,7 +28,7 @@ export class ResetCategoryNetworkIdsAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) this.categoryId = categoryId } diff --git a/src/action/resetparentblockedtimes.ts b/src/action/resetparentblockedtimes.ts index 754975f..690095c 100644 --- a/src/action/resetparentblockedtimes.ts +++ b/src/action/resetparentblockedtimes.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'ResetParentBlockedTimesAction' export class ResetParentBlockedTimesAction extends ParentAction { readonly parentId: string @@ -26,7 +28,7 @@ export class ResetParentBlockedTimesAction extends ParentAction { }) { super() - assertIdWithinFamily(parentId) + assertIdWithinFamily({ actionType, field: 'parentId', value: parentId }) this.parentId = parentId } diff --git a/src/action/serialization/applogicaction.ts b/src/action/serialization/applogicaction.ts index a3c4070..b464af1 100644 --- a/src/action/serialization/applogicaction.ts +++ b/src/action/serialization/applogicaction.ts @@ -20,6 +20,7 @@ import { AddUsedTimeAction, SerializedAddUsedTimeAction } from '../addusedtime' import { AddUsedTimeActionVersion2, SerializedAddUsedTimeActionVersion2 } from '../addusedtime2' import { AppLogicAction } from '../basetypes' import { ForceSyncAction, SerializedForceSyncAction } from '../forcesync' +import { UnknownActionTypeException } from '../meta/exception' import { RemoveInstalledAppsAction, SerializedRemoveInstalledAppsAction } from '../removeinstalledapps' import { SerializedSignOutAtDeviceAction, SignOutAtDeviceAction } from '../signoutatdevice' import { SerialiezdTriedDisablingDeviceAdminAction, TriedDisablingDeviceAdminAction } from '../trieddisablingdeviceadmin' @@ -57,6 +58,6 @@ export const parseAppLogicAction = (serialized: SerializedAppLogicAction): AppLo } else if (serialized.type === 'UPDATE_DEVICE_STATUS') { return UpdateDeviceStatusAction.parse(serialized) } else { - throw new Error('illegal state: unsupported type at parseAppLogicAction') + throw new UnknownActionTypeException({ group: 'app logic' }) } } diff --git a/src/action/serialization/childaction.ts b/src/action/serialization/childaction.ts index 880568c..231fb3c 100644 --- a/src/action/serialization/childaction.ts +++ b/src/action/serialization/childaction.ts @@ -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 { ChildChangePasswordAction, SerializedChildChangePasswordAction } from '../childchangepassword' import { ChildSignInAction, SerializedChildSignInAction } from '../childsignin' +import { UnknownActionTypeException } from '../meta/exception' export type SerializedChildAction = SerializedChildChangePasswordAction | SerializedChildSignInAction @@ -26,6 +27,6 @@ export const parseChildAction = (serialized: SerializedChildAction) => { } else if (serialized.type === 'CHILD_SIGN_IN') { return ChildSignInAction.parse(serialized) } else { - throw new Error('illegal state: unsupported type at parseChildAction') + throw new UnknownActionTypeException({ group: 'child' }) } } diff --git a/src/action/serialization/parentaction.ts b/src/action/serialization/parentaction.ts index 349ca2b..2fb7d90 100644 --- a/src/action/serialization/parentaction.ts +++ b/src/action/serialization/parentaction.ts @@ -26,6 +26,7 @@ import { DeleteCategoryAction, SerializedDeleteCategoryAction } from '../deletec import { DeleteTimeLimitRuleAction, SerializedDeleteTimeLimitRuleAction } from '../deletetimelimitrule' import { IgnoreManipulationAction, SerializedIgnoreManipulationAction } from '../ignoremanipulation' import { IncrementCategoryExtraTimeAction, SerializedIncrementCategoryExtraTimeAction } from '../incrementcategoryextratime' +import { UnknownActionTypeException } from '../meta/exception' import { RemoveCategoryAppsAction, SerializedRemoveCategoryAppsAction } from '../removecategoryapps' import { RemoveUserAction, SerializedRemoveUserAction } from '../removeuser' import { RenameChildAction, SerializedRenameChildAction } from '../renamechild' @@ -193,6 +194,6 @@ export const parseParentAction = (action: SerializedParentAction): ParentAction } else if (action.type === 'UPDATE_USER_LIMIT_LOGIN_CATEGORY') { return UpdateUserLimitLoginCategory.parse(action) } else { - throw new Error('illegal state: invalid type for action at parseParentAction') + throw new UnknownActionTypeException({ group: 'parent' }) } } diff --git a/src/action/setcategoryextratime.ts b/src/action/setcategoryextratime.ts index b3f9ba9..f1e1df7 100644 --- a/src/action/setcategoryextratime.ts +++ b/src/action/setcategoryextratime.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'SetCategoryExtraTimeAction' export class SetCategoryExtraTimeAction extends ParentAction { readonly categoryId: string @@ -26,14 +28,18 @@ export class SetCategoryExtraTimeAction extends ParentAction { constructor ({ categoryId, newExtraTime, day }: {categoryId: string, newExtraTime: number, day: number}) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) - if (newExtraTime < 0 || (!Number.isSafeInteger(newExtraTime))) { - throw Error('newExtraTime must be >= 0') + assertSafeInteger({ actionType, field: 'newExtraTime', value: newExtraTime }) + + if (newExtraTime < 0) { + throwOutOfRange({ actionType, field: 'newExtraTime', value: newExtraTime }) } - if (day < -1 || (!Number.isSafeInteger(day))) { - throw Error('day must be valid') + assertSafeInteger({ actionType, field: 'day', value: day }) + + if (day < -1) { + throwOutOfRange({ actionType, field: 'day', value: day }) } this.categoryId = categoryId diff --git a/src/action/setcategoryforunassignedapps.ts b/src/action/setcategoryforunassignedapps.ts index 81623cd..b33ff87 100644 --- a/src/action/setcategoryforunassignedapps.ts +++ b/src/action/setcategoryforunassignedapps.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetCategoryForUnassignedAppsAction' export class SetCategoryForUnassignedAppsAction extends ParentAction { readonly childId: string @@ -28,10 +30,10 @@ export class SetCategoryForUnassignedAppsAction extends ParentAction { }) { super() - assertIdWithinFamily(childId) + assertIdWithinFamily({ actionType, field: 'childId', value: childId }) if (categoryId !== '') { - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) } this.childId = childId diff --git a/src/action/setchildpassword.ts b/src/action/setchildpassword.ts index 4196e2f..34fcb8b 100644 --- a/src/action/setchildpassword.ts +++ b/src/action/setchildpassword.ts @@ -15,9 +15,12 @@ * along with this program. If not, see . */ -import { assertParentPasswordValid, ParentPassword } from '../api/schema' -import { assertIdWithinFamily } from '../util/token' +import { assertParentPasswordValid, ParentPassword, ParentPasswordValidationException } from '../api/schema' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetChildPasswordAction' export class SetChildPasswordAction extends ParentAction { readonly childUserId: string @@ -29,8 +32,18 @@ export class SetChildPasswordAction extends ParentAction { }) { super() - assertIdWithinFamily(childUserId) - assertParentPasswordValid(newPassword) + assertIdWithinFamily({ actionType, field: 'childUserId', value: childUserId }) + + try { + assertParentPasswordValid(newPassword) + } catch (ex) { + if (ex instanceof ParentPasswordValidationException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid parent password' + }) + } else throw ex + } this.childUserId = childUserId this.newPassword = newPassword diff --git a/src/action/setconsiderrebootmanipulation.ts b/src/action/setconsiderrebootmanipulation.ts index df3480b..5a6b678 100644 --- a/src/action/setconsiderrebootmanipulation.ts +++ b/src/action/setconsiderrebootmanipulation.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetConsiderRebootManipulationAction' export class SetConsiderRebootManipulationAction extends ParentAction { readonly deviceId: string @@ -25,7 +27,7 @@ export class SetConsiderRebootManipulationAction extends ParentAction { constructor ({ deviceId, enable }: {deviceId: string, enable: boolean}) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) this.deviceId = deviceId this.enable = enable diff --git a/src/action/setdevicedefaultuser.ts b/src/action/setdevicedefaultuser.ts index 333e214..da9dd98 100644 --- a/src/action/setdevicedefaultuser.ts +++ b/src/action/setdevicedefaultuser.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetDeviceDefaultUserAction' export class SetDeviceDefaultUserAction extends ParentAction { readonly deviceId: string @@ -28,10 +30,10 @@ export class SetDeviceDefaultUserAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) if (defaultUserId !== '') { - assertIdWithinFamily(defaultUserId) + assertIdWithinFamily({ actionType, field: 'defaultUserId', value: defaultUserId }) } this.deviceId = deviceId diff --git a/src/action/setdevicedefaultusertimeout.ts b/src/action/setdevicedefaultusertimeout.ts index fbb043f..af02d1d 100644 --- a/src/action/setdevicedefaultusertimeout.ts +++ b/src/action/setdevicedefaultusertimeout.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertSafeInteger } from './meta/util' + +const actionType = 'SetDeviceDefaultUserTimeoutAction' export class SetDeviceDefaultUserTimeoutAction extends ParentAction { readonly deviceId: string @@ -28,10 +31,15 @@ export class SetDeviceDefaultUserTimeoutAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) + assertSafeInteger({ actionType, field: 'timeout', value: timeout }) - if ((!Number.isInteger(timeout)) || (timeout < 0)) { - throw new Error('timeout must be a non-negative integer') + if (timeout < 0) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'timeout must be a non-negative integer', + dynamicMessage: 'timeout must be a non-negative integer, was ' + timeout + }) } this.deviceId = deviceId diff --git a/src/action/setdeviceuser.ts b/src/action/setdeviceuser.ts index 1a34c0f..3bd7fd7 100644 --- a/src/action/setdeviceuser.ts +++ b/src/action/setdeviceuser.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetDeviceUserAction' export class SetDeviceUserAction extends ParentAction { readonly deviceId: string @@ -28,10 +30,10 @@ export class SetDeviceUserAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) if (userId !== '') { - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) } this.deviceId = deviceId diff --git a/src/action/setkeepsignedin.ts b/src/action/setkeepsignedin.ts index bf433ad..0395f6e 100644 --- a/src/action/setkeepsignedin.ts +++ b/src/action/setkeepsignedin.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetKeepSignedInAction' export class SetKeepSignedInAction extends ParentAction { readonly deviceId: string @@ -28,7 +30,7 @@ export class SetKeepSignedInAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) this.deviceId = deviceId this.keepSignedIn = keepSignedIn diff --git a/src/action/setparentcategory.ts b/src/action/setparentcategory.ts index 52ad0dc..012a72f 100644 --- a/src/action/setparentcategory.ts +++ b/src/action/setparentcategory.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetParentCategoryAction' export class SetParentCategoryAction extends ParentAction { readonly categoryId: string @@ -28,10 +30,10 @@ export class SetParentCategoryAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) if (parentCategory !== '') { - assertIdWithinFamily(parentCategory) + assertIdWithinFamily({ actionType, field: 'parentCategory', value: parentCategory }) } this.categoryId = categoryId diff --git a/src/action/setrelaxprimarydevice.ts b/src/action/setrelaxprimarydevice.ts index 72f2a27..b23818e 100644 --- a/src/action/setrelaxprimarydevice.ts +++ b/src/action/setrelaxprimarydevice.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetRelaxPrimaryDeviceAction' export class SetRelaxPrimaryDeviceAction extends ParentAction { readonly userId: string @@ -28,7 +30,7 @@ export class SetRelaxPrimaryDeviceAction extends ParentAction { }) { super() - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) this.userId = userId this.relax = relax diff --git a/src/action/setsenddeviceconnected.ts b/src/action/setsenddeviceconnected.ts index c8dffa7..ead7da4 100644 --- a/src/action/setsenddeviceconnected.ts +++ b/src/action/setsenddeviceconnected.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetSendDeviceConnected' export class SetSendDeviceConnected extends ParentAction { readonly deviceId: string @@ -28,7 +30,7 @@ export class SetSendDeviceConnected extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) this.deviceId = deviceId this.enable = enable diff --git a/src/action/setuserdisablelimitsuntil.ts b/src/action/setuserdisablelimitsuntil.ts index cd09035..daefb53 100644 --- a/src/action/setuserdisablelimitsuntil.ts +++ b/src/action/setuserdisablelimitsuntil.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertSafeInteger } from './meta/util' + +const actionType = 'SetUserDisableLimitsUntilAction' export class SetUserDisableLimitsUntilAction extends ParentAction { readonly childId: string @@ -28,10 +31,15 @@ export class SetUserDisableLimitsUntilAction extends ParentAction { }) { super() - assertIdWithinFamily(childId) + assertIdWithinFamily({ actionType, field: 'childId', value: childId }) + assertSafeInteger({ actionType, field: 'timestamp', value: timestamp }) - if (timestamp < 0 || (!Number.isSafeInteger(timestamp))) { - throw new Error('timestamp for set user disabe limits until must be >= 0') + if (timestamp < 0) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'timestamp for set user disabe limits until must be >= 0', + dynamicMessage: 'timestamp for set user disabe limits until must be >= 0, but was ' + timestamp + }) } this.childId = childId diff --git a/src/action/setusertimezone.ts b/src/action/setusertimezone.ts index 33690e5..c718c06 100644 --- a/src/action/setusertimezone.ts +++ b/src/action/setusertimezone.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'SetUserTimezoneAction' export class SetUserTimezoneAction extends ParentAction { readonly userId: string @@ -28,7 +30,7 @@ export class SetUserTimezoneAction extends ParentAction { }) { super() - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) this.userId = userId this.timezone = timezone diff --git a/src/action/signoutatdevice.ts b/src/action/signoutatdevice.ts index 05b0820..773cdbd 100644 --- a/src/action/signoutatdevice.ts +++ b/src/action/signoutatdevice.ts @@ -24,7 +24,7 @@ export class SignOutAtDeviceAction extends AppLogicAction { super() } - static parse = (action: SerializedSignOutAtDeviceAction) => SignOutAtDeviceAction.instance + static parse = (_: SerializedSignOutAtDeviceAction) => SignOutAtDeviceAction.instance } export interface SerializedSignOutAtDeviceAction { diff --git a/src/action/updateappactivities.ts b/src/action/updateappactivities.ts index 8c761a2..45d5f7a 100644 --- a/src/action/updateappactivities.ts +++ b/src/action/updateappactivities.ts @@ -15,9 +15,14 @@ * along with this program. If not, see . */ -import { AppActivityItem, RemovedAppActivityItem, SerializedAppActivityItem, SerializedRemovedAppActivityItem } from '../model/appactivity' -import { assertListWithoutDuplicates } from '../util/list' +import { + AppActivityItem, IncompleteAppActivityItemException, RemovedAppActivityItem, SerializedAppActivityItem, SerializedRemovedAppActivityItem +} from '../model/appactivity' import { AppLogicAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertListWithoutDuplicates } from './meta/util' + +const actionType = 'UpdateAppActivitiesAction' export class UpdateAppActivitiesAction extends AppLogicAction { readonly removed: Array @@ -29,23 +34,44 @@ export class UpdateAppActivitiesAction extends AppLogicAction { }) { super() - assertListWithoutDuplicates(removed.map((item) => item.packageName + ':' + item.activityName)) - assertListWithoutDuplicates(updatedOrAdded.map((item) => item.packageName + ':' + item.activityName)) + assertListWithoutDuplicates({ + actionType, + field: 'removed', + list: removed.map((item) => item.packageName + ':' + item.activityName) + }) + + assertListWithoutDuplicates({ + actionType, + field: 'updatedOrAdded', + list: updatedOrAdded.map((item) => item.packageName + ':' + item.activityName) + }) if (removed.length === 0 && updatedOrAdded.length === 0) { - throw new Error('UpdateAppActivitiesAction is empty') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'UpdateAppActivitiesAction is empty' + }) } this.removed = removed this.updatedOrAdded = updatedOrAdded } - static parse = ({ removed, updatedOrAdded }: SerializedUpdateAppActivitiesAction) => ( - new UpdateAppActivitiesAction({ - removed: removed.map((item) => RemovedAppActivityItem.parse(item)), - updatedOrAdded: updatedOrAdded.map((item) => AppActivityItem.parse(item)) - }) - ) + static parse = ({ removed, updatedOrAdded }: SerializedUpdateAppActivitiesAction) => { + try { + return new UpdateAppActivitiesAction({ + removed: removed.map((item) => RemovedAppActivityItem.parse(item)), + updatedOrAdded: updatedOrAdded.map((item) => AppActivityItem.parse(item)) + }) + } catch (ex) { + if (ex instanceof IncompleteAppActivityItemException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid app activity item' + }) + } else throw ex + } + } } export interface SerializedUpdateAppActivitiesAction { diff --git a/src/action/updatecategorybatterylimit.ts b/src/action/updatecategorybatterylimit.ts index a4362ec..b58cde4 100644 --- a/src/action/updatecategorybatterylimit.ts +++ b/src/action/updatecategorybatterylimit.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'UpdateCategoryBatteryLimitAction' export class UpdateCategoryBatteryLimitAction extends ParentAction { readonly categoryId: string @@ -30,17 +32,21 @@ export class UpdateCategoryBatteryLimitAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) if (chargeLimit !== undefined) { - if ((!Number.isSafeInteger(chargeLimit)) || chargeLimit < 0 || chargeLimit > 100) { - throw new Error('charge limit out of range') + assertSafeInteger({ actionType, field: 'chargeLimit', value: chargeLimit }) + + if (chargeLimit < 0 || chargeLimit > 100) { + throwOutOfRange({ actionType, field: 'chargeLimit', value: chargeLimit }) } } if (mobileLimit !== undefined) { - if ((!Number.isSafeInteger(mobileLimit)) || mobileLimit < 0 || mobileLimit > 100) { - throw new Error('mobile limit out of range') + assertSafeInteger({ actionType, field: 'mobileLimit', value: mobileLimit }) + + if (mobileLimit < 0 || mobileLimit > 100) { + throwOutOfRange({ actionType, field: 'mobileLimit', value: mobileLimit }) } } diff --git a/src/action/updatecategoryblockallnotifications.ts b/src/action/updatecategoryblockallnotifications.ts index eec3a0a..40924e5 100644 --- a/src/action/updatecategoryblockallnotifications.ts +++ b/src/action/updatecategoryblockallnotifications.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateCategoryBlockAllNotificationsAction' export class UpdateCategoryBlockAllNotificationsAction extends ParentAction { readonly categoryId: string @@ -25,7 +27,7 @@ export class UpdateCategoryBlockAllNotificationsAction extends ParentAction { constructor ({ categoryId, blocked }: {categoryId: string, blocked: boolean}) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) this.categoryId = categoryId this.blocked = blocked diff --git a/src/action/updatecategoryblockedtimes.ts b/src/action/updatecategoryblockedtimes.ts index 8c79144..f60ec97 100644 --- a/src/action/updatecategoryblockedtimes.ts +++ b/src/action/updatecategoryblockedtimes.ts @@ -15,12 +15,15 @@ * along with this program. If not, see . */ -import { validateBitmask } from '../util/bitmask' -import { assertIdWithinFamily } from '../util/token' +import { BitmapValidationException, validateBitmask } from '../util/bitmask' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' export const blockedTimesBitmaskLength = 60 * 24 * 7 /* number of minutes per week */ +const actionType = 'UpdateCategoryBlockedTimesAction' + export class UpdateCategoryBlockedTimesAction extends ParentAction { readonly categoryId: string readonly blockedTimes: string @@ -31,8 +34,18 @@ export class UpdateCategoryBlockedTimesAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) - validateBitmask(blockedTimes, blockedTimesBitmaskLength) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + + try { + validateBitmask(blockedTimes, blockedTimesBitmaskLength) + } catch (ex) { + if (ex instanceof BitmapValidationException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid bitmask' + }) + } else throw ex + } this.categoryId = categoryId this.blockedTimes = blockedTimes diff --git a/src/action/updatecategorysorting.ts b/src/action/updatecategorysorting.ts index 1605ae8..c1c1590 100644 --- a/src/action/updatecategorysorting.ts +++ b/src/action/updatecategorysorting.ts @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -import { uniq } from 'lodash' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertNonEmptyListWithoutDuplicates } from './meta/util' + +const actionType = 'UpdateCategorySortingAction' export class UpdateCategorySortingAction extends ParentAction { readonly categoryIds: Array @@ -27,15 +28,13 @@ export class UpdateCategorySortingAction extends ParentAction { }) { super() - if (categoryIds.length === 0) { - throw new Error('empty category sorting list') - } + assertNonEmptyListWithoutDuplicates({ actionType, field: 'categoryIds', list: categoryIds }) - if (uniq(categoryIds).length !== categoryIds.length) { - throw new Error('category sorting list has duplicates') - } - - categoryIds.forEach((categoryId) => assertIdWithinFamily(categoryId)) + categoryIds.forEach((categoryId) => assertIdWithinFamily({ + actionType, + field: 'categoryIds', + value: categoryId + })) this.categoryIds = categoryIds } diff --git a/src/action/updatecategorytemporarilyblocked.ts b/src/action/updatecategorytemporarilyblocked.ts index 4bd1a53..2ba00e4 100644 --- a/src/action/updatecategorytemporarilyblocked.ts +++ b/src/action/updatecategorytemporarilyblocked.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertSafeInteger } from './meta/util' + +const actionType = 'UpdateCategoryTemporarilyBlockedAction' export class UpdateCategoryTemporarilyBlockedAction extends ParentAction { readonly categoryId: string @@ -30,15 +33,16 @@ export class UpdateCategoryTemporarilyBlockedAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) if (endTime !== undefined) { - if (!Number.isSafeInteger(endTime)) { - throw new Error() - } + assertSafeInteger({ actionType, field: 'endTime', value: endTime }) if (!blocked) { - throw new Error() + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'can not set a end time when disabling blocking' + }) } } diff --git a/src/action/updatecategorytimewarnings.ts b/src/action/updatecategorytimewarnings.ts index 5b4171d..d69583e 100644 --- a/src/action/updatecategorytimewarnings.ts +++ b/src/action/updatecategorytimewarnings.ts @@ -16,8 +16,10 @@ */ import { allowedTimeWarningFlags } from '../database/category' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'UpdateCategoryTimeWarningsAction' export class UpdateCategoryTimeWarningsAction extends ParentAction { readonly categoryId: string @@ -31,10 +33,11 @@ export class UpdateCategoryTimeWarningsAction extends ParentAction { }) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) + assertSafeInteger({ actionType, field: 'flags', value: flags }) if ((flags & allowedTimeWarningFlags) !== flags) { - throw new Error('illegal flags') + throwOutOfRange({ actionType, field: 'flags', value: flags }) } this.categoryId = categoryId diff --git a/src/action/updatecategorytitle.ts b/src/action/updatecategorytitle.ts index 3c46454..5411968 100644 --- a/src/action/updatecategorytitle.ts +++ b/src/action/updatecategorytitle.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateCategoryTitleAction' export class UpdateCategoryTitleAction extends ParentAction { readonly categoryId: string @@ -25,7 +27,7 @@ export class UpdateCategoryTitleAction extends ParentAction { constructor ({ categoryId, newTitle }: {categoryId: string, newTitle: string}) { super() - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) this.categoryId = categoryId this.newTitle = newTitle diff --git a/src/action/updatedevicename.ts b/src/action/updatedevicename.ts index d097714..5c72c9d 100644 --- a/src/action/updatedevicename.ts +++ b/src/action/updatedevicename.ts @@ -15,8 +15,11 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateDeviceNameAction' export class UpdateDeviceNameAction extends ParentAction { readonly deviceId: string @@ -31,10 +34,13 @@ export class UpdateDeviceNameAction extends ParentAction { this.deviceId = deviceId this.name = name - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) if (name.trim().length === 0) { - throw new Error('new device name must not be blank') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'new device name must not be blank' + }) } } diff --git a/src/action/updatedevicestatus.ts b/src/action/updatedevicestatus.ts index a5d9a77..33a0115 100644 --- a/src/action/updatedevicestatus.ts +++ b/src/action/updatedevicestatus.ts @@ -19,6 +19,9 @@ import { NewPermissionStatus } from '../model/newpermissionstatus' import { ProtectionLevel } from '../model/protectionlevel' import { RuntimePermissionStatus } from '../model/runtimepermissionstatus' import { AppLogicAction } from './basetypes' +import { assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'UpdateDeviceStatusAction' export class UpdateDeviceStatusAction extends AppLogicAction { readonly newProtetionLevel?: ProtectionLevel @@ -52,8 +55,10 @@ export class UpdateDeviceStatusAction extends AppLogicAction { super() if (newAppVersion !== undefined) { - if (!Number.isSafeInteger(newAppVersion) || (newAppVersion < 0)) { - throw new Error('invalid new ap version') + assertSafeInteger({ actionType, field: 'newAppVersion', value: newAppVersion }) + + if (newAppVersion < 0) { + throwOutOfRange({ actionType, field: 'newAppVersion', value: newAppVersion }) } } diff --git a/src/action/updateenableactivitylevelblocking.ts b/src/action/updateenableactivitylevelblocking.ts index 93f4626..ae2fa4a 100644 --- a/src/action/updateenableactivitylevelblocking.ts +++ b/src/action/updateenableactivitylevelblocking.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateEnableActivityLevelBlockingAction' export class UpdateEnableActivityLevelBlockingAction extends ParentAction { readonly deviceId: string @@ -25,7 +27,7 @@ export class UpdateEnableActivityLevelBlockingAction extends ParentAction { constructor ({ deviceId, enable }: {deviceId: string, enable: boolean}) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) this.deviceId = deviceId this.enable = enable diff --git a/src/action/updatenetworktimeverification.ts b/src/action/updatenetworktimeverification.ts index c50808e..b74f7d4 100644 --- a/src/action/updatenetworktimeverification.ts +++ b/src/action/updatenetworktimeverification.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateNetworkTimeVerificationAction' export class UpdateNetworkTimeVerificationAction extends ParentAction { readonly deviceId: string @@ -28,7 +30,7 @@ export class UpdateNetworkTimeVerificationAction extends ParentAction { }) { super() - assertIdWithinFamily(deviceId) + assertIdWithinFamily({ actionType, field: 'deviceId', value: deviceId }) this.deviceId = deviceId this.mode = mode diff --git a/src/action/updateparentblockedtimes.ts b/src/action/updateparentblockedtimes.ts index 4a346cb..ff35725 100644 --- a/src/action/updateparentblockedtimes.ts +++ b/src/action/updateparentblockedtimes.ts @@ -15,9 +15,12 @@ * along with this program. If not, see . */ -import { validateAndParseBitmask } from '../util/bitmask' -import { assertIdWithinFamily } from '../util/token' +import { BitmapValidationException, validateAndParseBitmask } from '../util/bitmask' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateParentBlockedTimesAction' export class UpdateParentBlockedTimesAction extends ParentAction { readonly parentId: string @@ -29,9 +32,9 @@ export class UpdateParentBlockedTimesAction extends ParentAction { }) { super() - assertIdWithinFamily(parentId) + assertIdWithinFamily({ actionType, field: 'parentId', value: parentId }) - { + try { const parsedBlockedTimes = validateAndParseBitmask(blockedTimes, 60 * 24 * 7 /* number of minutes per week */) for (let day = 0; day < 7; day++) { @@ -44,9 +47,19 @@ export class UpdateParentBlockedTimesAction extends ParentAction { } if (blockedMinutes > 60 * 18 /* 18 hours */) { - throw new Error('too much blocked minutes per day') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'too much blocked minutes per day' + }) } } + } catch (ex) { + if (ex instanceof BitmapValidationException) { + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'invalid bitmask' + }) + } else throw ex } this.parentId = parentId diff --git a/src/action/updateparentnotificationflags.ts b/src/action/updateparentnotificationflags.ts index 8289d3d..329ae05 100644 --- a/src/action/updateparentnotificationflags.ts +++ b/src/action/updateparentnotificationflags.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'UpdateParentNotificationFlagsAction' export class UpdateParentNotificationFlagsAction extends ParentAction { readonly parentId: string @@ -30,14 +32,12 @@ export class UpdateParentNotificationFlagsAction extends ParentAction { }) { super() - assertIdWithinFamily(parentId) + assertIdWithinFamily({ actionType, field: 'parentId', value: parentId }) - if (!Number.isSafeInteger(flags)) { - throw new Error('flags must be an integer') - } + assertSafeInteger({ actionType, field: 'flags', value: flags }) if (flags < 0 || flags > 1) { - throw new Error('flags are out of the valid range') + throwOutOfRange({ actionType, field: 'flags', value: flags }) } this.parentId = parentId diff --git a/src/action/updatetimelimitrule.ts b/src/action/updatetimelimitrule.ts index 1ebbff9..af8e1ed 100644 --- a/src/action/updatetimelimitrule.ts +++ b/src/action/updatetimelimitrule.ts @@ -16,8 +16,11 @@ */ import { MinuteOfDay } from '../util/minuteofday' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' + +const actionType = 'UpdateTimelimitRuleAction' export class UpdateTimelimitRuleAction extends ParentAction { readonly ruleId: string @@ -53,35 +56,37 @@ export class UpdateTimelimitRuleAction extends ParentAction { this.sessionDurationMilliseconds = sessionDurationMilliseconds this.sessionPauseMilliseconds = sessionPauseMilliseconds - assertIdWithinFamily(ruleId) + assertIdWithinFamily({ actionType, field: 'ruleId', value: ruleId }) - if (maximumTimeInMillis < 0 || (!Number.isSafeInteger(maximumTimeInMillis))) { - throw new Error('maximumTimeInMillis must be >= 0') + assertSafeInteger({ actionType, field: 'maximumTimeInMillis', value: maximumTimeInMillis }) + + if (maximumTimeInMillis < 0) { + throwOutOfRange({ actionType, field: 'maximumTimeInMillis', value: maximumTimeInMillis }) } - if (!( - Number.isSafeInteger(dayMask) || - dayMask < 0 || - dayMask > (1 | 2 | 4 | 8 | 16 | 32 | 64) - )) { - throw new Error('invalid day mask') + assertSafeInteger({ actionType, field: 'dayMask', value: dayMask }) + + if (dayMask < 0 || dayMask > (1 | 2 | 4 | 8 | 16 | 32 | 64)) { + throwOutOfRange({ actionType, field: 'dayMask', value: dayMask }) } - if ( - (!Number.isSafeInteger(start)) || - (!Number.isSafeInteger(end)) || - (!Number.isSafeInteger(sessionDurationMilliseconds)) || - (!Number.isSafeInteger(sessionPauseMilliseconds)) - ) { - throw new Error() - } + assertSafeInteger({ actionType, field: 'start', value: start }) + assertSafeInteger({ actionType, field: 'end', value: end }) + assertSafeInteger({ actionType, field: 'sessionDurationMilliseconds', value: sessionDurationMilliseconds }) + assertSafeInteger({ actionType, field: 'sessionPauseMilliseconds', value: sessionPauseMilliseconds }) if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) { - throw new Error() + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'time slot out of range' + }) } if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) { - throw new Error() + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'session duration lesser than zero' + }) } } diff --git a/src/action/updateuserflags.ts b/src/action/updateuserflags.ts index 8c5f3d1..ad4d8da 100644 --- a/src/action/updateuserflags.ts +++ b/src/action/updateuserflags.ts @@ -16,8 +16,11 @@ */ import { UserFlags } from '../model/userflags' -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { InvalidActionParameterException } from './meta/exception' +import { assertIdWithinFamily, assertSafeInteger } from './meta/util' + +const actionType = 'UpdateUserFlagsAction' export class UpdateUserFlagsAction extends ParentAction { readonly userId: string @@ -31,14 +34,16 @@ export class UpdateUserFlagsAction extends ParentAction { }) { super() - assertIdWithinFamily(userId) - - if ((!Number.isSafeInteger(modifiedBits)) || (!Number.isSafeInteger(newValues))) { - throw new Error('flags must be an integer') - } + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) + assertSafeInteger({ actionType, field: 'modifiedBits', value: modifiedBits }) + assertSafeInteger({ actionType, field: 'newValues', value: newValues }) if ((modifiedBits | UserFlags.ALL_FLAGS) !== UserFlags.ALL_FLAGS || (modifiedBits | newValues) !== modifiedBits) { - throw new Error('flags are out of the valid range') + throw new InvalidActionParameterException({ + actionType, + staticMessage: 'flags are out of the valid range', + dynamicMessage: 'flags are out of the valid range: ' + modifiedBits + ', ' + newValues + }) } this.userId = userId diff --git a/src/action/updateuserlimitlogincategory.ts b/src/action/updateuserlimitlogincategory.ts index 994f828..9978ae2 100644 --- a/src/action/updateuserlimitlogincategory.ts +++ b/src/action/updateuserlimitlogincategory.ts @@ -15,8 +15,10 @@ * along with this program. If not, see . */ -import { assertIdWithinFamily } from '../util/token' import { ParentAction } from './basetypes' +import { assertIdWithinFamily } from './meta/util' + +const actionType = 'UpdateUserLimitLoginCategory' export class UpdateUserLimitLoginCategory extends ParentAction { readonly userId: string @@ -28,10 +30,10 @@ export class UpdateUserLimitLoginCategory extends ParentAction { }) { super() - assertIdWithinFamily(userId) + assertIdWithinFamily({ actionType, field: 'userId', value: userId }) if (categoryId !== undefined) { - assertIdWithinFamily(categoryId) + assertIdWithinFamily({ actionType, field: 'categoryId', value: categoryId }) } this.userId = userId diff --git a/src/api/schema.ts b/src/api/schema.ts index 7103c4e..27ac31b 100644 --- a/src/api/schema.ts +++ b/src/api/schema.ts @@ -52,14 +52,16 @@ export interface ParentPassword { export const assertParentPasswordValid = (password: ParentPassword) => { if (password.hash === '' || password.secondHash === '' || password.secondSalt === '') { - throw new Error('missing fields at parent password') + throw new ParentPasswordValidationException('missing fields at parent password') } if (!(optionalPasswordRegex.test(password.hash) && optionalPasswordRegex.test(password.secondHash) && optionalSaltRegex.test(password.secondSalt))) { - throw new Error('invalid parent password') + throw new ParentPasswordValidationException('invalid parent password') } } +export class ParentPasswordValidationException extends Error {} + export interface CreateFamilyByMailTokenRequest { mailAuthToken: string parentPassword: ParentPassword diff --git a/src/api/sync.ts b/src/api/sync.ts index f5c7d38..88c8755 100644 --- a/src/api/sync.ts +++ b/src/api/sync.ts @@ -85,7 +85,7 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager, throw new BadRequest() } - await database.transaction(async (transaction) => { + const serverStatus = await database.transaction(async (transaction) => { const deviceEntryUnsafe = await database.device.findOne({ where: { deviceAuthToken: body.deviceAuthToken @@ -112,23 +112,23 @@ export const createSyncRouter = ({ database, websocket, connectedDevicesManager, }) } - const serverStatus = await generateServerDataStatus({ + return generateServerDataStatus({ database, familyId, clientStatus: body.status, transaction }) - - if (serverStatus.devices) { eventHandler.countEvent('pullStatusRequest devices') } - if (serverStatus.apps) { eventHandler.countEvent('pullStatusRequest apps') } - if (serverStatus.categoryBase) { eventHandler.countEvent('pullStatusRequest categoryBase') } - if (serverStatus.categoryApp) { eventHandler.countEvent('pullStatusRequest categoryApp') } - if (serverStatus.usedTimes) { eventHandler.countEvent('pullStatusRequest usedTimes') } - if (serverStatus.rules) { eventHandler.countEvent('pullStatusRequest rules') } - if (serverStatus.users) { eventHandler.countEvent('pullStatusRequest users') } - - res.json(serverStatus) }) + + if (serverStatus.devices) { eventHandler.countEvent('pullStatusRequest devices') } + if (serverStatus.apps) { eventHandler.countEvent('pullStatusRequest apps') } + if (serverStatus.categoryBase) { eventHandler.countEvent('pullStatusRequest categoryBase') } + if (serverStatus.categoryApp) { eventHandler.countEvent('pullStatusRequest categoryApp') } + if (serverStatus.usedTimes) { eventHandler.countEvent('pullStatusRequest usedTimes') } + if (serverStatus.rules) { eventHandler.countEvent('pullStatusRequest rules') } + if (serverStatus.users) { eventHandler.countEvent('pullStatusRequest users') } + + res.json(serverStatus) } catch (ex) { next(ex) } diff --git a/src/config.ts b/src/config.ts index 316a913..837581a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,7 +29,7 @@ function parseYesNo (value: string) { } else if (value === 'no') { return false } else { - throw new Error('invalid value "' + value + '", expected "yes" or "no"') + throw new ParseYesNoException('invalid value "' + value + '", expected "yes" or "no"') } } @@ -39,3 +39,5 @@ export const config: Config = { pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000, alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false } + +class ParseYesNoException extends Error {} diff --git a/src/database/sessionduration.ts b/src/database/sessionduration.ts index 7d99eff..a3ec0c5 100644 --- a/src/database/sessionduration.ts +++ b/src/database/sessionduration.ts @@ -16,6 +16,7 @@ */ import * as Sequelize from 'sequelize' +import { ValidationException } from '../exception' import { MinuteOfDay } from '../util/minuteofday' import { familyIdColumn, idWithinFamilyColumn, timestampColumn } from './columns' import { SequelizeAttributes } from './types' @@ -85,11 +86,11 @@ export const attributesVersion1: SequelizeAttributes endMinuteOfDay) { - throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay') + throw new ValidationException({ staticMessage: 'startMinuteOfDay must not be bigger than endMinuteOfDay for a session duration' }) } } } diff --git a/src/database/timelimitrule.ts b/src/database/timelimitrule.ts index 573f99a..fe16614 100644 --- a/src/database/timelimitrule.ts +++ b/src/database/timelimitrule.ts @@ -16,6 +16,7 @@ */ import * as Sequelize from 'sequelize' +import { ValidationException } from '../exception' import { MinuteOfDay } from '../util/minuteofday' import { booleanColumn, familyIdColumn, idWithinFamilyColumn } from './columns' import { SequelizeAttributes } from './types' @@ -90,11 +91,11 @@ export const attributesVersion2: SequelizeAttributes endMinuteOfDay) { - throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay') + throw new ValidationException({ staticMessage: 'startMinuteOfDay must not be bigger than endMinuteOfDay for a time limit rule' }) } } }, diff --git a/src/database/usedtime.ts b/src/database/usedtime.ts index 63f2dde..4f96169 100644 --- a/src/database/usedtime.ts +++ b/src/database/usedtime.ts @@ -16,6 +16,7 @@ */ import * as Sequelize from 'sequelize' +import { ValidationException } from '../exception' import { MinuteOfDay } from '../util/minuteofday' import { familyIdColumn, idWithinFamilyColumn, timestampColumn } from './columns' import { SequelizeAttributes } from './types' @@ -100,11 +101,11 @@ export const attributesVersion3: SequelizeAttributes const startMinuteOfDay = this.startMinuteOfDay if (typeof endMinuteOfDay !== 'number' || typeof startMinuteOfDay !== 'number') { - throw new Error('wrong data types') + throw new ValidationException({ staticMessage: 'wrong data types for start and end minute at the used time' }) } if (startMinuteOfDay > endMinuteOfDay) { - throw new Error('startMinuteOfDay must not be bigger than endMinuteOfDay') + throw new ValidationException({ staticMessage: 'startMinuteOfDay must not be bigger than endMinuteOfDay for a used time' }) } } } diff --git a/src/exception/index.ts b/src/exception/index.ts new file mode 100644 index 0000000..10537fd --- /dev/null +++ b/src/exception/index.ts @@ -0,0 +1,19 @@ +/* + * 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 . + */ + +export { StaticMessageException, IllegalStateException } from './static-message-exception' +export { ValidationException } from './validation' diff --git a/src/exception/static-message-exception.ts b/src/exception/static-message-exception.ts new file mode 100644 index 0000000..f3c8f4e --- /dev/null +++ b/src/exception/static-message-exception.ts @@ -0,0 +1,30 @@ +/* + * 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 . + */ + +export class StaticMessageException extends Error { + readonly staticMessage: string + readonly dynamicMessage?: string + + constructor ({ staticMessage, dynamicMessage }: { staticMessage: string, dynamicMessage?: string }) { + super(dynamicMessage || staticMessage) + + this.staticMessage = staticMessage + this.dynamicMessage = dynamicMessage + } +} + +export class IllegalStateException extends StaticMessageException {} diff --git a/src/exception/validation.ts b/src/exception/validation.ts new file mode 100644 index 0000000..e37af22 --- /dev/null +++ b/src/exception/validation.ts @@ -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 . + */ + +import { StaticMessageException } from './static-message-exception' + +export class ValidationException extends StaticMessageException {} diff --git a/src/function/child/set-primary-device.ts b/src/function/child/set-primary-device.ts index 0d63390..6da1e21 100644 --- a/src/function/child/set-primary-device.ts +++ b/src/function/child/set-primary-device.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { Conflict, Unauthorized } from 'http-errors' +import { Conflict, InternalServerError, Unauthorized } from 'http-errors' import * as Sequelize from 'sequelize' import { config } from '../../config' import { Database } from '../../database' @@ -147,7 +147,7 @@ export const setPrimaryDevice = async ({ database, websocket, deviceAuthToken, c throw new Conflict() } } else { - throw new Error('illegal state') + throw new InternalServerError('illegal state') } // invalidiate user list diff --git a/src/function/device/report-device-removed.ts b/src/function/device/report-device-removed.ts index c500363..3f40c66 100644 --- a/src/function/device/report-device-removed.ts +++ b/src/function/device/report-device-removed.ts @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +import { Unauthorized } from 'http-errors' import { Database } from '../../database' import { generateAuthToken, generateVersionId } from '../../util/token' import { WebsocketApi } from '../../websocket' @@ -83,7 +84,7 @@ export async function reportDeviceRemoved ({ database, deviceAuthToken, websocke }) if (!oldDeviceEntry) { - throw new Error('device not found') + throw new Unauthorized('device not found') } } }) diff --git a/src/function/parent/get-status-by-mail-address.ts b/src/function/parent/get-status-by-mail-address.ts index bfa91c1..80efb86 100644 --- a/src/function/parent/get-status-by-mail-address.ts +++ b/src/function/parent/get-status-by-mail-address.ts @@ -16,13 +16,14 @@ */ import { Database, Transaction } from '../../database' +import { StaticMessageException } from '../../exception' import { requireMailByAuthToken } from '../authentication' const getStatusByMailAddress = async ({ mail, database, transaction }: { mail: string, database: Database, transaction: Transaction }) => { if (!mail) { - throw new Error('no mail address') + throw new StaticMessageException({ staticMessage: 'getStatusByMailAddress: no mail address provided' }) } const entry = await database.user.findOne({ diff --git a/src/function/sync/apply-actions/baseinfo.ts b/src/function/sync/apply-actions/baseinfo.ts new file mode 100644 index 0000000..334192b --- /dev/null +++ b/src/function/sync/apply-actions/baseinfo.ts @@ -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 . + */ + +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 { + 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 + } +} diff --git a/src/function/sync/apply-actions/cache.ts b/src/function/sync/apply-actions/cache.ts index 58199d7..1399242 100644 --- a/src/function/sync/apply-actions/cache.ts +++ b/src/function/sync/apply-actions/cache.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts index a7ceb91..be1a28e 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime.ts @@ -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 = { diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts index 7c6f245..8fff885 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/addusedtime2.ts @@ -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 = { diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts index cae0827..eaed2d6 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/index.ts @@ -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() } } diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/signoutatdevice.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/signoutatdevice.ts index 154da97..a15684c 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/signoutatdevice.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/signoutatdevice.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/trieddisablingdeviceadmin.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/trieddisablingdeviceadmin.ts index e2283f9..a376abd 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/trieddisablingdeviceadmin.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/trieddisablingdeviceadmin.ts @@ -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) diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/updateappactivities.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/updateappactivities.ts index 73f2e4d..c232478 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/updateappactivities.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/updateappactivities.ts @@ -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, diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/updatedevicestatus.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/updatedevicestatus.ts index 520ea0c..39d8290 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/updatedevicestatus.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/updatedevicestatus.ts @@ -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) diff --git a/src/function/sync/apply-actions/dispatch-child-action/childchangepassword.ts b/src/function/sync/apply-actions/dispatch-child-action/childchangepassword.ts index 3b1c662..ceecdfe 100644 --- a/src/function/sync/apply-actions/dispatch-child-action/childchangepassword.ts +++ b/src/function/sync/apply-actions/dispatch-child-action/childchangepassword.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-child-action/childsignin.ts b/src/function/sync/apply-actions/dispatch-child-action/childsignin.ts index f84d80b..9699fb2 100644 --- a/src/function/sync/apply-actions/dispatch-child-action/childsignin.ts +++ b/src/function/sync/apply-actions/dispatch-child-action/childsignin.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-child-action/index.ts b/src/function/sync/apply-actions/dispatch-child-action/index.ts index d0cbc76..fa0bde1 100644 --- a/src/function/sync/apply-actions/dispatch-child-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-child-action/index.ts @@ -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() } } diff --git a/src/function/sync/apply-actions/dispatch-helper/app-logic-action.ts b/src/function/sync/apply-actions/dispatch-helper/app-logic-action.ts new file mode 100644 index 0000000..1d81747 --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-helper/app-logic-action.ts @@ -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 . + */ + +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 }) + } + }) +} diff --git a/src/function/sync/apply-actions/dispatch-helper/child-action.ts b/src/function/sync/apply-actions/dispatch-helper/child-action.ts new file mode 100644 index 0000000..0e0538d --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-helper/child-action.ts @@ -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 . + */ + +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 }) + } + }) +} diff --git a/src/function/sync/apply-actions/dispatch-helper/helper.ts b/src/function/sync/apply-actions/dispatch-helper/helper.ts new file mode 100644 index 0000000..7fb000f --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-helper/helper.ts @@ -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 . + */ + +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 ({ 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 + 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 + } +} diff --git a/src/function/sync/apply-actions/dispatch-helper/index.ts b/src/function/sync/apply-actions/dispatch-helper/index.ts new file mode 100644 index 0000000..e6128a3 --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-helper/index.ts @@ -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 . + */ + +export { dispatchAppLogicAction } from './app-logic-action' +export { dispatchChildAction } from './child-action' +export { dispatchParentAction } from './parent-action' diff --git a/src/function/sync/apply-actions/dispatch-helper/parent-action.ts b/src/function/sync/apply-actions/dispatch-helper/parent-action.ts new file mode 100644 index 0000000..c0d8a6e --- /dev/null +++ b/src/function/sync/apply-actions/dispatch-helper/parent-action.ts @@ -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 . + */ + +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 + }) + } + } + }) +} diff --git a/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts b/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts index 7acfd68..1385b5d 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts @@ -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 } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/addcategorynetworkid.ts b/src/function/sync/apply-actions/dispatch-parent-action/addcategorynetworkid.ts index f68af53..5f3ddd5 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/addcategorynetworkid.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/addcategorynetworkid.ts @@ -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({ diff --git a/src/function/sync/apply-actions/dispatch-parent-action/changeparentpassword.ts b/src/function/sync/apply-actions/dispatch-parent-action/changeparentpassword.ts index c329c9b..9450531 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/changeparentpassword.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/changeparentpassword.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts b/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts index 46cd92f..a342c68 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts @@ -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', { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts b/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts index bf64b99..35a0741 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts @@ -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() } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/deletecategory.ts b/src/function/sync/apply-actions/dispatch-parent-action/deletecategory.ts index a55a460..b9a2a7b 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/deletecategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/deletecategory.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/deletetimelimitrule.ts b/src/function/sync/apply-actions/dispatch-parent-action/deletetimelimitrule.ts index eb7ea47..e1e6909 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/deletetimelimitrule.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/deletetimelimitrule.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/ignoremanipulation.ts b/src/function/sync/apply-actions/dispatch-parent-action/ignoremanipulation.ts index b16a646..81d6712 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/ignoremanipulation.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/ignoremanipulation.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/incrementcategoryextratime.ts b/src/function/sync/apply-actions/dispatch-parent-action/incrementcategoryextratime.ts index 72e52d5..5ff9710 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/incrementcategoryextratime.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/incrementcategoryextratime.ts @@ -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) diff --git a/src/function/sync/apply-actions/dispatch-parent-action/index.ts b/src/function/sync/apply-actions/dispatch-parent-action/index.ts index 058c685..953154d 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/index.ts @@ -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') } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/removecategoryapps.ts b/src/function/sync/apply-actions/dispatch-parent-action/removecategoryapps.ts index de56752..7f471af 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/removecategoryapps.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/removecategoryapps.ts @@ -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) diff --git a/src/function/sync/apply-actions/dispatch-parent-action/removeuser.ts b/src/function/sync/apply-actions/dispatch-parent-action/removeuser.ts index 6e1a8c2..dc450ea 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/removeuser.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/removeuser.ts @@ -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' }) } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/renamechild.ts b/src/function/sync/apply-actions/dispatch-parent-action/renamechild.ts index 1ce5b59..51f9560 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/renamechild.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/renamechild.ts @@ -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) } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/resetcategorynetworkids.ts b/src/function/sync/apply-actions/dispatch-parent-action/resetcategorynetworkids.ts index 879ae92..ee6de73 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/resetcategorynetworkids.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/resetcategorynetworkids.ts @@ -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({ diff --git a/src/function/sync/apply-actions/dispatch-parent-action/resetparentblockedtimes.ts b/src/function/sync/apply-actions/dispatch-parent-action/resetparentblockedtimes.ts index 6957f7f..e1e2bda 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/resetparentblockedtimes.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/resetparentblockedtimes.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setcategoryextratime.ts b/src/function/sync/apply-actions/dispatch-parent-action/setcategoryextratime.ts index e77f974..3e125c0 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setcategoryextratime.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setcategoryextratime.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setcategoryforunassignedapps.ts b/src/function/sync/apply-actions/dispatch-parent-action/setcategoryforunassignedapps.ts index 5ed561a..d21d9a1 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setcategoryforunassignedapps.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setcategoryforunassignedapps.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setchildpassword.ts b/src/function/sync/apply-actions/dispatch-parent-action/setchildpassword.ts index 95cb831..6e7e634 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setchildpassword.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setchildpassword.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setconsiderrebootmanipulation.ts b/src/function/sync/apply-actions/dispatch-parent-action/setconsiderrebootmanipulation.ts index 588eee7..398d0d1 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setconsiderrebootmanipulation.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setconsiderrebootmanipulation.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultuser.ts b/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultuser.ts index ac4c45b..dea14b8 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultuser.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultuser.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultusertimeout.ts b/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultusertimeout.ts index 8b3e217..645a56d 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultusertimeout.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setdevicedefaultusertimeout.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setdeviceuser.ts b/src/function/sync/apply-actions/dispatch-parent-action/setdeviceuser.ts index e242c86..3edf454 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setdeviceuser.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setdeviceuser.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setkeepsignedin.ts b/src/function/sync/apply-actions/dispatch-parent-action/setkeepsignedin.ts index d001ead..4a91c6d 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setkeepsignedin.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setkeepsignedin.ts @@ -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' }) } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts b/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts index b1050bb..7423933 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts @@ -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() @@ -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 } } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setrelaxprimarydevice.ts b/src/function/sync/apply-actions/dispatch-parent-action/setrelaxprimarydevice.ts index d981a1e..a10a7c4 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setrelaxprimarydevice.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setrelaxprimarydevice.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setsenddeviceconnected.ts b/src/function/sync/apply-actions/dispatch-parent-action/setsenddeviceconnected.ts index 3d3cce0..04a4cac 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setsenddeviceconnected.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setsenddeviceconnected.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setuserdisablelmitsuntil.ts b/src/function/sync/apply-actions/dispatch-parent-action/setuserdisablelmitsuntil.ts index e7a6f8a..db51649 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setuserdisablelmitsuntil.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setuserdisablelmitsuntil.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/setusertimezone.ts b/src/function/sync/apply-actions/dispatch-parent-action/setusertimezone.ts index 4487a11..c74da7c 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setusertimezone.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setusertimezone.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorybatterylimit.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorybatterylimit.ts index 3f94679..6f7a7b8 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorybatterylimit.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorybatterylimit.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockallnotifications.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockallnotifications.ts index a67959c..8868b99 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockallnotifications.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockallnotifications.ts @@ -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' }) } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockedtimes.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockedtimes.ts index 4fda2bf..046c9e1 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockedtimes.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockedtimes.ts @@ -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' }) } }) } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts index ffe2f04..61f7573 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts @@ -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' }) } } } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytimewarnings.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytimewarnings.ts index b611e2a..103cb5e 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytimewarnings.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytimewarnings.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytitle.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytitle.ts index c0726f6..058ee66 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytitle.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytitle.ts @@ -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 }, { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatedevicename.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatedevicename.ts index 2a9af64..c052861 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatedevicename.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatedevicename.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updateenableactivitylevelblocking.ts b/src/function/sync/apply-actions/dispatch-parent-action/updateenableactivitylevelblocking.ts index dccc22b..4050c1b 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updateenableactivitylevelblocking.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updateenableactivitylevelblocking.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatenetworktimeverification.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatenetworktimeverification.ts index 7772c46..eb8475a 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatenetworktimeverification.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatenetworktimeverification.ts @@ -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 }, { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updateparentblockedtimes.ts b/src/function/sync/apply-actions/dispatch-parent-action/updateparentblockedtimes.ts index 304aff7..12a46e4 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updateparentblockedtimes.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updateparentblockedtimes.ts @@ -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 } diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updateparentnotificationflags.ts b/src/function/sync/apply-actions/dispatch-parent-action/updateparentnotificationflags.ts index 695f26e..2465de7 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updateparentnotificationflags.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updateparentnotificationflags.ts @@ -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) { diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updatetimelimitrule.ts b/src/function/sync/apply-actions/dispatch-parent-action/updatetimelimitrule.ts index f8af940..d7bf4b5 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatetimelimitrule.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatetimelimitrule.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updateuserflags.ts b/src/function/sync/apply-actions/dispatch-parent-action/updateuserflags.ts index d2433da..4e40900 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updateuserflags.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updateuserflags.ts @@ -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 diff --git a/src/function/sync/apply-actions/dispatch-parent-action/updateuserlimitlogincategory.ts b/src/function/sync/apply-actions/dispatch-parent-action/updateuserlimitlogincategory.ts index d37c076..6b7e419 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updateuserlimitlogincategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updateuserlimitlogincategory.ts @@ -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({ diff --git a/src/function/sync/apply-actions/exception/illegal-state.ts b/src/function/sync/apply-actions/exception/illegal-state.ts new file mode 100644 index 0000000..61dad1a --- /dev/null +++ b/src/function/sync/apply-actions/exception/illegal-state.ts @@ -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 . + */ + +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' }) + } +} diff --git a/src/function/sync/apply-actions/exception/index.ts b/src/function/sync/apply-actions/exception/index.ts new file mode 100644 index 0000000..de570f3 --- /dev/null +++ b/src/function/sync/apply-actions/exception/index.ts @@ -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 . + */ + +import { StaticMessageException } from '../../../../exception' + +export class ApplyActionException extends StaticMessageException {} diff --git a/src/function/sync/apply-actions/exception/integrity.ts b/src/function/sync/apply-actions/exception/integrity.ts new file mode 100644 index 0000000..3f733eb --- /dev/null +++ b/src/function/sync/apply-actions/exception/integrity.ts @@ -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 . + */ + +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' }) } +} diff --git a/src/function/sync/apply-actions/exception/invalidaction.ts b/src/function/sync/apply-actions/exception/invalidaction.ts new file mode 100644 index 0000000..89aefa3 --- /dev/null +++ b/src/function/sync/apply-actions/exception/invalidaction.ts @@ -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 . + */ + +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 + } +} diff --git a/src/function/sync/apply-actions/exception/missing-item.ts b/src/function/sync/apply-actions/exception/missing-item.ts new file mode 100644 index 0000000..ec6d975 --- /dev/null +++ b/src/function/sync/apply-actions/exception/missing-item.ts @@ -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 . + */ + +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' }) + } +} diff --git a/src/function/sync/apply-actions/exception/premium.ts b/src/function/sync/apply-actions/exception/premium.ts new file mode 100644 index 0000000..806f96c --- /dev/null +++ b/src/function/sync/apply-actions/exception/premium.ts @@ -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 . + */ + +import { ApplyActionException } from './index' + +export class PremiumVersionMissingException extends ApplyActionException { + constructor () { + super({ staticMessage: 'premium version required but missing' }) + } +} diff --git a/src/function/sync/apply-actions/exception/self-limit.ts b/src/function/sync/apply-actions/exception/self-limit.ts new file mode 100644 index 0000000..f3b9241 --- /dev/null +++ b/src/function/sync/apply-actions/exception/self-limit.ts @@ -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 . + */ + +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 {} diff --git a/src/function/sync/apply-actions/exception/sequence.ts b/src/function/sync/apply-actions/exception/sequence.ts new file mode 100644 index 0000000..d5edb20 --- /dev/null +++ b/src/function/sync/apply-actions/exception/sequence.ts @@ -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 . + */ + +import { ApplyActionException } from './index' + +export class SequenceNumberRepeatedException extends ApplyActionException { + constructor () { super({ staticMessage: 'sequence number repeated' }) } +} diff --git a/src/function/sync/apply-actions/index.ts b/src/function/sync/apply-actions/index.ts index 7743e35..692387c 100644 --- a/src/function/sync/apply-actions/index.ts +++ b/src/function/sync/apply-actions/index.ts @@ -15,20 +15,19 @@ * along with this program. If not, see . */ -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, diff --git a/src/function/sync/apply-actions/integrity.ts b/src/function/sync/apply-actions/integrity.ts index f10083a..8e5bf6d 100644 --- a/src/function/sync/apply-actions/integrity.ts +++ b/src/function/sync/apply-actions/integrity.ts @@ -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() } } diff --git a/src/function/sync/apply-actions/parse-encoded-action.ts b/src/function/sync/apply-actions/parse-encoded-action.ts new file mode 100644 index 0000000..3f43559 --- /dev/null +++ b/src/function/sync/apply-actions/parse-encoded-action.ts @@ -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 . + */ + +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 + } +} diff --git a/src/function/sync/get-server-data-status.ts b/src/function/sync/get-server-data-status.ts index b2dc26d..44cd6b6 100644 --- a/src/function/sync/get-server-data-status.ts +++ b/src/function/sync/get-server-data-status.ts @@ -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 }) + } +} diff --git a/src/model/appactivity.ts b/src/model/appactivity.ts index ce5a47f..19ba967 100644 --- a/src/model/appactivity.ts +++ b/src/model/appactivity.ts @@ -15,7 +15,7 @@ export class AppActivityItem { title: string }) { if ((!packageName) || (!activityName)) { - throw new Error('incomplete app activity') + throw new IncompleteAppActivityItemException('incomplete app activity') } this.packageName = packageName @@ -49,7 +49,7 @@ export class RemovedAppActivityItem { activityName: string }) { if ((!packageName) || (!activityName)) { - throw new Error('incomplete app activity') + throw new IncompleteAppActivityItemException('incomplete app activity') } this.packageName = packageName @@ -68,3 +68,5 @@ export class RemovedAppActivityItem { this.activityName ]) } + +export class IncompleteAppActivityItemException extends Error {} diff --git a/src/model/timelimitrule.ts b/src/model/timelimitrule.ts index f5d9e82..d82d627 100644 --- a/src/model/timelimitrule.ts +++ b/src/model/timelimitrule.ts @@ -57,7 +57,7 @@ export class TimelimitRule { assertIdWithinFamily(categoryId) if (maxTimeInMillis < 0 || (!Number.isSafeInteger(maxTimeInMillis))) { - throw new Error('maxTimeInMillis must be >= 0') + throw new ParseTimeLimitRuleException('maxTimeInMillis must be >= 0') } if (!( @@ -65,7 +65,7 @@ export class TimelimitRule { dayMask < 0 || dayMask > (1 | 2 | 4 | 8 | 16 | 32 | 64) )) { - throw new Error('invalid day mask') + throw new ParseTimeLimitRuleException('invalid day mask') } if ( @@ -74,15 +74,15 @@ export class TimelimitRule { (!Number.isSafeInteger(sessionDurationMilliseconds)) || (!Number.isSafeInteger(sessionPauseMilliseconds)) ) { - throw new Error() + throw new ParseTimeLimitRuleException() } if (start < MinuteOfDay.MIN || end > MinuteOfDay.MAX || start > end) { - throw new Error() + throw new ParseTimeLimitRuleException() } if (sessionDurationMilliseconds < 0 || sessionPauseMilliseconds < 0) { - throw new Error() + throw new ParseTimeLimitRuleException() } } @@ -124,3 +124,5 @@ export interface SerializedTimeLimitRule { dur?: number pause?: number } + +export class ParseTimeLimitRuleException extends Error {} diff --git a/src/util/bitmask.ts b/src/util/bitmask.ts index 2d3b529..dcbe010 100644 --- a/src/util/bitmask.ts +++ b/src/util/bitmask.ts @@ -21,7 +21,7 @@ export const serializedBitmaskRegex = /^(\d*,\d*(,\d*,\d*)*)?$/ export const validateBitmask = (bitmask: string, maxLength: number) => { if (!serializedBitmaskRegex.test(bitmask)) { - throw new Error('bitmask does not match regex') + throw new BitmapValidationException('bitmask does not match regex') } if (bitmask === '') { @@ -31,22 +31,22 @@ export const validateBitmask = (bitmask: string, maxLength: number) => { const splitpoints = split(bitmask, ',').map((item) => parseInt(item, 10)) if (splitpoints.findIndex((item) => !Number.isSafeInteger(item)) !== -1) { - throw new Error('bitmask contains non-safe integers') + throw new BitmapValidationException('bitmask contains non-safe integers') } if (splitpoints.findIndex((item) => item < 0) !== -1) { - throw new Error('bitmask contains negative integers') + throw new BitmapValidationException('bitmask contains negative integers') } if (splitpoints.findIndex((item) => item > maxLength) !== -1) { - throw new Error('bitmask contains integers bigger than maxSize') + throw new BitmapValidationException('bitmask contains integers bigger than maxSize') } let previousValue = -1 splitpoints.forEach((item) => { if (item <= previousValue) { - throw new Error('bitmask numbers are not strictly sorted') + throw new BitmapValidationException('bitmask numbers are not strictly sorted') } previousValue = item @@ -72,3 +72,5 @@ export const validateAndParseBitmask = (bitmask: string, maxLength: number) => { return result } + +export class BitmapValidationException extends Error {} diff --git a/src/util/category.ts b/src/util/category.ts index 310d918..c229a62 100644 --- a/src/util/category.ts +++ b/src/util/category.ts @@ -23,7 +23,7 @@ export function getCategoryWithParentCategories (categories: Array<{ categoryId: const startCategory = categoryById.get(startCategoryId) if (!startCategory) { - throw new Error('start category not found') + throw new GetParentCategoriesException('start category not found') } const categoryIds = [ startCategoryId ] @@ -38,3 +38,5 @@ export function getCategoryWithParentCategories (categories: Array<{ categoryId: return categoryIds } + +export class GetParentCategoriesException extends Error {} diff --git a/src/util/hexstring.ts b/src/util/hexstring.ts index ee90fb2..e2b5d44 100644 --- a/src/util/hexstring.ts +++ b/src/util/hexstring.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -15,16 +15,26 @@ * along with this program. If not, see . */ -export function assertIsHexString (value: string) { +export function checkIfHexString (value: string) { if (value.length % 2 !== 0) { - throw new Error('expected hex string but has got uneven length') + return false } for (let i = 0; i < value.length; i++) { const char = value[i] if ('0123456789abcdef'.indexOf(char) === -1) { - throw new Error('expected hex string but got invalid char') + return false } } + + return true } + +export function assertIsHexString (value: string) { + if (!checkIfHexString(value)) { + throw new HexStringValidationException('wanted hex string but did not get one') + } +} + +export class HexStringValidationException extends Error {} diff --git a/src/util/list.ts b/src/util/list.ts index 5c1c969..c89909b 100644 --- a/src/util/list.ts +++ b/src/util/list.ts @@ -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,18 +17,6 @@ import { uniq } from 'lodash' -export function assertNonEmptyListWithoutDuplicates (list: Array) { - if (list.length === 0) { - throw new Error('expected not empty list') - } - - if (uniq(list).length !== list.length) { - throw new Error('expected list without duplicates') - } -} - -export function assertListWithoutDuplicates (list: Array) { - if (uniq(list).length !== list.length) { - throw new Error('expected list without duplicates') - } +export function hasDuplicates (list: Array): boolean { + return uniq(list).length !== list.length } diff --git a/src/util/mail.ts b/src/util/mail.ts index 989ef75..e38404f 100644 --- a/src/util/mail.ts +++ b/src/util/mail.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2020 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -19,6 +19,7 @@ import { parseOneAddress } from 'email-addresses' import * as Email from 'email-templates' import { join } from 'path' import { config } from '../config' +import { IllegalStateException } from '../exception' const mailimprint = process.env.MAIL_IMPRINT || 'not defined' const mailServerBlacklist = (process.env.MAIL_SERVER_BLACKLIST || '').split(',').filter((item) => !!item) @@ -129,7 +130,7 @@ export function sanitizeMailAddress (input: string): string | null { const address = (parsed as any).address if (typeof address !== 'string') { - throw new Error('illegal state') + throw new IllegalStateException({ staticMessage: 'mail address is not a string' }) } return address diff --git a/src/util/token.ts b/src/util/token.ts index 7f901f5..39f683a 100644 --- a/src/util/token.ts +++ b/src/util/token.ts @@ -16,6 +16,7 @@ */ import * as TokenGenerator from 'tokgen' +import { ValidationException } from '../exception' const authTokenGenerator = new TokenGenerator({ length: 32, @@ -33,7 +34,10 @@ export const generateIdWithinFamily = () => idWithinFamilyGenerator.generate() export const isIdWithinFamily = (id: string) => id.length === 6 && /^[a-zA-Z0-9]+$/.test(id) export const assertIdWithinFamily = (id: string) => { if (!isIdWithinFamily(id)) { - throw new Error('invalid id within family') + throw new ValidationException({ + staticMessage: 'invalid id within family', + dynamicMessage: 'invalid id within family: ' + id + }) } }