From a633c6f13196421518d19a91bacdff4f8de4fb10 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 21 Jul 2023 12:31:30 +0200 Subject: [PATCH 01/42] feat(file): add updatedAt and export file status enum --- src/app/models/file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/models/file.ts b/src/app/models/file.ts index bef85fe4..e14122cc 100644 --- a/src/app/models/file.ts +++ b/src/app/models/file.ts @@ -1,7 +1,6 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; - -enum FileStatus { +export enum FileStatus { EXISTS = 'EXISTS', DELETED = 'DELETED', TRASHED = 'TRASHED', @@ -19,6 +18,7 @@ export interface FileAttributes { folderId: number; folderUuid: string; createdAt: Date; + updatedAt: Date; encryptVersion: string; deleted: boolean; deletedAt: Date; From bc74c9cc02edc24f7dbf2187db7ecc68f5acae18 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 21 Jul 2023 12:52:11 +0200 Subject: [PATCH 02/42] feat(bin): change criteria to select files on delete-files --- bin/delete-files/utils.ts | 41 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/bin/delete-files/utils.ts b/bin/delete-files/utils.ts index df6072ed..7b8c4ece 100644 --- a/bin/delete-files/utils.ts +++ b/bin/delete-files/utils.ts @@ -1,14 +1,15 @@ import { request } from '@internxt/lib'; +import { Op } from 'sequelize'; import axios, { AxiosRequestConfig } from 'axios'; import { sign } from 'jsonwebtoken'; -import { Attributes as DeletedFileAttributes, DeletedFileModel } from '../../src/app/models/deletedFile'; +import { FileAttributes, FileModel, FileStatus } from '../../src/app/models/file'; type Timer = { start: () => void, end: () => number } export function signToken(duration: string, secret: string) { return sign( - {}, - Buffer.from(secret, 'base64').toString('utf8'), + {}, + Buffer.from(secret, 'base64').toString('utf8'), { algorithm: 'RS256', expiresIn: duration @@ -17,7 +18,7 @@ export function signToken(duration: string, secret: string) { } export const createTimer = (): Timer => { - let timeStart: [number,number]; + let timeStart: [number, number]; return { start: () => { @@ -27,26 +28,40 @@ export const createTimer = (): Timer => { const NS_PER_SEC = 1e9; const NS_TO_MS = 1e6; const diff = process.hrtime(timeStart); - + return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS; } }; }; -export function getFilesToDelete(deletedFile: DeletedFileModel, limit: number): Promise { - return deletedFile.findAll({ - limit, +export function getFilesToDelete( + files: FileModel, + limit: number, + updatedAt: Date, + lastId: number +): Promise { + return files.findAll({ + limit, raw: true, + where: { + status: FileStatus.DELETED, + id: { + [Op.lt]: lastId + }, + updatedAt: { + [Op.gte]: updatedAt + }, + }, order: [['id', 'DESC']] }).then(res => { - return res as unknown as DeletedFileAttributes[]; + return res as unknown as FileAttributes[]; }); } -export type DeleteFilesResponse = { - message: { - confirmed: string[], - notConfirmed: string [] +export type DeleteFilesResponse = { + message: { + confirmed: string[], + notConfirmed: string[] } } From f16007997687dd6356bfb3901ccf82ec168f082f Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 21 Jul 2023 13:13:48 +0200 Subject: [PATCH 03/42] feat(bin): delete-file v2 --- bin/delete-files/index.ts | 272 +++++++++++++++++++++----------------- tsconfig.json | 1 - 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/bin/delete-files/index.ts b/bin/delete-files/index.ts index dd808d59..f97bffab 100644 --- a/bin/delete-files/index.ts +++ b/bin/delete-files/index.ts @@ -1,98 +1,97 @@ -import { Op } from 'sequelize'; import { Command } from 'commander'; import Database from '../../src/config/initializers/database'; -import initDeletedFileModel from '../../src/app/models/deletedFile'; +import initFileModel from '../../src/app/models/file'; import { createTimer, deleteFiles, DeleteFilesResponse, getFilesToDelete, signToken } from './utils'; type CommandOptions = { - secret: string; - dbHostname: string; - dbPort: string; - dbName: string; - dbUsername: string; - dbPassword: string; - concurrency?: string; - limit?: string; - endpoint: string; + secret: string; + dbHostname: string; + dbPort: string; + dbName: string; + dbUsername: string; + dbPassword: string; + concurrency?: string; + limit?: string; + endpoint: string; }; const commands: { flags: string; description: string; required: boolean }[] = [ - { - flags: '-s, --secret ', - description: 'The secret used to sign the token to request files deletion', - required: true, - }, - { - flags: '--db-hostname ', - description: 'The hostname of the database where deleted files are stored', - required: true, - }, - { - flags: '--db-name ', - description: 'The name of the database where deleted files are stored', - required: true, - }, - { - flags: '--db-username ', - description: 'The username authorized to read and delete from the deleted files table', - required: true, - }, - { - flags: '--db-password ', - description: 'The database username password', - required: true, - }, - { - flags: '--db-port ', - description: 'The database port', - required: true, - }, - { - flags: '-c, --concurrency ', - description: 'The concurrency level of the requests that will be made', - required: false, - }, - { - flags: '-l, --limit ', - description: 'The files limit to handle each time', - required: false, - }, - { - flags: '-e, --endpoint ', - description: 'The API endpoint where the delete files requests are sent', - required: true, - }, + { + flags: '-s, --secret ', + description: 'The secret used to sign the token to request files deletion', + required: true, + }, + { + flags: '--db-hostname ', + description: 'The hostname of the database where deleted files are stored', + required: true, + }, + { + flags: '--db-name ', + description: 'The name of the database where deleted files are stored', + required: true, + }, + { + flags: '--db-username ', + description: 'The username authorized to read and delete from the deleted files table', + required: true, + }, + { + flags: '--db-password ', + description: 'The database username password', + required: true, + }, + { + flags: '--db-port ', + description: 'The database port', + required: true, + }, + { + flags: '-c, --concurrency ', + description: 'The concurrency level of the requests that will be made', + required: false, + }, + { + flags: '-l, --limit ', + description: 'The files limit to handle each time', + required: false, + }, + { + flags: '-e, --endpoint ', + description: 'The API endpoint where the delete files requests are sent', + required: true, + }, ]; -const command = new Command('delete-files').version('0.0.1'); +const command = new Command('delete-files').version('2.0.0'); commands.forEach((c) => { - if (c.required) { - command.requiredOption(c.flags, c.description); - } else { - command.option(c.flags, c.description); - } + if (c.required) { + command.requiredOption(c.flags, c.description); + } else { + command.option(c.flags, c.description); + } }); command.parse(); const opts: CommandOptions = command.opts(); const db = Database.getInstance({ - sequelizeConfig: { - host: opts.dbHostname, - port: opts.dbPort, - database: opts.dbName, - username: opts.dbUsername, - password: opts.dbPassword, - dialect: 'postgres', - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: false, - }, + sequelizeConfig: { + host: opts.dbHostname, + port: opts.dbPort, + database: opts.dbName, + username: opts.dbUsername, + password: opts.dbPassword, + dialect: 'postgres', + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false, + }, + }, }, - }, }); const timer = createTimer(); @@ -103,76 +102,103 @@ let totalRequests = 0; let failedRequests = 0; const logIntervalId = setInterval(() => { - console.log( - 'DELETION RATE: %s/s | FAILURE RATE %s%', - totalFilesRemoved / (timer.end() / 1000), - (failedRequests / totalRequests) * 100, - ); + console.log( + 'DELETION RATE: %s/s | FAILURE RATE %s%', + totalFilesRemoved / (timer.end() / 1000), + (failedRequests / totalRequests) * 100, + ); }, 10000); function finishProgram() { - clearInterval(logIntervalId); + clearInterval(logIntervalId); + + console.log('TOTAL FILES REMOVED %s | DURATION %ss', totalFilesRemoved, (timer.end() / 1000).toFixed(2)); + db.close() + .then(() => { + console.log('DISCONNECTED FROM DB'); + }) + .catch((err) => { + console.log('Error closing connection %s. %s', err.message.err.stack || 'NO STACK.'); + }); +} - console.log('TOTAL FILES REMOVED %s | DURATION %ss', totalFilesRemoved, (timer.end() / 1000).toFixed(2)); - db.close() - .then(() => { - console.log('DISCONNECTED FROM DB'); - }) - .catch((err) => { - console.log('Error closing connection %s. %s', err.message.err.stack || 'NO STACK.'); - }); +function getDateOneMonthAgo() { + const today = new Date(); + const oneMonthAgo = new Date(today); + + oneMonthAgo.setMonth(today.getMonth() - 1); + + return oneMonthAgo; } process.on('SIGINT', () => finishProgram()); +let lastId = 2147483647; +let deletedSize = 0; + async function start(limit = 20, concurrency = 5) { - const deletedFile = initDeletedFileModel(db); + const fileModel = initFileModel(db); + let fileIds: string[] = []; + const startDate = getDateOneMonthAgo(); - let fileIds: string[] = []; + console.log('Starting date set at', startDate); - do { - const files = await getFilesToDelete(deletedFile, limit); + do { + const files = await getFilesToDelete( + fileModel, + limit, + startDate, + lastId + ); - fileIds = files.map((f) => f.fileId); + console.log('files to delete %s', files.map((f) => f.id)); - const promises: Promise[] = []; - const chunksOf = Math.ceil(limit / concurrency); + fileIds = files.map((f) => f.fileId); - console.time('df-network-req'); + const promises: Promise[] = []; + const chunksOf = Math.ceil(limit / concurrency); - for (let i = 0; i < fileIds.length; i += chunksOf) { - promises.push(deleteFiles(opts.endpoint, fileIds.slice(i, i + chunksOf), signToken('5m', opts.secret))); - } + console.time('df-network-req'); - const results = await Promise.allSettled(promises); + for (let i = 0; i < fileIds.length; i += chunksOf) { + promises.push(deleteFiles(opts.endpoint, fileIds.slice(i, i + chunksOf), signToken('5m', opts.secret))); + } - console.timeEnd('df-network-req'); + const results = await Promise.allSettled(promises); - const filesIdsToRemove = results - .filter((r) => r.status === 'fulfilled') - .flatMap((r) => (r as PromiseFulfilledResult).value.message.confirmed); + console.timeEnd('df-network-req'); - totalRequests += results.length; - failedRequests += results.filter((r) => r.status === 'rejected').length; + const filesIdsToRemove = results + .filter((r) => r.status === 'fulfilled') + .flatMap((r) => (r as PromiseFulfilledResult).value.message.confirmed); - const deletedFilesToDelete = files.filter((f) => { - return filesIdsToRemove.some((fId) => fId === f.fileId); - }); + totalRequests += results.length; + failedRequests += results.filter((r) => r.status === 'rejected').length; - if (deletedFilesToDelete.length > 0) { - await deletedFile.destroy({ where: { id: { [Op.in]: deletedFilesToDelete.map((f) => f.id) } } }); - } else { - console.warn('Something not going fine, no files deleted'); - } + const deletedFilesToDelete = files.filter((f) => { + return filesIdsToRemove.some((fId) => fId === f.fileId); + }); + + if (deletedFilesToDelete.length === 0) { + console.warn('Something not going fine, no files deleted'); + lastId = Infinity; + } else { + lastId = deletedFilesToDelete.sort((a, b) => b.id - a.id)[0].id; + } + + totalFilesRemoved += deletedFilesToDelete.length; + deletedSize += deletedFilesToDelete.reduce((acc, curr) => acc + parseInt(curr.size.toString()), 0); - totalFilesRemoved += deletedFilesToDelete.length; - } while (fileIds.length === limit); + console.log('Deleted bytes', deletedSize); + } while (fileIds.length === limit); } start(parseInt(opts.limit || '20'), parseInt(opts.concurrency || '5')) - .catch((err) => { - console.log('err', err); - }) - .finally(() => { - finishProgram(); - }); + .catch((err) => { + console.log('err', err); + }) + .finally(() => { + console.log('Deleted bytes', deletedSize); + console.log('lastId was', lastId); + finishProgram(); + }); diff --git a/tsconfig.json b/tsconfig.json index c375ca4c..19419a60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -93,6 +93,5 @@ "./build", "./tests", "./migrations", - "./bin" ] } \ No newline at end of file From 96687dd3a77aa9562f8f5009a3568f501f3edc89 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 21 Jul 2023 13:43:17 +0200 Subject: [PATCH 04/42] fix(bin): set lastId to the lower value possible on delete-files --- bin/delete-files/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/delete-files/index.ts b/bin/delete-files/index.ts index f97bffab..7b71b951 100644 --- a/bin/delete-files/index.ts +++ b/bin/delete-files/index.ts @@ -183,7 +183,7 @@ async function start(limit = 20, concurrency = 5) { console.warn('Something not going fine, no files deleted'); lastId = Infinity; } else { - lastId = deletedFilesToDelete.sort((a, b) => b.id - a.id)[0].id; + lastId = deletedFilesToDelete.sort((a, b) => a.id - b.id)[0].id; } totalFilesRemoved += deletedFilesToDelete.length; From dca8b2b42d1510cd30f71efd6e67e0c504307856 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 21 Jul 2023 13:53:18 +0200 Subject: [PATCH 05/42] conf: remove bin from tsconfig --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 19419a60..c375ca4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -93,5 +93,6 @@ "./build", "./tests", "./migrations", + "./bin" ] } \ No newline at end of file From 239dcd26b100afb29a08cade03c41b1583a62a98 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 7 Aug 2023 11:50:59 +0200 Subject: [PATCH 06/42] Send to client correct http error and no generic one --- src/app/routes/auth.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/routes/auth.ts b/src/app/routes/auth.ts index e4c0598e..bbdead6d 100644 --- a/src/app/routes/auth.ts +++ b/src/app/routes/auth.ts @@ -9,6 +9,7 @@ import { HttpError } from 'http-errors'; import Logger from '../../lib/logger'; import winston from 'winston'; import { ReferralsNotAvailableError } from '../services/errors/referrals'; + interface Services { User: any; Analytics: any; @@ -265,6 +266,10 @@ export default (router: Router, service: any, config: Config) => { await controller.access(req, res); } catch (err) { logger.error(`[AUTH/ACCESS]: ERROR for user ${req.body.email}: ${(err as Error).message}`); + if (err instanceof HttpError) { + return res.status(err.statusCode).send({ error: err.message, code: err.code }); + } + res.status(500).send({ error: 'Internal Server Error' }); } }); From bdb2fd724b21946c44a14fff21d76c73a66f77f8 Mon Sep 17 00:00:00 2001 From: joan vicens Date: Mon, 7 Aug 2023 13:38:37 +0200 Subject: [PATCH 07/42] fix: return http code 409 when trying to rename a file with an existing name --- src/app/routes/storage.ts | 28 +++++++++++-------- .../errors/FileWithNameAlreadyExistsError.ts | 6 ++++ src/app/services/files.js | 3 +- 3 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 src/app/services/errors/FileWithNameAlreadyExistsError.ts diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 4fb29652..e9f8d316 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -12,6 +12,7 @@ import { FileAttributes } from '../models/file'; import CONSTANTS from '../constants'; import { LockNotAvaliableError } from '../services/errors/locks'; import { ConnectionTimedOutError } from 'sequelize'; +import { FileWithNameAlreadyExistsError } from '../services/errors/FileWithNameAlreadyExistsError'; type AuthorizedRequest = Request & { user: UserAttributes }; interface Services { @@ -49,7 +50,15 @@ export class StorageController { file.fileId = file.file_id; } - if (!file || !file.fileId || !file.bucket || file.size === undefined || file.size === null || !file.folder_id || !file.name) { + if ( + !file || + !file.fileId || + !file.bucket || + file.size === undefined || + file.size === null || + !file.folder_id || + !file.name + ) { this.logger.error( `Invalid metadata trying to create a file for user ${behalfUser.email}: ${JSON.stringify(file, null, 2)}`, ); @@ -76,9 +85,7 @@ export class StorageController { ); } catch (err) { this.logger.error( - `[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify( - file, - )}, STACK: ${(err as Error).stack}`, + `[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack}`, ); res.status(500).send({ error: 'Internal Server Error' }); } @@ -293,13 +300,7 @@ export class StorageController { } await Promise.all([ - this.services.Folder.getByIdAndUserIds( - id, - [ - behalfUser.id, - (req as AuthorizedRequest).user.id - ] - ), + this.services.Folder.getByIdAndUserIds(id, [behalfUser.id, (req as AuthorizedRequest).user.id]), this.services.Folder.getFolders(id, behalfUser.id, deleted), this.services.Files.getByFolderAndUserId(id, behalfUser.id, deleted), ]) @@ -421,6 +422,11 @@ export class StorageController { }) .catch((err: Error) => { this.logger.error(`Error updating metadata from file ${fileId} : ${err}`); + + if (err.message === FileWithNameAlreadyExistsError.message) { + res.status(409).send().end(); + } + res.status(500).send(); }); } diff --git a/src/app/services/errors/FileWithNameAlreadyExistsError.ts b/src/app/services/errors/FileWithNameAlreadyExistsError.ts new file mode 100644 index 00000000..df076f5e --- /dev/null +++ b/src/app/services/errors/FileWithNameAlreadyExistsError.ts @@ -0,0 +1,6 @@ +export class FileWithNameAlreadyExistsError extends Error { + static message = 'File with this name exists'; + constructor() { + super(FileWithNameAlreadyExistsError.message); + } +} diff --git a/src/app/services/files.js b/src/app/services/files.js index 71ace8cf..dd40f7eb 100644 --- a/src/app/services/files.js +++ b/src/app/services/files.js @@ -3,6 +3,7 @@ const async = require('async'); const createHttpError = require('http-errors'); const AesUtil = require('../../lib/AesUtil'); const { v4 } = require('uuid'); +const { FileWithNameAlreadyExistsError } = require('./errors/FileWithNameAlreadyExistsError'); // Filenames that contain "/", "\" or only spaces are invalid const invalidName = /[/\\]|^\s*$/; @@ -210,7 +211,7 @@ module.exports = (Model, App) => { }) .then((duplicateFile) => { if (duplicateFile) { - return next(Error('File with this name exists')); + return next(new FileWithNameAlreadyExistsError()); } newMeta.name = cryptoFileName; newMeta.plain_name = metadata.itemName; From 4794c636e4a95bfa6cb2737a4311d5e0f67e8b8e Mon Sep 17 00:00:00 2001 From: joan vicens Date: Mon, 7 Aug 2023 13:48:24 +0200 Subject: [PATCH 08/42] fix: return 409 instead fo 500 on foler rename conflict --- src/app/routes/storage.ts | 6 ++++++ .../services/errors/FolderWithNameAlreadyExistsError.ts | 7 +++++++ src/app/services/folder.js | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/app/services/errors/FolderWithNameAlreadyExistsError.ts diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index e9f8d316..b5b6c910 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -13,6 +13,7 @@ import CONSTANTS from '../constants'; import { LockNotAvaliableError } from '../services/errors/locks'; import { ConnectionTimedOutError } from 'sequelize'; import { FileWithNameAlreadyExistsError } from '../services/errors/FileWithNameAlreadyExistsError'; +import { FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; type AuthorizedRequest = Request & { user: UserAttributes }; interface Services { @@ -286,6 +287,11 @@ export class StorageController { }) .catch((err: Error) => { this.logger.error(`Error updating metadata from folder ${folderId}: ${err}`); + + if (err.message === FolderWithNameAlreadyExistsError.message) { + res.status(409).send().end(); + } + res.status(500).send(); }); } diff --git a/src/app/services/errors/FolderWithNameAlreadyExistsError.ts b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts new file mode 100644 index 00000000..038cd3c5 --- /dev/null +++ b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts @@ -0,0 +1,7 @@ +export class FolderWithNameAlreadyExistsError extends Error { + static message = 'Folder with this name exists'; + + constructor() { + super(FolderWithNameAlreadyExistsError.message); + } +} diff --git a/src/app/services/folder.js b/src/app/services/folder.js index ee6df024..c3f7cee1 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -7,6 +7,7 @@ const logger = require('../../lib/logger').default.getInstance(); const { default: Redis } = require('../../config/initializers/redis'); import { v4 } from 'uuid'; import { LockNotAvaliableError } from './errors/locks'; +import { FolderWithNameAlreadyExistsError } from './errors/FolderWithNameAlreadyExistsError'; const invalidName = /[\\/]|^\s*$/; @@ -432,7 +433,7 @@ module.exports = (Model, App) => { }) .then((isDuplicated) => { if (isDuplicated) { - return next(Error('Folder with this name exists')); + return next(new FolderWithNameAlreadyExistsError()); } newMeta.name = cryptoFolderName; newMeta.plain_name = metadata.itemName; From a2e76b8b8135d73445743ad31682d1516a599e9d Mon Sep 17 00:00:00 2001 From: joan vicens Date: Mon, 7 Aug 2023 16:36:11 +0200 Subject: [PATCH 09/42] pr changes --- src/app/routes/storage.ts | 4 ++-- src/app/services/errors/FileWithNameAlreadyExistsError.ts | 7 ++++--- .../services/errors/FolderWithNameAlreadyExistsError.ts | 6 +++--- src/app/services/files.js | 2 +- src/app/services/folder.js | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index b5b6c910..cf1ba733 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -288,7 +288,7 @@ export class StorageController { .catch((err: Error) => { this.logger.error(`Error updating metadata from folder ${folderId}: ${err}`); - if (err.message === FolderWithNameAlreadyExistsError.message) { + if (err instanceof FolderWithNameAlreadyExistsError) { res.status(409).send().end(); } @@ -429,7 +429,7 @@ export class StorageController { .catch((err: Error) => { this.logger.error(`Error updating metadata from file ${fileId} : ${err}`); - if (err.message === FileWithNameAlreadyExistsError.message) { + if (err instanceof FileWithNameAlreadyExistsError) { res.status(409).send().end(); } diff --git a/src/app/services/errors/FileWithNameAlreadyExistsError.ts b/src/app/services/errors/FileWithNameAlreadyExistsError.ts index df076f5e..c4d8d1b5 100644 --- a/src/app/services/errors/FileWithNameAlreadyExistsError.ts +++ b/src/app/services/errors/FileWithNameAlreadyExistsError.ts @@ -1,6 +1,7 @@ export class FileWithNameAlreadyExistsError extends Error { - static message = 'File with this name exists'; - constructor() { - super(FileWithNameAlreadyExistsError.message); + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, FileWithNameAlreadyExistsError.prototype); } } diff --git a/src/app/services/errors/FolderWithNameAlreadyExistsError.ts b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts index 038cd3c5..09284dd6 100644 --- a/src/app/services/errors/FolderWithNameAlreadyExistsError.ts +++ b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts @@ -1,7 +1,7 @@ export class FolderWithNameAlreadyExistsError extends Error { - static message = 'Folder with this name exists'; + constructor(message: string) { + super(message); - constructor() { - super(FolderWithNameAlreadyExistsError.message); + Object.setPrototypeOf(this, FolderWithNameAlreadyExistsError.prototype); } } diff --git a/src/app/services/files.js b/src/app/services/files.js index dd40f7eb..b1725843 100644 --- a/src/app/services/files.js +++ b/src/app/services/files.js @@ -211,7 +211,7 @@ module.exports = (Model, App) => { }) .then((duplicateFile) => { if (duplicateFile) { - return next(new FileWithNameAlreadyExistsError()); + return next(new FileWithNameAlreadyExistsError('File with this name exists')); } newMeta.name = cryptoFileName; newMeta.plain_name = metadata.itemName; diff --git a/src/app/services/folder.js b/src/app/services/folder.js index c3f7cee1..1cea2091 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -433,7 +433,7 @@ module.exports = (Model, App) => { }) .then((isDuplicated) => { if (isDuplicated) { - return next(new FolderWithNameAlreadyExistsError()); + return next(new FolderWithNameAlreadyExistsError('Folder with this name exists')); } newMeta.name = cryptoFolderName; newMeta.plain_name = metadata.itemName; From 9e5c7820d96c41a67ddce677d3a00f9fcd466eb2 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 7 Aug 2023 16:44:27 +0200 Subject: [PATCH 10/42] Changed error message when email or password is wrong --- src/app/routes/auth.ts | 6 +- src/app/services/user.js | 2 +- tests/controllers/auth.test.ts | 200 +++++++++++++++++++-------------- tests/e2e/e2e-spec.ts | 4 +- 4 files changed, 122 insertions(+), 90 deletions(-) diff --git a/src/app/routes/auth.ts b/src/app/routes/auth.ts index bbdead6d..566044cf 100644 --- a/src/app/routes/auth.ts +++ b/src/app/routes/auth.ts @@ -78,7 +78,7 @@ export class AuthController { try { user = await this.service.User.FindUserByEmail(req.body.email); } catch { - throw createHttpError(401, 'Wrong email/password'); + throw createHttpError(401, 'Wrong login credentials'); } const encSalt = this.service.Crypt.encryptText(user.hKey.toString()); @@ -119,7 +119,7 @@ export class AuthController { const userData: any = await this.service.User.FindUserByEmail(req.body.email).catch(() => { this.logger.info('[AUTH/LOGIN] Attempted login with a non-existing email: %s', req.body.email); - throw createHttpError(401, 'Wrong email/password'); + throw createHttpError(401, 'Wrong login credentials'); }); const loginAttemptsLimitReached = userData.errorLoginCount >= MAX_LOGIN_FAIL_ATTEMPTS; @@ -132,7 +132,7 @@ export class AuthController { if (hashedPass !== userData.password.toString()) { this.service.User.LoginFailed(req.body.email, true); - throw createHttpError(401, 'Wrong email/password'); + throw createHttpError(401, 'Wrong login credentials'); } const twoFactorEnabled = userData.secret_2FA && userData.secret_2FA.length > 0; diff --git a/src/app/services/user.js b/src/app/services/user.js index 8bf6c8b6..0214acb4 100644 --- a/src/app/services/user.js +++ b/src/app/services/user.js @@ -191,7 +191,7 @@ module.exports = (Model, App) => { .then((userData) => { if (!userData) { logger.error('ERROR user %s not found on database', email); - return reject(Error('Wrong email/password')); + return reject(Error('Wrong login credentials')); } const user = userData.dataValues; diff --git a/tests/controllers/auth.test.ts b/tests/controllers/auth.test.ts index 6591f2c4..5b5f09a6 100644 --- a/tests/controllers/auth.test.ts +++ b/tests/controllers/auth.test.ts @@ -104,11 +104,10 @@ describe('Auth controller', () => { }); describe('/login', () => { - it('should throw exception when no email on body', async () => { // Arrange const request = getRequest({ - body: '' + body: '', }); const response = getResponse(); const controller = getController(); @@ -126,33 +125,37 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) .returns({ hKey: '', - secret_2FA: '' - }) + secret_2FA: '', + }), }, Crypt: { - encryptText: sinon.spy() + encryptText: sinon.spy(), }, KeyServer: { - keysExists: sinon.spy() - } + keysExists: sinon.spy(), + }, }; const request = getRequest({ body: { - email: 'CAPS@EMAIL.COM' - } + email: 'CAPS@EMAIL.COM', + }, }); const response = getResponse({ status: () => { return { - send: sinon.spy() + send: sinon.spy(), }; - } + }, }); const controller = getController(services); @@ -167,24 +170,28 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') - .rejects({}) + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) + .rejects({}), }, }; const request = getRequest({ body: { - email: 'CAPS@EMAIL.COM' - } + email: 'CAPS@EMAIL.COM', + }, }); const response = getResponse({ status: () => { return { - send: sinon.spy() + send: sinon.spy(), }; - } + }, }); const controller = getController(services); @@ -193,7 +200,7 @@ describe('Auth controller', () => { await controller.login(request, response); } catch ({ message }) { // Assert - expect(message).to.equal('Wrong email/password'); + expect(message).to.equal('Wrong login credentials'); } }); @@ -201,34 +208,38 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) .returns({ hKey: '', - secret_2FA: '' - }) + secret_2FA: '', + }), }, Crypt: { - encryptText: sinon.spy() + encryptText: sinon.spy(), }, KeyServer: { - keysExists: sinon.spy() - } + keysExists: sinon.spy(), + }, }; const request = getRequest({ body: { - email: 'CAPS@EMAIL.COM' - } + email: 'CAPS@EMAIL.COM', + }, }); const sendSpy = sinon.spy(); const response = getResponse({ status: () => { return { - send: sendSpy + send: sendSpy, }; - } + }, }); const controller = getController(services); @@ -244,11 +255,9 @@ describe('Auth controller', () => { tfa: '', }); }); - }); describe('/access', () => { - it('should fail if no user data is found', async () => { // Arrange const services = { @@ -259,8 +268,8 @@ describe('Auth controller', () => { const controller = getController(services); const request = getRequest({ body: { - email: '' - } + email: '', + }, }); const response = getResponse(); @@ -268,7 +277,7 @@ describe('Auth controller', () => { // Act await controller.access(request, response); } catch ({ message }) { - expect(message).to.equal('Wrong email/password'); + expect(message).to.equal('Wrong login credentials'); } }); @@ -276,19 +285,23 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) .resolves({ - errorLoginCount: 10 - }) - } + errorLoginCount: 10, + }), + }, }; const controller = getController(services); const request = getRequest({ body: { - email: '' - } + email: '', + }, }); const response = getResponse(); @@ -304,27 +317,35 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) .resolves({ errorLoginCount: 9, - password: 'stored_hash' + password: 'stored_hash', }), - LoginFailed: sinon.spy() + LoginFailed: sinon.spy(), }, Crypt: { - decryptText: sinon.stub({ - decryptText: null - }, 'decryptText') - .returns('given_hash') - } + decryptText: sinon + .stub( + { + decryptText: null, + }, + 'decryptText', + ) + .returns('given_hash'), + }, }; const controller = getController(services); const request = getRequest({ body: { - email: '' - } + email: '', + }, }); const response = getResponse(); @@ -332,7 +353,7 @@ describe('Auth controller', () => { // Act await controller.access(request, response); } catch ({ message }) { - expect(message).to.equal('Wrong email/password'); + expect(message).to.equal('Wrong login credentials'); expect(services.User.LoginFailed.calledOnce).to.be.true; expect(services.User.LoginFailed.args[0]).to.deep.equal(['', true]); } @@ -342,58 +363,70 @@ describe('Auth controller', () => { // Arrange const services = { User: { - FindUserByEmail: sinon.stub({ - FindUserByEmail: null - }, 'FindUserByEmail') + FindUserByEmail: sinon + .stub( + { + FindUserByEmail: null, + }, + 'FindUserByEmail', + ) .resolves({ errorLoginCount: 9, - password: 'stored_hash' + password: 'stored_hash', }), LoginFailed: sinon.spy(), UpdateAccountActivity: sinon.spy(), - GetUserBucket: sinon.spy() + GetUserBucket: sinon.spy(), }, Crypt: { - decryptText: sinon.stub({ - decryptText: null - }, 'decryptText') - .returns('stored_hash') + decryptText: sinon + .stub( + { + decryptText: null, + }, + 'decryptText', + ) + .returns('stored_hash'), }, KeyServer: { keysExists: sinon.spy(), getKeys: sinon.spy(), }, Team: { - getTeamByMember: sinon.spy() + getTeamByMember: sinon.spy(), }, AppSumo: { - GetDetails: sinon.stub({ - GetDetails: null - }, 'GetDetails') - .returns(Promise.all([])) + GetDetails: sinon + .stub( + { + GetDetails: null, + }, + 'GetDetails', + ) + .returns(Promise.all([])), }, UsersReferrals: { - hasReferralsProgram: sinon.spy() - } + hasReferralsProgram: sinon.spy(), + }, }; const controller = getController(services, { - JWT: 'token' + JWT: 'token', }); const request = getRequest({ headers: { - 'internxt-client': 'drive-web' + 'internxt-client': 'drive-web', }, body: { - email: '' - } + email: '', + }, }); const jsonSpy = sinon.spy(); const response = getResponse({ status: () => { return { - json: jsonSpy + json: jsonSpy, }; - } + }, }); // Act @@ -411,7 +444,6 @@ describe('Auth controller', () => { expect(services.UsersReferrals.hasReferralsProgram.calledOnce).to.be.true; expect(jsonSpy.calledOnce).to.be.true; }); - }); }); diff --git a/tests/e2e/e2e-spec.ts b/tests/e2e/e2e-spec.ts index dc6cefd1..47783294 100644 --- a/tests/e2e/e2e-spec.ts +++ b/tests/e2e/e2e-spec.ts @@ -849,7 +849,7 @@ describe('E2E TEST', () => { }); expect(status).toBe(HttpStatus.UNAUTHORIZED); - expect(body.error).toBe('Wrong email/password'); + expect(body.error).toBe('Wrong login credentials'); }); it('should fail to login with a wrong password', async () => { @@ -871,7 +871,7 @@ describe('E2E TEST', () => { }); expect(status).toBe(HttpStatus.UNAUTHORIZED); - expect(body.error).toBe('Wrong email/password'); + expect(body.error).toBe('Wrong login credentials'); expect(data[0].error_login_count).toBe(1); }); From f8f492003e4d5f0d587111455b3e9fb4e57a2336 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Wed, 16 Aug 2023 11:06:40 +0200 Subject: [PATCH 11/42] feat(private-sharing): add persistence models --- src/app/models/index.ts | 11 +++++ src/app/models/permissions.ts | 50 ++++++++++++++++++++++ src/app/models/privateSharingFolder.ts | 47 ++++++++++++++++++++ src/app/models/privateSharingFolderRole.ts | 47 ++++++++++++++++++++ src/app/models/roles.ts | 46 ++++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 src/app/models/permissions.ts create mode 100644 src/app/models/privateSharingFolder.ts create mode 100644 src/app/models/privateSharingFolderRole.ts create mode 100644 src/app/models/roles.ts diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 5b2502f7..ac5d8960 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -9,8 +9,10 @@ import initFriendInvitation, { FriendInvitationModel } from './friendinvitation' import initInvitation, { InvitationModel } from './invitation'; import initKeyServer, { KeyServerModel } from './keyserver'; import initMailLimit, { MailLimitModel } from './mailLimit'; +import initPermission, { PermissionModel } from './permissions'; import initPlan, { PlanModel } from './plan'; import initReferral, { ReferralModel } from './referral'; +import initRole, { RoleModel } from './roles'; import initShare, { ShareModel } from './share'; import initTeam, { TeamModel } from './team'; import initTeamInvitation, { TeamInvitationModel } from './teaminvitation'; @@ -29,8 +31,10 @@ export type ModelType = | InvitationModel | KeyServerModel | MailLimitModel + | PermissionModel | PlanModel | ReferralModel + | RoleModel | ShareModel | TeamModel | TeamInvitationModel @@ -49,8 +53,10 @@ export default (database: Sequelize) => { const Invitation = initInvitation(database); const KeyServer = initKeyServer(database); const MailLimit = initMailLimit(database); + const Permission = initPermission(database); const Plan = initPlan(database); const Referral = initReferral(database); + const Role = initReferral(database); const Share = initShare(database); const Team = initTeam(database); const TeamMember = initTeamMember(database); @@ -84,9 +90,13 @@ export default (database: Sequelize) => { MailLimit.belongsTo(User); + Permission.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + Plan.belongsTo(User); Referral.belongsToMany(User, { through: UserReferral }); + + Role.hasMany(Permission, { foreignKey: 'role_id', sourceKey: 'id' }); Share.hasOne(File, { as: 'fileInfo', foreignKey: 'id', sourceKey: 'file_id' }); Share.hasOne(Folder, { as: 'folderInfo', foreignKey: 'id', sourceKey: 'folder_id' }); @@ -116,6 +126,7 @@ export default (database: Sequelize) => { [KeyServer.name]: KeyServer, ['mailLimit']: MailLimit, [Plan.name]: Plan, + ['privateSharingFolder']: PrivateSharingFolder, [Referral.name]: Referral, [Share.name]: Share, [Team.name]: Team, diff --git a/src/app/models/permissions.ts b/src/app/models/permissions.ts new file mode 100644 index 00000000..080dddca --- /dev/null +++ b/src/app/models/permissions.ts @@ -0,0 +1,50 @@ +import { Sequelize, DataTypes, ModelDefined } from 'sequelize'; + +interface PermissionsAttributes { + id: string; + type: string; + roleId: string, + createdAt: Date, + updatedAt: Date, +} + +export type PermissionModel = ModelDefined< + PermissionsAttributes, + PermissionsAttributes +>; + +export default (database: Sequelize): PermissionModel => { + const Permissions: PermissionModel = database.define( + 'permissions', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + type: { + type: DataTypes.STRING, + allowNull: false, + }, + role_id: { + type: DataTypes.UUIDV4, + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false + } + }, + { + tableName: 'permissions', + underscored: true, + timestamps: true + }, + ); + + return Permissions; +}; diff --git a/src/app/models/privateSharingFolder.ts b/src/app/models/privateSharingFolder.ts new file mode 100644 index 00000000..f567816a --- /dev/null +++ b/src/app/models/privateSharingFolder.ts @@ -0,0 +1,47 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface Attributes { + id: number; + folderId: string; + ownerId: string; + sharedWith: string; +} + +export type PrivateSharingFolderModel = ModelDefined; + +export default (database: Sequelize): PrivateSharingFolderModel => { + const PrivateSharingFolder: PrivateSharingFolderModel = database.define( + 'private_sharing_folder', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + ownerId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + folderId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + sharedWith: { + type: DataTypes.UUIDV4, + allowNull: false + }, + encryptionKey: { + type: DataTypes.STRING, + allowNull: false + }, + }, + { + tableName: 'private_sharing_folder', + underscored: true, + timestamps: false + }, + ); + + return PrivateSharingFolder; +}; + diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts new file mode 100644 index 00000000..418d0103 --- /dev/null +++ b/src/app/models/privateSharingFolderRole.ts @@ -0,0 +1,47 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface Attributes { + id: number; + folderId: string; + ownerId: string; + sharedWith: string; +} + +export type PrivateSharingFolderRoleModel = ModelDefined; + +export default (database: Sequelize): PrivateSharingFolderRoleModel => { + const PrivateSharingFolder: PrivateSharingFolderRoleModel = database.define( + 'private_sharing_folder_role', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + ownerId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + folderId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + sharedWith: { + type: DataTypes.UUIDV4, + allowNull: false + }, + encryptionKey: { + type: DataTypes.STRING, + allowNull: false + }, + }, + { + tableName: 'private_sharing_folder', + underscored: true, + timestamps: false + }, + ); + + return PrivateSharingFolder; +}; + diff --git a/src/app/models/roles.ts b/src/app/models/roles.ts new file mode 100644 index 00000000..f725fc1c --- /dev/null +++ b/src/app/models/roles.ts @@ -0,0 +1,46 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface RoleAttributes { + id: string; + role: string; + createdAt: Date; + updatedAt: Date; +} + +export type RoleModel = ModelDefined< + RoleAttributes, + RoleAttributes +>; + +export default (database: Sequelize): RoleModel => { + const Roles: RoleModel = database.define( + 'roles', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + role: { + type: DataTypes.STRING, + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false + } + }, + { + tableName: 'roles', + underscored: true, + timestamps: true + }, + ); + + return Roles; +}; + From fc4f3eef18970e1d1898ce6a52d1f8441d2e16f2 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Wed, 16 Aug 2023 11:07:23 +0200 Subject: [PATCH 12/42] feat(private-sharing): can user perform action usecase --- src/app/services/privateSharing.js | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/app/services/privateSharing.js diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js new file mode 100644 index 00000000..675283e7 --- /dev/null +++ b/src/app/services/privateSharing.js @@ -0,0 +1,49 @@ +class PrivateSharingFolderRoleNotFound extends Error { + constructor() { + super('Role inside this resource not found'); + + Object.setPrototypeOf(this, PrivateSharingFolderRoleNotFound.prototype); + } +} + +class PrivateSharingFolderPermissionsNotFound extends Error { + constructor() { + super('Permissions inside this resource not found'); + + Object.setPrototypeOf(this, PrivateSharingFolderPermissionsNotFound.prototype); + } +} + +module.exports = (Model) => { + const FindUserPermissionsInsidePrivateSharing = async (sharedWithId, folderId) => { + const privateFolderRole = await Model.privateSharingFolderRoles.find({ + userId: sharedWithId, + folderId, + }); + + if (!privateFolderRole) { + throw new PrivateSharingFolderRoleNotFound(); + } + + const permissions = await Model.permissions.find({ + roleId: privateFolderRole.roleId + }); + + if (!permissions) { + throw new PrivateSharingFolderPermissionsNotFound(); + } + + return permissions; + }; + + const CanUserPerformAction = async (sharedWith, resourceId, action) => { + const permissions = await FindUserPermissionsInsidePrivateSharing(sharedWith.uuid, resourceId); + + return permissions.includes(action); + }; + + return { + Name: 'PrivateSharing', + CanUserPerformAction + }; +}; From f49cc11cd9738723b6347e936d181c0a7ae20673 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Wed, 16 Aug 2023 11:07:53 +0200 Subject: [PATCH 13/42] feat(private-sharing): middleware (wip) --- .../middleware/resource-sharing.middleware.ts | 86 +++++++++++++++++++ src/app/routes/storage.ts | 24 +++++- 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/app/middleware/resource-sharing.middleware.ts diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts new file mode 100644 index 00000000..762579d9 --- /dev/null +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -0,0 +1,86 @@ +import jwt from 'jsonwebtoken'; +import { Request, Response, NextFunction } from 'express'; + +import { AuthorizedUser } from '../routes/types'; +import { FolderAttributes } from '../models/folder'; + +type User = AuthorizedUser['user']; +type Middleware = (req: Request & { behalfUser?: User }, res: Response, next: NextFunction) => Promise; + +// This should match with the permissions table for the given user role. +enum Actions { + UPLOAD_FILE = 'UPLOAD_FILE', + SHARE_ITEMS = 'SHARE_ITEMS', + RENAME_ITEMS = 'RENAME_ITEMS', + DOWNLOAD_ITEMS = 'DOWNLOAD_ITEMS', +} + +const build = ( + Service: { + User: { + FindUserByUuid: (uuid: string) => Promise; + }, + PrivateSharing: { + CanUserPerformAction: ( + sharedWith: User, + resourceId: FolderAttributes['uuid'], + action: Actions, + ) => Promise; + }, + }, +) => { + const mdBuilder = (action: Actions) => (async (req, res, next) => { + try { + const resourcesToken = req.headers['internxt-resources-token']; + const { user: requester } = (req as AuthorizedUser); + + if (!resourcesToken || typeof resourcesToken !== 'string') { + return next(); + } + + const decoded = jwt.verify(resourcesToken, process.env.JWT_SECRET as string) as { + owner?: { + uuid?: User['uuid']; + }; + sharedRootFolderId?: FolderAttributes['uuid']; + }; + + if (!decoded.owner ||!decoded.owner.uuid || !decoded.sharedRootFolderId) { + return res.status(400).send('Unrecognized / Wrong token'); + } + + const userIsAllowedToPerfomAction = await Service.PrivateSharing.CanUserPerformAction( + requester, + decoded.sharedRootFolderId, + action + ); + + if (!userIsAllowedToPerfomAction) { + return res.status(403).send('User not allowed to perform action'); + } + + const resourceOwner = await Service.User.FindUserByUuid(decoded.owner.uuid); + + if (!resourceOwner) { + return res.status(404).send('Resource owner not found'); + } + + req.behalfUser = resourceOwner; + + next(); + } catch (err) { + next(err); + } + }) as Middleware; + + return { + UploadFile: mdBuilder(Actions.UPLOAD_FILE), + UploadThumbnail: mdBuilder(Actions.UPLOAD_FILE), + ShareItem: mdBuilder(Actions.SHARE_ITEMS), + RenameFile: mdBuilder(Actions.RENAME_ITEMS), + }; +}; + +export { + build, +}; diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index cf1ba733..b62997d7 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -14,6 +14,7 @@ import { LockNotAvaliableError } from '../services/errors/locks'; import { ConnectionTimedOutError } from 'sequelize'; import { FileWithNameAlreadyExistsError } from '../services/errors/FileWithNameAlreadyExistsError'; import { FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; +import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware'; type AuthorizedRequest = Request & { user: UserAttributes }; interface Services { @@ -727,10 +728,21 @@ export default (router: Router, service: any) => { const { passportAuth } = passport; const sharedAdapter = sharedMiddlewareBuilder.build(service); const teamsAdapter = teamsMiddlewareBuilder.build(service); + const resourceSharingAdapter = resourceSharingMiddlewareBuilder.build(service); const controller = new StorageController(service, Logger); - router.post('/storage/file', passportAuth, sharedAdapter, controller.createFile.bind(controller)); - router.post('/storage/thumbnail', passportAuth, sharedAdapter, controller.createThumbnail.bind(controller)); + router.post('/storage/file', + passportAuth, + resourceSharingAdapter.UploadFile, + sharedAdapter, + controller.createFile.bind(controller) + ); + router.post('/storage/thumbnail', + passportAuth, + resourceSharingAdapter.UploadThumbnail, + sharedAdapter, + controller.createThumbnail.bind(controller) + ); router.post('/storage/folder', passportAuth, sharedAdapter, controller.createFolder.bind(controller)); router.get('/storage/tree', passportAuth, controller.getTree.bind(controller)); router.get('/storage/tree/:folderId', passportAuth, controller.getTreeSpecific.bind(controller)); @@ -745,7 +757,13 @@ export default (router: Router, service: any) => { controller.getFolderContents.bind(controller), ); router.post('/storage/move/file', passportAuth, sharedAdapter, controller.moveFile.bind(controller)); - router.post('/storage/file/:fileid/meta', passportAuth, sharedAdapter, controller.updateFile.bind(controller)); + router.post( + '/storage/file/:fileid/meta', + passportAuth, + resourceSharingAdapter.RenameFile, + sharedAdapter, + controller.updateFile.bind(controller) + ); router.delete('/storage/bucket/:bucketid/file/:fileid', passportAuth, controller.deleteFileBridge.bind(controller)); router.delete( '/storage/folder/:folderid/file/:fileid', From 83c3f0cb23c61f965dc4a26b5f918f36a2753e7f Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 16:08:18 +0200 Subject: [PATCH 14/42] init PrivateSharingFolder model --- src/app/models/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/models/index.ts b/src/app/models/index.ts index ac5d8960..2547af47 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -20,6 +20,7 @@ import initTeamMember, { TeamMemberModel } from './teammember'; import initThumbnail, { ThumbnailModel } from './thumbnail'; import initUser, { UserModel } from './user'; import initUserReferral, { UserReferralModel } from './userReferral'; +import initPrivateSharingFolderModel, { PrivateSharingFolderModel } from './privateSharingFolder'; export type ModelType = | AppSumoModel @@ -41,7 +42,8 @@ export type ModelType = | TeamMemberModel | UserModel | UserReferralModel - | FriendInvitationModel; + | FriendInvitationModel + | PrivateSharingFolderModel; export default (database: Sequelize) => { const AppSumo = initAppSumo(database); @@ -64,6 +66,7 @@ export default (database: Sequelize) => { const User = initUser(database); const UserReferral = initUserReferral(database); const FriendInvitation = initFriendInvitation(database); + const PrivateSharingFolder = initPrivateSharingFolderModel(database); AppSumo.belongsTo(User); From f53d6cd4012cfed37ed17098fd770f260a970d96 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 17:39:50 +0200 Subject: [PATCH 15/42] init PrivateSharingFolderRoles model --- src/app/models/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 2547af47..89402b8e 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -21,6 +21,7 @@ import initThumbnail, { ThumbnailModel } from './thumbnail'; import initUser, { UserModel } from './user'; import initUserReferral, { UserReferralModel } from './userReferral'; import initPrivateSharingFolderModel, { PrivateSharingFolderModel } from './privateSharingFolder'; +import initPrivateSharingFolderRolesModel, { PrivateSharingFolderRolesModel } from './privateSharingFolderRole'; export type ModelType = | AppSumoModel @@ -43,7 +44,8 @@ export type ModelType = | UserModel | UserReferralModel | FriendInvitationModel - | PrivateSharingFolderModel; + | PrivateSharingFolderModel + | PrivateSharingFolderRolesModel; export default (database: Sequelize) => { const AppSumo = initAppSumo(database); @@ -67,6 +69,7 @@ export default (database: Sequelize) => { const UserReferral = initUserReferral(database); const FriendInvitation = initFriendInvitation(database); const PrivateSharingFolder = initPrivateSharingFolderModel(database); + const PrivateSharingFolderRoles = initPrivateSharingFolderRolesModel(database); AppSumo.belongsTo(User); @@ -129,7 +132,8 @@ export default (database: Sequelize) => { [KeyServer.name]: KeyServer, ['mailLimit']: MailLimit, [Plan.name]: Plan, - ['privateSharingFolder']: PrivateSharingFolder, + [PrivateSharingFolder.name]: PrivateSharingFolder, + [PrivateSharingFolderRoles.name]: PrivateSharingFolderRoles, [Referral.name]: Referral, [Share.name]: Share, [Team.name]: Team, From 4b05f1f677d185de2eeffeaf8d3ecb3040d16329 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 17:40:31 +0200 Subject: [PATCH 16/42] fix PrivateSharingFolder model --- src/app/models/privateSharingFolder.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/models/privateSharingFolder.ts b/src/app/models/privateSharingFolder.ts index f567816a..3f406b90 100644 --- a/src/app/models/privateSharingFolder.ts +++ b/src/app/models/privateSharingFolder.ts @@ -1,10 +1,11 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; interface Attributes { - id: number; + id: string; folderId: string; ownerId: string; sharedWith: string; + encryptionKey: string; } export type PrivateSharingFolderModel = ModelDefined; @@ -18,11 +19,11 @@ export default (database: Sequelize): PrivateSharingFolderModel => { primaryKey: true, allowNull: false, }, - ownerId: { + folderId: { type: DataTypes.UUIDV4, allowNull: false }, - folderId: { + ownerId: { type: DataTypes.UUIDV4, allowNull: false }, @@ -38,7 +39,7 @@ export default (database: Sequelize): PrivateSharingFolderModel => { { tableName: 'private_sharing_folder', underscored: true, - timestamps: false + timestamps: true }, ); From 3f8966e4526847a2598c1f7ab7b78b1c85503a66 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 17:40:47 +0200 Subject: [PATCH 17/42] fix PrivateSharingFolderRoles model --- src/app/models/privateSharingFolderRole.ts | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts index 418d0103..b13c118e 100644 --- a/src/app/models/privateSharingFolderRole.ts +++ b/src/app/models/privateSharingFolderRole.ts @@ -2,46 +2,42 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; interface Attributes { id: number; + userId: string; folderId: string; - ownerId: string; - sharedWith: string; + roleId: string; } -export type PrivateSharingFolderRoleModel = ModelDefined; +export type PrivateSharingFolderRolesModel = ModelDefined; -export default (database: Sequelize): PrivateSharingFolderRoleModel => { - const PrivateSharingFolder: PrivateSharingFolderRoleModel = database.define( - 'private_sharing_folder_role', +export default (database: Sequelize): PrivateSharingFolderRolesModel => { + const PrivateSharingFolderRoles: PrivateSharingFolderRolesModel = database.define( + 'private_sharing_folder_roles', { id: { type: DataTypes.UUIDV4, primaryKey: true, allowNull: false, }, - ownerId: { - type: DataTypes.UUIDV4, + userId: { + type: DataTypes.STRING, allowNull: false }, folderId: { type: DataTypes.UUIDV4, allowNull: false }, - sharedWith: { + roleId: { type: DataTypes.UUIDV4, allowNull: false }, - encryptionKey: { - type: DataTypes.STRING, - allowNull: false - }, }, { - tableName: 'private_sharing_folder', + tableName: 'private_sharing_folder_roles', underscored: true, - timestamps: false + timestamps: true }, ); - return PrivateSharingFolder; + return PrivateSharingFolderRoles; }; From 3d1c46f06172a22521db7db2bc7cee82509cef40 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 18:36:56 +0200 Subject: [PATCH 18/42] fixed timestamps --- src/app/models/permissions.ts | 16 ++++------------ src/app/models/privateSharingFolder.ts | 2 ++ src/app/models/privateSharingFolderRole.ts | 2 ++ 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/app/models/permissions.ts b/src/app/models/permissions.ts index 080dddca..906a3fca 100644 --- a/src/app/models/permissions.ts +++ b/src/app/models/permissions.ts @@ -3,9 +3,9 @@ import { Sequelize, DataTypes, ModelDefined } from 'sequelize'; interface PermissionsAttributes { id: string; type: string; - roleId: string, - createdAt: Date, - updatedAt: Date, + roleId: string; + createdAt: Date; + updatedAt: Date; } export type PermissionModel = ModelDefined< @@ -26,18 +26,10 @@ export default (database: Sequelize): PermissionModel => { type: DataTypes.STRING, allowNull: false, }, - role_id: { + roleId: { type: DataTypes.UUIDV4, allowNull: false, }, - created_at: { - type: DataTypes.DATE, - allowNull: false, - }, - updated_at: { - type: DataTypes.DATE, - allowNull: false - } }, { tableName: 'permissions', diff --git a/src/app/models/privateSharingFolder.ts b/src/app/models/privateSharingFolder.ts index 3f406b90..c26b0d97 100644 --- a/src/app/models/privateSharingFolder.ts +++ b/src/app/models/privateSharingFolder.ts @@ -6,6 +6,8 @@ interface Attributes { ownerId: string; sharedWith: string; encryptionKey: string; + createdAt: Date; + updatedAt: Date; } export type PrivateSharingFolderModel = ModelDefined; diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts index b13c118e..c2474ddd 100644 --- a/src/app/models/privateSharingFolderRole.ts +++ b/src/app/models/privateSharingFolderRole.ts @@ -5,6 +5,8 @@ interface Attributes { userId: string; folderId: string; roleId: string; + createdAt: Date; + updatedAt: Date; } export type PrivateSharingFolderRolesModel = ModelDefined; From 05821e0c33179fea14f3f588a29e620328c90f6c Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 18:37:52 +0200 Subject: [PATCH 19/42] linted code --- .../middleware/resource-sharing.middleware.ts | 27 ++++++++++--------- src/app/routes/storage.ts | 20 +++++++------- src/lib/performance/network.ts | 4 +-- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index 762579d9..c04cb0ca 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -16,13 +16,13 @@ enum Actions { } const build = ( - Service: { - User: { + Service: { + User: { FindUserByUuid: (uuid: string) => Promise; }, PrivateSharing: { CanUserPerformAction: ( - sharedWith: User, + sharedWith: User, resourceId: FolderAttributes['uuid'], action: Actions, ) => Promise; @@ -33,40 +33,40 @@ const build = ( try { const resourcesToken = req.headers['internxt-resources-token']; const { user: requester } = (req as AuthorizedUser); - + if (!resourcesToken || typeof resourcesToken !== 'string') { return next(); } - + const decoded = jwt.verify(resourcesToken, process.env.JWT_SECRET as string) as { owner?: { uuid?: User['uuid']; }; sharedRootFolderId?: FolderAttributes['uuid']; }; - - if (!decoded.owner ||!decoded.owner.uuid || !decoded.sharedRootFolderId) { + + if (!decoded.owner || !decoded.owner.uuid || !decoded.sharedRootFolderId) { return res.status(400).send('Unrecognized / Wrong token'); } - + const userIsAllowedToPerfomAction = await Service.PrivateSharing.CanUserPerformAction( requester, decoded.sharedRootFolderId, action ); - + if (!userIsAllowedToPerfomAction) { return res.status(403).send('User not allowed to perform action'); } - + const resourceOwner = await Service.User.FindUserByUuid(decoded.owner.uuid); - + if (!resourceOwner) { return res.status(404).send('Resource owner not found'); } - + req.behalfUser = resourceOwner; - + next(); } catch (err) { next(err); @@ -78,6 +78,7 @@ const build = ( UploadThumbnail: mdBuilder(Actions.UPLOAD_FILE), ShareItem: mdBuilder(Actions.SHARE_ITEMS), RenameFile: mdBuilder(Actions.RENAME_ITEMS), + DownloadFile: mdBuilder(Actions.DOWNLOAD_ITEMS), }; }; diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index b62997d7..b27978bf 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -731,16 +731,16 @@ export default (router: Router, service: any) => { const resourceSharingAdapter = resourceSharingMiddlewareBuilder.build(service); const controller = new StorageController(service, Logger); - router.post('/storage/file', - passportAuth, - resourceSharingAdapter.UploadFile, - sharedAdapter, + router.post('/storage/file', + passportAuth, + resourceSharingAdapter.UploadFile, + sharedAdapter, controller.createFile.bind(controller) ); - router.post('/storage/thumbnail', - passportAuth, + router.post('/storage/thumbnail', + passportAuth, resourceSharingAdapter.UploadThumbnail, - sharedAdapter, + sharedAdapter, controller.createThumbnail.bind(controller) ); router.post('/storage/folder', passportAuth, sharedAdapter, controller.createFolder.bind(controller)); @@ -758,10 +758,10 @@ export default (router: Router, service: any) => { ); router.post('/storage/move/file', passportAuth, sharedAdapter, controller.moveFile.bind(controller)); router.post( - '/storage/file/:fileid/meta', - passportAuth, + '/storage/file/:fileid/meta', + passportAuth, resourceSharingAdapter.RenameFile, - sharedAdapter, + sharedAdapter, controller.updateFile.bind(controller) ); router.delete('/storage/bucket/:bucketid/file/:fileid', passportAuth, controller.deleteFileBridge.bind(controller)); diff --git a/src/lib/performance/network.ts b/src/lib/performance/network.ts index 2421e7fb..a237ad44 100644 --- a/src/lib/performance/network.ts +++ b/src/lib/performance/network.ts @@ -1,5 +1,5 @@ -import Agent from "agentkeepalive"; -import axios from "axios"; +import Agent from 'agentkeepalive'; +import axios from 'axios'; function createHttpAgent() { return new Agent({ From e3ee600550b17ecc206eddc3f73f5aeac43a1e8e Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 19:07:08 +0200 Subject: [PATCH 20/42] added sequelize relations --- src/app/models/index.ts | 51 ++++++++++++++-------- src/app/models/privateSharingFolder.ts | 1 - src/app/models/privateSharingFolderRole.ts | 9 ++-- src/app/models/roles.ts | 3 +- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 89402b8e..107602da 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -9,10 +9,8 @@ import initFriendInvitation, { FriendInvitationModel } from './friendinvitation' import initInvitation, { InvitationModel } from './invitation'; import initKeyServer, { KeyServerModel } from './keyserver'; import initMailLimit, { MailLimitModel } from './mailLimit'; -import initPermission, { PermissionModel } from './permissions'; import initPlan, { PlanModel } from './plan'; import initReferral, { ReferralModel } from './referral'; -import initRole, { RoleModel } from './roles'; import initShare, { ShareModel } from './share'; import initTeam, { TeamModel } from './team'; import initTeamInvitation, { TeamInvitationModel } from './teaminvitation'; @@ -20,8 +18,10 @@ import initTeamMember, { TeamMemberModel } from './teammember'; import initThumbnail, { ThumbnailModel } from './thumbnail'; import initUser, { UserModel } from './user'; import initUserReferral, { UserReferralModel } from './userReferral'; -import initPrivateSharingFolderModel, { PrivateSharingFolderModel } from './privateSharingFolder'; -import initPrivateSharingFolderRolesModel, { PrivateSharingFolderRolesModel } from './privateSharingFolderRole'; +import initRole, { RoleModel } from './roles'; +import initPermission, { PermissionModel } from './permissions'; +import initPrivateSharingFolder, { PrivateSharingFolderModel } from './privateSharingFolder'; +import initPrivateSharingFolderRole, { PrivateSharingFolderRoleModel } from './privateSharingFolderRole'; export type ModelType = | AppSumoModel @@ -33,10 +33,8 @@ export type ModelType = | InvitationModel | KeyServerModel | MailLimitModel - | PermissionModel | PlanModel | ReferralModel - | RoleModel | ShareModel | TeamModel | TeamInvitationModel @@ -44,8 +42,10 @@ export type ModelType = | UserModel | UserReferralModel | FriendInvitationModel + | RoleModel + | PermissionModel | PrivateSharingFolderModel - | PrivateSharingFolderRolesModel; + | PrivateSharingFolderRoleModel; export default (database: Sequelize) => { const AppSumo = initAppSumo(database); @@ -57,10 +57,8 @@ export default (database: Sequelize) => { const Invitation = initInvitation(database); const KeyServer = initKeyServer(database); const MailLimit = initMailLimit(database); - const Permission = initPermission(database); const Plan = initPlan(database); const Referral = initReferral(database); - const Role = initReferral(database); const Share = initShare(database); const Team = initTeam(database); const TeamMember = initTeamMember(database); @@ -68,8 +66,10 @@ export default (database: Sequelize) => { const User = initUser(database); const UserReferral = initUserReferral(database); const FriendInvitation = initFriendInvitation(database); - const PrivateSharingFolder = initPrivateSharingFolderModel(database); - const PrivateSharingFolderRoles = initPrivateSharingFolderRolesModel(database); + const Role = initRole(database); + const Permission = initPermission(database); + const PrivateSharingFolder = initPrivateSharingFolder(database); + const PrivateSharingFolderRole = initPrivateSharingFolderRole(database); AppSumo.belongsTo(User); @@ -90,19 +90,17 @@ export default (database: Sequelize) => { Folder.belongsTo(User); Folder.hasMany(Folder, { foreignKey: 'parent_id', as: 'children' }); Folder.hasMany(Share, { as: 'shares', foreignKey: 'folder_id', sourceKey: 'id' }); + Folder.hasMany(PrivateSharingFolderRole, { foreignKey: 'folder_id', sourceKey: 'uuid' }); + Folder.hasMany(PrivateSharingFolder, { foreignKey: 'folder_id', sourceKey: 'uuid' }); Invitation.belongsTo(User, { foreignKey: 'host', targetKey: 'id' }); Invitation.belongsTo(User, { foreignKey: 'guest', targetKey: 'id' }); MailLimit.belongsTo(User); - Permission.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); - Plan.belongsTo(User); Referral.belongsToMany(User, { through: UserReferral }); - - Role.hasMany(Permission, { foreignKey: 'role_id', sourceKey: 'id' }); Share.hasOne(File, { as: 'fileInfo', foreignKey: 'id', sourceKey: 'file_id' }); Share.hasOne(Folder, { as: 'folderInfo', foreignKey: 'id', sourceKey: 'folder_id' }); @@ -117,10 +115,27 @@ export default (database: Sequelize) => { User.belongsToMany(Referral, { through: UserReferral }); User.hasMany(MailLimit, { foreignKey: 'user_id' }); User.hasMany(FriendInvitation, { foreignKey: 'host' }); + User.hasMany(PrivateSharingFolderRole, { foreignKey: 'user_id', sourceKey: 'uuid' }); + User.hasMany(PrivateSharingFolder, { foreignKey: 'owner_id', sourceKey: 'uuid' }); + User.hasMany(PrivateSharingFolder, { foreignKey: 'shared_with', sourceKey: 'uuid' }); UserReferral.belongsTo(User, { foreignKey: 'user_id' }); UserReferral.belongsTo(Referral, { foreignKey: 'referral_id' }); + + Role.hasMany(Permission, { foreignKey: 'role_id', sourceKey: 'id' }); + Role.hasMany(PrivateSharingFolderRole, { foreignKey: 'role_id', sourceKey: 'id' }); + + Permission.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + + PrivateSharingFolderRole.belongsTo(Folder, { foreignKey: 'folder_id', targetKey: 'uuid' }); + PrivateSharingFolderRole.belongsTo(User, { foreignKey: 'user_id', targetKey: 'uuid' }); + PrivateSharingFolderRole.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + + PrivateSharingFolder.belongsTo(Folder, { foreignKey: 'folder_id', targetKey: 'uuid' }); + PrivateSharingFolder.belongsTo(User, { foreignKey: 'owner_id', targetKey: 'uuid' }); + PrivateSharingFolder.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); + return { [AppSumo.name]: AppSumo, [Backup.name]: Backup, @@ -132,8 +147,6 @@ export default (database: Sequelize) => { [KeyServer.name]: KeyServer, ['mailLimit']: MailLimit, [Plan.name]: Plan, - [PrivateSharingFolder.name]: PrivateSharingFolder, - [PrivateSharingFolderRoles.name]: PrivateSharingFolderRoles, [Referral.name]: Referral, [Share.name]: Share, [Team.name]: Team, @@ -142,5 +155,9 @@ export default (database: Sequelize) => { [User.name]: User, [UserReferral.name]: UserReferral, [FriendInvitation.name]: FriendInvitation, + [Role.name]: Role, + [Permission.name]: Permission, + [PrivateSharingFolder.name]: PrivateSharingFolder, + [PrivateSharingFolderRole.name]: PrivateSharingFolderRole, }; }; diff --git a/src/app/models/privateSharingFolder.ts b/src/app/models/privateSharingFolder.ts index c26b0d97..704fc883 100644 --- a/src/app/models/privateSharingFolder.ts +++ b/src/app/models/privateSharingFolder.ts @@ -47,4 +47,3 @@ export default (database: Sequelize): PrivateSharingFolderModel => { return PrivateSharingFolder; }; - diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts index c2474ddd..2a87698f 100644 --- a/src/app/models/privateSharingFolderRole.ts +++ b/src/app/models/privateSharingFolderRole.ts @@ -9,10 +9,10 @@ interface Attributes { updatedAt: Date; } -export type PrivateSharingFolderRolesModel = ModelDefined; +export type PrivateSharingFolderRoleModel = ModelDefined; -export default (database: Sequelize): PrivateSharingFolderRolesModel => { - const PrivateSharingFolderRoles: PrivateSharingFolderRolesModel = database.define( +export default (database: Sequelize): PrivateSharingFolderRoleModel => { + const PrivateSharingFolderRole: PrivateSharingFolderRoleModel = database.define( 'private_sharing_folder_roles', { id: { @@ -40,6 +40,5 @@ export default (database: Sequelize): PrivateSharingFolderRolesModel => { }, ); - return PrivateSharingFolderRoles; + return PrivateSharingFolderRole; }; - diff --git a/src/app/models/roles.ts b/src/app/models/roles.ts index f725fc1c..04833605 100644 --- a/src/app/models/roles.ts +++ b/src/app/models/roles.ts @@ -8,7 +8,7 @@ interface RoleAttributes { } export type RoleModel = ModelDefined< - RoleAttributes, + RoleAttributes, RoleAttributes >; @@ -43,4 +43,3 @@ export default (database: Sequelize): RoleModel => { return Roles; }; - From e08dd04c8b54afc863d81cd454bb90568c6ce4ee Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 16 Aug 2023 19:52:45 +0200 Subject: [PATCH 21/42] added moveItems middleware --- src/app/middleware/resource-sharing.middleware.ts | 7 +++---- src/app/models/roles.ts | 8 -------- src/app/routes/storage.ts | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index c04cb0ca..baa6be76 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -10,9 +10,8 @@ type Middleware = (req: Request & { behalfUser?: User }, res: Response, next: Ne // This should match with the permissions table for the given user role. enum Actions { UPLOAD_FILE = 'UPLOAD_FILE', - SHARE_ITEMS = 'SHARE_ITEMS', RENAME_ITEMS = 'RENAME_ITEMS', - DOWNLOAD_ITEMS = 'DOWNLOAD_ITEMS', + MOVE_ITEMS = 'MOVE_ITEMS', } const build = ( @@ -76,9 +75,9 @@ const build = ( return { UploadFile: mdBuilder(Actions.UPLOAD_FILE), UploadThumbnail: mdBuilder(Actions.UPLOAD_FILE), - ShareItem: mdBuilder(Actions.SHARE_ITEMS), RenameFile: mdBuilder(Actions.RENAME_ITEMS), - DownloadFile: mdBuilder(Actions.DOWNLOAD_ITEMS), + MoveFile: mdBuilder(Actions.MOVE_ITEMS), + MoveFolder: mdBuilder(Actions.MOVE_ITEMS), }; }; diff --git a/src/app/models/roles.ts b/src/app/models/roles.ts index 04833605..c7b9bfc9 100644 --- a/src/app/models/roles.ts +++ b/src/app/models/roles.ts @@ -25,14 +25,6 @@ export default (database: Sequelize): RoleModel => { type: DataTypes.STRING, allowNull: false, }, - created_at: { - type: DataTypes.DATE, - allowNull: false, - }, - updated_at: { - type: DataTypes.DATE, - allowNull: false - } }, { tableName: 'roles', diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index b27978bf..59c31804 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -747,7 +747,7 @@ export default (router: Router, service: any) => { router.get('/storage/tree', passportAuth, controller.getTree.bind(controller)); router.get('/storage/tree/:folderId', passportAuth, controller.getTreeSpecific.bind(controller)); router.delete('/storage/folder/:id', passportAuth, sharedAdapter, controller.deleteFolder.bind(controller)); - router.post('/storage/move/folder', passportAuth, sharedAdapter, controller.moveFolder.bind(controller)); + router.post('/storage/move/folder', passportAuth, resourceSharingAdapter.MoveFolder, sharedAdapter, controller.moveFolder.bind(controller)); router.post('/storage/folder/:folderid/meta', passportAuth, sharedAdapter, controller.updateFolder.bind(controller)); router.get( '/storage/v2/folder/:id/:idTeam?', @@ -756,7 +756,7 @@ export default (router: Router, service: any) => { teamsAdapter, controller.getFolderContents.bind(controller), ); - router.post('/storage/move/file', passportAuth, sharedAdapter, controller.moveFile.bind(controller)); + router.post('/storage/move/file', passportAuth, resourceSharingAdapter.MoveFile, sharedAdapter, controller.moveFile.bind(controller)); router.post( '/storage/file/:fileid/meta', passportAuth, From b55b3a1877e8b8336ff2435df2e380668a63390c Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 17 Aug 2023 11:19:22 +0200 Subject: [PATCH 22/42] permit middleware if requester is the owner --- src/app/middleware/resource-sharing.middleware.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index baa6be76..48243344 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -48,6 +48,11 @@ const build = ( return res.status(400).send('Unrecognized / Wrong token'); } + if (decoded.owner.uuid === requester.uuid) { + req.behalfUser = requester; + return next(); + } + const userIsAllowedToPerfomAction = await Service.PrivateSharing.CanUserPerformAction( requester, decoded.sharedRootFolderId, From 50510de33bb5d70f28d6796734f64d8af96e1140 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 17 Aug 2023 17:20:02 +0200 Subject: [PATCH 23/42] fix models --- src/app/models/privateSharingFolder.ts | 2 +- src/app/models/privateSharingFolderRole.ts | 2 +- src/app/services/privateSharing.js | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/models/privateSharingFolder.ts b/src/app/models/privateSharingFolder.ts index 704fc883..aba92139 100644 --- a/src/app/models/privateSharingFolder.ts +++ b/src/app/models/privateSharingFolder.ts @@ -14,7 +14,7 @@ export type PrivateSharingFolderModel = ModelDefined; export default (database: Sequelize): PrivateSharingFolderModel => { const PrivateSharingFolder: PrivateSharingFolderModel = database.define( - 'private_sharing_folder', + 'privateSharingFolder', { id: { type: DataTypes.UUIDV4, diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts index 2a87698f..360b9300 100644 --- a/src/app/models/privateSharingFolderRole.ts +++ b/src/app/models/privateSharingFolderRole.ts @@ -13,7 +13,7 @@ export type PrivateSharingFolderRoleModel = ModelDefined export default (database: Sequelize): PrivateSharingFolderRoleModel => { const PrivateSharingFolderRole: PrivateSharingFolderRoleModel = database.define( - 'private_sharing_folder_roles', + 'privateSharingFolderRole', { id: { type: DataTypes.UUIDV4, diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js index 675283e7..58350f0c 100644 --- a/src/app/services/privateSharing.js +++ b/src/app/services/privateSharing.js @@ -16,17 +16,21 @@ class PrivateSharingFolderPermissionsNotFound extends Error { module.exports = (Model) => { const FindUserPermissionsInsidePrivateSharing = async (sharedWithId, folderId) => { - const privateFolderRole = await Model.privateSharingFolderRoles.find({ - userId: sharedWithId, - folderId, + const privateFolderRole = await Model.privateSharingFolderRole.findOne({ + where: { + userId: sharedWithId, + folderId, + } }); if (!privateFolderRole) { throw new PrivateSharingFolderRoleNotFound(); } - const permissions = await Model.permissions.find({ - roleId: privateFolderRole.roleId + const permissions = await Model.permissions.findAll({ + where: { + roleId: privateFolderRole.roleId + } }); if (!permissions) { From 053233891c84e27bb4e50e0c5db054b5154185be Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 17 Aug 2023 17:51:18 +0200 Subject: [PATCH 24/42] removed moveItems permission as it can only be used by owner --- src/app/middleware/resource-sharing.middleware.ts | 3 --- src/app/routes/storage.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index 48243344..6a0b9af9 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -11,7 +11,6 @@ type Middleware = (req: Request & { behalfUser?: User }, res: Response, next: Ne enum Actions { UPLOAD_FILE = 'UPLOAD_FILE', RENAME_ITEMS = 'RENAME_ITEMS', - MOVE_ITEMS = 'MOVE_ITEMS', } const build = ( @@ -81,8 +80,6 @@ const build = ( UploadFile: mdBuilder(Actions.UPLOAD_FILE), UploadThumbnail: mdBuilder(Actions.UPLOAD_FILE), RenameFile: mdBuilder(Actions.RENAME_ITEMS), - MoveFile: mdBuilder(Actions.MOVE_ITEMS), - MoveFolder: mdBuilder(Actions.MOVE_ITEMS), }; }; diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 59c31804..b27978bf 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -747,7 +747,7 @@ export default (router: Router, service: any) => { router.get('/storage/tree', passportAuth, controller.getTree.bind(controller)); router.get('/storage/tree/:folderId', passportAuth, controller.getTreeSpecific.bind(controller)); router.delete('/storage/folder/:id', passportAuth, sharedAdapter, controller.deleteFolder.bind(controller)); - router.post('/storage/move/folder', passportAuth, resourceSharingAdapter.MoveFolder, sharedAdapter, controller.moveFolder.bind(controller)); + router.post('/storage/move/folder', passportAuth, sharedAdapter, controller.moveFolder.bind(controller)); router.post('/storage/folder/:folderid/meta', passportAuth, sharedAdapter, controller.updateFolder.bind(controller)); router.get( '/storage/v2/folder/:id/:idTeam?', @@ -756,7 +756,7 @@ export default (router: Router, service: any) => { teamsAdapter, controller.getFolderContents.bind(controller), ); - router.post('/storage/move/file', passportAuth, resourceSharingAdapter.MoveFile, sharedAdapter, controller.moveFile.bind(controller)); + router.post('/storage/move/file', passportAuth, sharedAdapter, controller.moveFile.bind(controller)); router.post( '/storage/file/:fileid/meta', passportAuth, From c03fdc4660c0d2337082b47aad984aacb4f9f658 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 17 Aug 2023 18:21:49 +0200 Subject: [PATCH 25/42] fix afs permissions check --- src/app/services/privateSharing.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js index 58350f0c..47c01c8c 100644 --- a/src/app/services/privateSharing.js +++ b/src/app/services/privateSharing.js @@ -23,7 +23,7 @@ module.exports = (Model) => { } }); - if (!privateFolderRole) { + if (!privateFolderRole || !privateFolderRole.roleId) { throw new PrivateSharingFolderRoleNotFound(); } @@ -43,7 +43,12 @@ module.exports = (Model) => { const CanUserPerformAction = async (sharedWith, resourceId, action) => { const permissions = await FindUserPermissionsInsidePrivateSharing(sharedWith.uuid, resourceId); - return permissions.includes(action); + for (const permission of permissions) { + if (permission.type === action) { + return true; + } + } + return false; }; return { From 70d0a03c1ca2c39e03b1e04aa1da8167efedaff0 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 18 Aug 2023 13:30:16 +0200 Subject: [PATCH 26/42] fix(private-sharing): ensure behalfUser is always set on middleware --- src/app/middleware/resource-sharing.middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index 6a0b9af9..ec8d05b7 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -31,6 +31,7 @@ const build = ( try { const resourcesToken = req.headers['internxt-resources-token']; const { user: requester } = (req as AuthorizedUser); + req.behalfUser = requester; if (!resourcesToken || typeof resourcesToken !== 'string') { return next(); @@ -48,7 +49,6 @@ const build = ( } if (decoded.owner.uuid === requester.uuid) { - req.behalfUser = requester; return next(); } From 34bf017a4b0107712418297d8b5053aa5484c204 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Fri, 18 Aug 2023 14:12:43 +0200 Subject: [PATCH 27/42] fix(private-sharing): make middleware work with shared workspace adapter --- src/app/middleware/resource-sharing.middleware.ts | 4 ++-- src/app/routes/storage.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts index ec8d05b7..bebd5613 100644 --- a/src/app/middleware/resource-sharing.middleware.ts +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -30,8 +30,8 @@ const build = ( const mdBuilder = (action: Actions) => (async (req, res, next) => { try { const resourcesToken = req.headers['internxt-resources-token']; - const { user: requester } = (req as AuthorizedUser); - req.behalfUser = requester; + const requester = (req as any).behalfUser || (req as AuthorizedUser).user; + req.behalfUser = requester; if (!resourcesToken || typeof resourcesToken !== 'string') { return next(); diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index b27978bf..c336630e 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -733,14 +733,14 @@ export default (router: Router, service: any) => { router.post('/storage/file', passportAuth, - resourceSharingAdapter.UploadFile, sharedAdapter, + resourceSharingAdapter.UploadFile, controller.createFile.bind(controller) ); router.post('/storage/thumbnail', passportAuth, - resourceSharingAdapter.UploadThumbnail, sharedAdapter, + resourceSharingAdapter.UploadThumbnail, controller.createThumbnail.bind(controller) ); router.post('/storage/folder', passportAuth, sharedAdapter, controller.createFolder.bind(controller)); @@ -760,8 +760,8 @@ export default (router: Router, service: any) => { router.post( '/storage/file/:fileid/meta', passportAuth, - resourceSharingAdapter.RenameFile, sharedAdapter, + resourceSharingAdapter.RenameFile, controller.updateFile.bind(controller) ); router.delete('/storage/bucket/:bucketid/file/:fileid', passportAuth, controller.deleteFileBridge.bind(controller)); From 9ea9ef9a7d54450782369eb80129da13000fa519 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 12:12:21 +0200 Subject: [PATCH 28/42] refactor(sharing): change "type" by "name" on permissions --- src/app/models/permissions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/models/permissions.ts b/src/app/models/permissions.ts index 906a3fca..5e2e2c99 100644 --- a/src/app/models/permissions.ts +++ b/src/app/models/permissions.ts @@ -2,7 +2,7 @@ import { Sequelize, DataTypes, ModelDefined } from 'sequelize'; interface PermissionsAttributes { id: string; - type: string; + name: string; roleId: string; createdAt: Date; updatedAt: Date; @@ -22,13 +22,17 @@ export default (database: Sequelize): PermissionModel => { primaryKey: true, allowNull: false }, - type: { + name: { type: DataTypes.STRING, allowNull: false, }, roleId: { type: DataTypes.UUIDV4, allowNull: false, + references: { + model: 'roles', + key: 'id', + } }, }, { From 9eb3cd3fd2716a501460742cda5f53e90e8abd00 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 12:13:03 +0200 Subject: [PATCH 29/42] refactor(sharing): change "role" by "name" on roles --- src/app/models/roles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/models/roles.ts b/src/app/models/roles.ts index c7b9bfc9..12fac192 100644 --- a/src/app/models/roles.ts +++ b/src/app/models/roles.ts @@ -2,7 +2,7 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; interface RoleAttributes { id: string; - role: string; + name: string; createdAt: Date; updatedAt: Date; } @@ -21,7 +21,7 @@ export default (database: Sequelize): RoleModel => { primaryKey: true, allowNull: false }, - role: { + name: { type: DataTypes.STRING, allowNull: false, }, From 82b2723429076e05d4e39382c611f934262ce41c Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 13:10:42 +0200 Subject: [PATCH 30/42] feat(sharing): add new models --- src/app/models/index.ts | 31 +++++++++++-- src/app/models/sharingInvites.ts | 74 ++++++++++++++++++++++++++++++++ src/app/models/sharingRoles.ts | 47 ++++++++++++++++++++ src/app/models/sharings.ts | 68 +++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/app/models/sharingInvites.ts create mode 100644 src/app/models/sharingRoles.ts create mode 100644 src/app/models/sharings.ts diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 107602da..d03f2f5e 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -22,6 +22,10 @@ import initRole, { RoleModel } from './roles'; import initPermission, { PermissionModel } from './permissions'; import initPrivateSharingFolder, { PrivateSharingFolderModel } from './privateSharingFolder'; import initPrivateSharingFolderRole, { PrivateSharingFolderRoleModel } from './privateSharingFolderRole'; +import initSharings, { SharingsModel } from './sharings'; +import initSharingInvites, { SharingInvitesModel } from './sharingInvites'; +import initSharingRoles, { SharingRolesModel } from './sharingRoles'; + export type ModelType = | AppSumoModel @@ -45,7 +49,10 @@ export type ModelType = | RoleModel | PermissionModel | PrivateSharingFolderModel - | PrivateSharingFolderRoleModel; + | PrivateSharingFolderRoleModel + | SharingsModel + | SharingInvitesModel + | SharingRolesModel; export default (database: Sequelize) => { const AppSumo = initAppSumo(database); @@ -70,6 +77,9 @@ export default (database: Sequelize) => { const Permission = initPermission(database); const PrivateSharingFolder = initPrivateSharingFolder(database); const PrivateSharingFolderRole = initPrivateSharingFolderRole(database); + const Sharings = initSharings(database); + const SharingInvites = initSharingInvites(database); + const SharingRoles = initSharingRoles(database); AppSumo.belongsTo(User); @@ -118,6 +128,8 @@ export default (database: Sequelize) => { User.hasMany(PrivateSharingFolderRole, { foreignKey: 'user_id', sourceKey: 'uuid' }); User.hasMany(PrivateSharingFolder, { foreignKey: 'owner_id', sourceKey: 'uuid' }); User.hasMany(PrivateSharingFolder, { foreignKey: 'shared_with', sourceKey: 'uuid' }); + User.hasMany(Sharings, { foreignKey: 'owner_id', sourceKey: 'uuid' }); + User.hasMany(Sharings, { foreignKey: 'shared_with', sourceKey: 'uuid' }); UserReferral.belongsTo(User, { foreignKey: 'user_id' }); UserReferral.belongsTo(Referral, { foreignKey: 'referral_id' }); @@ -125,13 +137,23 @@ export default (database: Sequelize) => { Role.hasMany(Permission, { foreignKey: 'role_id', sourceKey: 'id' }); Role.hasMany(PrivateSharingFolderRole, { foreignKey: 'role_id', sourceKey: 'id' }); - - Permission.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + Role.hasMany(SharingRoles, { foreignKey: 'role_id', sourceKey: 'id' }); + SharingInvites.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); PrivateSharingFolderRole.belongsTo(Folder, { foreignKey: 'folder_id', targetKey: 'uuid' }); PrivateSharingFolderRole.belongsTo(User, { foreignKey: 'user_id', targetKey: 'uuid' }); PrivateSharingFolderRole.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + SharingRoles.belongsTo(Sharings, { foreignKey: 'sharing_id', targetKey: 'id' }); + SharingRoles.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + + Permission.belongsTo(Role, { foreignKey: 'role_id', targetKey: 'id' }); + + Sharings.belongsTo(User, { foreignKey: 'owner_id', targetKey: 'uuid' }); + Sharings.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); + + SharingInvites.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); + PrivateSharingFolder.belongsTo(Folder, { foreignKey: 'folder_id', targetKey: 'uuid' }); PrivateSharingFolder.belongsTo(User, { foreignKey: 'owner_id', targetKey: 'uuid' }); PrivateSharingFolder.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); @@ -159,5 +181,8 @@ export default (database: Sequelize) => { [Permission.name]: Permission, [PrivateSharingFolder.name]: PrivateSharingFolder, [PrivateSharingFolderRole.name]: PrivateSharingFolderRole, + [Sharings.name]: Sharings, + [SharingInvites.name]: SharingInvites, + [SharingRoles.name]: SharingRoles, }; }; diff --git a/src/app/models/sharingInvites.ts b/src/app/models/sharingInvites.ts new file mode 100644 index 00000000..1bc25237 --- /dev/null +++ b/src/app/models/sharingInvites.ts @@ -0,0 +1,74 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +export interface SharingInviteAttributes { + id: string; + itemId: string; + itemType: 'file' | 'folder'; + ownerId: string; + sharedWith: string + encryptionKey: string; + encryptionAlgorithm: string; + createdAt: Date; + updatedAt: Date; + type: 'SELF' | 'OWNER'; + roleId: string +} + +export type SharingInvitesModel = ModelDefined; + +export default (database: Sequelize): SharingInvitesModel => { + const SharingInvites: SharingInvitesModel = database.define( + 'sharingInvites', + { + id: { + type: DataTypes.UUIDV4, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + itemId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + itemType: { + type: DataTypes.STRING, + allowNull: false + }, + sharedWith: { + type: DataTypes.STRING(36), + allowNull: false, + references: { + model: 'users', + key: 'uuid' + } + }, + encryptionKey: { + type: DataTypes.STRING(800), + allowNull: false + }, + encryptionAlgorithm: { + type: DataTypes.STRING, + allowNull: false, + }, + roleId: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: 'roles', + key: 'id' + } + }, + type: { + type: DataTypes.STRING, + allowNull: false + }, + }, + { + tableName: 'sharing_invites', + underscored: true, + timestamps: true + }, + ); + + return SharingInvites; +}; diff --git a/src/app/models/sharingRoles.ts b/src/app/models/sharingRoles.ts new file mode 100644 index 00000000..9fb5f738 --- /dev/null +++ b/src/app/models/sharingRoles.ts @@ -0,0 +1,47 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +export interface SharingRoleAttributes { + id: string; + sharingId: string; + roleId: string; + createdAt: Date; + updatedAt: Date; +} +export type SharingRolesModel = ModelDefined; + +export default (database: Sequelize): SharingRolesModel => { + const SharingRoles: SharingRolesModel = database.define( + 'sharingRoles', + { + id: { + type: DataTypes.UUIDV4, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + sharingId: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: 'sharings', + key: 'id' + }, + }, + roleId: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: 'roles', + key: 'id' + } + }, + }, + { + tableName: 'sharing_roles', + underscored: true, + timestamps: true + }, + ); + + return SharingRoles; +}; diff --git a/src/app/models/sharings.ts b/src/app/models/sharings.ts new file mode 100644 index 00000000..eb96f811 --- /dev/null +++ b/src/app/models/sharings.ts @@ -0,0 +1,68 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +export interface SharingAttributes { + id: string; + itemId: string; + itemType: 'file' | 'folder'; + ownerId: string; + sharedWith: string; + encryptionKey: string; + encryptionAlgorithm: string; + createdAt: Date; + updatedAt: Date; +} + +export type SharingsModel = ModelDefined; + +export default (database: Sequelize): SharingsModel => { + const Sharings: SharingsModel = database.define( + 'sharings', + { + id: { + type: DataTypes.UUIDV4, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + itemId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + itemType: { + type: DataTypes.STRING, + allowNull: false + }, + ownerId: { + type: DataTypes.STRING(36), + allowNull: false, + references: { + model: 'users', + key: 'uuid' + } + }, + sharedWith: { + type: DataTypes.STRING(36), + allowNull: false, + references: { + model: 'users', + key: 'uuid' + } + }, + encryptionKey: { + type: DataTypes.STRING(800), + allowNull: false + }, + encryptionAlgorithm: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'sharings', + underscored: true, + timestamps: true + }, + ); + + return Sharings; +}; From 0119b1506097fccd3ffe5cad78e2861e4a6aef9d Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 13:12:11 +0200 Subject: [PATCH 31/42] feat(sharing): adapt permissions middleware helper --- src/app/services/privateSharing.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js index 47c01c8c..fb4c33c7 100644 --- a/src/app/services/privateSharing.js +++ b/src/app/services/privateSharing.js @@ -15,22 +15,22 @@ class PrivateSharingFolderPermissionsNotFound extends Error { } module.exports = (Model) => { - const FindUserPermissionsInsidePrivateSharing = async (sharedWithId, folderId) => { - const privateFolderRole = await Model.privateSharingFolderRole.findOne({ - where: { - userId: sharedWithId, - folderId, - } - }); - - if (!privateFolderRole || !privateFolderRole.roleId) { - throw new PrivateSharingFolderRoleNotFound(); - } - + const FindUserPermissionsInsidePrivateSharing = async (sharedWithId, itemId) => { const permissions = await Model.permissions.findAll({ - where: { - roleId: privateFolderRole.roleId - } + include: [ + { + model: Model.sharingRoles, + include: [ + { + model: Model.sharings, + where: { + sharedWith: sharedWithId, + itemId, + } + } + ] + } + ] }); if (!permissions) { From 4ee8987b005c2f5df677a49241504a7b700585cd Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 13:57:53 +0200 Subject: [PATCH 32/42] fix(sharing): explicitly that sharings have roles --- src/app/models/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/models/index.ts b/src/app/models/index.ts index d03f2f5e..661c333f 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -151,6 +151,7 @@ export default (database: Sequelize) => { Sharings.belongsTo(User, { foreignKey: 'owner_id', targetKey: 'uuid' }); Sharings.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); + Sharings.hasMany(SharingRoles, { foreignKey: 'sharing_id', sourceKey: 'id' }); SharingInvites.belongsTo(User, { foreignKey: 'shared_with', targetKey: 'uuid' }); From 94fb70618582d3e485dda3ada9e4f5020e4df0dd Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 29 Aug 2023 13:58:48 +0200 Subject: [PATCH 33/42] fix(sharing): obtain permissions when finding checking the access to a resource --- src/app/services/privateSharing.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js index fb4c33c7..f2ca1efa 100644 --- a/src/app/services/privateSharing.js +++ b/src/app/services/privateSharing.js @@ -19,14 +19,19 @@ module.exports = (Model) => { const permissions = await Model.permissions.findAll({ include: [ { - model: Model.sharingRoles, + model: Model.roles, include: [ { - model: Model.sharings, - where: { - sharedWith: sharedWithId, - itemId, - } + model: Model.sharingRoles, + include: [ + { + model: Model.sharings, + where: { + sharedWith: sharedWithId, + itemId, + } + } + ] } ] } @@ -37,14 +42,14 @@ module.exports = (Model) => { throw new PrivateSharingFolderPermissionsNotFound(); } - return permissions; + return permissions.map(p => p.get({ plain: true })); }; const CanUserPerformAction = async (sharedWith, resourceId, action) => { const permissions = await FindUserPermissionsInsidePrivateSharing(sharedWith.uuid, resourceId); for (const permission of permissions) { - if (permission.type === action) { + if (permission.name === action) { return true; } } From 549d57a660370b9cc27041245845290dc4086966 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Mon, 4 Sep 2023 18:40:51 +0200 Subject: [PATCH 34/42] feat(storage): improve efficiency of usage query --- src/app/services/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/services/user.js b/src/app/services/user.js index 0214acb4..ffe66b4b 100644 --- a/src/app/services/user.js +++ b/src/app/services/user.js @@ -501,7 +501,7 @@ module.exports = (Model, App) => { const getUsage = async (user) => { const targetUser = await Model.users.findOne({ where: { username: user.bridgeUser } }); const usage = await Model.file.findAll({ - where: { user_id: targetUser.id, removed: false }, + where: { user_id: targetUser.id, status: 'EXISTS' }, attributes: [[fn('sum', col('size')), 'total']], raw: true, }); From 93a151e991c24e07efcd9e023faf34ffc3b5eb50 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 5 Sep 2023 14:34:41 +0200 Subject: [PATCH 35/42] refactor(file): stop using deleted, removed fields --- src/app/services/files.js | 8 +++----- src/app/services/folder.js | 9 ++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/services/files.js b/src/app/services/files.js index b1725843..9ab168cc 100644 --- a/src/app/services/files.js +++ b/src/app/services/files.js @@ -36,7 +36,7 @@ module.exports = (Model, App) => { folder_id: { [Op.eq]: folder.id }, type: { [Op.eq]: file.type }, userId: { [Op.eq]: user.id }, - deleted: { [Op.eq]: false }, + status: { [Op.eq]: 'EXISTS' }, }, }); @@ -206,7 +206,7 @@ module.exports = (Model, App) => { folder_id: { [Op.eq]: file.folder_id }, name: { [Op.eq]: cryptoFileName }, type: { [Op.eq]: file.type }, - deleted: { [Op.eq]: false }, + status: { [Op.eq]: 'EXISTS' }, }, }) .then((duplicateFile) => { @@ -256,7 +256,7 @@ module.exports = (Model, App) => { folder_id: { [Op.eq]: destination }, type: { [Op.eq]: file.type }, fileId: { [Op.ne]: fileId }, - deleted: { [Op.eq]: false }, + status: { [Op.eq]: 'EXISTS' }, }, }); @@ -270,8 +270,6 @@ module.exports = (Model, App) => { folder_id: parseInt(destination, 10), folderUuid: folderTarget.uuid, name: destinationName, - deleted: false, - deletedAt: null, status: 'EXISTS', }); diff --git a/src/app/services/folder.js b/src/app/services/folder.js index 1cea2091..38f87a02 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -281,7 +281,11 @@ module.exports = (Model, App) => { }); const foldersId = folders.map((result) => result.id); const files = await Model.file.findAll({ - where: { folder_id: { [Op.in]: foldersId }, userId: userObject.id, deleted: filterOptions.deleted || false }, + where: { + folder_id: { [Op.in]: foldersId }, + userId: userObject.id, + status: filterOptions.deleted ? 'TRASHED' : 'EXISTS' + }, include: [ { model: Model.thumbnail, @@ -334,8 +338,7 @@ module.exports = (Model, App) => { where: { folder_id: { [Op.in]: foldersId }, userId: userObject.id, - deleted: filterOptions.deleted || false, - removed: false, + status: filterOptions.deleted ? 'TRASHED' : 'EXISTS', }, }); From 02c43a0cd88d50bda6028c8b330ac3ed7fa08b47 Mon Sep 17 00:00:00 2001 From: Pixo Date: Mon, 11 Sep 2023 17:13:22 +0200 Subject: [PATCH 36/42] [_]: Add info of the user that is creating a file if it fails --- src/app/routes/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index c336630e..4fbaac37 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -87,7 +87,7 @@ export class StorageController { ); } catch (err) { this.logger.error( - `[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack}`, + `[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack} USER: ${behalfUser.email}`, ); res.status(500).send({ error: 'Internal Server Error' }); } From 3fc73c03be531be8d9566095b55423fc71e5e016 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Mon, 25 Sep 2023 18:23:32 +0200 Subject: [PATCH 37/42] fix(files): return status code 409 on creation when it already exists --- src/app/routes/storage.ts | 8 +++++++- src/app/services/errors/FileWithNameAlreadyExistsError.ts | 8 ++++++++ src/app/services/files.js | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 4fbaac37..7b452920 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -12,7 +12,10 @@ import { FileAttributes } from '../models/file'; import CONSTANTS from '../constants'; import { LockNotAvaliableError } from '../services/errors/locks'; import { ConnectionTimedOutError } from 'sequelize'; -import { FileWithNameAlreadyExistsError } from '../services/errors/FileWithNameAlreadyExistsError'; +import { + FileAlreadyExistsError, + FileWithNameAlreadyExistsError +} from '../services/errors/FileWithNameAlreadyExistsError'; import { FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware'; @@ -86,6 +89,9 @@ export class StorageController { }), ); } catch (err) { + if (err instanceof FileAlreadyExistsError) { + return res.status(409).send({ error: err.message }); + } this.logger.error( `[FILE/CREATE] ERROR: ${(err as Error).message}, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack} USER: ${behalfUser.email}`, ); diff --git a/src/app/services/errors/FileWithNameAlreadyExistsError.ts b/src/app/services/errors/FileWithNameAlreadyExistsError.ts index c4d8d1b5..1f3a44e1 100644 --- a/src/app/services/errors/FileWithNameAlreadyExistsError.ts +++ b/src/app/services/errors/FileWithNameAlreadyExistsError.ts @@ -5,3 +5,11 @@ export class FileWithNameAlreadyExistsError extends Error { Object.setPrototypeOf(this, FileWithNameAlreadyExistsError.prototype); } } + +export class FileAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, FileAlreadyExistsError.prototype); + } +} diff --git a/src/app/services/files.js b/src/app/services/files.js index 9ab168cc..1b3311d9 100644 --- a/src/app/services/files.js +++ b/src/app/services/files.js @@ -3,7 +3,7 @@ const async = require('async'); const createHttpError = require('http-errors'); const AesUtil = require('../../lib/AesUtil'); const { v4 } = require('uuid'); -const { FileWithNameAlreadyExistsError } = require('./errors/FileWithNameAlreadyExistsError'); +const { FileWithNameAlreadyExistsError, FileAlreadyExistsError } = require('./errors/FileWithNameAlreadyExistsError'); // Filenames that contain "/", "\" or only spaces are invalid const invalidName = /[/\\]|^\s*$/; @@ -43,7 +43,7 @@ module.exports = (Model, App) => { const fileAlreadyExists = !!maybeAlreadyExistentFile; if (fileAlreadyExists) { - throw new Error('File already exists'); + throw new FileAlreadyExistsError('File already exists'); } const fileInfo = { From fd0f482178bd67c75dad337e5aeb633b149cdb96 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Mon, 25 Sep 2023 18:26:19 +0200 Subject: [PATCH 38/42] fix(folder): return status code 409 on creation when already exists --- src/app/routes/storage.ts | 5 ++++- .../services/errors/FolderWithNameAlreadyExistsError.ts | 8 ++++++++ src/app/services/folder.js | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 7b452920..296cd564 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -16,7 +16,7 @@ import { FileAlreadyExistsError, FileWithNameAlreadyExistsError } from '../services/errors/FileWithNameAlreadyExistsError'; -import { FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; +import { FolderAlreadyExistsError, FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware'; type AuthorizedRequest = Request & { user: UserAttributes }; @@ -138,6 +138,9 @@ export class StorageController { ); }) .catch((err: Error) => { + if (err instanceof FolderAlreadyExistsError) { + return res.status(409).send({ error: err.message }); + } this.logger.error(`Error creating folder for user ${user.id}: ${err}`); res.status(500).send(); }); diff --git a/src/app/services/errors/FolderWithNameAlreadyExistsError.ts b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts index 09284dd6..08b41b53 100644 --- a/src/app/services/errors/FolderWithNameAlreadyExistsError.ts +++ b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts @@ -5,3 +5,11 @@ export class FolderWithNameAlreadyExistsError extends Error { Object.setPrototypeOf(this, FolderWithNameAlreadyExistsError.prototype); } } + +export class FolderAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, FolderAlreadyExistsError.prototype); + } +} diff --git a/src/app/services/folder.js b/src/app/services/folder.js index 38f87a02..b94f98e5 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -7,7 +7,7 @@ const logger = require('../../lib/logger').default.getInstance(); const { default: Redis } = require('../../config/initializers/redis'); import { v4 } from 'uuid'; import { LockNotAvaliableError } from './errors/locks'; -import { FolderWithNameAlreadyExistsError } from './errors/FolderWithNameAlreadyExistsError'; +import { FolderAlreadyExistsError, FolderWithNameAlreadyExistsError } from './errors/FolderWithNameAlreadyExistsError'; const invalidName = /[\\/]|^\s*$/; @@ -109,7 +109,7 @@ module.exports = (Model, App) => { // TODO: If the folder already exists, // return the folder data to make desktop // incorporate new info to its database - throw Error('Folder with the same name already exists'); + throw new FolderAlreadyExistsError('Folder with the same name already exists'); } // Since we upload everything in the same bucket, this line is no longer needed From a9bb4b8b65df521a2f6fab31356167d4cf7699e8 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 26 Sep 2023 13:56:40 +0200 Subject: [PATCH 39/42] feat(file): check existence EP --- src/app/routes/storage.ts | 41 ++++++++++++++++++++++++++++++++++++ src/app/services/files.js | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 296cd564..abee64be 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -99,6 +99,42 @@ export class StorageController { } } + public async checkFileExistence(req: Request, res: Response) { + const { behalfUser } = req as SharedRequest; + const { file } = req.body as { file: { name: string; folderId: number; type: string } }; + + if ( + !file || + !file.name || + !file.folderId || + !file.type + ) { + this.logger.error( + `Missing body params to check the file existence for user ${ + behalfUser.email + }: ${JSON.stringify(file, null, 2)}`, + ); + return res.status(400).json({ error: 'Missing information to check file existence' }); + } + + try { + const result = await this.services.Files.CheckFileExistence(behalfUser, file); + + if (!result.exists) { + return res.status(404).send(); + } + + res.status(200).json(result.file); + } catch (err) { + this.logger.error( + `[FILE/CHECK-EXISTENCE] ERROR: ${ + (err as Error).message + }, BODY ${JSON.stringify(file)}, STACK: ${(err as Error).stack} USER: ${behalfUser.email}`, + ); + res.status(500).send({ error: 'Internal Server Error' }); + } + } + public async createFolder(req: Request, res: Response): Promise { const { folderName, parentFolderId } = req.body; const { behalfUser: user } = req as any; @@ -746,6 +782,11 @@ export default (router: Router, service: any) => { resourceSharingAdapter.UploadFile, controller.createFile.bind(controller) ); + router.post('/storage/file/exists', + passportAuth, + sharedAdapter, + controller.checkFileExistence.bind(controller) + ); router.post('/storage/thumbnail', passportAuth, sharedAdapter, diff --git a/src/app/services/files.js b/src/app/services/files.js index 1b3311d9..42fa0e38 100644 --- a/src/app/services/files.js +++ b/src/app/services/files.js @@ -13,6 +13,49 @@ const { Op } = sequelize; module.exports = (Model, App) => { const log = App.logger; + const CheckFileExistence = async (user, file) => { + const maybeAlreadyExistentFile = await Model.file.findOne({ + where: { + name: { [Op.eq]: file.name }, + folder_id: { [Op.eq]: file.folderId }, + type: { [Op.eq]: file.type }, + userId: { [Op.eq]: user.id }, + status: { [Op.eq]: 'EXISTS' }, + }, + }); + + const fileExists = !!maybeAlreadyExistentFile; + + if (!fileExists) { + return { exists: false, file: null }; + } + + const fileInfo = { + name: file.name, + plainName: file.plain_name, + type: file.type, + size: file.size, + folderId: file.folder_id, + fileId: file.fileId, + bucket: file.bucket, + userId: user.id, + uuid: v4(), + modificationTime: file.modificationTime || new Date(), + }; + + try { + fileInfo.plainName = fileInfo.plainName ?? AesUtil.decrypt(file.name, file.fileId); + } catch { + // eslint-disable-next-line no-empty + } + + if (file.date) { + fileInfo.createdAt = file.date; + } + + return { exists: true, file: fileInfo }; + }; + const CreateFile = async (user, file) => { const folder = await Model.folder.findOne({ where: { @@ -405,6 +448,7 @@ module.exports = (Model, App) => { return { Name: 'Files', CreateFile, + CheckFileExistence, Delete, DeleteFile, UpdateMetadata, From 66c579eb7b61298155dc22fa4e2ba4fcffb015e2 Mon Sep 17 00:00:00 2001 From: Sergio Gutierrez Villalba Date: Tue, 26 Sep 2023 14:08:43 +0200 Subject: [PATCH 40/42] feat(folder): check existence EP --- src/app/routes/storage.ts | 27 ++++++++++++++++++++++ src/app/services/folder.js | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index abee64be..58061197 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -182,6 +182,32 @@ export class StorageController { }); } + public async checkFolderExistence(req: Request, res: Response): Promise { + const { name, parentId } = req.body; + const { behalfUser: user } = req as any; + + if (Validator.isInvalidString(name)) { + throw createHttpError(400, 'Folder name must be a valid string'); + } + + if (!parentId || parentId <= 0) { + throw createHttpError(400, 'Invalid parent folder id'); + } + + return this.services.Folder.CheckFolderExistence(user, name, parentId) + .then((result: { folder: FolderAttributes, exists: boolean }) => { + if (result.exists) { + res.status(200).json(result); + } else { + res.status(404).send(); + } + }) + .catch((err: Error) => { + this.logger.error(`Error checking folder existence for user ${user.id}: ${err}`); + res.status(500).send(); + }); + } + public async getTree(req: Request, res: Response): Promise { const { user } = req as PassportRequest; const deleted = req.query?.trash === 'true'; @@ -794,6 +820,7 @@ export default (router: Router, service: any) => { controller.createThumbnail.bind(controller) ); router.post('/storage/folder', passportAuth, sharedAdapter, controller.createFolder.bind(controller)); + router.post('/storage/folder/exists', passportAuth, sharedAdapter, controller.checkFolderExistence.bind(controller)); router.get('/storage/tree', passportAuth, controller.getTree.bind(controller)); router.get('/storage/tree/:folderId', passportAuth, controller.getTreeSpecific.bind(controller)); router.delete('/storage/folder/:id', passportAuth, sharedAdapter, controller.deleteFolder.bind(controller)); diff --git a/src/app/services/folder.js b/src/app/services/folder.js index b94f98e5..2f712670 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -127,6 +127,51 @@ module.exports = (Model, App) => { return folder; }; + const CheckFolderExistence = async (user, folderName, parentFolderId) => { + if (parentFolderId >= 2147483648) { + throw Error('Invalid parent folder'); + } + const isGuest = user.email !== user.bridgeUser; + + if (isGuest) { + const { bridgeUser } = user; + + user = await Model.users.findOne({ + where: { username: bridgeUser }, + }); + } + + const parentFolder = await Model.folder.findOne({ + id: { [Op.eq]: parentFolderId }, + user_id: { [Op.eq]: user.id }, + }); + + if (!parentFolder) { + throw Error('Parent folder is not yours'); + } + + if (folderName === '' || invalidName.test(folderName)) { + throw Error('Invalid folder name'); + } + + // Encrypt folder name, TODO: use versioning for encryption + const cryptoFolderName = App.services.Crypt.encryptName(folderName, parentFolderId); + + const maybeExistentFolder = await Model.folder.findOne({ + where: { + parentId: { [Op.eq]: parentFolderId }, + name: { [Op.eq]: cryptoFolderName }, + deleted: { [Op.eq]: false }, + }, + }); + + if (maybeExistentFolder) { + return { exists: true, folder: maybeExistentFolder }; + } + + return { exists: false, folder: null }; + }; + // Requires stored procedure const DeleteOrphanFolders = async (userId) => { const clear = await App.database.query('CALL clear_orphan_folders_by_user (:userId, :output)', { @@ -772,6 +817,7 @@ module.exports = (Model, App) => { Name: 'Folder', getById, Create, + CheckFolderExistence, Delete, GetChildren, GetTree, From d79f0a7aad0f6a00aee5f148c11a20167568708c Mon Sep 17 00:00:00 2001 From: joan vicens Date: Mon, 2 Oct 2023 14:25:21 +0200 Subject: [PATCH 41/42] chore: accept folder uuid from clients --- src/app/routes/storage.ts | 11 +++++++++-- src/app/services/folder.js | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 58061197..69ed90cc 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -18,6 +18,7 @@ import { } from '../services/errors/FileWithNameAlreadyExistsError'; import { FolderAlreadyExistsError, FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware'; +import {validate } from 'uuid'; type AuthorizedRequest = Request & { user: UserAttributes }; interface Services { @@ -136,7 +137,8 @@ export class StorageController { } public async createFolder(req: Request, res: Response): Promise { - const { folderName, parentFolderId } = req.body; + this.logger.error(`A ${validate}`); + const { folderName, parentFolderId, uuid: clientCreatedUuuid } = req.body; const { behalfUser: user } = req as any; if (Validator.isInvalidString(folderName)) { @@ -147,6 +149,10 @@ export class StorageController { throw createHttpError(400, 'Invalid parent folder id'); } + if (clientCreatedUuuid && !validate(clientCreatedUuuid)) { + throw createHttpError(400, 'Invalid uuid'); + } + const clientId = String(req.headers['internxt-client-id']); const parentFolder = await this.services.Folder.getById(parentFolderId); @@ -158,8 +164,9 @@ export class StorageController { if (parentFolder.userId !== user.id) { throw createHttpError(403, 'Parent folder does not belong to user'); } + - return this.services.Folder.Create(user, folderName, parentFolderId) + return this.services.Folder.Create(user, folderName, parentFolderId, null, clientCreatedUuuid) .then(async (result: FolderAttributes) => { res.status(201).json(result); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); diff --git a/src/app/services/folder.js b/src/app/services/folder.js index 2f712670..cf80eda6 100644 --- a/src/app/services/folder.js +++ b/src/app/services/folder.js @@ -56,7 +56,7 @@ module.exports = (Model, App) => { }; // Create folder entry, for desktop - const Create = async (user, folderName, parentFolderId, teamId = null) => { + const Create = async (user, folderName, parentFolderId, teamId = null, uuid = null) => { if (parentFolderId >= 2147483648) { throw Error('Invalid parent folder'); } @@ -117,7 +117,7 @@ module.exports = (Model, App) => { const folder = await user.createFolder({ name: cryptoFolderName, plain_name: folderName, - uuid: v4(), + uuid: uuid || v4(), bucket: null, parentId: parentFolderId || null, parentUuid: parentFolder.uuid, From a1abd1acd5df0b9b21342cd794cc5c4591f9a35c Mon Sep 17 00:00:00 2001 From: joan vicens Date: Tue, 3 Oct 2023 10:46:13 +0200 Subject: [PATCH 42/42] fix: remove unnecessary log --- src/app/routes/storage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 69ed90cc..9542e468 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -137,7 +137,6 @@ export class StorageController { } public async createFolder(req: Request, res: Response): Promise { - this.logger.error(`A ${validate}`); const { folderName, parentFolderId, uuid: clientCreatedUuuid } = req.body; const { behalfUser: user } = req as any;