diff --git a/.env.template b/.env.template index 68828aed..b8fe59db 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,7 @@ APP_SEGMENT_KEY=LT12AvUt4u1NgNA751gYXdJ1Dvf0dmqx AVATAR_ACCESS_KEY=internxt AVATAR_BUCKET=avatars -AVATAR_ENDPOINT=http://s3:9000 +AVATAR_ENDPOINT=http://network-storage:9000 AVATAR_ENDPOINT_REWRITE_FOR_SIGNED_URLS=http://localhost:9000 AVATAR_FORCE_PATH_STYLE=true AVATAR_REGION= @@ -20,10 +20,10 @@ JWT_SECRET=38FTANE5LY90NHYZ MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023 MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e NODE_ENV=development -NOTIFICATIONS_URL=http://localhost:3000 +NOTIFICATIONS_URL=http://notifications:4000 NOTIFICATIONS_API_KEY=secret RDS_DBNAME=xCloud -RDS_HOSTNAME=postgres +RDS_HOSTNAME=drive-database RDS_PASSWORD=example RDS_PORT=5432 RDS_USERNAME=postgres @@ -33,8 +33,8 @@ SENDGRID_NAME= DRIVE_VERIFY_ACCOUNT_TEMPLATE_ID= DRIVE_INVITE_FRIEND_TEMPLATE_ID= SESSION_KEY=t&jJr8N{D:u6fkFK -STORJ_BRIDGE_HTTPS=http://bridge:6382 -STORJ_BRIDGE=http://bridge:6382 +STORJ_BRIDGE_HTTPS=http://network-api:6382 +STORJ_BRIDGE=http://network-api:6382 STRIPE_SK=sk_test_F3Ny2VGUnPga9FtyXkl7mzPc STRIPE_SK_TEST=sk_test_R3Ny2VG7nPgeJFrtXdt8mrPc SENDGRID_MODE_SANDBOX=false diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index f69aab0d..11a7d1da 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -30,13 +30,6 @@ jobs: file: ./Dockerfile push: true tags: ${{ secrets.DOCKERHUB_USERNAME }}/drive-server:${{ github.sha }} - - name: Build and push to desktop-server - uses: docker/build-push-action@v2 - with: - context: ./ - file: ./Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/desktop-server:${{ github.sha }} deploy: needs: build runs-on: ubuntu-latest @@ -53,18 +46,5 @@ jobs: uses: steebchen/kubectl@v2.0.0 with: config: ${{ secrets.KUBE_CONFIG_DATA }} - version: v1.20.2 # specify kubectl binary version explicitly + version: v1.25.9 # specify kubectl binary version explicitly command: rollout status deployment/drive-server - - - uses: actions/checkout@master - - name: Update desktop-server image - uses: steebchen/kubectl@v2.0.0 - with: - config: ${{ secrets.KUBE_CONFIG_DATA }} - command: set image --record deployment/desktop-server desktop-server=${{ secrets.DOCKERHUB_USERNAME }}/desktop-server:${{ github.sha }} - - name: Verify desktop-server deployment - uses: steebchen/kubectl@v2.0.0 - with: - config: ${{ secrets.KUBE_CONFIG_DATA }} - version: v1.20.2 - command: rollout status deployment/desktop-server diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..5f896a6e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +name: Build +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 1362f3f2..15f85e56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM mhart/alpine-node:14 +FROM node:14 LABEL author="internxt" WORKDIR /drive-server # Add useful packages -RUN apk add git curl +# RUN apk add git curl COPY . . @@ -12,7 +12,7 @@ COPY . . RUN yarn && yarn build && yarn --production && yarn cache clean # Create prometheus directories -RUN mkdir -p /mnt/prometheusvol{1,2} +# RUN mkdir -p /mnt/prometheusvol{1,2} # Start server -CMD node /drive-server/build/app.js \ No newline at end of file +CMD node /drive-server/build/app.js diff --git a/bin/delete-files/index.ts b/bin/delete-files/index.ts index dd808d59..7b71b951 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) => a.id - b.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/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[] } } diff --git a/infrastructure/development.Dockerfile b/infrastructure/development.Dockerfile index 31349167..0a25a468 100644 --- a/infrastructure/development.Dockerfile +++ b/infrastructure/development.Dockerfile @@ -1,4 +1,4 @@ -FROM node:14 +FROM node:16 WORKDIR /usr/app diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml index f92f8802..a4062813 100644 --- a/infrastructure/docker-compose.yml +++ b/infrastructure/docker-compose.yml @@ -101,9 +101,9 @@ services: /usr/bin/mc config host add myminio http://s3:9000 minioadmin minioadmin; /usr/bin/mc mb --ignore-existing myminio/internxt; /usr/bin/mc mb --ignore-existing myminio/avatars; - /usr/bin/mc policy set public myminio/internxt; + /usr/bin/mc policy attach public myminio/internxt; /usr/bin/mc admin user add myminio internxt internxt; - /usr/bin/mc admin policy set myminio readwrite user=internxt; + /usr/bin/mc admin policy attach myminio readwrite --user internxt; exit 0; " networks: @@ -115,7 +115,7 @@ services: dockerfile: infrastructure/development.Dockerfile container_name: drive-server ports: - - 7000:8000 + - 8000:8000 depends_on: - bridge - postgres @@ -161,22 +161,23 @@ services: networks: - internxt - # drive-server-wip: - # build: - # context: ../../drive-server-wip - # dockerfile: development.Dockerfile - # volumes: - # - ../../drive-server-wip:/usr/app - # - ../../drive-server-wip/.env.development:/usr/app/.env.development - # - /usr/app/node_modules - # ports: - # - 3004:3000 - # networks: - # - internxt - # environment: - # NODE_ENV: development - # env_file: - # - ../../drive-server-wip/.env.development + drive-server-wip: + container_name: drive-server-wip + build: + context: ../../drive-server-wip + dockerfile: development.Dockerfile + volumes: + - ../../drive-server-wip:/usr/app + - ../../drive-server-wip/.env.development:/usr/app/.env.development + - /usr/app/node_modules + ports: + - 3004:3004 + networks: + - internxt + environment: + NODE_ENV: development + env_file: + - ../../drive-server-wip/.env.development networks: internxt: diff --git a/migrations/20230302120000-rename-mailType-enum.js b/migrations/20230302120000-rename-mailType-enum.js new file mode 100644 index 00000000..da4c1510 --- /dev/null +++ b/migrations/20230302120000-rename-mailType-enum.js @@ -0,0 +1,8 @@ +module.exports = { + up: async (queryInterface) => { + await queryInterface.sequelize.query('ALTER TYPE enum_mail_limits_mail_type RENAME TO mail_type;'); + }, + down: async (queryInterface) => { + await queryInterface.sequelize.query('ALTER TYPE mail_type RENAME TO enum_mail_limits_mail_type;'); + }, +}; diff --git a/migrations/20230303133000-add-deactivateUser-to-mailType-enum.js b/migrations/20230303133000-add-deactivateUser-to-mailType-enum.js new file mode 100644 index 00000000..e8382eab --- /dev/null +++ b/migrations/20230303133000-add-deactivateUser-to-mailType-enum.js @@ -0,0 +1,17 @@ +module.exports = { + up: async (queryInterface) => { + await queryInterface.sequelize.query( + `ALTER TYPE mail_type ADD VALUE 'deactivate_user';`, + ); + }, + down: async (queryInterface) => { + await queryInterface.sequelize.query( + ` + ALTER TYPE mail_type RENAME TO mail_type_old; + CREATE TYPE mail_type AS ENUM('invite_friend', 'reset_password', 'remove_account'); + ALTER TABLE mail_limits ALTER COLUMN mail_type TYPE mail_type USING mail_type::text::mail_type; + DROP TYPE mail_type_old; + `, + ); + }, +}; diff --git a/package.json b/package.json index b5fe1318..95f15933 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@rudderstack/rudder-sdk-node": "^1.1.4", "@sendgrid/mail": "^7.2.1", "@types/pg": "^8.6.5", + "agentkeepalive": "^4.3.0", "analytics-node": "^5.1.0", "async": "^3.2.0", "aws-sdk": "^2.1138.0", @@ -20,7 +21,7 @@ "cors": "^2.8.5", "crypto-js": "^4.0.0", "dotenv": "^10.0.0", - "express": "^4.17.1", + "express": "4.18.2", "express-async-errors": "^3.1.1", "express-rate-limit": "^5.4.1", "express-request-id": "^1.4.1", @@ -43,6 +44,8 @@ "passport-jwt": "^4.0.0", "pg": "^8.7.3", "pg-hstore": "^2.3.4", + "prom-client": "^14.1.1", + "prometheus-api-metrics": "^3.2.2", "qrcode": "^1.4.4", "sanitize-filename": "^1.6.3", "sequelize": "^6.3.5", diff --git a/sonar-project.properties b/sonar-project.properties index 5a5da714..461e1f3e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,2 +1,12 @@ sonar.projectKey=internxt_drive-server +sonar.organization=internxt +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=drive-server +#sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 diff --git a/src/app.ts b/src/app.ts index d89ec21b..48ebb69b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,10 +3,12 @@ require('dotenv').config(); import Server from './config/initializers/server'; import Routes from './app/routes/routes'; +import { configureHttp } from './lib/performance/network'; const Services = require('./app/services/services'); const Middleware = require('./config/initializers/middleware'); const SocketServer = require('./app/sockets/socketServer'); +configureHttp(); const App = new Server(); App.start(() => { diff --git a/src/app/middleware/analytics.ts b/src/app/middleware/analytics.ts index 4477deb5..a749a2ae 100644 --- a/src/app/middleware/analytics.ts +++ b/src/app/middleware/analytics.ts @@ -3,7 +3,7 @@ import { page as pageTracking } from '../../lib/analytics/AnalyticsService'; export function page(req: Request, res: Response, next: NextFunction) { try { - pageTracking(req); + pageTracking(req).catch(() => null); } catch(err) { // NO OP diff --git a/src/app/middleware/resource-sharing.middleware.ts b/src/app/middleware/resource-sharing.middleware.ts new file mode 100644 index 00000000..bebd5613 --- /dev/null +++ b/src/app/middleware/resource-sharing.middleware.ts @@ -0,0 +1,88 @@ +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', + RENAME_ITEMS = 'RENAME_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 requester = (req as any).behalfUser || (req as AuthorizedUser).user; + req.behalfUser = requester; + + 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'); + } + + if (decoded.owner.uuid === requester.uuid) { + return next(); + } + + 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), + RenameFile: mdBuilder(Actions.RENAME_ITEMS), + }; +}; + +export { + build, +}; diff --git a/src/app/models/file.ts b/src/app/models/file.ts index 863d15ac..e14122cc 100644 --- a/src/app/models/file.ts +++ b/src/app/models/file.ts @@ -1,5 +1,11 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; +export enum FileStatus { + EXISTS = 'EXISTS', + DELETED = 'DELETED', + TRASHED = 'TRASHED', +} + export interface FileAttributes { id: number; uuid: string; @@ -12,11 +18,13 @@ export interface FileAttributes { folderId: number; folderUuid: string; createdAt: Date; + updatedAt: Date; encryptVersion: string; deleted: boolean; deletedAt: Date; userId: number; modificationTime: Date; + status: FileStatus; } export type FileModel = ModelDefined; @@ -87,6 +95,12 @@ export default (database: Sequelize): FileModel => { modificationTime: { type: DataTypes.DATE, }, + status: { + type: DataTypes.ENUM, + values: Object.values(FileStatus), + defaultValue: FileStatus.EXISTS, + allowNull: false, + }, }, { timestamps: true, diff --git a/src/app/models/folder.ts b/src/app/models/folder.ts index f234c20b..c38597e3 100644 --- a/src/app/models/folder.ts +++ b/src/app/models/folder.ts @@ -11,6 +11,8 @@ export interface FolderAttributes { encryptVersion: string; deleted: boolean; deletedAt: Date; + removed: boolean; + removedAt: Date; } export type FolderModel = ModelDefined; @@ -71,6 +73,14 @@ export default (database: Sequelize): FolderModel => { type: DataTypes.DATE, allowNull: true, }, + removed: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + removedAt: { + type: DataTypes.DATE, + allowNull: true, + } }, { timestamps: true, diff --git a/src/app/models/index.ts b/src/app/models/index.ts index 5b2502f7..661c333f 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -18,6 +18,14 @@ import initTeamMember, { TeamMemberModel } from './teammember'; import initThumbnail, { ThumbnailModel } from './thumbnail'; import initUser, { UserModel } from './user'; import initUserReferral, { UserReferralModel } from './userReferral'; +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 @@ -37,7 +45,14 @@ export type ModelType = | TeamMemberModel | UserModel | UserReferralModel - | FriendInvitationModel; + | FriendInvitationModel + | RoleModel + | PermissionModel + | PrivateSharingFolderModel + | PrivateSharingFolderRoleModel + | SharingsModel + | SharingInvitesModel + | SharingRolesModel; export default (database: Sequelize) => { const AppSumo = initAppSumo(database); @@ -58,6 +73,13 @@ export default (database: Sequelize) => { const User = initUser(database); const UserReferral = initUserReferral(database); const FriendInvitation = initFriendInvitation(database); + const Role = initRole(database); + 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); @@ -78,6 +100,8 @@ 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' }); @@ -101,10 +125,40 @@ 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' }); + 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' }); + + Role.hasMany(Permission, { foreignKey: 'role_id', sourceKey: 'id' }); + Role.hasMany(PrivateSharingFolderRole, { foreignKey: 'role_id', sourceKey: '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' }); + Sharings.hasMany(SharingRoles, { foreignKey: 'sharing_id', sourceKey: 'id' }); + + 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' }); + return { [AppSumo.name]: AppSumo, [Backup.name]: Backup, @@ -124,5 +178,12 @@ 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, + [Sharings.name]: Sharings, + [SharingInvites.name]: SharingInvites, + [SharingRoles.name]: SharingRoles, }; }; diff --git a/src/app/models/mailLimit.ts b/src/app/models/mailLimit.ts index e50c0f00..a9a6d4d6 100644 --- a/src/app/models/mailLimit.ts +++ b/src/app/models/mailLimit.ts @@ -1,10 +1,11 @@ import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; -enum MailTypes { +export enum MailTypes { InviteFriend = 'invite_friend', ResetPassword = 'reset_password', RemoveAccount = 'remove_account', - EmailVerification = 'email_verification' + EmailVerification = 'email_verification', + DeactivateUser = 'deactivate_user', } interface Attributes { diff --git a/src/app/models/permissions.ts b/src/app/models/permissions.ts new file mode 100644 index 00000000..5e2e2c99 --- /dev/null +++ b/src/app/models/permissions.ts @@ -0,0 +1,46 @@ +import { Sequelize, DataTypes, ModelDefined } from 'sequelize'; + +interface PermissionsAttributes { + id: string; + name: 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 + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + roleId: { + type: DataTypes.UUIDV4, + allowNull: false, + references: { + model: 'roles', + key: 'id', + } + }, + }, + { + 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..aba92139 --- /dev/null +++ b/src/app/models/privateSharingFolder.ts @@ -0,0 +1,49 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface Attributes { + id: string; + folderId: string; + ownerId: string; + sharedWith: string; + encryptionKey: string; + createdAt: Date; + updatedAt: Date; +} + +export type PrivateSharingFolderModel = ModelDefined; + +export default (database: Sequelize): PrivateSharingFolderModel => { + const PrivateSharingFolder: PrivateSharingFolderModel = database.define( + 'privateSharingFolder', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + folderId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + ownerId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + sharedWith: { + type: DataTypes.UUIDV4, + allowNull: false + }, + encryptionKey: { + type: DataTypes.STRING, + allowNull: false + }, + }, + { + tableName: 'private_sharing_folder', + underscored: true, + timestamps: true + }, + ); + + return PrivateSharingFolder; +}; diff --git a/src/app/models/privateSharingFolderRole.ts b/src/app/models/privateSharingFolderRole.ts new file mode 100644 index 00000000..360b9300 --- /dev/null +++ b/src/app/models/privateSharingFolderRole.ts @@ -0,0 +1,44 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface Attributes { + id: number; + userId: string; + folderId: string; + roleId: string; + createdAt: Date; + updatedAt: Date; +} + +export type PrivateSharingFolderRoleModel = ModelDefined; + +export default (database: Sequelize): PrivateSharingFolderRoleModel => { + const PrivateSharingFolderRole: PrivateSharingFolderRoleModel = database.define( + 'privateSharingFolderRole', + { + id: { + type: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false, + }, + userId: { + type: DataTypes.STRING, + allowNull: false + }, + folderId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + roleId: { + type: DataTypes.UUIDV4, + allowNull: false + }, + }, + { + tableName: 'private_sharing_folder_roles', + underscored: true, + timestamps: true + }, + ); + + return PrivateSharingFolderRole; +}; diff --git a/src/app/models/roles.ts b/src/app/models/roles.ts new file mode 100644 index 00000000..12fac192 --- /dev/null +++ b/src/app/models/roles.ts @@ -0,0 +1,37 @@ +import { Sequelize, ModelDefined, DataTypes } from 'sequelize'; + +interface RoleAttributes { + id: string; + name: 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 + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'roles', + underscored: true, + timestamps: true + }, + ); + + return Roles; +}; 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; +}; diff --git a/src/app/routes/auth.ts b/src/app/routes/auth.ts index c7c6a344..8a27d359 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; @@ -42,7 +43,7 @@ export class AuthController { res.status(200).send(result); await this.service.Newsletter.subscribe(result.user.email); - this.service.Analytics.trackSignUp(req, result.user); + this.service.Analytics.trackSignUp(req, result.user).catch(() => null); } catch (err: any) { if (err instanceof HttpError) { return res.status(err.status).send({ @@ -77,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()); @@ -118,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; @@ -131,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; @@ -239,10 +240,22 @@ export class AuthController { export default (router: Router, service: any, config: Config) => { const controller = new AuthController(service, config); + const logger = Logger.getInstance(); router.post('/register', controller.register.bind(controller)); router.post('/login', controller.login.bind(controller)); - router.post('/access', controller.access.bind(controller)); + router.post('/access', async (req, res) => { + try { + 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' }); + } + }); router.get('/new-token', passportAuth, controller.getNewToken.bind(controller)); router.get('/are-credentials-correct', passportAuth, controller.areCredentialsCorrect.bind(controller)); }; diff --git a/src/app/routes/plan.js b/src/app/routes/plan.js index fcd8252a..9b377576 100644 --- a/src/app/routes/plan.js +++ b/src/app/routes/plan.js @@ -31,11 +31,10 @@ module.exports = (Router, Service) => { return res.status(200).json(plan); } catch (error) { - const statusCode = error.statusCode || 500; const errorMessage = error.message || ''; Logger.error(`Error getting stripe individual plan ${req.user.email}: ${error.message}`); - return res.status(statusCode).send({ error: errorMessage }); + return res.status(500).send({ error: errorMessage }); } }); diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 1ec11efc..9542e468 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -12,6 +12,13 @@ import { FileAttributes } from '../models/file'; import CONSTANTS from '../constants'; import { LockNotAvaliableError } from '../services/errors/locks'; import { ConnectionTimedOutError } from 'sequelize'; +import { + FileAlreadyExistsError, + FileWithNameAlreadyExistsError +} 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 { @@ -49,34 +56,89 @@ export class StorageController { file.fileId = file.file_id; } - if (!file || !file.fileId || !file.bucket || !file.size || !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)}`, ); return res.status(400).json({ error: 'Invalid metadata for new file' }); } - const result = await this.services.Files.CreateFile(behalfUser, file); + try { + const result = await this.services.Files.CreateFile(behalfUser, file); - res.status(200).json(result); + res.status(200).json(result); + + this.services.Analytics.trackUploadCompleted(req, behalfUser); + + const workspaceMembers = await this.services.User.findWorkspaceMembers(behalfUser.bridgeUser); + + workspaceMembers.forEach( + ({ email, uuid }: { email: string; uuid: string }) => + void this.services.Notifications.fileCreated({ + file: result, + email, + uuid, + clientId: clientId, + }), + ); + } 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}`, + ); + res.status(500).send({ error: 'Internal Server Error' }); + } + } + + 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' }); + } - const workspaceMembers = await this.services.User.findWorkspaceMembers(behalfUser.bridgeUser); + try { + const result = await this.services.Files.CheckFileExistence(behalfUser, file); - workspaceMembers.forEach( - ({ email }: { email: string }) => - void this.services.Notifications.fileCreated({ - file: result, - email: email, - clientId: clientId, - }), - ); + if (!result.exists) { + return res.status(404).send(); + } - this.services.Analytics.trackUploadCompleted(req, behalfUser); + 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 { user } = req as PassportRequest; + const { folderName, parentFolderId, uuid: clientCreatedUuuid } = req.body; + const { behalfUser: user } = req as any; if (Validator.isInvalidString(folderName)) { throw createHttpError(400, 'Folder name must be a valid string'); @@ -86,6 +148,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); @@ -97,26 +163,57 @@ 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); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { uuid: string; email: string }) => void this.services.Notifications.folderCreated({ folder: result, email: email, + uuid, clientId: clientId, }), ); }) .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(); }); } + 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'; @@ -142,10 +239,9 @@ export class StorageController { return this.services.Folder.GetTree(user, folderId) .then((result: unknown) => { - const treeSize = this.services.Folder.GetTreeSize(result); res.status(200).send({ tree: result, - size: treeSize, + size: 0, }); }) .catch((err: Error) => { @@ -158,7 +254,7 @@ export class StorageController { }); } - public async deleteFolder(req: Request, res: Response): Promise { + async deleteFolder(req: Request, res: Response) { const { behalfUser: user } = req as SharedRequest; const folderId = Number(req.params.id); @@ -168,23 +264,36 @@ export class StorageController { const clientId = String(req.headers['internxt-client-id']); - return this.services.Folder.Delete(user, folderId) - .then(async (result: unknown) => { - res.status(204).send(result); - const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); + try { + const result = await this.services.Folder.Delete(user, folderId); + + res.status(204).send(result); + + this.services.User.findWorkspaceMembers(user.bridgeUser).then((workspaceMembers: any) => { workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.folderDeleted({ id: folderId, email: email, + uuid, clientId: clientId, }), ); - }) - .catch((err: Error) => { - this.logger.error(`${err.message}\n${err.stack}`); - res.status(500).send(); }); + } catch (error) { + const err = error as Error; + + if (err.message === 'Folder does not exist') { + return res.status(404).send({ error: err.message }); + } + + if (err.message === 'Cannot delete root folder') { + return res.status(406).send({ error: err.message }); + } + + this.logger.error(`[FOLDER/DELETE] ERROR: ${err.message}, STACK: ${err.stack || 'NO STACK'}`); + res.status(500).send({ error: 'Internal Server Error' }); + } } public async moveFolder(req: Request, res: Response): Promise { @@ -210,10 +319,11 @@ export class StorageController { res.status(200).json(result); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.folderUpdated({ folder: result.result, email: email, + uuid, clientId: clientId, }), ); @@ -244,16 +354,22 @@ export class StorageController { res.status(200).json(result); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.folderUpdated({ folder: result, email: email, + uuid, clientId: clientId, }), ); }) .catch((err: Error) => { this.logger.error(`Error updating metadata from folder ${folderId}: ${err}`); + + if (err instanceof FolderWithNameAlreadyExistsError) { + res.status(409).send().end(); + } + res.status(500).send(); }); } @@ -268,13 +384,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), ]) @@ -342,10 +452,11 @@ export class StorageController { res.status(200).json(result); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.fileUpdated({ file: result.result, email: email, + uuid, clientId: clientId, }), ); @@ -365,7 +476,6 @@ export class StorageController { const { behalfUser: user } = req as SharedRequest; const fileId = req.params.fileid; const { metadata, bucketId, relativePath } = req.body; - const mnemonic = req.headers['internxt-mnemonic']; const clientId = String(req.headers['internxt-client-id']); if (Validator.isInvalidString(fileId)) { @@ -380,25 +490,27 @@ export class StorageController { throw createHttpError(400, 'Relative path is not valid'); } - if (Validator.isInvalidString(mnemonic)) { - throw createHttpError(400, 'Mnemonic is not valid'); - } - - return this.services.Files.UpdateMetadata(user, fileId, metadata, mnemonic, bucketId, relativePath) + return this.services.Files.UpdateMetadata(user, fileId, metadata, '', bucketId, relativePath) .then(async (result: FileAttributes) => { res.status(200).json(result); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.fileUpdated({ file: result, email, + uuid, clientId, }), ); }) .catch((err: Error) => { this.logger.error(`Error updating metadata from file ${fileId} : ${err}`); + + if (err instanceof FileWithNameAlreadyExistsError) { + res.status(409).send().end(); + } + res.status(500).send(); }); } @@ -451,10 +563,11 @@ export class StorageController { res.status(200).json({ deleted: true }); const workspaceMembers = await this.services.User.findWorkspaceMembers(user.bridgeUser); workspaceMembers.forEach( - ({ email }: { email: string }) => + ({ email, uuid }: { email: string; uuid: string }) => void this.services.Notifications.fileDeleted({ id: Number(fileid), email, + uuid, clientId, }), ); @@ -500,7 +613,8 @@ export class StorageController { .then(() => { res.status(201).end(); }) - .catch(() => { + .catch((err: Error) => { + this.logger.error(`Error acquiring lock for user ${user.email} : ${err.message}. ${err.stack || 'NO STACK'}`); res.status(409).end(); }); } @@ -691,11 +805,28 @@ 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/folder', passportAuth, controller.createFolder.bind(controller)); + router.post('/storage/file', + passportAuth, + sharedAdapter, + resourceSharingAdapter.UploadFile, + controller.createFile.bind(controller) + ); + router.post('/storage/file/exists', + passportAuth, + sharedAdapter, + controller.checkFileExistence.bind(controller) + ); + router.post('/storage/thumbnail', + passportAuth, + sharedAdapter, + resourceSharingAdapter.UploadThumbnail, + 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)); @@ -709,7 +840,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, + sharedAdapter, + resourceSharingAdapter.RenameFile, + controller.updateFile.bind(controller) + ); router.delete('/storage/bucket/:bucketid/file/:fileid', passportAuth, controller.deleteFileBridge.bind(controller)); router.delete( '/storage/folder/:folderid/file/:fileid', diff --git a/src/app/services/errors/FileWithNameAlreadyExistsError.ts b/src/app/services/errors/FileWithNameAlreadyExistsError.ts new file mode 100644 index 00000000..1f3a44e1 --- /dev/null +++ b/src/app/services/errors/FileWithNameAlreadyExistsError.ts @@ -0,0 +1,15 @@ +export class FileWithNameAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + + 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/errors/FolderWithNameAlreadyExistsError.ts b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts new file mode 100644 index 00000000..08b41b53 --- /dev/null +++ b/src/app/services/errors/FolderWithNameAlreadyExistsError.ts @@ -0,0 +1,15 @@ +export class FolderWithNameAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + + 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/files.js b/src/app/services/files.js index baa8d75f..42fa0e38 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, FileAlreadyExistsError } = require('./errors/FileWithNameAlreadyExistsError'); // Filenames that contain "/", "\" or only spaces are invalid const invalidName = /[/\\]|^\s*$/; @@ -12,61 +13,109 @@ 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) => { - return Model.folder - .findOne({ - where: { - id: { [Op.eq]: file.folder_id }, - user_id: { [Op.eq]: user.id }, - }, - }) - .then(async (folder) => { - if (!folder) { - throw Error('Folder not found / Is not your folder'); - } + const folder = await Model.folder.findOne({ + where: { + id: { [Op.eq]: file.folder_id }, + }, + }); - const fileExists = await Model.file.findOne({ - where: { - name: { [Op.eq]: file.name }, - folder_id: { [Op.eq]: folder.id }, - type: { [Op.eq]: file.type }, - userId: { [Op.eq]: user.id }, - deleted: { [Op.eq]: false }, - }, - }); + if (!folder) { + throw new Error('Folder not found'); + } - if (fileExists) { - throw Error('File entry already exists'); - } + const isTheFolderOwner = user.id === folder.user_id; - const fileInfo = { - name: file.name, - plain_name: file.plain_name, - type: file.type, - size: file.size, - folder_id: folder.id, - fileId: file.fileId, - bucket: file.bucket, - encrypt_version: file.encrypt_version, - userId: user.id, - uuid: v4(), - folderUuid: folder.uuid, - modificationTime: file.modificationTime || new Date(), - }; - - try { - AesUtil.decrypt(file.name, file.fileId); - fileInfo.encrypt_version = '03-aes'; - } catch { - // eslint-disable-next-line no-empty - } + if (!isTheFolderOwner) { + throw new Error('Folder is not yours'); + } - if (file.date) { - fileInfo.createdAt = file.date; - } + const maybeAlreadyExistentFile = await Model.file.findOne({ + where: { + name: { [Op.eq]: file.name }, + folder_id: { [Op.eq]: folder.id }, + type: { [Op.eq]: file.type }, + userId: { [Op.eq]: user.id }, + status: { [Op.eq]: 'EXISTS' }, + }, + }); - return Model.file.create(fileInfo); - }); + const fileAlreadyExists = !!maybeAlreadyExistentFile; + + if (fileAlreadyExists) { + throw new FileAlreadyExistsError('File already exists'); + } + + const fileInfo = { + name: file.name, + plain_name: file.plain_name, + type: file.type, + size: file.size, + folder_id: folder.id, + fileId: file.fileId, + bucket: file.bucket, + encrypt_version: file.encrypt_version, + userId: user.id, + uuid: v4(), + folderUuid: folder.uuid, + modificationTime: file.modificationTime || new Date(), + }; + + try { + AesUtil.decrypt(file.name, file.fileId); + fileInfo.encrypt_version = '03-aes'; + } catch { + // eslint-disable-next-line no-empty + } + + if (file.date) { + fileInfo.createdAt = file.date; + } + + return Model.file.create(fileInfo); }; const Delete = (user, bucket, fileId) => @@ -200,12 +249,12 @@ 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) => { if (duplicateFile) { - return next(Error('File with this name exists')); + return next(new FileWithNameAlreadyExistsError('File with this name exists')); } newMeta.name = cryptoFileName; newMeta.plain_name = metadata.itemName; @@ -217,9 +266,7 @@ module.exports = (Model, App) => { if (newMeta.name !== file.name) { file .update(newMeta) - .then(async (update) => { - await App.services.Inxt.renameFile(user.email, user.userId, mnemonic, bucketId, fileId, relativePath); - + .then((update) => { next(null, update); }) .catch(next); @@ -252,7 +299,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' }, }, }); @@ -266,8 +313,7 @@ module.exports = (Model, App) => { folder_id: parseInt(destination, 10), folderUuid: folderTarget.uuid, name: destinationName, - deleted: false, - deletedAt: null, + status: 'EXISTS', }); // we don't want ecrypted name on front @@ -365,7 +411,7 @@ module.exports = (Model, App) => { bucket: { [Op.ne]: user.backupsBucket }, - deleted: { [Op.eq]: false } + status: { [Op.eq]: 'EXISTS' } }, include: [ { @@ -402,6 +448,7 @@ module.exports = (Model, App) => { return { Name: 'Files', CreateFile, + CheckFileExistence, Delete, DeleteFile, UpdateMetadata, diff --git a/src/app/services/folder.js b/src/app/services/folder.js index 15ba2ab6..cf80eda6 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 { FolderAlreadyExistsError, FolderWithNameAlreadyExistsError } from './errors/FolderWithNameAlreadyExistsError'; const invalidName = /[\\/]|^\s*$/; @@ -55,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'); } @@ -108,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 @@ -116,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, @@ -126,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)', { @@ -146,18 +192,18 @@ module.exports = (Model, App) => { }); if (!folder) { - throw new Error('Folder does not exists'); + throw new Error('Folder does not exist'); } if (folder.id === user.root_folder_id) { throw new Error('Cannot delete root folder'); } - // Destroy folder - const removed = await folder.destroy(); - - DeleteOrphanFolders(user.id).catch((err) => { - logger.error('ERROR deleting orphan folders from user %s, reason: %s', user.email, err.message); + const removed = await folder.update({ + deleted: true, + deletedAt: new Date(), + removed: true, + removedAt: new Date(), }); return removed; @@ -280,7 +326,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, @@ -321,8 +371,8 @@ module.exports = (Model, App) => { } const folders = await Model.folder.findAll({ - where: { user_id: { [Op.eq]: userObject.id }, deleted: filterOptions.deleted || false }, - attributes: ['id', 'parent_id', 'name', 'bucket', 'updated_at', 'created_at'], + where: { user_id: { [Op.eq]: userObject.id }, deleted: filterOptions.deleted || false, removed: false }, + attributes: ['id', 'parent_id', 'name', 'bucket', 'updated_at', 'created_at', 'plain_name'], order: [['id', 'DESC']], limit: 5000, offset: index, @@ -333,7 +383,7 @@ module.exports = (Model, App) => { where: { folder_id: { [Op.in]: foldersId }, userId: userObject.id, - deleted: filterOptions.deleted || false + status: filterOptions.deleted ? 'TRASHED' : 'EXISTS', }, }); @@ -431,7 +481,7 @@ module.exports = (Model, App) => { }) .then((isDuplicated) => { if (isDuplicated) { - return next(Error('Folder with this name exists')); + return next(new FolderWithNameAlreadyExistsError('Folder with this name exists')); } newMeta.name = cryptoFolderName; newMeta.plain_name = metadata.itemName; @@ -767,6 +817,7 @@ module.exports = (Model, App) => { Name: 'Folder', getById, Create, + CheckFolderExistence, Delete, GetChildren, GetTree, diff --git a/src/app/services/plan.js b/src/app/services/plan.js index 0c07b134..15511625 100644 --- a/src/app/services/plan.js +++ b/src/app/services/plan.js @@ -82,7 +82,7 @@ module.exports = (Model, App) => { const getIndividualPlan = async (userEmail, userId) => { const subscriptionPlans = (await stripeService.getUserSubscriptionPlans(userEmail, userId)) - .filter((subscription) => subscription.status === 'active') + .filter((subscription) => subscription.status === 'active' || subscription.status === 'trialing') .filter((plan) => !plan.isTeam); let result = subscriptionPlans[0]; @@ -109,7 +109,7 @@ module.exports = (Model, App) => { const getTeamPlan = async (userEmail, userId) => { const subscriptionPlans = (await stripeService.getUserSubscriptionPlans(userEmail, userId)) - .filter((subscription) => subscription.status === 'active') + .filter((subscription) => subscription.status === 'active' || subscription.status === 'trialing') .filter((plan) => plan.isTeam); let result = subscriptionPlans[0]; diff --git a/src/app/services/privateSharing.js b/src/app/services/privateSharing.js new file mode 100644 index 00000000..f2ca1efa --- /dev/null +++ b/src/app/services/privateSharing.js @@ -0,0 +1,63 @@ +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, itemId) => { + const permissions = await Model.permissions.findAll({ + include: [ + { + model: Model.roles, + include: [ + { + model: Model.sharingRoles, + include: [ + { + model: Model.sharings, + where: { + sharedWith: sharedWithId, + itemId, + } + } + ] + } + ] + } + ] + }); + + if (!permissions) { + throw new PrivateSharingFolderPermissionsNotFound(); + } + + 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.name === action) { + return true; + } + } + return false; + }; + + return { + Name: 'PrivateSharing', + CanUserPerformAction + }; +}; diff --git a/src/app/services/user.js b/src/app/services/user.js index 55c961a7..41c1519f 100644 --- a/src/app/services/user.js +++ b/src/app/services/user.js @@ -1,5 +1,6 @@ import axios from 'axios'; import crypto from 'crypto'; +import { MailTypes } from '../models/mailLimit'; const sequelize = require('sequelize'); const bip39 = require('bip39'); const { request } = require('@internxt/lib'); @@ -190,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; @@ -220,9 +221,9 @@ module.exports = (Model, App) => { const [mailLimit] = await Model.mailLimit.findOrCreate({ where: { userId: user.id, - mailType: 'deactivate_user', + mailType: MailTypes.DeactivateUser, }, - default: { + defaults: { attemptsCount: 0, attemptsLimit: 10, }, @@ -242,7 +243,7 @@ module.exports = (Model, App) => { { where: { userId: user.id, - mailType: 'deactivate_user', + mailType: MailTypes.DeactivateUser, }, }, ); @@ -502,7 +503,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 }, + where: { user_id: targetUser.id, status: 'EXISTS' }, attributes: [[fn('sum', col('size')), 'total']], raw: true, }); @@ -576,7 +577,7 @@ module.exports = (Model, App) => { let mailLimit = await Model.mailLimit.findOne({ where: { userId: hostUserId, - mailType: 'invite_friend', + mailType: MailTypes.InviteFriend, }, }); @@ -585,7 +586,7 @@ module.exports = (Model, App) => { if (!mailLimit) { mailLimit = await Model.mailLimit.create({ userId: hostUserId, - mailType: 'invite_friend', + mailType: MailTypes.InviteFriend, attemptsCount: 0, attemptsLimit: 10, }); @@ -621,7 +622,7 @@ module.exports = (Model, App) => { { where: { userId: hostUserId, - mailType: 'invite_friend', + mailType: MailTypes.InviteFriend, }, }, ); @@ -716,9 +717,9 @@ module.exports = (Model, App) => { const [mailLimit] = await Model.mailLimit.findOrCreate({ where: { userId: user.id, - mailType: 'email_verification', + mailType: MailTypes.EmailVerification, }, - default: { + defaults: { attemptsCount: 0, attemptsLimit: 10, }, @@ -737,7 +738,7 @@ module.exports = (Model, App) => { { where: { userId: user.id, - mailType: 'email_verification', + mailType: MailTypes.EmailVerification, lastMailSent: new Date(), }, }, diff --git a/src/config/config.ts b/src/config/config.ts index f6f21894..82d2adab 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -3,9 +3,10 @@ import nconf from 'nconf'; const development = require('./environments/development.js').data; const test = require('./environments/test.js').data; const staging = require('./environments/staging.js').data; +const production = require('./environments/production.js').data; const e2e = require('./environments/e2e.js').data; -const environments: any = { development, test, staging, e2e }; +const environments: any = { development, test, staging, e2e, production }; export default class Config { private static instance: Config; diff --git a/src/config/environments/development.js b/src/config/environments/development.js index e8ff7738..4c1c4ba8 100644 --- a/src/config/environments/development.js +++ b/src/config/environments/development.js @@ -3,13 +3,14 @@ exports.data = { port: 8000, }, database: { + host: process.env.RDS_HOSTNAME, name: process.env.RDS_DBNAME, - user: process.env.RDS_USERNAME || 'root', - password: process.env.RDS_PASSWORD || '', + user: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, sequelizeConfig: { + host: process.env.RDS_HOSTNAME, dialect: 'postgres', port: process.env.RDS_PORT, - host: process.env.RDS_HOSTNAME || 'localhost', }, }, secrets: { diff --git a/src/config/environments/production.js b/src/config/environments/production.js new file mode 100644 index 00000000..91ede15c --- /dev/null +++ b/src/config/environments/production.js @@ -0,0 +1,47 @@ +exports.data = { + server: { + port: 8000, + }, + database: { + host: process.env.RDS_HOSTNAME, + name: process.env.RDS_DBNAME, + user: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, + sequelizeConfig: { + dialect: 'postgres', + port: process.env.RDS_PORT, + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false, + }, + }, + replication: { + read: [ + { host: process.env.RDS_HOSTNAME, username: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD }, + { host: process.env.RDS_HOSTNAME2, username: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD }, + { host: process.env.RDS_HOSTNAME3, username: process.env.RDS_USERNAME, password: process.env.RDS_PASSWORD }, + ], + write: { + host: process.env.RDS_HOSTNAME, + username: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, + }, + }, + }, + }, + secrets: { + JWT: process.env.JWT_SECRET || 'asdf1234', + CRYPTO: 'asdf1234', + CAPTCHA: process.env.CAPTCHA_SECRET, + CRYPTO_SECRET: process.env.CRYPTO_SECRET || 'ASDFGHJKL1234567', + STRIPE_SK: process.env.STRIPE_SK, + MAGIC_SALT: process.env.MAGIC_SALT, + MAGIC_IV: process.env.MAGIC_IV, + CRYPTO_SECRET2: process.env.CRYPTO_SECRET2, + }, + logger: { + level: 4, + }, + STORJ_BRIDGE: 'https://api.internxt.com', +}; diff --git a/src/config/initializers/database.ts b/src/config/initializers/database.ts index 773af183..5683eed4 100644 --- a/src/config/initializers/database.ts +++ b/src/config/initializers/database.ts @@ -4,6 +4,22 @@ import Logger from '../../lib/logger'; const SqlFormatter = require('sql-formatter'); const _ = require('lodash'); +const maxPoolConnections = + process.env.DATABASE_CONFIG_MAX_POOL_CONNECTIONS && + parseInt(process.env.DATABASE_CONFIG_MAX_POOL_CONNECTIONS) || 20; + +const minPoolConnections = + process.env.DATABASE_CONFIG_MIN_POOL_CONNECTIONS && + parseInt(process.env.DATABASE_CONFIG_MIN_POOL_CONNECTIONS) || 0; + +const idle = + process.env.DATABASE_CONFIG_MAX_IDLE_CONNECTION_TIME_MS && + parseInt(process.env.DATABASE_CONFIG_MAX_IDLE_CONNECTION_TIME_MS) || 20000; + +const acquire = + process.env.DATABASE_CONFIG_MAX_ACQUIRE_CONNECTION_TIME_MS && + parseInt(process.env.DATABASE_CONFIG_MAX_ACQUIRE_CONNECTION_TIME_MS) || 20000; + export default class Database { private static instance: Sequelize; @@ -14,16 +30,12 @@ export default class Database { const logger = Logger.getInstance(); - const defaultSettings = { - resetAfterUse: true, - operatorsAliases: 0, + const defaultSettings: Partial = { pool: { - maxConnections: Number.MAX_SAFE_INTEGER, - maxIdleTime: 30000, - max: 20, - min: 0, - idle: 20000, - acquire: 20000, + max: maxPoolConnections, + min: minPoolConnections, + idle, + acquire, }, logging: (content: string) => { const parse = content.match(/^(Executing \(.*\):) (.*)$/); @@ -36,6 +48,8 @@ export default class Database { }, }; + logger.info('Database connection started with the following config:' + JSON.stringify(defaultSettings)); + const sequelizeSettings: Options = _.merge(defaultSettings, config.sequelizeConfig); const instance = new Sequelize(config.name, config.user, config.password, sequelizeSettings); @@ -46,7 +60,7 @@ export default class Database { logger.info('Connected to database'); }) .catch((err) => { - logger.error('Database connection error: %s', err); + logger.error('Database connection error:', err); }); Database.instance = instance; diff --git a/src/config/initializers/middleware.js b/src/config/initializers/middleware.js index 22e00101..e0f1851b 100644 --- a/src/config/initializers/middleware.js +++ b/src/config/initializers/middleware.js @@ -10,10 +10,12 @@ const addRequestId = require('express-request-id'); const util = require('util'); const Logger = require('../../lib/logger').default; const logger = Logger.getInstance(); +const apiMetrics = require('prometheus-api-metrics'); module.exports = (App, Config) => { App.express.use(helmet()); App.express.use(addRequestId()); + App.express.use(apiMetrics()); App.express.use((req, res, next) => { const meta = { requestId: req.id, diff --git a/src/config/initializers/notifications.ts b/src/config/initializers/notifications.ts index 6fe176f2..8515d058 100644 --- a/src/config/initializers/notifications.ts +++ b/src/config/initializers/notifications.ts @@ -7,7 +7,7 @@ import Logger from '../../lib/logger'; type RequestData = { event: string; payload: Record; - email: string; + email?: string; clientId: string; userId?: UserAttributes['uuid']; }; @@ -30,51 +30,71 @@ export default class Notifications { fileCreated({ file, + uuid, email, clientId, - }: { file: FileAttributes } & Pick): Promise { - return this.post({ event: 'FILE_CREATED', payload: file, email, clientId }); + }: { file: FileAttributes; uuid: RequestData['userId'] } & Pick): Promise { + return this.post({ event: 'FILE_CREATED', payload: file, userId: uuid, clientId, email }); } fileUpdated({ file, + uuid, email, clientId, - }: { file: FileAttributes } & Pick): Promise { - return this.post({ event: 'FILE_UPDATED', payload: file, email, clientId }); + }: { file: FileAttributes; uuid: RequestData['userId'] } & Pick): Promise { + return this.post({ event: 'FILE_UPDATED', payload: file, userId: uuid, email, clientId }); } - fileDeleted({ id, email, clientId }: { id: number } & Pick): Promise { - return this.post({ event: 'FILE_DELETED', payload: { id }, email, clientId }); + fileDeleted({ + id, + email, + uuid, + clientId, + }: { id: number; uuid: RequestData['userId'] } & Pick): Promise { + return this.post({ event: 'FILE_DELETED', payload: { id }, userId: uuid, email, clientId }); } folderCreated({ folder, + uuid, email, clientId, - }: { folder: FolderAttributes } & Pick): Promise { - return this.post({ event: 'FOLDER_CREATED', payload: folder, email, clientId }); + }: { folder: FolderAttributes; uuid: RequestData['userId'] } & Pick< + RequestData, + 'email' | 'clientId' + >): Promise { + return this.post({ event: 'FOLDER_CREATED', payload: folder, email, userId: uuid, clientId }); } folderUpdated({ folder, + uuid, email, clientId, - }: { folder: FolderAttributes } & Pick): Promise { - return this.post({ event: 'FOLDER_UPDATED', payload: folder, email, clientId }); + }: { folder: FolderAttributes; uuid: RequestData['userId'] } & Pick< + RequestData, + 'email' | 'clientId' + >): Promise { + return this.post({ event: 'FOLDER_UPDATED', payload: folder, userId: uuid, email, clientId }); } - folderDeleted({ id, email, clientId }: { id: number } & Pick): Promise { - return this.post({ event: 'FOLDER_DELETED', payload: { id }, email, clientId }); + folderDeleted({ + id, + uuid, + email, + clientId, + }: { id: number; uuid: RequestData['userId'] } & Pick): Promise { + return this.post({ event: 'FOLDER_DELETED', payload: { id }, userId: uuid, email, clientId }); } - userStorageUpdated( - { uuid, clientId }: { uuid: UserAttributes['uuid'] } & Pick - ): Promise { - - return this.post({ event: 'USER_STORAGE_UPDATED', payload: {}, userId: uuid, email: '', clientId }); + userStorageUpdated({ + uuid, + clientId, + }: { uuid: UserAttributes['uuid'] } & Pick): Promise { + return this.post({ event: 'USER_STORAGE_UPDATED', payload: {}, userId: uuid, clientId }); } - + private async post(data: RequestData): Promise { try { const res = await axios.post(process.env.NOTIFICATIONS_URL as string, data, { diff --git a/src/config/initializers/redis.ts b/src/config/initializers/redis.ts index 8d1a5ff4..9c270017 100644 --- a/src/config/initializers/redis.ts +++ b/src/config/initializers/redis.ts @@ -16,6 +16,7 @@ export default class Redis { const config: RedisOptions = { enableAutoPipelining: true, + showFriendlyErrorStack: true, }; const uri = process.env.REDIS_CONNECTION_STRING; diff --git a/src/config/initializers/server.ts b/src/config/initializers/server.ts index 9c831cf3..81705c32 100644 --- a/src/config/initializers/server.ts +++ b/src/config/initializers/server.ts @@ -61,7 +61,8 @@ export default class Server { } handleuncaughtException(err: Error) { - this.logger.info('Unhandled exception: %s\n%s', err.message, err.stack); + this.logger.info(`Unhandled exception: ${err.message}\n${err.stack}`); + this.database?.close(); // eslint-disable-next-line no-process-exit process.exit(1); } diff --git a/src/lib/analytics/AnalyticsService.ts b/src/lib/analytics/AnalyticsService.ts index 1ab28765..1b83d5c6 100644 --- a/src/lib/analytics/AnalyticsService.ts +++ b/src/lib/analytics/AnalyticsService.ts @@ -219,32 +219,30 @@ export async function trackSignIn() { export async function page(req: express.Request) { const appContext = getContext(req); - const { anonymousId, userId, name, properties } = req.body.page; const context = { ...appContext, ...req.body.page.context }; Analytics.page({ - anonymousId, - userId, + anonymousId: req.body.page.anonymousId, + userId: req.body.page.userId, context, - name, - properties + name: req.body.page.name, + properties: req.body.page.properties, }); } export async function trackSignupServerSide(req: express.Request) { const appContext = await getContext(req); - const { anonymousId } = req.body.page; const context = { ...appContext, ...req.body.page.context }; const { properties, traits, userId } = req.body.track; Analytics.identify({ userId, - anonymousId, + anonymousId: req.body.page.anonymousId, context, traits, }); Analytics.track({ userId, event: TrackName.SignUp, - anonymousId, + anonymousId: req.body.page.anonymousId, context, properties }); diff --git a/src/lib/performance/network.ts b/src/lib/performance/network.ts new file mode 100644 index 00000000..a237ad44 --- /dev/null +++ b/src/lib/performance/network.ts @@ -0,0 +1,27 @@ +import Agent from 'agentkeepalive'; +import axios from 'axios'; + +function createHttpAgent() { + return new Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 10, + timeout: 10000, + freeSocketTimeout: 30000, + }); +} + +function createHttpsAgent() { + return new Agent.HttpsAgent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 10, + timeout: 10000, + freeSocketTimeout: 30000, + }); +} + +export function configureHttp(): void { + axios.defaults.httpAgent = createHttpAgent(); + axios.defaults.httpsAgent = createHttpsAgent(); +} \ No newline at end of file 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); }); diff --git a/yarn.lock b/yarn.lock index fe70cba8..d434a8ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -858,6 +858,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== +"@types/accepts@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + "@types/analytics-node@^3.1.7": version "3.1.7" resolved "https://registry.yarnpkg.com/@types/analytics-node/-/analytics-node-3.1.7.tgz#cb97c80ee505094e44a0188c3ad25f70c67e3c65" @@ -926,6 +933,11 @@ dependencies: "@types/node" "*" +"@types/content-disposition@*": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" + integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== + "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" @@ -936,6 +948,16 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== +"@types/cookies@*": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" + integrity sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + "@types/cors@^2.8.12": version "2.8.12" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" @@ -966,6 +988,15 @@ "@types/qs" "*" "@types/range-parser" "*" +"@types/express-serve-static-core@^4.17.28": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/express@*", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" @@ -993,6 +1024,16 @@ dependencies: "@types/node" "*" +"@types/http-assert@*": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661" + integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA== + +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + "@types/http-errors@^1.8.1": version "1.8.1" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67" @@ -1049,6 +1090,32 @@ dependencies: "@types/node" "*" +"@types/keygrip@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== + +"@types/koa-compose@*": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@^2.13.4": + version "2.13.5" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61" + integrity sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA== + dependencies: + "@types/accepts" "*" + "@types/content-disposition" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/http-errors" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + "@types/lodash@^4.14.182": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" @@ -1262,7 +1329,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.4, accepts@~1.3.7: +accepts@~1.3.4: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== @@ -1270,6 +1337,14 @@ accepts@~1.3.4, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1290,6 +1365,15 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== +agentkeepalive@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" + integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== + dependencies: + debug "^4.1.0" + depd "^2.0.0" + humanize-ms "^1.2.1" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1665,6 +1749,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + bip39@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" @@ -1685,7 +1774,25 @@ bn.js@^4.0.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -body-parser@1.19.0, body-parser@^1.19.0: +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -1842,6 +1949,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + bytes@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" @@ -2160,12 +2272,12 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" @@ -2184,10 +2296,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== cookie@~0.4.1: version "0.4.1" @@ -2300,7 +2412,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@^3.2.7: +debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -2406,15 +2518,20 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== +depd@2.0.0, depd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-newline@^3.0.0: version "3.1.0" @@ -2933,38 +3050,39 @@ express-slow-down@^1.3.1: dependencies: defaults "^1.0.3" -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.20.1" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.5.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "~1.1.2" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.11.0" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -3052,17 +3170,17 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" - statuses "~1.5.0" + statuses "2.0.1" unpipe "~1.0.0" find-up@4.1.0, find-up@^4.0.0, find-up@^4.1.0: @@ -3460,6 +3578,17 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-errors@^1.8.0: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" @@ -3471,17 +3600,6 @@ http-errors@^1.8.0: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -3492,6 +3610,13 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" @@ -4799,7 +4924,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -4929,17 +5054,12 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5019,6 +5139,11 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -5154,7 +5279,7 @@ object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -on-finished@^2.3.0: +on-finished@2.4.1, on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -5469,6 +5594,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkginfo@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" + integrity sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ== + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -5550,6 +5680,25 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.1.1.tgz#e9bebef0e2269bfde22a322f4ca803cb52b4a0c0" + integrity sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw== + dependencies: + tdigest "^0.1.1" + +prometheus-api-metrics@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/prometheus-api-metrics/-/prometheus-api-metrics-3.2.2.tgz#c7e622d6bf5688a0aafb54970ed7b6ea6ccf80e6" + integrity sha512-5hT17HAjflPkrHSYQ7lorsKygo0PhLau/FQ6SQhw0XWAm10GwKfLQmIVP6b3LJBnc4WNOf/QKHce2RfcZMLjJQ== + dependencies: + "@types/express" "^4.17.13" + "@types/express-serve-static-core" "^4.17.28" + "@types/koa" "^2.13.4" + debug "^3.2.6" + lodash.get "^4.4.2" + pkginfo "^0.4.1" + promise.allsettled@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" @@ -5574,7 +5723,7 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -proxy-addr@~2.0.5: +proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -5630,6 +5779,13 @@ qrcode@^1.4.4: pngjs "^3.3.0" yargs "^13.2.4" +qs@6.11.0, qs@^6.10.3: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -5640,13 +5796,6 @@ qs@6.9.3: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== -qs@^6.10.3: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - qs@^6.6.0: version "6.10.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" @@ -5686,6 +5835,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -5907,16 +6066,16 @@ rxjs@^7.5.6: dependencies: tslib "^2.1.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-stable-stringify@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" @@ -5990,24 +6149,24 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "2.0.0" mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" + ms "2.1.3" + on-finished "2.4.1" range-parser "~1.2.1" - statuses "~1.5.0" + statuses "2.0.1" sequelize-cli@^6.2.0: version "6.3.0" @@ -6058,15 +6217,15 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.18.0" set-blocking@^2.0.0: version "2.0.0" @@ -6258,7 +6417,12 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -6473,6 +6637,13 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"