@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
424 lines (423 loc) • 21.9 kB
JavaScript
/*
* 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 _functions = require("../../../common/functions");
const _image = require("../../../common/image");
const _configconstants = require("../../../configuration/config.constants");
const _files = require("../../files/utils/files");
const _member = require("../constants/member");
const _user = require("../constants/user");
const _usermodel = require("../models/user.model");
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) {
if (user.role === _user.USER_ROLE.LINK) {
this.logger.error(`${this.logUser.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.logUser.name} - ${e}`));
this.logger.error(`${this.logUser.name} - user account *${user.login}* is locked`);
throw new _common.HttpException('Account locked', _common.HttpStatus.FORBIDDEN);
}
const authSuccess = await (0, _functions.comparePassword)(password, user.password);
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;
}
async me(authUser) {
const user = await this.fromUserId(authUser.id);
if (!user) {
throw new _common.HttpException(`User *${authUser.login} (${authUser.id}) not found`, _common.HttpStatus.NOT_FOUND);
}
user.impersonated = !!authUser.impersonatedFromId;
user.clientId = authUser.clientId;
return {
user: user
};
}
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', _common.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async updateAvatar(req) {
const part = await req.file({
limits: {
fileSize: this.maxAvatarUploadSize
}
});
if (!part.mimetype.startsWith('image/')) {
throw new _common.HttpException('Unsupported file type', _common.HttpStatus.BAD_REQUEST);
}
const dstPath = _nodepath.default.join(req.user.tmpPath, this.userAvatarFileName);
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, this.userAvatarFileName), true);
} catch (e) {
this.logger.error(`${this.updateAvatar.name} - ${e}`);
throw new _common.HttpException('Unable to create avatar', _common.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async updateAccesses(user, ip, success) {
const 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), this.userAvatarFileName);
const avatarExists = await (0, _files.isPathExists)(avatarPath);
if (!avatarExists && generateIsNotExists) {
generate = true;
}
if (!generate) {
return [
avatarExists ? avatarPath : this.defaultAvatarFilePath,
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 = (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 getAvatarBase64(userLogin) {
const userAvatarPath = _nodepath.default.join(_usermodel.UserModel.getHomePath(userLogin), this.userAvatarFileName);
return (0, _image.convertImageToBase64)(await (0, _files.isPathExists)(userAvatarPath) ? userAvatarPath : this.defaultAvatarFilePath);
}
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 || !currentGroup.members.find((m)=>m.id === user.id)) {
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);
}
constructor(usersQueries, adminUsersManager){
this.usersQueries = usersQueries;
this.adminUsersManager = adminUsersManager;
this.defaultAvatarFilePath = _nodepath.default.join(_configconstants.STATIC_ASSETS_PATH, 'avatar.svg');
this.userAvatarFileName = 'avatar.png';
this.maxAvatarUploadSize = 1024 * 1024 * 5;
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
])
], UsersManager);
//# sourceMappingURL=users-manager.service.js.map