From 6e6216485dec80f58b766b514f2343bf5188be7f Mon Sep 17 00:00:00 2001 From: "a.chistov" Date: Thu, 21 Aug 2025 16:35:25 +0300 Subject: [PATCH 1/2] -fix after rebase -remove user watcher -user handler for alert message default quotas handler -fix merge del handler update quoats handler user handler method some fix -move without change method of devices to separate class -fix after move users method -move db.users method to separate class --- lib/db/api.ts | 8 +- lib/db/handlers/group/scheduler.js | 3 +- lib/db/handlers/user/index.js | 103 +++++ lib/db/index.ts | 3 +- lib/db/models/all/model.js | 505 +--------------------- lib/db/models/device/index.js | 4 + lib/db/models/device/model.js | 127 ++++++ lib/db/models/group/model.js | 71 +-- lib/db/models/user/index.js | 4 + lib/db/models/user/model.js | 441 +++++++++++++++++++ lib/units/api/controllers/groups.js | 10 +- lib/units/api/controllers/users.js | 64 +-- lib/units/groups-engine/index.js | 2 - lib/units/groups-engine/watchers/users.js | 105 ----- lib/units/processor/index.ts | 3 +- lib/units/websocket/index.js | 39 +- 16 files changed, 813 insertions(+), 679 deletions(-) create mode 100644 lib/db/handlers/user/index.js create mode 100644 lib/db/models/device/index.js create mode 100644 lib/db/models/device/model.js create mode 100644 lib/db/models/user/index.js create mode 100644 lib/db/models/user/model.js delete mode 100644 lib/units/groups-engine/watchers/users.js diff --git a/lib/db/api.ts b/lib/db/api.ts index 1e2e3acaab..5200126262 100644 --- a/lib/db/api.ts +++ b/lib/db/api.ts @@ -1,6 +1,8 @@ import AllModel from './models/all/index.js' import GroupModel from './models/group/index.js' import TeamModel from './models/team/index.js' +import UserModel from './models/user/index.js' +import DeviceModel from './models/device/index.js' const concatModels = (...models: T) => Object.assign({}, ...models) @@ -12,5 +14,7 @@ const concatModels = (...models: T) => export default concatModels( AllModel, GroupModel, - TeamModel -) as typeof AllModel & typeof GroupModel & typeof TeamModel + TeamModel, + UserModel, + DeviceModel, +) as typeof AllModel & typeof GroupModel & typeof TeamModel & typeof UserModel & typeof DeviceModel diff --git a/lib/db/handlers/group/scheduler.js b/lib/db/handlers/group/scheduler.js index 3ac71e199b..ddfbca40f6 100644 --- a/lib/db/handlers/group/scheduler.js +++ b/lib/db/handlers/group/scheduler.js @@ -5,6 +5,7 @@ import db from '../../index.js' import logger from '../../../util/logger.js' import mongo from 'mongodb' +import UserModel from '../../models/user/index.js' const log = logger.createLogger('groups-scheduler') @@ -211,7 +212,7 @@ export default class GroupsScheduler { await dbapi.updateDevicesCurrentGroupFromOrigin(group.devices) } - await dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration) + await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) await this.scheduleAllGroupsTasks() } catch (err) { diff --git a/lib/db/handlers/user/index.js b/lib/db/handlers/user/index.js new file mode 100644 index 0000000000..132d9a35a0 --- /dev/null +++ b/lib/db/handlers/user/index.js @@ -0,0 +1,103 @@ +import _ from 'lodash' +import logger from '../../../util/logger.js' +import wireutil from '../../../wire/util.js' +import wire from '../../../wire/index.js' +import timeutil from '../../../util/timeutil.js' + +class UserChangeHandler { + + isPrepared = false + log = logger.createLogger('change-handler-users') + + init(push, pushdev, channelRouter) { + this.pushdev = pushdev + this.isPrepared = !!this.pushdev + } + + sendUserChange = (user, isAddedGroup, groups, action, targets) => { + function wireUserField() { + const wireUser = _.cloneDeep(user) + delete wireUser._id + delete wireUser.ip + delete wireUser.group + delete wireUser.lastLoggedInAt + delete wireUser.createdAt + delete wireUser.forwards + delete wireUser.acceptedPolicy + delete wireUser.groups.lock + delete wireUser.groups.defaultGroupsDuration + delete wireUser.groups.defaultGroupsNumber + delete wireUser.groups.defaultGroupsRepetitions + delete wireUser.groups.repetitions + return wireUser + } + const userField = wireUserField() + this.pushdev.send([ + wireutil.global, + wireutil.envelope(new wire.UserChangeMessage( + userField + , isAddedGroup + , groups + , action + , targets + , timeutil.now('nano') + )) + ]) + } + + updateUserHandler = (oldUser, newUser) => { + if (newUser === null && oldUser === null) { + this.log.info('New user doc and old user doc is NULL') + return false + } + const targets = [] + if (newUser?.groups && oldUser?.groups) { + if (newUser.groups.quotas && oldUser.groups.quotas) { + if (!_.isEqual(newUser.groups.quotas.allocated, oldUser.groups.quotas.allocated)) { + targets.push('settings') + targets.push('view') + } + else if (!_.isEqual(newUser.groups.quotas.consumed, oldUser.groups.quotas.consumed)) { + targets.push('view') + } + else if (newUser.groups.quotas.defaultGroupsNumber !== + oldUser.groups.quotas.defaultGroupsNumber || + newUser.groups.quotas.defaultGroupsDuration !== + oldUser.groups.quotas.defaultGroupsDuration || + newUser.groups.quotas.defaultGroupsRepetitions !== + oldUser.groups.quotas.defaultGroupsRepetitions || + newUser.groups.quotas.repetitions !== + oldUser.groups.quotas.repetitions || + !_.isEqual(newUser.groups.subscribed, oldUser.groups.subscribed)) { + targets.push('settings') + } + } + } + if (!_.isEqual(newUser?.settings.alertMessage, oldUser?.settings.alertMessage)) { + targets.push('menu') + } + if (targets.length) { + this.sendUserChange(newUser, newUser.groups.subscribed.length > oldUser.groups.subscribed.length + , _.xor(newUser.groups.subscribed, oldUser.groups.subscribed), 'updated', targets) + } + return !_.isEqual(newUser, oldUser) + } + +} + +// Temporary solution needed to avoid situations +// where a unit may not initialize the change handler, +// but use the db module. In this case, any methods of this handler +// do nothing and will not cause an error. +/** @type {UserChangeHandler} */ +export default new Proxy(new UserChangeHandler(), { + + /** @param {string} prop */ + get(target, prop) { + if (target.isPrepared || prop === 'init' || typeof target[prop] !== 'function') { + return target[prop] + } + + return () => {} + } +}) diff --git a/lib/db/index.ts b/lib/db/index.ts index 2928f5abab..8e3ab3b1bf 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -3,6 +3,7 @@ import _setup from './setup.js' import srv from '../util/srv.js' import EventEmitter from 'events' import GroupChangeHandler from './handlers/group/index.js' +import UserChangeHandler from './handlers/user/index.js' import * as zmqutil from '../util/zmqutil.js' import lifecycle from '../util/lifecycle.js' import logger from '../util/logger.js' @@ -27,7 +28,7 @@ const handlers: { channelRouter?: EventEmitter ) => Promise | void; isPrepared: boolean; -}[] = [GroupChangeHandler] +}[] = [GroupChangeHandler, UserChangeHandler] export default class DbClient { static connection: mongo.Db diff --git a/lib/db/models/all/model.js b/lib/db/models/all/model.js index 4cb3b46f63..1082680ac9 100644 --- a/lib/db/models/all/model.js +++ b/lib/db/models/all/model.js @@ -8,6 +8,7 @@ import wireutil from '../../../wire/util.js' import {v4 as uuidv4} from 'uuid' import * as apiutil from '../../../util/apiutil.js' import GroupModel from '../group/index.js' +import UserModel from '../user/index.js' import logger from '../../../util/logger.js' import {getRootGroup, getGroup} from '../group/model.js' @@ -51,7 +52,7 @@ export const unlockBookingObjects = function() { $set: {'group.lock': false} } ), - db.collection('groups').updateMany( + db.groups.updateMany( {}, { $set: { @@ -74,7 +75,7 @@ export const createBootStrap = function(env) { const now = Date.now() function updateUsersForMigration(group) { - return getUsers().then(function(users) { + return UserModel.getUsers().then(function(users) { return Promise.all(users.map(async(user) => { const data = { privilege: user?.email !== group?.owner.email ? apiutil.USER : apiutil.ADMIN, @@ -161,7 +162,7 @@ export const createBootStrap = function(env) { envUserGroupsRepetitions: apiutil.MAX_USER_GROUPS_REPETITIONS }) .then(function(group) { - return saveUserAfterLogin({ + return UserModel.saveUserAfterLogin({ name: group?.owner.name, email: group?.owner.email, ip: '127.0.0.1' @@ -173,42 +174,11 @@ export const createBootStrap = function(env) { return updateDevicesForMigration(group) }) .then(function() { - return reserveUserGroupInstance(group?.owner?.email) + return UserModel.reserveUserGroupInstance(group?.owner?.email) }) }) } -export const deleteUser = function(email) { - return db.users.deleteOne({email: email}) -} - -// dbapi.getUsers = function() { -export const getUsers = function() { - return db.users.find().toArray() -} - -// dbapi.getEmails = function() { -export const getEmails = function() { - return db.users - .find({ - privilege: { - $ne: apiutil.ADMIN - } - }) - .project({email: 1, _id: 0}) - .toArray() -} - -// dbapi.getAdmins = function() { -export const getAdmins = function() { - return db.users - .find({ - privilege: apiutil.ADMIN - }) - .project({email: 1, _id: 0}) - .toArray() -} - export const lockDeviceByCurrent = function(groups, serial) { function wrappedlockDeviceByCurrent() { return db.devices.findOne({serial: serial}).then(oldDoc => { @@ -345,39 +315,10 @@ export const setLockOnDevices = function(serials, lock) { ) } -/** - * @deprecated Do not use locks in database. - */ -function setLockOnUser(email, state) { - return db.users.findOne({email: email}).then(oldDoc => { - if (!oldDoc || !oldDoc.groups) { - throw new Error(`User with email ${email} not found or groups field is missing.`) - } - return db.users.updateOne( - {email: email}, - { - $set: { - 'groups.lock': oldDoc.groups.lock !== state ? state : oldDoc.groups.lock - } - } - ) - .then(updateStats => { - return db.users.findOne({email: email}).then(newDoc => { - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}, old_val: {...oldDoc}} - ] - return updateStats - }) - }) - }) -} - - // dbapi.lockUser = function(email) { export const lockUser = function(email) { function wrappedlockUser() { - return setLockOnUser(email, true) + return UserModel.setLockOnUser(email, true) .then(function(stats) { return apiutil.lockResult(stats) }) @@ -391,7 +332,7 @@ export const lockUser = function(email) { // dbapi.unlockUser = function(email) { export const unlockUser = function(email) { - return setLockOnUser(email, false) + return UserModel.setLockOnUser(email, false) } // dbapi.isDeviceBooked = function(serial) { @@ -400,197 +341,6 @@ export const isDeviceBooked = function(serial) { .then(groups => !!groups?.length) } -// dbapi.createUser = function(email, name, ip) { -export const createUser = function(email, name, ip, privilege) { - return GroupModel.getRootGroup().then(function(group) { - return loadUser(group?.owner.email).then(function(adminUser) { - let userObj = { - email: email, - name: name, - ip: ip, - group: wireutil.makePrivateChannel(), - lastLoggedInAt: getNow(), - createdAt: getNow(), - forwards: [], - settings: {}, - acceptedPolicy: false, - privilege: privilege || (adminUser ? apiutil.USER : apiutil.ADMIN), - groups: { - subscribed: [], - lock: false, - quotas: { - allocated: { - number: adminUser ? adminUser.groups.quotas.defaultGroupsNumber : group?.envUserGroupsNumber, - duration: adminUser ? adminUser.groups.quotas.defaultGroupsDuration : group?.envUserGroupsDuration - }, - consumed: { - number: 0, - duration: 0 - }, - defaultGroupsNumber: adminUser ? 0 : group?.envUserGroupsNumber, - defaultGroupsDuration: adminUser ? 0 : group?.envUserGroupsDuration, - defaultGroupsRepetitions: adminUser ? 0 : group?.envUserGroupsRepetitions, - repetitions: adminUser ? adminUser.groups.quotas.defaultGroupsRepetitions : group?.envUserGroupsRepetitions - } - } - } - return db.users.insertOne(userObj) - .then(function(stats) { - if (stats.insertedId) { - return GroupModel.addGroupUser(group?.id, email).then(function() { - return loadUser(email).then(function(user) { - // @ts-ignore - stats.changes = [ - {new_val: {...user}} - ] - return stats - }) - }) - } - return stats - }) - }) - }) -} - -// dbapi.saveUserAfterLogin = function(user) { -export const saveUserAfterLogin = function(user) { - const updateData = { - name: user?.name, - ip: user?.ip, - lastLoggedInAt: getNow() - } - - if (user?.privilege) { - updateData.privilege = user?.privilege - } - - return db.users.updateOne({email: user?.email}, {$set: updateData}) - // @ts-ignore - .then(stats => { - if (stats.modifiedCount === 0) { - return createUser(user?.email, user?.name, user?.ip, user?.privilege) - } - return stats - }) -} - -// dbapi.loadUser = function(email) { -export const loadUser = function(email) { - return db.users.findOne({email: email}) -} - -// dbapi.updateUsersAlertMessage = function(alertMessage) { -export const updateUsersAlertMessage = function(alertMessage) { - return db.users.updateOne( - { - email: apiutil.STF_ADMIN_EMAIL - } - , { - $set: Object.fromEntries(Object.entries(alertMessage).map(([key, value]) => - ['settings.alertMessage.' + key, value] - )), - } - ).then(updateStats => { - return db.users.findOne({email: apiutil.STF_ADMIN_EMAIL}).then(updatedMainAdmin => { - // @ts-ignore - updateStats.changes = [ - {new_val: {...updatedMainAdmin}} - ] - return updateStats - }) - }) -} - -// dbapi.updateUserSettings = function(email, changes) { -export const updateUserSettings = function(email, changes) { - return db.users.findOne({email: email}).then(user => { - return db.users.updateOne( - { - email: email - } - , { - $set: { - settings: {...user?.settings, ...changes} - } - } - ) - }) -} - -// dbapi.resetUserSettings = function(email) { -export const resetUserSettings = function(email) { - return db.users.updateOne({email: email}, - { - $set: { - settings: {} - } - }) -} - -export const getUserAdbKeys = function(email) { - return db.users.findOne({email: email}) - .then(user => user?.adbKeys || []) -} - -// dbapi.insertUserAdbKey = function(email, key) { -export const insertUserAdbKey = function(email, key) { - let data = { - title: key.title, - fingerprint: key.fingerprint - } - return db.users.findOne({email: email}).then(user => { - let adbKeys = user?.adbKeys ? user?.adbKeys : [] - adbKeys.push(data) - return db.users.updateOne( - {email: email} - , {$set: {adbKeys: user?.adbKeys ? adbKeys : [data]}} - ) - }) -} - -// dbapi.deleteUserAdbKey = function(email, fingerprint) { -export const deleteUserAdbKey = function(email, fingerprint) { - return db.users.findOne({email: email}).then(user => { - return db.users.updateOne( - {email: email} - , { - $set: { - adbKeys: user?.adbKeys ? user?.adbKeys.filter(key => { - return key.fingerprint !== fingerprint - }) : [] - } - } - ) - }) -} - -// dbapi.lookupUsersByAdbKey = function(fingerprint) { -export const lookupUsersByAdbKey = function(fingerprint) { - return db.users.find({ - adbKeys: fingerprint - }).toArray() -} - -// dbapi.lookupUserByAdbFingerprint = function(fingerprint) { -export const lookupUserByAdbFingerprint = function(fingerprint) { - return db.users.find( - {adbKeys: {$elemMatch: {fingerprint: fingerprint}}} - // @ts-ignore - , {email: 1, name: 1, group: 1, _id: 0} - ).toArray() - .then(function(users) { - switch (users.length) { - case 1: - return users[0] - case 0: - return null - default: - throw new Error('Found multiple users for same ADB fingerprint') - } - }) -} - // dbapi.lookupUserByVncAuthResponse = function(response, serial) { export const lookupUserByVncAuthResponse = function(response, serial) { return db.collection('vncauth').aggregate([ @@ -628,20 +378,6 @@ export const lookupUserByVncAuthResponse = function(response, serial) { }) } -// dbapi.loadUserDevices = function(email) { -export const loadUserDevices = function(email) { - return db.users.findOne({email: email}).then(user => { - let userGroups = user?.groups.subscribed - return db.devices.find( - { - 'owner.email': email, - present: true, - 'group.id': {$in: userGroups} - } - ).toArray() - }) -} - // dbapi.saveDeviceLog = function(serial, entry) { export const saveDeviceLog = function(serial, entry) { return db.connect().then(() => @@ -1161,11 +897,6 @@ export const loadDeviceBySerial = function(serial) { return findDevice({serial: serial}) } -// dbapi.loadDevicesBySerials = function(serials) { -export const loadDevicesBySerials = function(serials) { - return db.devices.find({serial: {$in: serials}}).toArray() -} - // dbapi.loadDevice = function(groups, serial) { export const loadDevice = function(groups, serial) { return findDevice({ @@ -1272,33 +1003,6 @@ export const loadAccessTokenByTitle = function(email, title) { return db.collection('accessTokens').findOne({email: email, title: title}) } -// dbapi.grantAdmin = function(email) { -export const grantAdmin = function(email) { - return db.users.findOneAndUpdate({email: email}, { - $set: { - privilege: apiutil.ADMIN - } - }, {returnDocument: 'after'}) -} - -// dbapi.revokeAdmin = function(email) { -export const revokeAdmin = function(email) { - return db.users.findOneAndUpdate({email: email}, { - $set: { - privilege: apiutil.USER - } - }, {returnDocument: 'after'}) -} - -// dbapi.acceptPolicy = function(email) { -export const acceptPolicy = function(email) { - return db.users.updateOne({email: email}, { - $set: { - acceptedPolicy: true - } - }) -} - // dbapi.writeStats = function(user, serial, action) { // { // event_type: string, @@ -1331,32 +1035,9 @@ export const sendEvent = function(eventType, eventDetails, linkedEntities, times }) } -// dbapi.getDevicesCount = function() { -export const getDevicesCount = function() { - return db.devices.find().count() -} - -// dbapi.getOfflineDevicesCount = function() { -export const getOfflineDevicesCount = function() { - return db.devices.find( - { - present: false - } - ).count() -} - -// dbapi.getOfflineDevices = function() { -export const getOfflineDevices = function() { - return db.devices.find( - {present: false}, - // @ts-ignore - {_id: 0, 'provider.name': 1} - ).toArray() -} - // dbapi.isPortExclusive = function(newPort) { export const isPortExclusive = function(newPort) { - return getAllocatedAdbPorts().then((ports) => { + return DeviceModel.getAllocatedAdbPorts().then((ports) => { let result = !!ports.find(port => port === newPort) return !result }) @@ -1364,7 +1045,7 @@ export const isPortExclusive = function(newPort) { // dbapi.getLastAdbPort = function() { export const getLastAdbPort = function() { - return getAllocatedAdbPorts().then((ports) => { + return DeviceModel.getAllocatedAdbPorts().then((ports) => { if (ports.length === 0) { return 0 } @@ -1372,27 +1053,6 @@ export const getLastAdbPort = function() { }) } -// dbapi.getAllocatedAdbPorts = function() { -export const getAllocatedAdbPorts = function() { - // @ts-ignore - return db.devices.find({}, {adbPort: 1, _id: 0}).toArray().then(ports => { - let result = [] - ports.forEach((port) => { - if (port.adbPort) { - let portNum - if (typeof port.adbPort === 'string') { - portNum = parseInt(port.adbPort.replace(/["']/g, ''), 10) - } - else { - portNum = port.adbPort - } - result.push(portNum) - } - }) - return result.sort((a, b) => a - b) - }) -} - // dbapi.initiallySetAdbPort = function(serial) { export const initiallySetAdbPort = function(serial) { return getFreeAdbPort() @@ -1509,12 +1169,19 @@ export const sizeIosDevice = function(serial, height, width, scale) { ) } -// dbapi.getDeviceDisplaySize = function(serial) { -export const getDeviceDisplaySize = function(serial) { - return db.devices.findOne({serial: serial}) - .then(result => { - return result?.display - }) +// TODO Check usage. Probably dead code +export const setAbsentDisconnectedDevices = function() { + return db.devices.updateOne( + { + platform: 'iOS' + }, + { + $set: { + present: false, + ready: false + } + } + ) } // dbapi.getInstalledApplications = function(message) { @@ -1536,14 +1203,6 @@ export const setDeviceType = function(serial, type) { ) } -// dbapi.getDeviceType = function(serial) { -export const getDeviceType = function(serial) { - return db.devices.findOne({serial: serial}) - .then(result => { - return result?.deviceType - }) -} - // dbapi.initializeIosDeviceState = function(publicIp, message) { export const initializeIosDeviceState = function(publicIp, message) { const screenWsUrlPattern = @@ -1621,126 +1280,6 @@ export const initializeIosDeviceState = function(publicIp, message) { }) } -export const reserveUserGroupInstance = async(email) => { - return db.users.updateMany( - {email} - , [{ - $set: {'groups.quotas.consumed.number': { - $min: [{ - $sum: ['$groups.quotas.consumed.number', 1] - }, '$groups.quotas.allocated.number']} - } - }] - ) -} - -export const releaseUserGroupInstance = async(email) => { - return db.users.updateMany( - { - email - } - , [{ - $set: {'groups.quotas.consumed.number': { - $max: [{ - $sum: ['$groups.quotas.consumed.number', -1] - }, 0]} - } - }] - ) -} - -export const updateUserGroupDuration = async(email, oldDuration, newDuration) => { - return db.users.updateOne( - {email: email} - , [{ - $set: { - 'groups.quotas.consumed.duration': { - $cond: [ - {$lte: [{$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, '$groups.quotas.allocated.duration']}, - {$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, - '$groups.quotas.consumed.duration' - ] - } - } - }] - ) -} - -export const updateUserGroupsQuotas = async(email, duration, number, repetitions) => { - const oldDoc = await db.users.findOne({email: email}) - - const consumed = oldDoc?.groups.quotas.consumed.duration - const allocated = oldDoc?.groups.quotas.allocated.duration - const consumedNumber = oldDoc?.groups.quotas.consumed.number - const allocatedNumber = oldDoc?.groups.quotas.allocated.number - - const updateStats = await db.users.updateOne( - {email: email} - , { - $set: { - 'groups.quotas.allocated.duration': duration && consumed <= duration && - (!number || consumedNumber <= number) ? duration : allocated, - 'groups.quotas.allocated.number': number && consumedNumber <= number && - (!duration || consumed <= duration) ? number : allocatedNumber, - 'groups.quotas.repetitions': repetitions || oldDoc?.groups.quotas.repetitions - } - } - ) - - const newDoc = await db.users.findOne({email: email}) - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}, old_val: {...oldDoc}} - ] - - return updateStats -} - -export const updateDefaultUserGroupsQuotas = async(email, duration, number, repetitions) => { - const updateStats = await db.users.updateOne( - {email: email} - , [{ - $set: { - 'groups.quotas.defaultGroupsDuration': { - $cond: [ - { - $ne: [duration, null] - }, - duration, - '$groups.quotas.defaultGroupsDuration' - ] - }, - 'groups.quotas.defaultGroupsNumber': { - $cond: [ - { - $ne: [number, null] - }, - number, - '$groups.quotas.defaultGroupsNumber' - ] - }, - 'groups.quotas.defaultGroupsRepetitions': { - $cond: [ - { - $ne: [repetitions, null] - }, - repetitions, - '$groups.quotas.defaultGroupsRepetitions' - ] - } - } - }] - ) - - const newDoc = await db.users.findOne({email: email}) - // @ts-ignore - updateStats.changes = [ - {new_val: {...newDoc}} - ] - - return updateStats -} - export const updateDeviceGroupName = async(serial, group) => { return db.devices.updateOne( {serial: serial} diff --git a/lib/db/models/device/index.js b/lib/db/models/device/index.js new file mode 100644 index 0000000000..61ca31630b --- /dev/null +++ b/lib/db/models/device/index.js @@ -0,0 +1,4 @@ +import proxiedModel from '../../proxiedModel.js' +import * as model from './model.js' + +export default proxiedModel(model) diff --git a/lib/db/models/device/model.js b/lib/db/models/device/model.js new file mode 100644 index 0000000000..ef29f27e49 --- /dev/null +++ b/lib/db/models/device/model.js @@ -0,0 +1,127 @@ +/* * + * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 + * */ +// @ts-nocheck +import logger from '../../../util/logger.js' +import db from '../../index.js' + +const log = logger.createLogger('dbapi:device') + +/* +=========================================================== +==================== without change DB ==================== +=========================================================== +*/ + +// dbapi.loadUserDevices = function(email) { +export const loadUserDevices = function(email) { + return db.users.findOne({email: email}).then(user => { + let userGroups = user?.groups.subscribed + return db.devices.find( + { + 'owner.email': email, + present: true, + 'group.id': {$in: userGroups} + } + ).toArray() + }) +} + +// dbapi.loadPresentDevices = function() { +export const loadPresentDevices = function() { + return db.devices.find({present: true}).toArray() +} + +// dbapi.loadDevicesBySerials = function(serials) { +export const loadDevicesBySerials = function(serials) { + return db.devices.find({serial: {$in: serials}}).toArray() +} + +// dbapi.getDevicesCount = function() { +export const getDevicesCount = function() { + return db.devices.countDocuments() +} + +// dbapi.getOfflineDevicesCount = function() { +export const getOfflineDevicesCount = function() { + return db.devices.countDocuments( + { + present: false + } + ) +} + +// dbapi.getOfflineDevices = function() { +export const getOfflineDevices = function() { + return db.devices.find( + {present: false}, + // @ts-ignore + {_id: 0, 'provider.name': 1} + ).toArray() +} + +// dbapi.getAllocatedAdbPorts = function() { +export const getAllocatedAdbPorts = function() { + // @ts-ignore + return db.devices.find({}, {adbPort: 1, _id: 0}).toArray().then(ports => { + let result = [] + ports.forEach((port) => { + if (port.adbPort) { + let portNum + if (typeof port.adbPort === 'string') { + portNum = parseInt(port.adbPort.replace(/["']/g, ''), 10) + } + else { + portNum = port.adbPort + } + result.push(portNum) + } + }) + return result.sort((a, b) => a - b) + }) +} + +// dbapi.getDeviceDisplaySize = function(serial) { +export const getDeviceDisplaySize = function(serial) { + return db.devices.findOne({serial: serial}) + .then(result => { + return result?.display + }) +} + +// dbapi.getDeviceType = function(serial) { +export const getDeviceType = function(serial) { + return db.devices.findOne({serial: serial}) + .then(result => { + return result?.deviceType + }) +} + +// dbapi.generateIndexes = function() { +export const generateIndexes = function() { + db.devices.createIndex({serial: -1}).then((result) => { + log.info('Created indexes with result - ' + result) + }) +} + +/* +==================================================================== +==================== changing DB - use handlers ==================== +==================================================================== +*/ + + + + +// dbapi.deleteDevice = function(serial) { +export const deleteDevice = function(serial) { + return db.devices.deleteOne({serial: serial}) +} + +/* +==================================================== +==================== deprecated ==================== +==================================================== +*/ + + diff --git a/lib/db/models/group/model.js b/lib/db/models/group/model.js index dbc836f85b..a68fd84eb8 100644 --- a/lib/db/models/group/model.js +++ b/lib/db/models/group/model.js @@ -2,14 +2,17 @@ * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 * */ + import {v4 as uuidv4} from 'uuid' import db from '../../index.js' import * as apiutil from '../../../util/apiutil.js' import logger from '../../../util/logger.js' import AllModel from '../all/index.js' +import UserModel from '../user/index.js' import _ from 'lodash' import util from 'util' import GroupChangeHandler from '../../handlers/group/index.js' +import UserChangeHandler from '../../handlers/user/index.js' import {isOriginGroup} from '../../../util/apiutil.js' import mongo from 'mongodb' @@ -54,7 +57,7 @@ export const getGroups = async(filter) => { export const addGroupUser = async(id, email) => { try { - const stats = await Promise.all([ + const [group, user] = await Promise.all([ db.groups.findOneAndUpdate( { id: id @@ -66,7 +69,7 @@ export const addGroupUser = async(id, email) => { }, {returnDocument: 'before'} ), - db.users.updateOne( + db.users.findOneAndUpdate( { email: email }, @@ -78,12 +81,17 @@ export const addGroupUser = async(id, email) => { ) ]) - if (stats[0]?.id) { - GroupChangeHandler.sendGroupChange(stats[0], stats[0].users.concat([email]), false, false, true, [email], false, [], 'updated') - GroupChangeHandler.treatGroupUsersChange(stats[0], [email], stats[0].isActive, true) + if (group?.id) { + GroupChangeHandler.sendGroupChange(group, group.users.concat([email]), false, false, true, [email], false, [], 'updated') + GroupChangeHandler.treatGroupUsersChange(group, [email], group.isActive, true) } - return stats[0] && stats[1].modifiedCount === 0 ? 'unchanged ' + email : 'added ' + email + var userModifided = false + if (user) { + const newUser = await UserModel.loadUser(email) + userModifided = UserChangeHandler.updateUserHandler(user, newUser) + } + return group && !userModifided ? 'unchanged ' + email : 'added ' + email } catch (err) { if (err instanceof TypeError) { @@ -96,7 +104,7 @@ export const addGroupUser = async(id, email) => { } export const addAdminsToGroup = async(id) => { - const admins = await AllModel.getAdmins() + const admins = await UserModel.getAdmins() const group = await db.groups.findOne({id: id}) const adminsEmails = Array.from( @@ -106,19 +114,19 @@ export const addAdminsToGroup = async(id) => { const newUsers = (group?.users || []).concat(adminsEmails) - await Promise.all( - adminsEmails.map( - email => db.users.findOne({email}) - .then(user => db.users.updateOne( - {email}, - { - $set: { - 'groups.subscribed': (user?.groups?.subscribed || []).concat([id]) - } - } - )) + adminsEmails.map(async email => { + const oldUser = await UserModel.loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email}, + { + $set: { + 'groups.subscribed': (oldUser?.groups?.subscribed || []).concat([id]) + } + }, + {returnDocument: 'after'} ) - ) + UserChangeHandler.updateUserHandler(oldUser, newUser) + }) const stats = await db.groups.updateOne( @@ -152,7 +160,7 @@ export const removeGroupUser = async(id, email) => { return "Can't remove owner of group." } - const [group] = await Promise.all([ + const [group, user] = await Promise.all([ db.groups.findOneAndUpdate( {id: id} , { @@ -163,7 +171,7 @@ export const removeGroupUser = async(id, email) => { } , {returnDocument: 'after'} ), - db.users.updateOne( + db.users.findOneAndUpdate( {email: email} , { $pull: {'groups.subscribed': id} @@ -174,6 +182,11 @@ export const removeGroupUser = async(id, email) => { GroupChangeHandler.sendGroupChange(group, group?.users, false, false, false, [email], false, [], 'updated') GroupChangeHandler.treatGroupUsersChange(group, [email], group?.isActive, false) + if (user) { + const newUser = await UserModel.loadUser(email) + UserChangeHandler.updateUserHandler(user, newUser) + } + return 'deleted' } @@ -239,7 +252,7 @@ export const addGroupDevices = async(group, serials) => { } const duration = apiutil.computeDuration(group, serials.length) - const stats = await AllModel.updateUserGroupDuration(group.owner.email, group.duration, duration) + const stats = await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) if (!stats.modifiedCount) { throw 'quota is reached' } @@ -280,7 +293,7 @@ export const addGroupDevices = async(group, serials) => { /** @returns {Promise | null>} */ export const removeGroupDevices = async(group, serials) => { const duration = apiutil.computeDuration(group, -serials.length) - await AllModel.updateUserGroupDuration(group.owner.email, group.duration, duration) + await UserModel.updateUserGroupDuration(group.owner.email, group.duration, duration) const newGroup = await db.groups.findOneAndUpdate( {id: group.id} @@ -326,7 +339,7 @@ export const getUserGroup = async(email, id) => { export const getUserGroups = async(email) => { const pipeline = {users: {$in: [email]}} - const admins = await AllModel.getAdmins() + const admins = await UserModel.getAdmins() const adminEmails = admins.map(admin => admin.email) if (!adminEmails.includes(email)) { @@ -419,7 +432,7 @@ export const getGroupAsOwnerOrAdmin = async(email, id) => { return group } - const user = await AllModel.loadUser(email) + const user = await UserModel.loadUser(email) if (user && user.privilege === apiutil.ADMIN) { return group } @@ -462,7 +475,7 @@ export const createGroup = async(data) => { /** @returns {Promise | boolean | null>} */ export const createUserGroup = async(data) => { - const stats = await AllModel.reserveUserGroupInstance(data.owner.email) + const stats = await UserModel.reserveUserGroupInstance(data.owner.email) if (!stats.modifiedCount) { log.info(`Could not reserve group for user ${data.owner.email}`) return false @@ -501,7 +514,7 @@ export const updateDeviceOriginGroup = (serial, group, signature) => /** @returns {Promise | boolean | null>} */ export const updateUserGroup = async(group, data) => { - const stats = await AllModel.updateUserGroupDuration(group.owner.email, group.duration, data.duration) + const stats = await UserModel.updateUserGroupDuration(group.owner.email, group.duration, data.duration) if (!stats.modifiedCount && !stats.matchedCount || group.duration !== data.duration) { return false } @@ -563,8 +576,8 @@ export const deleteUserGroup = async(id) => { )) ) - await AllModel.releaseUserGroupInstance(group?.owner.email) - await AllModel.updateUserGroupDuration(group?.owner.email, group?.duration, 0) + await UserModel.releaseUserGroupInstance(group?.owner.email) + await UserModel.updateUserGroupDuration(group?.owner.email, group?.duration, 0) if (apiutil.isOriginGroup(group?.class)) { await AllModel.returnDevicesToRoot(group?.devices) diff --git a/lib/db/models/user/index.js b/lib/db/models/user/index.js new file mode 100644 index 0000000000..61ca31630b --- /dev/null +++ b/lib/db/models/user/index.js @@ -0,0 +1,4 @@ +import proxiedModel from '../../proxiedModel.js' +import * as model from './model.js' + +export default proxiedModel(model) diff --git a/lib/db/models/user/model.js b/lib/db/models/user/model.js new file mode 100644 index 0000000000..2576610bc0 --- /dev/null +++ b/lib/db/models/user/model.js @@ -0,0 +1,441 @@ +/* * + * Copyright 2025 contains code contributed by V Kontakte LLC - Licensed under the Apache license 2.0 + * */ +import db from '../../index.js' +import * as apiutil from '../../../util/apiutil.js' +import logger from '../../../util/logger.js' + +import AllModel from '../all/index.js' +import GroupModel from '../group/index.js' +import wireutil from '../../../wire/util.js' +import UserChangeHandler from '../../handlers/user/index.js' + +const log = logger.createLogger('dbapi:user') + +/* +=========================================================== +==================== without change DB ==================== +=========================================================== +*/ + +// dbapi.getUsers = function() { +export const getUsers = function() { + return db.users.find().toArray() +} + +// dbapi.loadUser = function(email) { +export const loadUser = function(email) { + return db.users.findOne({email: email}) +} + +// dbapi.lookupUsersByAdbKey = function(fingerprint) { +export const lookupUsersByAdbKey = function(fingerprint) { + return db.users.find({ + adbKeys: fingerprint + }).toArray() +} + +// dbapi.lookupUserByAdbFingerprint = function(fingerprint) { +export const lookupUserByAdbFingerprint = function(fingerprint) { + return db.users.find( + {adbKeys: {$elemMatch: {fingerprint: fingerprint}}} + // @ts-ignore + , {email: 1, name: 1, group: 1, _id: 0} + ).toArray() + .then(function(users) { + switch (users.length) { + case 1: + return users[0] + case 0: + return null + default: + throw new Error('Found multiple users for same ADB fingerprint') + } + }) +} + +// dbapi.getEmails = function() { +export const getEmails = function() { + return db.users + .find({ + privilege: { + $ne: apiutil.ADMIN + } + }) + .project({email: 1, _id: 0}) + .toArray() +} + +// dbapi.getAdmins = function() { +export const getAdmins = function() { + return db.users + .find({ + privilege: apiutil.ADMIN + }) + .project({email: 1, _id: 0}) + .toArray() +} + +/* +==================================================================== +==================== changing DB - use handlers ==================== +==================================================================== +*/ + +// dbapi.createUser = function(email, name, ip) { +export const createUser = async(email, name, ip, privilege) => { + const rootGroup = await GroupModel.getRootGroup() + const adminUser = await loadUser(rootGroup?.owner.email) + let userObj = { + email: email, + name: name, + ip: ip, + group: wireutil.makePrivateChannel(), + lastLoggedInAt: AllModel.getNow(), + createdAt: AllModel.getNow(), + forwards: [], + settings: {}, + acceptedPolicy: false, + privilege: privilege || (adminUser ? apiutil.USER : apiutil.ADMIN), + groups: { + subscribed: [], + lock: false, + quotas: { + allocated: { + number: adminUser ? adminUser.groups.quotas.defaultGroupsNumber : rootGroup?.envUserGroupsNumber, + duration: adminUser ? adminUser.groups.quotas.defaultGroupsDuration : rootGroup?.envUserGroupsDuration + }, + consumed: { + number: 0, + duration: 0 + }, + defaultGroupsNumber: adminUser ? 0 : rootGroup?.envUserGroupsNumber, + defaultGroupsDuration: adminUser ? 0 : rootGroup?.envUserGroupsDuration, + defaultGroupsRepetitions: adminUser ? 0 : rootGroup?.envUserGroupsRepetitions, + repetitions: adminUser ? adminUser.groups.quotas.defaultGroupsRepetitions : rootGroup?.envUserGroupsRepetitions + } + } + } + const stats = await db.users.insertOne(userObj) + if (stats.insertedId) { + UserChangeHandler.sendUserChange(userObj, false, [], 'created', ['settings']) + + await GroupModel.addGroupUser(rootGroup?.id, email) + const newUser = await loadUser(email) + + // @ts-ignore + stats.changes = [ + {new_val: {...newUser}} + ] + } + return stats +} + + +// dbapi.saveUserAfterLogin = function(user) { +export const saveUserAfterLogin = function(user) { + const updateData = { + name: user?.name, + ip: user?.ip, + lastLoggedInAt: AllModel.getNow() + } + + if (user?.privilege) { + updateData.privilege = user?.privilege + } + + return db.users.updateOne({email: user?.email}, {$set: updateData}) + // @ts-ignore + .then(stats => { + if (stats.modifiedCount === 0) { + return createUser(user?.email, user?.name, user?.ip, user?.privilege) + } + return stats + }) +} + +// dbapi.updateUsersAlertMessage = function(alertMessage) { +export const updateUsersAlertMessage = async function (alertMessage) { + const oldUser = await db.users.findOne({email: apiutil.STF_ADMIN_EMAIL}) + const newUser = await db.users.findOneAndUpdate( + { + email: apiutil.STF_ADMIN_EMAIL + } + , { + $set: Object.fromEntries(Object.entries(alertMessage).map(([key, value]) => + ['settings.alertMessage.' + key, value] + )), + }, + {returnDocument: 'after'} + ) + + var userModifided = false + const userMatching = oldUser !== null + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, matchedCount: userMatching ? 1 : 0, newUser: newUser} +} + + +// dbapi.updateUserSettings = function(email, changes) { +export const updateUserSettings = async function(email, changes) { + const oldUser = await db.users.findOne({email: email}) + const newUser = await db.users.findOneAndUpdate( + { + email: email + } + , { + $set: { + settings: {...oldUser?.settings, ...changes} + } + }, + {returnDocument: 'after'} + ) + UserChangeHandler.updateUserHandler(oldUser, newUser) +} + +// dbapi.insertUserAdbKey = function(email, key) { +export const insertUserAdbKey = function(email, key) { + let data = { + title: key.title, + fingerprint: key.fingerprint + } + return db.users.findOne({email: email}).then(user => { + let adbKeys = user?.adbKeys ? user?.adbKeys : [] + adbKeys.push(data) + return db.users.updateOne( + {email: email} + , {$set: {adbKeys: user?.adbKeys ? adbKeys : [data]}} + ) + }) +} + +// dbapi.grantAdmin = function(email) { +export const grantAdmin = function(email) { + return db.users.findOneAndUpdate({email: email}, { + $set: { + privilege: apiutil.ADMIN + } + }, {returnDocument: 'after'}) +} + +// dbapi.revokeAdmin = function(email) { +export const revokeAdmin = function(email) { + return db.users.findOneAndUpdate({email: email}, { + $set: { + privilege: apiutil.USER + } + }, {returnDocument: 'after'}) +} + +// dbapi.acceptPolicy = function(email) { +export const acceptPolicy = function(email) { + return db.users.updateOne({email: email}, { + $set: { + acceptedPolicy: true + } + }) +} + +export const reserveUserGroupInstance = async(email) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email} + , [{ + $set: {'groups.quotas.consumed.number': { + $min: [{ + $sum: ['$groups.quotas.consumed.number', 1] + }, '$groups.quotas.allocated.number']} + } + }], + {returnDocument: 'after'} + ) + const userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + return {modifiedCount: userModifided ? 1 : 0} +} + +export const releaseUserGroupInstance = async(email) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + { + email + } + , [{ + $set: {'groups.quotas.consumed.number': { + $max: [{ + $sum: ['$groups.quotas.consumed.number', -1] + }, 0]} + } + }], + {returnDocument: 'after'} + ) + const userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + return {modifiedCount: userModifided ? 1 : 0} +} + +export const updateUserGroupDuration = async(email, oldDuration, newDuration) => { + const oldUser = await loadUser(email) + const newUser = await db.users.findOneAndUpdate( + {email: email} + , [{ + $set: { + 'groups.quotas.consumed.duration': { + $cond: [ + {$lte: [{$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, '$groups.quotas.allocated.duration']}, + {$sum: ['$groups.quotas.consumed.duration', newDuration, -oldDuration]}, + '$groups.quotas.consumed.duration' + ] + } + } + }], + {returnDocument: 'after'} + ) + var userModifided = false + const userMatching = oldUser !== null + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, matchedCount: userMatching ? 1 : 0} +} + +export const updateUserGroupsQuotas = async(email, duration, number, repetitions) => { + const oldUser = await db.users.findOne({email: email}) + + const consumed = oldUser?.groups.quotas.consumed.duration + const allocated = oldUser?.groups.quotas.allocated.duration + const consumedNumber = oldUser?.groups.quotas.consumed.number + const allocatedNumber = oldUser?.groups.quotas.allocated.number + + const newUser = await db.users.findOneAndUpdate( + {email: email} + , { + $set: { + 'groups.quotas.allocated.duration': duration && consumed <= duration && + (!number || consumedNumber <= number) ? duration : allocated, + 'groups.quotas.allocated.number': number && consumedNumber <= number && + (!duration || consumed <= duration) ? number : allocatedNumber, + 'groups.quotas.repetitions': repetitions || oldUser?.groups.quotas.repetitions + } + }, + {returnDocument: 'after'} + ) + + var userModifided = false + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + + return {modifiedCount: userModifided ? 1 : 0, newUser: newUser} +} + +export const updateDefaultUserGroupsQuotas = async(email, duration, number, repetitions) => { + const oldUser = await db.users.findOne({email: email}) + const newUser = await db.users.findOneAndUpdate( + {email: email} + , [{ + $set: { + 'groups.quotas.defaultGroupsDuration': { + $cond: [ + { + $ne: [duration, null] + }, + duration, + '$groups.quotas.defaultGroupsDuration' + ] + }, + 'groups.quotas.defaultGroupsNumber': { + $cond: [ + { + $ne: [number, null] + }, + number, + '$groups.quotas.defaultGroupsNumber' + ] + }, + 'groups.quotas.defaultGroupsRepetitions': { + $cond: [ + { + $ne: [repetitions, null] + }, + repetitions, + '$groups.quotas.defaultGroupsRepetitions' + ] + } + } + }], + {returnDocument: 'after'} + ) + var userModifided = false + if (newUser) { + userModifided = UserChangeHandler.updateUserHandler(oldUser, newUser) + } + return {modifiedCount: userModifided ? 1 : 0, newUser: newUser} +} + +// dbapi.deleteUserAdbKey = function(email, fingerprint) { +export const deleteUserAdbKey = function(email, fingerprint) { + return db.users.findOne({email: email}).then(user => { + return db.users.updateOne( + {email: email} + , { + $set: { + adbKeys: user?.adbKeys ? user?.adbKeys.filter(key => { + return key.fingerprint !== fingerprint + }) : [] + } + } + ) + }) +} + +// dbapi.resetUserSettings = function(email) { +export const resetUserSettings = function(email) { + return db.users.updateOne({email: email}, + { + $set: { + settings: {} + } + }) +} + +export const deleteUser = async function(email) { + const deletedUser = await db.users.findOneAndDelete({email: email}) + UserChangeHandler.sendUserChange(deletedUser, false, [], 'deleted', ['settings']) + return {deletedCount: deletedUser !== null ? 1 : 0} +} + + +/* +==================================================== +==================== deprecated ==================== +==================================================== +*/ + +/** + * @deprecated Do not use locks in database. + */ +export const setLockOnUser = function(email, state) { + return db.users.findOne({email: email}).then(oldDoc => { + if (!oldDoc || !oldDoc.groups) { + throw new Error(`User with email ${email} not found or groups field is missing.`) + } + return db.users.updateOne( + {email: email}, + { + $set: { + 'groups.lock': oldDoc.groups.lock !== state ? state : oldDoc.groups.lock + } + } + ) + .then(updateStats => { + return db.users.findOne({email: email}).then(newDoc => { + // @ts-ignore + updateStats.changes = [ + {new_val: {...newDoc}, old_val: {...oldDoc}} + ] + return updateStats + }) + }) + }) +} diff --git a/lib/units/api/controllers/groups.js b/lib/units/api/controllers/groups.js index b39620ac78..c15b25db35 100644 --- a/lib/units/api/controllers/groups.js +++ b/lib/units/api/controllers/groups.js @@ -11,7 +11,7 @@ import {v4 as uuidv4} from 'uuid' import usersapi from './users.js' import lockutil from '../../../util/lockutil.js' import {isOriginGroup} from '../../../util/apiutil.js' -import {loadDevicesBySerials} from '../../../db/models/all/model.js' +import DeviceModel from '../../../db/models/device/index.js' const log = logger.createLogger('groups-controller:') /* ---------------------------------- PRIVATE FUNCTIONS --------------------------------- */ @@ -168,7 +168,7 @@ function addGroupDevices(req, res) { } if (isInternal) { - return loadDevicesBySerials(autotestsGroup?.devices) + return DeviceModel.loadDevicesBySerials(autotestsGroup?.devices) .then(devices => apiutil.respond(res, 200, `Added (group ${target})`, { group: { id: autotestsGroup?.id, @@ -824,7 +824,7 @@ function updateGroup(req, res) { .then(function(group) { if (group && typeof group !== 'boolean') { if (isInternal) { - loadDevicesBySerials(group.devices) + DeviceModel.loadDevicesBySerials(group.devices) .then(devices => apiutil.respond(res, 200, 'Updated (group)', {group: {id: group.id, devices: devices}})) } else { @@ -909,7 +909,7 @@ function updateGroup(req, res) { repetitions === group.repetitions ) { if (isInternal) { - return loadDevicesBySerials(group.devices) + return DeviceModel.loadDevicesBySerials(group.devices) .then(devices => apiutil.respond(res, 200, 'Unchanged (group)', {group: {id: group.id, devices: devices}})) } } @@ -999,7 +999,7 @@ function getGroupDevices(req, res) { }) } else { - loadDevicesBySerials(group.devices) + DeviceModel.loadDevicesBySerials(group.devices) .then(function(devices) { devices = devices.map(device => apiutil.filterDevice(req, device)) apiutil.respond(res, 200, 'Devices Information', {devices: devices}) diff --git a/lib/units/api/controllers/users.js b/lib/units/api/controllers/users.js index 7eff6be078..cbb3cecbc6 100644 --- a/lib/units/api/controllers/users.js +++ b/lib/units/api/controllers/users.js @@ -1,4 +1,3 @@ -import dbapi from '../../../db/api.js' import _ from 'lodash' import * as apiutil from '../../../util/apiutil.js' import * as lockutil from '../../../util/lockutil.js' @@ -8,11 +7,14 @@ import wireutil from '../../../wire/util.js' import userapi from './user.js' import * as service from '../../../util/serviceuser.js' import {MongoServerError} from 'mongodb' +import AllModel from '../../../db/models/all/index.js' +import GroupModel from '../../../db/models/group/index.js' +import UserModel from '../../../db/models/user/index.js' /* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */ function userApiWrapper(fn, req, res) { const email = req.params.email - dbapi.loadUser(email).then(function(user) { + UserModel.loadUser(email).then(function(user) { if (!user) { apiutil.respond(res, 404, 'Not Found (user)') } @@ -39,24 +41,24 @@ async function removeUser(email, req, res) { const groupOwnerState = req.query.groupOwner const anyGroupOwnerState = typeof groupOwnerState === 'undefined' const lock = {} - const user = await dbapi.loadUser(email) + const user = await UserModel.loadUser(email) if (!user) { return 'not found' } async function removeGroupUser(owner, id) { - const userGroup = await dbapi.getUserGroup(owner, id) + const userGroup = await GroupModel.getUserGroup(owner, id) if (!userGroup) { return 'not found' } return owner === email ? - dbapi.deleteUserGroup(id) : - dbapi.removeGroupUser(id, email) + GroupModel.deleteUserGroup(id) : + GroupModel.removeGroupUser(id, email) } function deleteUserInDatabase(channel) { - return dbapi.removeUserAccessTokens(email).then(function() { - return dbapi.deleteUser(email).then(function() { + return AllModel.removeUserAccessTokens(email).then(function() { + return UserModel.deleteUser(email).then(function() { req.options.pushdev.send([ channel, wireutil.envelope(new wire.DeleteUserMessage(email)) @@ -90,12 +92,12 @@ async function removeUser(email, req, res) { if (req.user.email === email) { return Promise.resolve('forbidden') } - return dbapi.lockUser(email).then(function(stats) { + return AllModel.lockUser(email).then(function(stats) { if (stats.modifiedCount === 0) { return apiutil.lightComputeStats(res, stats) } const user = lock.user = stats.changes[0].new_val - return dbapi.getGroupsByUser(user.email).then(function(groups) { + return GroupModel.getGroupsByUser(user.email).then(function(groups) { return computeUserGroupOwnership(groups).then(function(doContinue) { if (!doContinue) { return 'unchanged' @@ -117,7 +119,7 @@ function grantAdmin(req, res) { if (req.user.privilege !== apiutil.ADMIN) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } - dbapi.grantAdmin(req.params.email).then((user) => { + UserModel.grantAdmin(req.params.email).then((user) => { if (user) { // @ts-ignore return apiutil.respond(res, 200, 'Grant admin for user', {user: user}) @@ -131,7 +133,7 @@ function revokeAdmin(req, res) { if (req.user.privilege !== apiutil.ADMIN) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } - dbapi.revokeAdmin(req.params.email).then((user) => { + UserModel.revokeAdmin(req.params.email).then((user) => { if (user) { // @ts-ignore return apiutil.respond(res, 200, 'Revoke admin for user', {user: user}) @@ -142,7 +144,7 @@ function revokeAdmin(req, res) { }) } function lockStfAdminUser(res) { - return dbapi.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { + return AllModel.lockUser(apiutil.STF_ADMIN_EMAIL).then(function(stats) { if (stats.modifiedCount === 0) { return apiutil.lightComputeStats(res, stats) } @@ -154,12 +156,12 @@ function updateUsersAlertMessage(req, res) { return apiutil.respond(res, 403, 'Forbidden (user doesnt have admin privilege)') } const lock = lockStfAdminUser(res) - return dbapi.updateUsersAlertMessage(req.body).then(function(/** @type {any} */ stats) { + return UserModel.updateUsersAlertMessage(req.body).then(function(/** @type {any} */ stats) { if (stats.matchedCount > 0 && stats.modifiedCount === 0) { - apiutil.respond(res, 200, 'Unchanged (users alert message)', {alertMessage: stats.changes[0].new_val.settings.alertMessage}) + apiutil.respond(res, 200, 'Unchanged (users alert message)', {alertMessage: stats.newUser.settings.alertMessage}) } else { - apiutil.respond(res, 200, 'Updated (users alert message)', {alertMessage: stats.changes[0].new_val.settings.alertMessage}) + apiutil.respond(res, 200, 'Updated (users alert message)', {alertMessage: stats.newUser.settings.alertMessage}) } }) .catch(function(err) { @@ -178,9 +180,9 @@ function updateUsersAlertMessage(req, res) { /* --------------------------------- PUBLIC FUNCTIONS --------------------------------------- */ function getUserInfo(req, email) { const fields = req.query.fields - return dbapi.loadUser(email).then(function(user) { + return UserModel.loadUser(email).then(function(user) { if (user) { - return dbapi.getRootGroup().then(function(group) { + return GroupModel.getRootGroup().then(function(group) { return getPublishedUser(user, req.user.email, group?.owner?.email, fields) }) } @@ -189,7 +191,7 @@ function getUserInfo(req, email) { } function getUsersAlertMessage(req, res) { const fields = req.query.fields - return dbapi.loadUser(apiutil.STF_ADMIN_EMAIL).then(async function(user) { + return UserModel.loadUser(apiutil.STF_ADMIN_EMAIL).then(async function(user) { if (user?.settings?.alertMessage === undefined) { const lock = await lockStfAdminUser(res) const alertMessage = { @@ -197,9 +199,9 @@ function getUsersAlertMessage(req, res) { data: '*** this site is currently under maintenance, please wait ***', level: 'Critical' } - return dbapi.updateUsersAlertMessage(alertMessage).then(function(/** @type {any} */ stats) { - if (!stats.errors) { - return stats.changes[0].new_val.settings.alertMessage + return UserModel.updateUsersAlertMessage(alertMessage).then(function(/** @type {any} */ stats) { + if (stats.modifiedCount === 1) { + return stats.newUser.settings.alertMessage } throw new Error('Failed to initialize users alert message') }) @@ -239,15 +241,15 @@ function updateUserGroupsQuotas(req, res) { null const lock = {} - dbapi.loadUser(email).then(function(user) { + UserModel.loadUser(email).then(function(user) { if (user) { lockutil.lockUser(email, res, lock).then(function(lockingSuccessed) { if (lockingSuccessed) { - return dbapi.updateUserGroupsQuotas(email, duration, number, repetitions) + return UserModel.updateUserGroupsQuotas(email, duration, number, repetitions) .then(function(/** @type {any} */ stats) { if (stats.modifiedCount > 0) { return apiutil.respond(res, 200, 'Updated (user quotas)', { - user: apiutil.publishUser(stats.changes[0].new_val) + user: apiutil.publishUser(stats.newUser) }) } if ((duration === null || duration === lock.user.groups.quotas.allocated.duration) && @@ -285,11 +287,11 @@ function updateDefaultUserGroupsQuotas(req, res) { const lock = {} lockutil.lockUser(req.user.email, res, lock).then(function(lockingSuccessed) { if (lockingSuccessed) { - return dbapi.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions) + return UserModel.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions) .then(function(/** @type {any} */ stats) { if (stats.modifiedCount > 0) { return apiutil.respond(res, 200, 'Updated (user default quotas)', { - user: apiutil.publishUser(stats) + user: apiutil.publishUser(stats.newUser) }) } return apiutil.respond(res, 200, 'Unchanged (user default quotas)', {user: {}}) @@ -320,8 +322,8 @@ function getUserByEmail(req, res) { } function getUsers(req, res) { const fields = req.query.fields - dbapi.getUsers().then(function(users) { - return dbapi.getRootGroup().then(function(group) { + UserModel.getUsers().then(function(users) { + return GroupModel.getRootGroup().then(function(group) { apiutil.respond(res, 200, 'Users Information', { users: users.map(function(user) { return getPublishedUser(user, req.user.email, group?.owner?.email, fields) @@ -336,7 +338,7 @@ function getUsers(req, res) { function createUser(req, res) { const email = req.params.email const name = req.query.name - dbapi.createUser(email, name, req.user.ip).then(function(/** @type {any} */ stats) { + UserModel.createUser(email, name, req.user.ip).then(function(/** @type {any} */ stats) { apiutil.respond(res, 201, 'Created (user)', { user: apiutil.publishUser(stats.changes[0].new_val) }) @@ -405,7 +407,7 @@ function deleteUsers(req, res) { } (function() { if (typeof emails === 'undefined') { - return dbapi.getEmails().then(function(emails) { + return UserModel.getEmails().then(function(emails) { return removeUsers(emails) }) } diff --git a/lib/units/groups-engine/index.js b/lib/units/groups-engine/index.js index ee3d1f877c..7ccb596dd6 100644 --- a/lib/units/groups-engine/index.js +++ b/lib/units/groups-engine/index.js @@ -1,6 +1,5 @@ import devicesWatcher from './watchers/devices.js' import lifecycle from '../../util/lifecycle.js' -import usersWatcher from './watchers/users.js' import logger from '../../util/logger.js' import db from '../../db/index.js' @@ -15,7 +14,6 @@ export default (async function(options) { await db.connect() devicesWatcher(push, pushdev, channelRouter) - usersWatcher(pushdev) lifecycle.observe(() => [push, pushdev].forEach((sock) => sock.close()) diff --git a/lib/units/groups-engine/watchers/users.js b/lib/units/groups-engine/watchers/users.js deleted file mode 100644 index 6ae21ffc59..0000000000 --- a/lib/units/groups-engine/watchers/users.js +++ /dev/null @@ -1,105 +0,0 @@ -import timeutil from '../../../util/timeutil.js' -import _ from 'lodash' -import logger from '../../../util/logger.js' -import wireutil from '../../../wire/util.js' -import wire from '../../../wire/index.js' -import db from '../../../db/index.js' -export default (function(pushdev) { - const log = logger.createLogger('watcher-users') - function sendUserChange(user, isAddedGroup, groups, action, targets) { - pushdev.send([ - wireutil.global, - wireutil.envelope(new wire.UserChangeMessage(user, isAddedGroup, groups, action, targets, timeutil.now('nano'))) - ]) - } - let changeStream - db.connect().then(client => { - const users = client.collection('users') - changeStream = users.watch([ - { - $project: { - 'fullDocument.email': 1, - 'fullDocument.name': 1, - 'fullDocument.privilege': 1, - 'fullDocument.groups.quotas': 1, - 'fullDocument.groups.subscribed': 1, - 'fullDocument.settings.alertMessage': 1, - 'fullDocumentBeforeChange.email': 1, - 'fullDocumentBeforeChange.name': 1, - 'fullDocumentBeforeChange.privilege': 1, - 'fullDocumentBeforeChange.groups.quotas': 1, - 'fullDocumentBeforeChange.groups.subscribed': 1, - 'fullDocumentBeforeChange.settings.alertMessage': 1, - operationType: 1 - } - } - ], {fullDocument: 'whenAvailable', fullDocumentBeforeChange: 'whenAvailable'}) - changeStream.on('change', next => { - log.info('Users watcher next: ' + JSON.stringify(next)) - try { - let newDoc, oldDoc - let operationType = next.operationType - // @ts-ignore - if (next.fullDocument) { - // @ts-ignore - newDoc = next.fullDocument - } - else { - newDoc = null - } - // @ts-ignore - if (next.fullDocumentBeforeChange) { - // @ts-ignore - oldDoc = next.fullDocumentBeforeChange - } - else { - oldDoc = null - } - if (newDoc === null && oldDoc === null) { - log.info('New user doc and old user doc is NULL') - return false - } - if (operationType === 'insert') { - sendUserChange(newDoc, false, [], 'created', ['settings']) - } - else if (operationType === 'delete') { - sendUserChange(oldDoc, false, [], 'deleted', ['settings']) - } - else { - const targets = [] - if (newDoc.groups && oldDoc.groups) { - if (newDoc.groups.quotas && oldDoc.groups.quotas) { - if (!_.isEqual(newDoc.groups.quotas.allocated, oldDoc.groups.quotas.allocated)) { - targets.push('settings') - targets.push('view') - } - else if (!_.isEqual(newDoc.groups.quotas.consumed, oldDoc.groups.quotas.consumed)) { - targets.push('view') - } - else if (newDoc.groups.quotas.defaultGroupsNumber !== - oldDoc.groups.quotas.defaultGroupsNumber || - newDoc.groups.defaultGroupsDuration !== - oldDoc.groups.quotas.defaultGroupsDuration || - newDoc.groups.defaultGroupsRepetitions !== - oldDoc.groups.quotas.defaultGroupsRepetitions || - newDoc.groups.repetitions !== - oldDoc.groups.quotas.repetitions || - !_.isEqual(newDoc.groups.subscribed, oldDoc.groups.subscribed)) { - targets.push('settings') - } - } - } - else if (!_.isEqual(newDoc.settings.alertMessage, oldDoc.settings.alertMessage)) { - targets.push('menu') - } - if (targets.length) { - sendUserChange(newDoc, newDoc.groups.subscribed.length > oldDoc.groups.subscribed.length, _.xor(newDoc.groups.subscribed, oldDoc.groups.subscribed), 'updated', targets) - } - } - } - catch (e) { - log.error(e) - } - }) - }) -}) diff --git a/lib/units/processor/index.ts b/lib/units/processor/index.ts index 266594b347..0490196b0d 100644 --- a/lib/units/processor/index.ts +++ b/lib/units/processor/index.ts @@ -8,6 +8,7 @@ import dbapi from '../../db/models/all/index.js' import lifecycle from '../../util/lifecycle.js' import srv from '../../util/srv.js' import * as zmqutil from '../../util/zmqutil.js' +import UserModel from '../../db/models/user/index.js' import { UserChangeMessage, GroupChangeMessage, @@ -162,7 +163,7 @@ export default db.ensureConnectivity(async(options: Options) => { }) .on(JoinGroupByAdbFingerprintMessage, async (channel, message) => { try { - const user = await dbapi.lookupUserByAdbFingerprint(message.fingerprint) + const user = await UserModel.lookupUserByAdbFingerprint(message.fingerprint) if (user) { devDealer.send([ channel, diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 07e08e8c22..24c64a727c 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -13,7 +13,6 @@ import logger from '../../util/logger.js' import wire from '../../wire/index.js' import wireutil from '../../wire/util.js' import {WireRouter} from '../../wire/router.js' -import dbapi from '../../db/api.js' import datautil from '../../util/datautil.js' import lifecycle from '../../util/lifecycle.js' import cookieSession from './middleware/cookie-session.js' @@ -26,6 +25,8 @@ import db from '../../db/index.js' import EventEmitter from 'events' import generateToken from '../api/helpers/generateToken.js' import {UpdateAccessTokenMessage, DeleteUserMessage, DeviceChangeMessage, UserChangeMessage, GroupChangeMessage, DeviceGroupChangeMessage, GroupUserChangeMessage, DeviceLogMessage, DeviceIntroductionMessage, DeviceReadyMessage, DevicePresentMessage, DeviceAbsentMessage, InstalledApplications, JoinGroupMessage, JoinGroupByAdbFingerprintMessage, LeaveGroupMessage, DeviceStatusMessage, DeviceIdentityMessage, TransactionProgressMessage, TransactionDoneMessage, TransactionTreeMessage, DeviceLogcatEntryMessage, AirplaneModeEvent, BatteryEvent, GetServicesAvailabilityMessage, DeviceBrowserMessage, ConnectivityEvent, PhoneStateEvent, RotationEvent, CapabilitiesMessage, ReverseForwardsEvent, TemporarilyUnavailableMessage, UpdateRemoteConnectUrl} from '../../wire/wire.js' +import AllModel from '../../db/models/all/index.js' +import UserModel from '../../db/models/user/index.js' const request = Promise.promisifyAll(postmanRequest) export default (async function(options) { const log = logger.createLogger('websocket') @@ -245,7 +246,7 @@ export default (async function(options) { }) // @TODO refactore JoimGroupMessage route .on(JoinGroupMessage, function(channel, message) { - dbapi.getInstalledApplications({serial: message.serial}) + AllModel.getInstalledApplications({serial: message.serial}) .then(applications => { if (!user?.ownedChannels) { user.ownedChannels = new Set() @@ -448,10 +449,10 @@ export default (async function(options) { // // Device note .on('device.note', function(data) { - return dbapi + return AllModel .setDeviceNote(data.serial, data.note) .then(function() { - return dbapi.loadDevice(user.groups.subscribed, data.serial) + return AllModel.loadDevice(user.groups.subscribed, data.serial) }) .then(function(device) { if (device) { @@ -470,19 +471,19 @@ export default (async function(options) { // Settings .on('user.settings.update', function(data) { if (data.alertMessage === undefined) { - dbapi.updateUserSettings(user.email, data) + UserModel.updateUserSettings(user.email, data) } else { - dbapi.updateUserSettings(apiutil.STF_ADMIN_EMAIL, data) + UserModel.updateUserSettings(apiutil.STF_ADMIN_EMAIL, data) } }) .on('user.settings.reset', function() { - dbapi.resetUserSettings(user.email) + UserModel.resetUserSettings(user.email) }) .on('user.keys.accessToken.generate', async(data) => { const {title} = data const token = generateToken(user, options.secret) - await dbapi + await AllModel .saveUserAccessToken(user.email, { title: title, id: token.id, @@ -496,7 +497,7 @@ export default (async function(options) { .on('user.keys.accessToken.remove', function(data) { const isAdmin = user.privilege === apiutil.ADMIN const email = (isAdmin ? data.email : null) || user.email - return dbapi + return AllModel .removeUserAccessToken(email, data.title) .then(function() { socket.emit('user.keys.accessToken.updated') @@ -506,16 +507,16 @@ export default (async function(options) { // @ts-ignore return Adb.util.parsePublicKey(data.key) .then(function(key) { - return dbapi.lookupUsersByAdbKey(key.fingerprint) + return UserModel.lookupUsersByAdbKey(key.fingerprint) .then(function(keys) { return keys }) .then(function(users) { if (users.length) { - throw new dbapi.DuplicateSecondaryIndexError() + throw new AllModel.DuplicateSecondaryIndexError() } else { - return dbapi.insertUserAdbKey(user.email, { + return UserModel.insertUserAdbKey(user.email, { title: data.title, fingerprint: key.fingerprint }) @@ -541,16 +542,16 @@ export default (async function(options) { }) }) .on('user.keys.adb.accept', function(data) { - return dbapi.lookupUsersByAdbKey(data.fingerprint) + return UserModel.lookupUsersByAdbKey(data.fingerprint) .then(function(keys) { return keys }) .then(function(users) { if (users.length) { - throw new dbapi.DuplicateSecondaryIndexError() + throw new AllModel.DuplicateSecondaryIndexError() } else { - return dbapi.insertUserAdbKey(user.email, { + return UserModel.insertUserAdbKey(user.email, { title: data.title, fingerprint: data.fingerprint }) @@ -569,12 +570,12 @@ export default (async function(options) { ]) }) // @ts-ignore - .catch(dbapi.DuplicateSecondaryIndexError, function() { + .catch(AllModel.DuplicateSecondaryIndexError, function() { // No-op }) }) .on('user.keys.adb.remove', function(data) { - return dbapi + return UserModel .deleteUserAdbKey(user.email, data.fingerprint) .then(function() { socket.emit('user.keys.adb.removed', data) @@ -582,7 +583,7 @@ export default (async function(options) { }) .on('shell.settings.execute', function(data) { let command = data.command - dbapi.loadDevices().then(devices => { + AllModel.loadDevices().then(devices => { devices.forEach(device => { push.send([ device.channel, @@ -1342,7 +1343,7 @@ export default (async function(options) { ]) }) .on('policy.accept', function(data) { - dbapi.acceptPolicy(user.email) + UserModel.acceptPolicy(user.email) }) }) .finally(function() { From f15cf092a692d0a6f0185e100cc599257fe2d8c0 Mon Sep 17 00:00:00 2001 From: "e.khalilov" Date: Tue, 30 Sep 2025 15:34:01 +0300 Subject: [PATCH 2/2] add missing db method --- lib/db/models/user/model.js | 7 ++++++- lib/units/websocket/index.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/db/models/user/model.js b/lib/db/models/user/model.js index 2576610bc0..6513dc154c 100644 --- a/lib/db/models/user/model.js +++ b/lib/db/models/user/model.js @@ -155,7 +155,7 @@ export const saveUserAfterLogin = function(user) { } // dbapi.updateUsersAlertMessage = function(alertMessage) { -export const updateUsersAlertMessage = async function (alertMessage) { +export const updateUsersAlertMessage = async function(alertMessage) { const oldUser = await db.users.findOne({email: apiutil.STF_ADMIN_EMAIL}) const newUser = await db.users.findOneAndUpdate( { @@ -439,3 +439,8 @@ export const setLockOnUser = function(email, state) { }) }) } + +export const getUserAdbKeys = function(email) { + return db.users.findOne({email: email}) + .then(user => user?.adbKeys || []) +} diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 24c64a727c..6f59f52e4c 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -902,7 +902,7 @@ export default (async function(options) { }) .on('group.invite', async(channel, responseChannel, data) => { joinChannel(responseChannel) - const keys = await dbapi.getUserAdbKeys(user.email) + const keys = await UserModel.getUserAdbKeys(user.email) push.send([ channel, wireutil.transaction(responseChannel, new wire.GroupMessage(