UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

539 lines (538 loc) 27.4 kB
/* * Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com> * This file is part of Sync-in | The open source file sync and share solution * See the LICENSE file for licensing details */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "UsersManager", { enumerable: true, get: function() { return UsersManager; } }); const _common = require("@nestjs/common"); const _bcryptjs = /*#__PURE__*/ _interop_require_default(require("bcryptjs")); const _nodefs = require("node:fs"); const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path")); const _promises = require("node:stream/promises"); const _constants = require("../../../common/constants"); const _functions = require("../../../common/functions"); const _image = require("../../../common/image"); const _shared = require("../../../common/shared"); const _configenvironment = require("../../../configuration/config.environment"); const _files = require("../../files/utils/files"); const _notifications = require("../../notifications/constants/notifications"); const _notificationsmanagerservice = require("../../notifications/services/notifications-manager.service"); const _member = require("../constants/member"); const _user = require("../constants/user"); const _usermodel = require("../models/user.model"); const _avatar = require("../utils/avatar"); const _adminusersmanagerservice = require("./admin-users-manager.service"); const _usersqueriesservice = require("./users-queries.service"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } let UsersManager = class UsersManager { async fromUserId(id) { const user = await this.usersQueries.from(id); return user ? new _usermodel.UserModel(user, true) : null; } async findUser(loginOrEmail, removePassword = true) { const user = await this.usersQueries.from(null, loginOrEmail); return user ? new _usermodel.UserModel(user, removePassword) : null; } async logUser(user, password, ip, scope) { this.validateUserAccess(user, ip); let authSuccess = await (0, _functions.comparePassword)(password, user.password); if (!authSuccess && scope) { authSuccess = await this.validateAppPassword(user, password, ip, scope); } this.updateAccesses(user, ip, authSuccess).catch((e)=>this.logger.error(`${this.logUser.name} - ${e}`)); if (authSuccess) { await user.makePaths(); return user; } this.logger.warn(`${this.logUser.name} - bad password for *${user.login}*`); return null; } validateUserAccess(user, ip) { if (user.role === _user.USER_ROLE.LINK) { this.logger.error(`${this.validateUserAccess.name} - guest link account ${user} is not authorized to login`); throw new _common.HttpException('Account is not allowed', _common.HttpStatus.FORBIDDEN); } if (!user.isActive || user.passwordAttempts >= _user.USER_MAX_PASSWORD_ATTEMPTS) { this.updateAccesses(user, ip, false).catch((e)=>this.logger.error(`${this.validateUserAccess.name} - ${e}`)); this.logger.error(`${this.validateUserAccess.name} - user account *${user.login}* is locked`); this.notifyAccountLocked(user, ip); throw new _common.HttpException('Account locked', _common.HttpStatus.FORBIDDEN); } } async me(authUser) { const user = await this.fromUserId(authUser.id); if (!user) { this.logger.warn(`User *${authUser.login} (${authUser.id}) not found`); throw new _common.HttpException(`User not found`, _common.HttpStatus.NOT_FOUND); } user.impersonated = !!authUser.impersonatedFromId; user.clientId = authUser.clientId; return { user: user, server: _configenvironment.serverConfig }; } async compareUserPassword(userId, password) { return this.usersQueries.compareUserPassword(userId, password); } async updateLanguage(user, userLanguageDto) { if (!userLanguageDto.language) userLanguageDto.language = null; if (!await this.usersQueries.updateUserOrGuest(user.id, userLanguageDto)) { throw new _common.HttpException('Unable to update language', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updatePassword(user, userPasswordDto) { const r = await this.usersQueries.selectUserProperties(user.id, [ 'password' ]); if (!r) { throw new _common.HttpException('Unable to check password', _common.HttpStatus.NOT_FOUND); } if (!await (0, _functions.comparePassword)(userPasswordDto.oldPassword, r.password)) { throw new _common.HttpException('Password mismatch', _common.HttpStatus.BAD_REQUEST); } const hash = await _bcryptjs.default.hash(userPasswordDto.newPassword, 10); if (!await this.usersQueries.updateUserOrGuest(user.id, { password: hash })) { throw new _common.HttpException('Unable to update password', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updateNotification(user, userNotificationDto) { if (!await this.usersQueries.updateUserOrGuest(user.id, userNotificationDto)) { throw new _common.HttpException('Unable to update notification preference', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updateStorageIndexing(user, userStorageIndexingDto) { if (!await this.usersQueries.updateUserOrGuest(user.id, userStorageIndexingDto)) { throw new _common.HttpException('Unable to update full-text search preference', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updateAvatar(req) { const part = await req.file({ limits: { fileSize: _avatar.USER_AVATAR_MAX_UPLOAD_SIZE } }); if (!part.mimetype.startsWith('image/')) { throw new _common.HttpException('Unsupported file type', _common.HttpStatus.BAD_REQUEST); } const dstPath = _nodepath.default.join(req.user.tmpPath, _avatar.USER_AVATAR_FILE_NAME); try { await (0, _promises.pipeline)(part.file, (0, _nodefs.createWriteStream)(dstPath)); } catch (e) { this.logger.error(`${this.updateAvatar.name} - ${e}`); throw new _common.HttpException('Unable to upload avatar', _common.HttpStatus.INTERNAL_SERVER_ERROR); } if (part.file.truncated) { this.logger.warn(`${this.updateAvatar.name} - image is too large`); throw new _common.HttpException('Image is too large (5MB max)', _common.HttpStatus.PAYLOAD_TOO_LARGE); } try { await (0, _files.moveFiles)(dstPath, _nodepath.default.join(req.user.homePath, _avatar.USER_AVATAR_FILE_NAME), true); } catch (e) { this.logger.error(`${this.updateAvatar.name} - ${e}`); throw new _common.HttpException('Unable to create avatar', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updateSecrets(userId, secrets) { const userSecrets = await this.usersQueries.getUserSecrets(userId); const updatedSecrets = { ...userSecrets, ...secrets }; if (!await this.usersQueries.updateUserOrGuest(userId, { secrets: updatedSecrets })) { throw new _common.HttpException('Unable to update secrets', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updateAccesses(user, ip, success, isAuthTwoFa = false) { let passwordAttempts; if (!isAuthTwoFa && _configenvironment.configuration.auth.mfa.totp.enabled && user.twoFaEnabled) { // Do not reset password attempts if the login still requires 2FA validation passwordAttempts = user.passwordAttempts; } else { passwordAttempts = success ? 0 : Math.min(user.passwordAttempts + 1, _user.USER_MAX_PASSWORD_ATTEMPTS); } await this.usersQueries.updateUserOrGuest(user.id, { lastAccess: user.currentAccess, currentAccess: new Date(), lastIp: user.currentIp, currentIp: ip, passwordAttempts: passwordAttempts, isActive: user.isActive && passwordAttempts < _user.USER_MAX_PASSWORD_ATTEMPTS }); } async getAvatar(userLogin, generate = false, generateIsNotExists) { const avatarPath = _nodepath.default.join(_usermodel.UserModel.getHomePath(userLogin), _avatar.USER_AVATAR_FILE_NAME); const avatarExists = await (0, _files.isPathExists)(avatarPath); if (!avatarExists && generateIsNotExists) { generate = true; } if (!generate) { return [ avatarExists ? avatarPath : _avatar.USER_DEFAULT_AVATAR_FILE_PATH, avatarExists ? _image.pngMimeType : _image.svgMimeType ]; } if (!await (0, _files.isPathExists)(_usermodel.UserModel.getHomePath(userLogin))) { throw new _common.HttpException(`Home path for user *${userLogin}* does not exist`, _common.HttpStatus.FORBIDDEN); } const user = await this.findUser(userLogin); if (!user) { throw new _common.HttpException(`avatar not found`, _common.HttpStatus.NOT_FOUND); } const avatarFile = (0, _nodefs.createWriteStream)(avatarPath); const avatarStream = await (0, _image.generateAvatar)(user.getInitials()); try { await (0, _promises.pipeline)(avatarStream, avatarFile); } catch (e) { this.logger.error(`${this.updateAvatar.name} - ${e}`); throw new _common.HttpException('Unable to create avatar', _common.HttpStatus.INTERNAL_SERVER_ERROR); } if (generateIsNotExists) { return [ avatarPath, _image.pngMimeType ]; } } async listAppPasswords(user) { const secrets = await this.usersQueries.getUserSecrets(user.id); if (Array.isArray(secrets.appPasswords)) { // remove passwords from response return secrets.appPasswords.map(({ password, ...rest })=>rest); } return []; } async generateAppPassword(user, userAppPasswordDto) { const secrets = await this.usersQueries.getUserSecrets(user.id); const slugName = (0, _shared.createLightSlug)(userAppPasswordDto.name); if (Array.isArray(secrets.appPasswords) && secrets.appPasswords.find((p)=>p.name === slugName)) { throw new _common.HttpException('Name already used', _common.HttpStatus.BAD_REQUEST); } secrets.appPasswords = Array.isArray(secrets.appPasswords) ? secrets.appPasswords : []; const clearPassword = (0, _shared.genPassword)(24); const appPassword = { name: (0, _shared.createLightSlug)(userAppPasswordDto.name), app: userAppPasswordDto.app, expiration: userAppPasswordDto.expiration, password: await (0, _functions.hashPassword)(clearPassword), createdAt: new Date(), currentIp: null, currentAccess: null, lastIp: null, lastAccess: null }; secrets.appPasswords.unshift(appPassword); if (!await this.usersQueries.updateUserOrGuest(user.id, { secrets: secrets })) { throw new _common.HttpException('Unable to update app passwords', _common.HttpStatus.INTERNAL_SERVER_ERROR); } // return clear password only once return { ...appPassword, password: clearPassword }; } async deleteAppPassword(user, passwordName) { const secrets = await this.usersQueries.getUserSecrets(user.id); if (!Array.isArray(secrets.appPasswords) || !secrets.appPasswords.find((p)=>p.name === passwordName)) { throw new _common.HttpException('App password not found', _common.HttpStatus.NOT_FOUND); } secrets.appPasswords = secrets.appPasswords.filter((p)=>p.name !== passwordName); if (!await this.usersQueries.updateUserOrGuest(user.id, { secrets: secrets })) { throw new _common.HttpException('Unable to delete app password', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async validateAppPassword(user, password, ip, scope) { if (!scope || !user.haveRole(_user.USER_ROLE.USER)) return false; const secrets = await this.usersQueries.getUserSecrets(user.id); if (!Array.isArray(secrets.appPasswords)) return false; for (const p of secrets.appPasswords){ if (p.app !== scope) continue; const expMs = p.expiration ? new Date(p.expiration) : null; if (p.expiration && new Date() > expMs) continue; // expired if (await (0, _functions.comparePassword)(password, p.password)) { p.lastAccess = p.currentAccess; p.currentAccess = new Date(); p.lastIp = p.currentIp; p.currentIp = ip; // update accesses this.usersQueries.updateUserOrGuest(user.id, { secrets: secrets }).catch((e)=>this.logger.error(`${this.validateAppPassword.name} - ${e}`)); return true; } } return false; } setOnlineStatus(user, onlineStatus) { this.usersQueries.setOnlineStatus(user.id, onlineStatus).catch((e)=>this.logger.error(`${this.setOnlineStatus.name} - ${e}`)); } getOnlineUsers(userIds) { return this.usersQueries.getOnlineUsers(userIds); } async usersWhitelist(userId) { return this.usersQueries.usersWhitelist(userId); } async browseGroups(user, name) { if (name) { const group = await this.usersQueries.groupFromName(user.id, name); if (!group) { throw new _common.HttpException('Group not found', _common.HttpStatus.NOT_FOUND); } return { parentGroup: group, members: await this.usersQueries.browseGroupMembers(group.id) }; } return { parentGroup: undefined, members: await this.usersQueries.browseRootGroups(user.id) }; } async getGroup(user, groupId, withMembers = true, asAdmin = false) { const group = withMembers ? await this.usersQueries.getGroupWithMembers(user.id, groupId, asAdmin) : await this.usersQueries.getGroup(user.id, groupId, asAdmin); if (!group) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } return group; } async createPersonalGroup(user, userCreateOrUpdateGroupDto) { if (!userCreateOrUpdateGroupDto.name) { this.logger.error(`${this.createPersonalGroup.name} - missing group name : ${JSON.stringify(userCreateOrUpdateGroupDto)}`); throw new _common.HttpException('Group name is missing', _common.HttpStatus.BAD_REQUEST); } if (await this.usersQueries.checkGroupNameExists(userCreateOrUpdateGroupDto.name)) { throw new _common.HttpException('Name already used', _common.HttpStatus.BAD_REQUEST); } try { const groupId = await this.usersQueries.createPersonalGroup(user.id, userCreateOrUpdateGroupDto); this.logger.log(`${this.createPersonalGroup.name} - group (${groupId}) was created : ${JSON.stringify(userCreateOrUpdateGroupDto)}`); // clear user whitelists this.usersQueries.clearWhiteListCaches([ user.id ]); return this.getGroup(user, groupId, false); } catch (e) { this.logger.error(`${this.createPersonalGroup.name} - group was not created : ${JSON.stringify(userCreateOrUpdateGroupDto)} : ${e}`); throw new _common.HttpException('Unable to create group', _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async updatePersonalGroup(user, groupId, userCreateOrUpdateGroupDto) { if (!Object.keys(userCreateOrUpdateGroupDto).length) { throw new _common.HttpException('No changes to update', _common.HttpStatus.BAD_REQUEST); } const currentGroup = await this.getGroup(user, groupId, false, user.isAdmin); if (currentGroup.type !== _member.MEMBER_TYPE.PGROUP) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } if (userCreateOrUpdateGroupDto.name && await this.usersQueries.checkGroupNameExists(userCreateOrUpdateGroupDto.name)) { throw new _common.HttpException('Name already used', _common.HttpStatus.BAD_REQUEST); } try { await this.usersQueries.updateGroup(groupId, userCreateOrUpdateGroupDto); } catch (e) { throw new _common.HttpException(e.message, _common.HttpStatus.INTERNAL_SERVER_ERROR); } return this.getGroup(user, groupId, false, user.isAdmin); } async addUsersToGroup(user, groupId, userIds) { const currentGroup = await this.getGroup(user, groupId); // only users can be added to users groups // guests and users can be added to personal groups const userWhiteList = await this.usersQueries.usersWhitelist(user.id, currentGroup.type === _member.MEMBER_TYPE.GROUP ? _user.USER_ROLE.USER : undefined); // ignore user ids that are already group members & filter on user ids allowed to current user userIds = userIds.filter((id)=>!currentGroup.members.find((m)=>m.id === id)).filter((id)=>userWhiteList.indexOf(id) > -1); if (!userIds.length) { throw new _common.HttpException('No users to add to group', _common.HttpStatus.BAD_REQUEST); } return this.usersQueries.updateGroupMembers(groupId, { add: userIds.map((id)=>({ id: id, groupRole: _user.USER_GROUP_ROLE.MEMBER })) }); } async updateUserFromPersonalGroup(user, groupId, userId, updateUserFromGroupDto) { const currentGroup = await this.getGroup(user, groupId); if (currentGroup.type !== _member.MEMBER_TYPE.PGROUP) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } const userToUpdate = currentGroup.members.find((m)=>m.id === userId); if (!userToUpdate) { throw new _common.HttpException('User was not found', _common.HttpStatus.BAD_REQUEST); } if (userToUpdate.groupRole !== updateUserFromGroupDto.role) { if (userToUpdate.groupRole === _user.USER_GROUP_ROLE.MANAGER) { if (currentGroup.members.filter((m)=>m.groupRole === _user.USER_GROUP_ROLE.MANAGER).length === 1) { throw new _common.HttpException('Group must have at least one manager', _common.HttpStatus.BAD_REQUEST); } } return this.adminUsersManager.updateUserFromGroup(groupId, userId, updateUserFromGroupDto); } } async removeUserFromGroup(user, groupId, userId) { const currentGroup = await this.getGroup(user, groupId); const userToRemove = currentGroup.members.find((m)=>m.id === userId); if (!userToRemove) { throw new _common.HttpException('User was not found', _common.HttpStatus.BAD_REQUEST); } if (userToRemove.groupRole === _user.USER_GROUP_ROLE.MANAGER) { if (currentGroup.type === _member.MEMBER_TYPE.GROUP) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } if (currentGroup.members.filter((m)=>m.groupRole === _user.USER_GROUP_ROLE.MANAGER).length === 1) { throw new _common.HttpException('Group must have at least one manager', _common.HttpStatus.BAD_REQUEST); } } return this.usersQueries.updateGroupMembers(groupId, { remove: [ userId ] }); } async leavePersonalGroup(user, groupId) { const currentGroup = await this.usersQueries.getGroupWithMembers(user.id, groupId, true); if (!currentGroup || currentGroup.type === _member.MEMBER_TYPE.GROUP) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } const userWhoLeaves = currentGroup.members.find((m)=>m.id === user.id); if (!userWhoLeaves) { throw new _common.HttpException('User was not found', _common.HttpStatus.BAD_REQUEST); } if (userWhoLeaves.groupRole === _user.USER_GROUP_ROLE.MANAGER) { if (currentGroup.members.filter((m)=>m.groupRole === _user.USER_GROUP_ROLE.MANAGER).length === 1) { throw new _common.HttpException('Group must have at least one manager', _common.HttpStatus.BAD_REQUEST); } } try { await this.usersQueries.updateGroupMembers(groupId, { remove: [ user.id ] }); this.logger.log(`${this.leavePersonalGroup.name} - user (${user.id}) has left group (${groupId})`); } catch (e) { this.logger.error(`${this.leavePersonalGroup.name} - user (${user.id}) has not left group (${groupId}) : ${e}`); throw new _common.HttpException(e.message, _common.HttpStatus.INTERNAL_SERVER_ERROR); } } async deletePersonalGroup(user, groupId) { if (!await this.usersQueries.canDeletePersonalGroup(user.id, groupId)) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } if (await this.usersQueries.deletePersonalGroup(groupId)) { this.logger.log(`${this.deletePersonalGroup.name} - group (${groupId}) was deleted`); } else { this.logger.warn(`${this.deletePersonalGroup.name} - group (${groupId}) does not exist`); throw new _common.HttpException('Unable to delete group', _common.HttpStatus.BAD_REQUEST); } } listGuests(user) { return this.usersQueries.listGuests(null, user.id); } async getGuest(user, guestId) { const guest = await this.usersQueries.listGuests(guestId, user.id); this.adminUsersManager.checkUser(guest, true); return guest; } async createGuest(user, createGuestDto) { // filter managers that are allowed for current user const userWhiteList = await this.usersQueries.usersWhitelist(user.id, _user.USER_ROLE.USER); createGuestDto.managers = createGuestDto.managers.filter((id)=>userWhiteList.indexOf(id) > -1); if (createGuestDto.managers.indexOf(user.id) === -1) { // force user as manager during creation createGuestDto.managers.push(user.id); } // clear user whitelists this.usersQueries.clearWhiteListCaches([ user.id ]); return this.adminUsersManager.createUserOrGuest(createGuestDto, _user.USER_ROLE.GUEST, true); } async updateGuest(user, guestId, updateGuestDto) { if (!Object.keys(updateGuestDto).length) { throw new _common.HttpException('No changes to update', _common.HttpStatus.BAD_REQUEST); } if (updateGuestDto.managers) { // filter managers that are allowed for current user const userWhiteList = await this.usersQueries.usersWhitelist(user.id, _user.USER_ROLE.USER); updateGuestDto.managers = updateGuestDto.managers.filter((id)=>userWhiteList.indexOf(id) > -1); if (!updateGuestDto.managers.length) { throw new _common.HttpException('Guest must have at least one manager', _common.HttpStatus.BAD_REQUEST); } } if (!await this.usersQueries.isGuestManager(user.id, guestId)) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } const guest = await this.adminUsersManager.updateUserOrGuest(guestId, updateGuestDto, _user.USER_ROLE.GUEST); return guest.managers.find((m)=>m.id === user.id) ? guest : null; } async deleteGuest(user, guestId) { const guest = await this.usersQueries.isGuestManager(user.id, guestId); if (!guest) { throw new _common.HttpException('You are not allowed to do this action', _common.HttpStatus.FORBIDDEN); } // guest has no space but a temporary directory return this.adminUsersManager.deleteUserOrGuest(guest.id, guest.login, { deleteSpace: true, isGuest: true }); } searchMembers(user, searchMembersDto) { return this.usersQueries.searchUsersOrGroups(searchMembersDto, user.id); } notifyAccountLocked(user, ip) { this.notificationsManager.sendEmailNotification([ user ], { app: _notifications.NOTIFICATION_APP.AUTH_LOCKED, event: _notifications.NOTIFICATION_APP_EVENT.AUTH_LOCKED[_constants.ACTION.DELETE], element: null, url: ip }).catch((e)=>this.logger.error(`${this.validateUserAccess.name} - ${e}`)); } constructor(usersQueries, adminUsersManager, notificationsManager){ this.usersQueries = usersQueries; this.adminUsersManager = adminUsersManager; this.notificationsManager = notificationsManager; this.logger = new _common.Logger(UsersManager.name); } }; UsersManager = _ts_decorate([ (0, _common.Injectable)(), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ typeof _usersqueriesservice.UsersQueries === "undefined" ? Object : _usersqueriesservice.UsersQueries, typeof _adminusersmanagerservice.AdminUsersManager === "undefined" ? Object : _adminusersmanagerservice.AdminUsersManager, typeof _notificationsmanagerservice.NotificationsManager === "undefined" ? Object : _notificationsmanagerservice.NotificationsManager ]) ], UsersManager); //# sourceMappingURL=users-manager.service.js.map