From 007b1f2ec919aec72aff33538fc058e50f0f47c3 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 3 Aug 2020 02:00:00 +0200 Subject: [PATCH] Add support for child users adding limits themself --- docs/api/sync.md | 1 + .../dispatch-parent-action/addcategoryapps.ts | 83 ++++++++- .../dispatch-parent-action/createcategory.ts | 9 +- .../createtimelimitrule.ts | 20 ++- .../dispatch-parent-action/index.ts | 161 +++++++++--------- .../setparentcategory.ts | 20 ++- .../updatecategoryblockallnotifications.ts | 28 ++- .../updatecategorytemporarilyblocked.ts | 38 ++++- src/function/sync/apply-actions/index.ts | 71 +++++++- src/model/userflags.ts | 3 +- src/util/category.ts | 40 +++++ 11 files changed, 375 insertions(+), 99 deletions(-) create mode 100644 src/util/category.ts diff --git a/docs/api/sync.md b/docs/api/sync.md index 01b0aa0..eeef21b 100644 --- a/docs/api/sync.md +++ b/docs/api/sync.md @@ -44,6 +44,7 @@ The integrity field of a action may have got one of the following values: - an empty string when no user authentication is required/ for app logic actions (e.g. incrementing the used time) - the string ``device`` in case of parent actions if a parent is assigned to the device and asking for the password was disabled - ``sha512(sequence number as string with the base 10 + the device id as string + the hash of the user password using the second salt as string + the encoded action as string)`` for parent and child actions +- the string ``childDevice`` in case the child wants to add limits for itself using parent actions; this feature must be enabled for the child and this allows only some actions with some parameters In case of a invalid integrity value, the action is ignored and the client is told to do a full sync 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 870bce1..7acfd68 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/addcategoryapps.ts @@ -18,11 +18,13 @@ import * as Sequelize from 'sequelize' import { AddCategoryAppsAction } from '../../../../action' import { CategoryAppAttributes } from '../../../../database/categoryapp' +import { getCategoryWithParentCategories } from '../../../../util/category' import { Cache } from '../cache' -export async function dispatchAddCategoryApps ({ action, cache }: { +export async function dispatchAddCategoryApps ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: AddCategoryAppsAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { const categoryEntryUnsafe = await cache.database.category.findOne({ where: { @@ -39,14 +41,25 @@ export async function dispatchAddCategoryApps ({ action, cache }: { const { childId } = categoryEntryUnsafe + if (fromChildSelfLimitAddChildUserId !== null) { + if (childId !== fromChildSelfLimitAddChildUserId) { + throw new Error('can not add apps to other users') + } + } + const categoriesOfSameChild = await cache.database.category.findAll({ where: { familyId: cache.familyId, childId }, - attributes: ['categoryId'], + attributes: ['categoryId', 'parentCategoryId'], transaction: cache.transaction - }).map((item) => ({ categoryId: item.categoryId })) + }).map((item) => ({ + categoryId: item.categoryId, + parentCategoryId: item.parentCategoryId + })) + + const userCategoryIds = categoriesOfSameChild.map((item) => item.categoryId) const oldCategories = await cache.database.categoryApp.findAll({ attributes: [ 'categoryId' ], @@ -54,7 +67,7 @@ export async function dispatchAddCategoryApps ({ action, cache }: { where: { familyId: cache.familyId, categoryId: { - [Sequelize.Op.in]: categoriesOfSameChild.map((item) => item.categoryId) + [Sequelize.Op.in]: userCategoryIds }, packageName: { [Sequelize.Op.in]: action.packageNames @@ -63,6 +76,68 @@ export async function dispatchAddCategoryApps ({ action, cache }: { transaction: cache.transaction }).map((item) => item.categoryId) + if (fromChildSelfLimitAddChildUserId !== null) { + const parentCategoriesOfTargetCategory = getCategoryWithParentCategories(categoriesOfSameChild, action.categoryId) + const userEntryUnsafe = await cache.database.user.findOne({ + attributes: [ 'categoryForNotAssignedApps' ], + where: { + familyId: cache.familyId, + userId: fromChildSelfLimitAddChildUserId + }, + transaction: cache.transaction + }) + + if (!userEntryUnsafe) { + throw new Error('illegal state') + } + + const userEntry = { categoryForNotAssignedApps: userEntryUnsafe.categoryForNotAssignedApps } + const validatedDefaultCategoryId = categoriesOfSameChild.find((item) => item.categoryId === userEntry.categoryForNotAssignedApps)?.categoryId + const allowUnassignedElements = validatedDefaultCategoryId !== undefined && + parentCategoriesOfTargetCategory.indexOf(validatedDefaultCategoryId) !== -1 + + const assertCanAddApp = async (packageName: string, isApp: boolean) => { + const categoryAppEntryUnsafe = await cache.database.categoryApp.findOne({ + attributes: [ 'categoryId' ], + where: { + familyId: cache.familyId, + categoryId: { + [Sequelize.Op.in]: userCategoryIds + }, + packageName: packageName + }, + transaction: cache.transaction + }) + + const categoryAppEntry = categoryAppEntryUnsafe ? { categoryId: categoryAppEntryUnsafe.categoryId } : null + + if (categoryAppEntry === null) { + if ((isApp && allowUnassignedElements) || (!isApp)) { + // allow + } else { + throw new Error('can not assign apps without category as child') + } + } else { + if (parentCategoriesOfTargetCategory.indexOf(categoryAppEntry.categoryId) !== -1) { + // allow + } else { + throw new Error('can not add app which is not contained in the parent category') + } + } + } + + for (let i = 0; i < action.packageNames.length; i++) { + const packageName = action.packageNames[i] + + if (packageName.indexOf(':') !== -1) { + await assertCanAddApp(packageName.substring(0, packageName.indexOf(':')), true) + await assertCanAddApp(packageName, false) + } else { + await assertCanAddApp(packageName, true) + } + } + } + if (oldCategories.length > 0) { await cache.database.categoryApp.destroy({ where: { 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 b6f8aa9..46cd92f 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/createcategory.ts @@ -19,10 +19,17 @@ import { CreateCategoryAction } from '../../../../action' import { generateVersionId } from '../../../../util/token' import { Cache } from '../cache' -export async function dispatchCreateCategory ({ action, cache }: { +export async function dispatchCreateCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: CreateCategoryAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { + if (fromChildSelfLimitAddChildUserId !== null) { + if (fromChildSelfLimitAddChildUserId !== action.childId) { + throw new Error('can not create categories for other child users') + } + } + // check that the child exists const childEntry = await cache.database.user.findOne({ where: { 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 a20beef..bf64b99 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/createtimelimitrule.ts @@ -18,16 +18,30 @@ import { CreateTimeLimitRuleAction } from '../../../../action' import { Cache } from '../cache' -export async function dispatchCreateTimeLimitRule ({ action, cache }: { +export async function dispatchCreateTimeLimitRule ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: CreateTimeLimitRuleAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { - const doesCategoryExist = await cache.doesCategoryExist(action.rule.categoryId) + const categoryEntryUnsafe = await cache.database.category.findOne({ + where: { + familyId: cache.familyId, + categoryId: action.rule.categoryId + }, + transaction: cache.transaction, + attributes: ['childId'] + }) - if (!doesCategoryExist) { + if (!categoryEntryUnsafe) { throw new Error('invalid category id for new rule') } + if (fromChildSelfLimitAddChildUserId !== null) { + if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) { + throw new Error('can not add rules for other users') + } + } + await cache.database.timelimitRule.create({ familyId: cache.familyId, ruleId: action.rule.ruleId, 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 51202c9..886e33e 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/index.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/index.ts @@ -102,94 +102,99 @@ import { dispatchUpdateTimelimitRule } from './updatetimelimitrule' import { dispatchUpdateUserFlagsAction } from './updateuserflags' import { dispatchUpdateUserLimitLoginCategoryAction } from './updateuserlimitlogincategory' -export const dispatchParentAction = async ({ action, cache, parentUserId, sourceDeviceId }: { +export const dispatchParentAction = async ({ action, cache, parentUserId, sourceDeviceId, fromChildSelfLimitAddChildUserId }: { action: ParentAction cache: Cache parentUserId: string sourceDeviceId: string | null + fromChildSelfLimitAddChildUserId: string | null }) => { if (action instanceof AddCategoryAppsAction) { - await dispatchAddCategoryApps({ action, cache }) - } else if (action instanceof AddUserAction) { - await dispatchAddUser({ action, cache }) - } else if (action instanceof RemoveCategoryAppsAction) { - await dispatchRemoveCategoryApps({ action, cache }) + return dispatchAddCategoryApps({ action, cache, fromChildSelfLimitAddChildUserId }) } else if (action instanceof CreateCategoryAction) { - await dispatchCreateCategory({ action, cache }) + return dispatchCreateCategory({ action, cache, fromChildSelfLimitAddChildUserId }) } else if (action instanceof CreateTimeLimitRuleAction) { - await dispatchCreateTimeLimitRule({ action, cache }) - } else if (action instanceof DeleteCategoryAction) { - await dispatchDeleteCategory({ action, cache }) - } else if (action instanceof UpdateCategoryTitleAction) { - await dispatchUpdateCategoryTitle({ action, cache }) - } else if (action instanceof SetCategoryExtraTimeAction) { - await dispatchSetCategoryExtraTime({ action, cache }) - } else if (action instanceof SetCategoryForUnassignedAppsAction) { - await dispatchSetCategoryForUnassignedApps({ action, cache }) - } else if (action instanceof SetChildPasswordAction) { - await dispatchSetChildPassword({ action, cache }) - } else if (action instanceof SetConsiderRebootManipulationAction) { - await dispatchSetConsiderRebootManipulation({ action, cache }) - } else if (action instanceof SetDeviceDefaultUserAction) { - await dispatchSetDeviceDefaultUser({ action, cache }) - } else if (action instanceof SetDeviceDefaultUserTimeoutAction) { - await dispatchSetDeviceDefaultUserTimeout({ action, cache }) - } else if (action instanceof SetDeviceUserAction) { - await dispatchSetDeviceUser({ action, cache }) - } else if (action instanceof SetKeepSignedInAction) { - await dispatchSetKeepSignedIn({ action, cache, parentUserId }) - } else if (action instanceof SetParentCategoryAction) { - await dispatchSetParentCategory({ action, cache }) - } else if (action instanceof SetRelaxPrimaryDeviceAction) { - await dispatchSetRelaxPrimaryDevice({ action, cache }) - } else if (action instanceof SetSendDeviceConnected) { - await dispatchSetSendDeviceConnected({ action, cache, sourceDeviceId }) - } else if (action instanceof SetUserDisableLimitsUntilAction) { - await dispatchUserSetDisableLimitsUntil({ action, cache }) - } else if (action instanceof SetUserTimezoneAction) { - await dispatchSetUserTimezone({ action, cache }) - } else if (action instanceof UpdateCategoryBatteryLimitAction) { - await dispatchUpdateCategoryBatteryLimit({ action, cache }) + return dispatchCreateTimeLimitRule({ action, cache, fromChildSelfLimitAddChildUserId }) } else if (action instanceof UpdateCategoryBlockAllNotificationsAction) { - await dispatchUpdateCategoryBlockAllNotifications({ action, cache }) - } else if (action instanceof UpdateCategoryBlockedTimesAction) { - await dispatchUpdateCategoryBlockedTimes({ action, cache }) - } else if (action instanceof UpdateCategorySortingAction) { - await dispatchUpdateCategorySorting({ action, cache }) - } else if (action instanceof IncrementCategoryExtraTimeAction) { - await dispatchIncrementCategoryExtraTime({ action, cache }) + return dispatchUpdateCategoryBlockAllNotifications({ action, cache, fromChildSelfLimitAddChildUserId }) + } else if (action instanceof SetParentCategoryAction) { + return dispatchSetParentCategory({ action, cache, fromChildSelfLimitAddChildUserId }) } else if (action instanceof UpdateCategoryTemporarilyBlockedAction) { - await dispatchUpdateCategoryTemporarilyBlocked({ action, cache }) - } else if (action instanceof DeleteTimeLimitRuleAction) { - await dispatchDeleteTimeLimitRule({ action, cache }) - } else if (action instanceof UpdateDeviceNameAction) { - await dispatchUpdateDeviceName({ action, cache }) - } else if (action instanceof UpdateEnableActivityLevelBlockingAction) { - await dispatchUpdateEnableActivityLevelBlocking({ action, cache }) - } else if (action instanceof UpdateNetworkTimeVerificationAction) { - await dispatchUpdateNetworkTimeVerification({ action, cache }) - } else if (action instanceof UpdateParentNotificationFlagsAction) { - await dispatchUpdateParentNotificationFlags({ action, cache }) - } else if (action instanceof UpdateTimelimitRuleAction) { - await dispatchUpdateTimelimitRule({ action, cache }) - } else if (action instanceof RemoveUserAction) { - await dispatchRemoveUser({ action, cache, parentUserId }) - } else if (action instanceof RenameChildAction) { - await dispatchRenameChild({ action, cache }) - } else if (action instanceof ChangeParentPasswordAction) { - await dispatchChangeParentPassword({ action, cache }) - } else if (action instanceof IgnoreManipulationAction) { - await dispatchIgnoreManipulation({ action, cache }) - } else if (action instanceof UpdateCategoryTimeWarningsAction) { - await dispatchUpdateCategoryTimeWarnings({ action, cache }) - } else if (action instanceof ResetParentBlockedTimesAction) { - await dispatchResetParentBlockedTimes({ action, cache }) - } else if (action instanceof UpdateParentBlockedTimesAction) { - await dispatchUpdateParentBlockedTimes({ action, cache, parentUserId }) - } else if (action instanceof UpdateUserFlagsAction) { - await dispatchUpdateUserFlagsAction({ action, cache }) - } else if (action instanceof UpdateUserLimitLoginCategory) { - await dispatchUpdateUserLimitLoginCategoryAction({ action, cache, parentUserId }) + return dispatchUpdateCategoryTemporarilyBlocked({ action, cache, fromChildSelfLimitAddChildUserId }) + } + + if (fromChildSelfLimitAddChildUserId === null) { + if (action instanceof AddUserAction) { + return dispatchAddUser({ action, cache }) + } else if (action instanceof RemoveCategoryAppsAction) { + return dispatchRemoveCategoryApps({ action, cache }) + } else if (action instanceof DeleteCategoryAction) { + return dispatchDeleteCategory({ action, cache }) + } else if (action instanceof UpdateCategoryTitleAction) { + return dispatchUpdateCategoryTitle({ action, cache }) + } else if (action instanceof SetCategoryExtraTimeAction) { + return dispatchSetCategoryExtraTime({ action, cache }) + } else if (action instanceof SetCategoryForUnassignedAppsAction) { + return dispatchSetCategoryForUnassignedApps({ action, cache }) + } else if (action instanceof SetChildPasswordAction) { + return dispatchSetChildPassword({ action, cache }) + } else if (action instanceof SetConsiderRebootManipulationAction) { + return dispatchSetConsiderRebootManipulation({ action, cache }) + } else if (action instanceof SetDeviceDefaultUserAction) { + return dispatchSetDeviceDefaultUser({ action, cache }) + } else if (action instanceof SetDeviceDefaultUserTimeoutAction) { + return dispatchSetDeviceDefaultUserTimeout({ action, cache }) + } else if (action instanceof SetDeviceUserAction) { + return dispatchSetDeviceUser({ action, cache }) + } else if (action instanceof SetKeepSignedInAction) { + return dispatchSetKeepSignedIn({ action, cache, parentUserId }) + } else if (action instanceof SetRelaxPrimaryDeviceAction) { + return dispatchSetRelaxPrimaryDevice({ action, cache }) + } else if (action instanceof SetSendDeviceConnected) { + return dispatchSetSendDeviceConnected({ action, cache, sourceDeviceId }) + } else if (action instanceof SetUserDisableLimitsUntilAction) { + return dispatchUserSetDisableLimitsUntil({ action, cache }) + } else if (action instanceof SetUserTimezoneAction) { + return dispatchSetUserTimezone({ action, cache }) + } else if (action instanceof UpdateCategoryBatteryLimitAction) { + return dispatchUpdateCategoryBatteryLimit({ action, cache }) + } else if (action instanceof UpdateCategoryBlockedTimesAction) { + return dispatchUpdateCategoryBlockedTimes({ action, cache }) + } else if (action instanceof UpdateCategorySortingAction) { + return dispatchUpdateCategorySorting({ action, cache }) + } else if (action instanceof IncrementCategoryExtraTimeAction) { + return dispatchIncrementCategoryExtraTime({ action, cache }) + } else if (action instanceof DeleteTimeLimitRuleAction) { + return dispatchDeleteTimeLimitRule({ action, cache }) + } else if (action instanceof UpdateDeviceNameAction) { + return dispatchUpdateDeviceName({ action, cache }) + } else if (action instanceof UpdateEnableActivityLevelBlockingAction) { + return dispatchUpdateEnableActivityLevelBlocking({ action, cache }) + } else if (action instanceof UpdateNetworkTimeVerificationAction) { + return dispatchUpdateNetworkTimeVerification({ action, cache }) + } else if (action instanceof UpdateParentNotificationFlagsAction) { + return dispatchUpdateParentNotificationFlags({ action, cache }) + } else if (action instanceof UpdateTimelimitRuleAction) { + return dispatchUpdateTimelimitRule({ action, cache }) + } else if (action instanceof RemoveUserAction) { + return dispatchRemoveUser({ action, cache, parentUserId }) + } else if (action instanceof RenameChildAction) { + return dispatchRenameChild({ action, cache }) + } else if (action instanceof ChangeParentPasswordAction) { + return dispatchChangeParentPassword({ action, cache }) + } else if (action instanceof IgnoreManipulationAction) { + return dispatchIgnoreManipulation({ action, cache }) + } else if (action instanceof UpdateCategoryTimeWarningsAction) { + return dispatchUpdateCategoryTimeWarnings({ action, cache }) + } else if (action instanceof ResetParentBlockedTimesAction) { + return dispatchResetParentBlockedTimes({ action, cache }) + } else if (action instanceof UpdateParentBlockedTimesAction) { + return dispatchUpdateParentBlockedTimes({ action, cache, parentUserId }) + } else if (action instanceof UpdateUserFlagsAction) { + return dispatchUpdateUserFlagsAction({ action, cache }) + } else if (action instanceof UpdateUserLimitLoginCategory) { + return dispatchUpdateUserLimitLoginCategoryAction({ action, cache, parentUserId }) + } } else { throw new Error('unsupported action type') } 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 5ad807f..b1050bb 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/setparentcategory.ts @@ -16,11 +16,13 @@ */ import { SetParentCategoryAction } from '../../../../action' +import { getCategoryWithParentCategories } from '../../../../util/category' import { Cache } from '../cache' -export async function dispatchSetParentCategory ({ action, cache }: { +export async function dispatchSetParentCategory ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: SetParentCategoryAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { const categoryEntry = await cache.database.category.findOne({ where: { @@ -34,6 +36,12 @@ export async function dispatchSetParentCategory ({ action, cache }: { throw new Error('tried to set parent category of non existent category') } + if (fromChildSelfLimitAddChildUserId !== null) { + if (categoryEntry.childId !== fromChildSelfLimitAddChildUserId) { + throw new Error('can not set parent category for other user') + } + } + if (action.parentCategory !== '') { const categoriesByUserId = (await cache.database.category.findAll({ where: { @@ -74,6 +82,16 @@ export async function dispatchSetParentCategory ({ action, cache }: { if (childCategoryIds.has(action.parentCategory) || action.parentCategory === action.categoryId) { throw new Error('can not set a category as parent which is a child of the category') } + + if (fromChildSelfLimitAddChildUserId !== null) { + const ownParentCategory = categoriesByUserId.find((item) => item.categoryId === categoryEntry.parentCategoryId) + const enableDueToLimitAddingWhenChild = ownParentCategory === undefined || + getCategoryWithParentCategories(categoriesByUserId, action.parentCategory).indexOf(ownParentCategory.categoryId) !== -1 + + if (!enableDueToLimitAddingWhenChild) { + throw new Error('can not change parent categories in a way which reduces limits') + } + } } await cache.database.category.update({ 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 542dd41..a67959c 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategoryblockallnotifications.ts +++ b/src/function/sync/apply-actions/dispatch-parent-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 @@ -18,10 +18,34 @@ import { UpdateCategoryBlockAllNotificationsAction } from '../../../../action' import { Cache } from '../cache' -export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cache }: { +export async function dispatchUpdateCategoryBlockAllNotifications ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: UpdateCategoryBlockAllNotificationsAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { + const categoryEntryUnsafe = await cache.database.category.findOne({ + where: { + familyId: cache.familyId, + categoryId: action.categoryId + }, + transaction: cache.transaction, + attributes: ['childId'] + }) + + if (!categoryEntryUnsafe) { + throw new Error('invalid category id for updating notification blocking') + } + + if (fromChildSelfLimitAddChildUserId !== null) { + if (fromChildSelfLimitAddChildUserId !== categoryEntryUnsafe.childId) { + throw new Error('can not add rules for other users') + } + + if (!action.blocked) { + throw new Error('can not disable filter as child') + } + } + const [affectedRows] = await cache.database.category.update({ blockAllNotifications: action.blocked }, { 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 070dda0..ffe2f04 100644 --- a/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts +++ b/src/function/sync/apply-actions/dispatch-parent-action/updatecategorytemporarilyblocked.ts @@ -18,9 +18,10 @@ import { UpdateCategoryTemporarilyBlockedAction } from '../../../../action' import { Cache } from '../cache' -export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache }: { +export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache, fromChildSelfLimitAddChildUserId }: { action: UpdateCategoryTemporarilyBlockedAction cache: Cache + fromChildSelfLimitAddChildUserId: string | null }) { if (action.blocked === true) { if (!cache.hasFullVersion) { @@ -28,6 +29,41 @@ export async function dispatchUpdateCategoryTemporarilyBlocked ({ action, cache } } + const categoryEntryUnsafe = await cache.database.category.findOne({ + where: { + familyId: cache.familyId, + categoryId: action.categoryId + }, + transaction: cache.transaction, + attributes: ['childId', 'temporarilyBlocked', 'temporarilyBlockedEndTime'] + }) + + if (!categoryEntryUnsafe) { + throw new Error('invalid category id for updating temporarily blocking') + } + + const categoryEntry = { + childId: categoryEntryUnsafe.childId, + temporarilyBlocked: categoryEntryUnsafe.temporarilyBlocked, + temporarilyBlockedEndTime: categoryEntryUnsafe.temporarilyBlockedEndTime + } + + if (fromChildSelfLimitAddChildUserId !== null) { + if (fromChildSelfLimitAddChildUserId !== categoryEntry.childId) { + throw new Error('can not update temporarily blocking as child for other users') + } + + if (action.endTime === undefined || !action.blocked) { + throw new Error('the child may only enable a temporarily blocking') + } + + if (categoryEntry.temporarilyBlocked) { + if (action.endTime < categoryEntry.temporarilyBlockedEndTime || categoryEntry.temporarilyBlockedEndTime === 0) { + throw new Error('the child may not reduce the temporarily blocking') + } + } + } + const [affectedRows] = await cache.database.category.update({ temporarilyBlocked: action.blocked, temporarilyBlockedEndTime: action.blocked ? (action.endTime ?? 0) : 0 diff --git a/src/function/sync/apply-actions/index.ts b/src/function/sync/apply-actions/index.ts index 30683ae..16d812f 100644 --- a/src/function/sync/apply-actions/index.ts +++ b/src/function/sync/apply-actions/index.ts @@ -22,6 +22,7 @@ import { ClientPushChangesRequest } from '../../../api/schema' import { isSerializedAppLogicAction, isSerializedChildAction, isSerializedParentAction } from '../../../api/validator' import { VisibleConnectedDevicesManager } from '../../../connected-devices' import { Database } from '../../../database' +import { UserFlags } from '../../../model/userflags' import { EventHandler } from '../../../monitoring/eventhandler' import { WebsocketApi } from '../../../websocket' import { notifyClientsAboutChanges } from '../../websocket' @@ -106,6 +107,8 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con // update the next sequence number nextSequenceNumber = action.sequenceNumber + 1 + let isChildLimitAdding = false + if (action.type === 'parent') { if (action.integrity === 'device') { const deviceEntryUnsafe2 = await cache.database.device.findOne({ @@ -125,6 +128,9 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con // this ensures that the parent exists await cache.getSecondPasswordHashOfParent(action.userId) + } else if (action.integrity === 'childDevice') { + // will be checked later + isChildLimitAdding = true } else { const parentSecondHash = await cache.getSecondPasswordHashOfParent(action.userId) @@ -191,19 +197,68 @@ export const applyActionsFromDevice = async ({ database, request, websocket, con throw new Error('invalid action' + action.encodedAction) } - eventHandler.countEvent('applyActionsFromDevice action:' + parsedSerializedAction.type) + eventHandler.countEvent('applyActionsFromDevice, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) const parsedAction = parseParentAction(parsedSerializedAction) try { - await dispatchParentAction({ - action: parsedAction, - cache, - parentUserId: action.userId, - sourceDeviceId: deviceEntry.deviceId - }) + if (isChildLimitAdding) { + const deviceEntryUnsafe2 = await cache.database.device.findOne({ + attributes: ['currentUserId'], + where: { + familyId: cache.familyId, + deviceId: deviceEntry.deviceId, + currentUserId: action.userId + }, + transaction: cache.transaction + }) + + if (!deviceEntryUnsafe2) { + throw new Error('illegal state') + } + + const deviceUserId = deviceEntryUnsafe2.currentUserId + + if (!deviceUserId) { + throw new Error('no device user id set but child add self limit action requested') + } + + const deviceUserEntryUnsafe = await cache.database.user.findOne({ + attributes: ['flags'], + where: { + familyId: cache.familyId, + userId: deviceUserId, + type: 'child' + }, + transaction: cache.transaction + }) + + if (!deviceUserEntryUnsafe) { + throw new Error('no child user found for child limit adding action') + } + + if ((parseInt(deviceUserEntryUnsafe.flags, 10) & UserFlags.ALLOW_SELF_LIMIT_ADD) !== UserFlags.ALLOW_SELF_LIMIT_ADD) { + throw new Error('child add limit action found but not allowed') + } + + await dispatchParentAction({ + action: parsedAction, + cache, + parentUserId: action.userId, + sourceDeviceId: deviceEntry.deviceId, + fromChildSelfLimitAddChildUserId: deviceUserId + }) + } else { + await dispatchParentAction({ + action: parsedAction, + cache, + parentUserId: action.userId, + sourceDeviceId: deviceEntry.deviceId, + fromChildSelfLimitAddChildUserId: null + }) + } } catch (ex) { - eventHandler.countEvent('applyActionsFromDevice actionWithError:' + parsedSerializedAction.type) + eventHandler.countEvent('applyActionsFromDeviceWithError, childAddLimit: ' + isChildLimitAdding + ' action:' + parsedSerializedAction.type) throw ex } diff --git a/src/model/userflags.ts b/src/model/userflags.ts index 9f4ee52..c6bf39d 100644 --- a/src/model/userflags.ts +++ b/src/model/userflags.ts @@ -17,5 +17,6 @@ export const UserFlags = { RESTRICT_VIEWING_TO_PARENTS: 1, - ALL_FLAGS: 1 + ALLOW_SELF_LIMIT_ADD: 2, + ALL_FLAGS: 1 | 2 } diff --git a/src/util/category.ts b/src/util/category.ts new file mode 100644 index 0000000..310d918 --- /dev/null +++ b/src/util/category.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 . + */ + +export function getCategoryWithParentCategories (categories: Array<{ categoryId: string, parentCategoryId: string }>, startCategoryId: string): Array { + const categoryById = new Map() + + categories.forEach((category) => categoryById.set(category.categoryId, category)) + + const startCategory = categoryById.get(startCategoryId) + + if (!startCategory) { + throw new Error('start category not found') + } + + const categoryIds = [ startCategoryId ] + + let currentCategory = categoryById.get(startCategory.parentCategoryId) + + while (currentCategory !== undefined && categoryIds.indexOf(currentCategory.categoryId) === -1) { + categoryIds.push(currentCategory.categoryId) + + currentCategory = categoryById.get(currentCategory.parentCategoryId) + } + + return categoryIds +}