diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..2e7dcdb4a406d310ccf1269d01542ee73011a4f6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.env.example \ No newline at end of file diff --git a/.env.docker b/.env.docker deleted file mode 100644 index b9fac61931ddd9434b1b85925b50f0171f1a37dc..0000000000000000000000000000000000000000 --- a/.env.docker +++ /dev/null @@ -1,16 +0,0 @@ - -# This was inserted by `prisma init`: -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -APP_PORT=3001 -DB_HOST=db_tonality_rest -DBMS=postgresql#if you use another dbms, change at prisma/schema.prisma too -DB_USER= -DB_PASSWORD= -DB_NAME= -DB_PORT= -DATABASE_URL="${DBMS}://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public&pgbouncer=true&connection_limit=1&pool_timeout=20" diff --git a/.env.example b/.env.example index 5fced6ad5dadbf4dc163eb7eda5ea4740e6ae905..afa7e9eed7289360bd7d898eecf607eb453423e5 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,13 @@ +DATABASE_URL= +JWT_SHARED_SECRET= +EXPRESS_PORT= -# This was inserted by `prisma init`: -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema +# services +# SOAP +SOAP_URL= +SOAP_WS_URL= +SOAP_API_KEY= -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -APP_PORT=3001 -DB_HOST=localhost -DBMS=postgresql#if you use another dbms, change at prisma/schema.prisma too -DB_USER= -DB_PASSWORD= -DB_NAME= -DB_PORT= -DATABASE_URL="${DBMS}://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public&pgbouncer=true&connection_limit=1&pool_timeout=20" +# PHP +PHP_URL= +PHP_API_KEY= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0dcab8d97e578ac5b0e945a23cade3e8b3cec3cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18.18.2-alpine + +WORKDIR /tonality/tonality-rest + +COPY package*.json . + +RUN npm install + +COPY . . + +RUN npx prisma generate + +CMD ["sh", "-c", "npx prisma migrate deploy && npm run build-start"] diff --git a/README.md b/README.md index 5ff15c90280b95e620d947ebc24871526fe0fc13..3dc475916a66f7df7a9f2ba421883b1cbd1ded57 100644 --- a/README.md +++ b/README.md @@ -1 +1,66 @@ ## REST Service for Tonality +This REST API service is used for the communication of client with other services. + +## Database Schema +| user | +|----------| +| user_id | +| username | +| password | + +Types:\ +"user_id" SERIAL NOT NULL\ +"username" VARCHAR(50) NOT NULL\ +"password" VARCHAR(255) NOT NULL\ +PRIMARY KEY ("user_id") + +| premium_album | +|----------------| +| album_id | +| album_name | +| release_date | +| genre | +| artist | +| cover_filename | + +Types:\ +"album_id" SERIAL NOT NULL\ +"album_name" VARCHAR(255) NOT NULL\ +"release_date" DATE NOT NULL\ +"genre" VARCHAR(255) NOT NULL\ +"artist" VARCHAR(255) NOT NULL\ +"cover_filename" VARCHAR(255) NOT NULL\ +PRIMARY KEY ("album_id") + +| premium_song | +|-----------------| +| song_id | +| album_id | +| title | +| artist | +| song_number | +| disc_number | +| duration | +| audio_filename | + +Types:\ +"song_id" SERIAL NOT NULL\ +"album_id" INTEGER NOT NULL\ +"title" VARCHAR(255) NOT NULL\ +"artist" VARCHAR(255) NOT NULL\ +"song_number" SMALLINT NOT NULL\ +"disc_number" SMALLINT\ +"duration" INTEGER NOT NULL\ +"audio_filename" VARCHAR(255) NOT NULL\ +PRIMARY KEY ("song_id") + +## API Endpoints +See [the routers](./src/routers) + +## Task Distribution +| Task | Student ID | +|--------------------------------|------------| +| Authentication & Authorization | 13521096 | +| Albums & Songs | 13521063 | +| Subscriptions | 13521087 | +| Environment & Build | 13521096 | \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..acc096f1705dbd0aa3f38ba7bb687f999c1026e5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,210 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +/** @type {import('jest').Config} */ +const config = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ["**/__tests__/**/*.ts?(x)", "**/?(*.)+(test).ts?(x)"], + transform: { + "^.+\\.ts$": "ts-jest", + }, + transformIgnorePatterns: [ + "/node_modules/(?![@autofiy/autofiyable|@autofiy/property]).+\\.js$", + "/node_modules/(?![@autofiy/autofiyable|@autofiy/property]).+\\.ts$", + "/node_modules/(?![@autofiy/autofiyable|@autofiy/property]).+\\.tsx$", + ], + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "C:\\Users\\ASUS\\AppData\\Local\\Temp\\jest", + + // Automatically clear mock calls, instances, contexts and results before every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + // coverageDirectory: undefined, + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "<rootDir>" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "\\\\node_modules\\\\" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "\\\\node_modules\\\\", + // "\\.pnp\\.[^\\\\]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +// eslint-disable-next-line no-undef +module.exports = config; diff --git a/package-lock.json b/package-lock.json index ae48e9eafe957dd6e92f8d43ea6217c859e8be71..0a3ef585a45153cee08c1bc639faf5d3db8b14bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,25 +10,39 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.4.2", - "@types/bcrypt": "^5.0.1", - "@types/jsonwebtoken": "^9.0.4", - "bcrypt": "^5.1.1", + "@types/multer": "^1.4.10", + "argon2": "^0.31.1", + "axios": "^1.6.1", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.17.1", - "ts-node": "^10.9.1" + "fast-xml-parser": "^4.3.2", + "http-status-codes": "2.3.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "zod": "^3.22.4" }, "devDependencies": { + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.16", "@types/express": "^4.17.1", "@types/jest": "^29.5.6", + "@types/jsonwebtoken": "^9.0.4", "@types/supertest": "^2.0.15", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", "eslint": "^8.52.0", "jest": "^29.7.0", "prisma": "^5.4.2", "supertest": "^6.3.3", - "typescript": "^5.2.2", - "zod": "^3.22.4" + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -773,6 +787,14 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -784,6 +806,16 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1434,6 +1466,14 @@ "node": ">= 8" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@prisma/client": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.4.2.tgz", @@ -1551,19 +1591,10 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/bcrypt": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.1.tgz", - "integrity": "sha512-dIIrEsLV1/v0AUNI8oHMaRRTSeVjoy5ID8oclJavtPj8CwPJoD1eFoNXEypuu6k091brEzBeOo3LlxeAH9zRZg==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.4", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1573,22 +1604,38 @@ "version": "3.4.37", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", - "dev": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.3.tgz", "integrity": "sha512-LZ8SD3LpNmLMDLkG2oCBjZg+ETnx6XdCjydUE0HwojDmnDfDUnhMKKbtth1TZh+hzcqb03azrYWoXLS8sMXdqg==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", + "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.1.tgz", "integrity": "sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -1599,7 +1646,6 @@ "version": "4.17.39", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1619,8 +1665,7 @@ "node_modules/@types/http-errors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", - "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==", - "dev": true + "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.5", @@ -1666,6 +1711,7 @@ "version": "9.0.4", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.4.tgz", "integrity": "sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1673,8 +1719,15 @@ "node_modules/@types/mime": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", - "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==", - "dev": true + "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==" + }, + "node_modules/@types/multer": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.10.tgz", + "integrity": "sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==", + "dependencies": { + "@types/express": "*" + } }, "node_modules/@types/node": { "version": "20.8.7", @@ -1687,14 +1740,12 @@ "node_modules/@types/qs": { "version": "6.9.9", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", - "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==", - "dev": true + "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==" }, "node_modules/@types/range-parser": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", - "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", - "dev": true + "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==" }, "node_modules/@types/semver": { "version": "7.5.4", @@ -1706,7 +1757,6 @@ "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1716,7 +1766,6 @@ "version": "1.15.4", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -1748,6 +1797,17 @@ "@types/superagent": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.4.tgz", + "integrity": "sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==" + }, + "node_modules/@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.29", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", @@ -2197,7 +2257,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2206,6 +2265,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -2228,6 +2292,20 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, + "node_modules/argon2": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.1.tgz", + "integrity": "sha512-ik2xnJrLXazya7m4Nz1XfBSRjXj8Koq8qF9PsQC8059p20ifWc9zx/hgU3ItZh/3TnwXkv0RbhvjodPkmFf0bg==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "@phc/format": "^1.0.0", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2254,11 +2332,25 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -2381,17 +2473,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "engines": { - "node": ">= 10.0.0" + "node": ">=8" } }, "node_modules/body-parser": { @@ -2427,7 +2514,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -2467,6 +2553,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2476,11 +2574,26 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } }, "node_modules/bytes": { "version": "3.1.0", @@ -2567,6 +2680,43 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -2626,6 +2776,15 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2641,8 +2800,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/color-support": { "version": "1.1.3", @@ -2652,11 +2819,32 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2675,6 +2863,42 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -2713,6 +2937,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -2724,6 +2968,23 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2819,7 +3080,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2921,6 +3181,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2949,6 +3217,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -3318,6 +3591,27 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -3336,6 +3630,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3352,7 +3651,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3413,11 +3711,34 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3504,7 +3825,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -3778,6 +4098,11 @@ "node": ">= 0.6" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3840,6 +4165,11 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3912,6 +4242,17 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -3928,7 +4269,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3954,7 +4294,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3966,7 +4305,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3984,7 +4322,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -3992,6 +4329,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4723,6 +5065,51 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4741,6 +5128,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4784,12 +5176,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4936,6 +5390,14 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -4983,6 +5445,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4998,9 +5488,9 @@ } }, "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" }, "node_modules/node-fetch": { "version": "2.7.0", @@ -5033,6 +5523,65 @@ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -5051,7 +5600,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5115,6 +5663,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -5280,7 +5836,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -5412,6 +5967,11 @@ "node": ">=16.13" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5437,6 +5997,16 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -5531,6 +6101,17 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5648,6 +6229,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5774,6 +6363,30 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5814,6 +6427,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -5843,6 +6464,14 @@ "node": ">= 0.6" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5937,6 +6566,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/superagent": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", @@ -6075,6 +6709,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6100,7 +6739,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -6116,11 +6754,44 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/touch/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6133,6 +6804,49 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -6220,6 +6934,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -6232,6 +6951,11 @@ "node": ">=14.17" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, "node_modules/undici-types": { "version": "5.25.3", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", @@ -6297,6 +7021,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6380,6 +7116,40 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/winston": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", + "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -6415,6 +7185,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6480,7 +7258,6 @@ "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2241b0cdc133c0fbb78b9a5dc37fd3b129ce235f..af251a1a79dc7da1cb19e0ccc4cb15ee3aa8d2ea 100644 --- a/package.json +++ b/package.json @@ -2,35 +2,56 @@ "name": "tonality-rest", "version": "1.0.0", "description": "", - "main": "dist/app.js", + "main": "dist/src/cores/app.js", "scripts": { - "start": "tsc && node dist/app.js", + "dev": "nodemon src/cores/app.ts", + "migrate:dev": "prisma migrate dev --preview-feature", + "start": "node dist/src/cores/app.js", + "build": "tsc", "lint": "eslint . --ext .ts", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest --runInBand --forceExit --detectOpenHandles --coverage", + "build-start": "npm run build && npm run start" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.16", "@types/express": "^4.17.1", "@types/jest": "^29.5.6", + "@types/jsonwebtoken": "^9.0.4", "@types/supertest": "^2.0.15", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", "eslint": "^8.52.0", "jest": "^29.7.0", "prisma": "^5.4.2", "supertest": "^6.3.3", - "typescript": "^5.2.2", - "zod": "^3.22.4" + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" }, "dependencies": { "@prisma/client": "^5.4.2", - "@types/bcrypt": "^5.0.1", - "@types/jsonwebtoken": "^9.0.4", - "bcrypt": "^5.1.1", + "@types/multer": "^1.4.10", + "argon2": "^0.31.1", + "axios": "^1.6.1", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.17.1", - "ts-node": "^10.9.1" + "fast-xml-parser": "^4.3.2", + "http-status-codes": "2.3.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "zod": "^3.22.4" } } diff --git a/prisma/migrations/20231031075205_init/migration.sql b/prisma/migrations/20231031075205_init/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..241bdde062db877af727a9a38f06aac64d19f968 --- /dev/null +++ b/prisma/migrations/20231031075205_init/migration.sql @@ -0,0 +1,40 @@ +-- CreateTable +CREATE TABLE "user" ( + "user_id" SERIAL NOT NULL, + "username" VARCHAR(50) NOT NULL, + "password" VARCHAR(255) NOT NULL, + + CONSTRAINT "user_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "premium_album" ( + "album_id" SERIAL NOT NULL, + "album_name" VARCHAR(255) NOT NULL, + "release_date" DATE NOT NULL, + "genre" VARCHAR(255) NOT NULL, + "artist" VARCHAR(255) NOT NULL, + "cover_filename" VARCHAR(255) NOT NULL, + + CONSTRAINT "premium_album_pkey" PRIMARY KEY ("album_id") +); + +-- CreateTable +CREATE TABLE "premium_song" ( + "song_id" SERIAL NOT NULL, + "album_id" INTEGER NOT NULL, + "title" VARCHAR(255) NOT NULL, + "artist" VARCHAR(255) NOT NULL, + "song_number" SMALLINT NOT NULL, + "disc_number" SMALLINT, + "duration" INTEGER NOT NULL, + "audio_filename" VARCHAR(255) NOT NULL, + + CONSTRAINT "premium_song_pkey" PRIMARY KEY ("song_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_username_key" ON "user"("username"); + +-- AddForeignKey +ALTER TABLE "premium_song" ADD CONSTRAINT "premium_song_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "premium_album"("album_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000000000000000000000000000000000..fbffa92c2bb7c748d6fc78f9f9dcac604dabb87d --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 843f6bdd02c83895f00752a8410a01f66d6bd05d..0ad09b2d862954593af1a99eeef8cf2ccf814f91 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,3 +10,36 @@ datasource db { url = env("DATABASE_URL") } +model User { + userId Int @id @default(autoincrement()) @map("user_id") + username String @unique @db.VarChar(50) + password String @db.VarChar(255) + + @@map("user") +} + +model PremiumAlbum { + albumId Int @id @default(autoincrement()) @map("album_id") + albumName String @map("album_name") @db.VarChar(255) + releaseDate DateTime @map("release_date") @db.Date + genre String @db.VarChar(255) + artist String @db.VarChar(255) + coverFilename String @map("cover_filename") @db.VarChar(255) + songs PremiumSong[] + + @@map("premium_album") +} + +model PremiumSong { + songId Int @id @default(autoincrement()) @map("song_id") + albumId Int @map("album_id") + title String @db.VarChar(255) + artist String @db.VarChar(255) + songNumber Int @map("song_number") @db.SmallInt + discNumber Int? @map("disc_number") @db.SmallInt + duration Int + audioFilename String @map("audio_filename") @db.VarChar(255) + PremiumAlbum PremiumAlbum @relation(fields: [albumId], references: [albumId], onUpdate: Cascade, onDelete: Cascade) + + @@map("premium_song") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index ce36fc4164e7b227cd6812a4b04916332235a556..0000000000000000000000000000000000000000 --- a/src/app.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express from 'express'; -import {z} from 'zod'; -import dotenv from 'dotenv'; - -dotenv.config(); -const app = express(); -const port = process.env.APP_PORT; -app.get('/', (req, res) => { - res.send( - { - message: 'Hello World!', - } - ); -}); - -app.listen(port, () => { - return console.log(`Express is listening at http://localhost:${port}`); -}); diff --git a/src/clients/php-client.ts b/src/clients/php-client.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f34e9e1fdd3bc773b506b5d6a658125009be9e6 --- /dev/null +++ b/src/clients/php-client.ts @@ -0,0 +1,55 @@ +import axios from "axios"; + +const phpClient = async (url : string, method : string, data : any, is_multipart_form_data : boolean = false) : Promise<object> => { + const responseData : string = await phpRequest(url, method, data, is_multipart_form_data); + return phpResponseDataParser(responseData); +} + +const phpRequest = async ( + url : string, + method : string, + data : any, + is_multipart_form_data : boolean, +) : Promise<string> => { + if (method === "GET") { + const response = await axios.get( + url, + { + params: data, + } + ); + return response.data as string; + } + else if (method === "POST") { + const formData = new FormData(); + if (is_multipart_form_data) { + // only accept 1 file + Object.keys(data).forEach((key) => { + (key !== "file") && formData.append(key, data[key]); + }); + const blobFile = new Blob([data.file.buffer]); + formData.append("rest-file", blobFile, data.file.filename) + } + + const response = await axios.post( + url, + is_multipart_form_data ? formData : data, + { + headers: { + "Content-Type": is_multipart_form_data ? "multipart/form-data" : "application/json", + "X-API-KEY": process.env.PHP_API_KEY + } + } + ); + return response.data as string; + } + else { + throw new Error("Invalid method"); + } +} + +const phpResponseDataParser = (data : string) : object => { + return JSON.parse(data); +} + +export default phpClient; \ No newline at end of file diff --git a/src/clients/soap-client.ts b/src/clients/soap-client.ts new file mode 100644 index 0000000000000000000000000000000000000000..236b3321cb20dca1628561dc04de700d17caa997 --- /dev/null +++ b/src/clients/soap-client.ts @@ -0,0 +1,75 @@ +import axios from "axios"; +import {XMLBuilder, XMLParser} from "fast-xml-parser"; + +const xmlOptions = { + ignoreAttributes: false, + attributeNamePrefix: "@_", +} + +const xmlBuilder = new XMLBuilder(xmlOptions); +const xmlParser = new XMLParser(); + +const soapClient = async (url : string, ws_url : string, function_name : string, data : object) : Promise<object> => { + const responseData : string = await soapRequest(url, ws_url, function_name, data); + return soapResponseDataParser(responseData); +} + +const soapRequest = async ( + url : string, + ws_url : string, + function_name : string, + data : object +) : Promise<string> => { + const envelope = { + "?xml": { + "@_version": "1.0", + "@_encoding": "utf-8", + }, + "Envelope": { + "@_xmlns": "http://schemas.xmlsoap.org/soap/envelope/", + // "Header": {}, + "Body": { + [function_name]: { + "@_xmlns": ws_url, + ...soapRequestDataBuilder(data), + } + }, + } + } + + const xmlRequest : string = xmlBuilder.build(envelope) as string; + const response = await axios.post( + url, + xmlRequest, + { + headers: { + "Content-Type": "text/xml", + "SOAPAction": "#POST", + "X-API-Key": process.env.SOAP_API_KEY, + Accept: "*/*", + }, + }); + return response.data as string; +} + +const soapRequestDataBuilder = (data : object) : object => { + const soapData = {} + for (const [key, value ] of Object.entries(data)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + soapData[key] = { + "@_xmlns": "", + "#text": value, + } + } + return soapData; +} + +const soapResponseDataParser = (data : string) : object => { + + const parsedData = xmlParser.parse(data); + const parsedBody = parsedData["S:Envelope"]["S:Body"] || parsedData["soap:Envelope"]["soap:Body"]; + return parsedBody[Object.keys(parsedBody)[0]]; +} + +export { soapClient } \ No newline at end of file diff --git a/src/controllers/auth-controller.ts b/src/controllers/auth-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb1044b670700a2e36f32f12d880da02f2653b1f --- /dev/null +++ b/src/controllers/auth-controller.ts @@ -0,0 +1,60 @@ +import * as AuthService from "../services/auth-service"; +import { NextFunction, Request, Response } from "express"; +import { generateResponse } from "../utils/response"; +import { StatusCodes } from "http-status-codes"; + +const signup = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const createdUser = await AuthService.signup(req.body); + generateResponse(res, StatusCodes.OK, createdUser); + } catch (err) { + next(err); + } +}; + +// The storage of tokens on the client side follows the recommendations provided by OWASP +// https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-storage-on-client-side +const login = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + console.log(req.body) + const accessTokenAndFingerPrint = await AuthService.login(req.body); + setFingerprintCookie(res, accessTokenAndFingerPrint.fingerprint); + generateResponse(res, StatusCodes.OK, {accessToken: accessTokenAndFingerPrint.accessToken}); + } catch (err) { + next(err); + } +}; + +const setFingerprintCookie = ( + res: Response, + fingerprint: string, +): void => { + res.cookie("__Secure-fingerprint", fingerprint, { + maxAge: 60 * 60, // 60 minutes max age (same as access token expiry) + httpOnly: true, + secure: true, + }); +}; + +const isUsernameAvailable = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const isUsernameAvailable = await AuthService.isUsernameAvailable(req.body); + generateResponse(res, StatusCodes.OK, isUsernameAvailable); + } catch (err) { + next(err) + } +} + +export { signup, login, isUsernameAvailable }; diff --git a/src/controllers/premium-album-controller.ts b/src/controllers/premium-album-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..bddbdef5dddfef6271819716bf9e906783f9ae18 --- /dev/null +++ b/src/controllers/premium-album-controller.ts @@ -0,0 +1,172 @@ +import { NextFunction, Request, Response } from "express"; +import * as PremiumAlbumService from "../services/premium-album-service"; +import { generateResponse } from "../utils/response"; +import { StatusCodes } from "http-status-codes"; +import {ErrorType, StandardError} from "../errors/standard-error"; +import phpClient from "../clients/php-client"; +import * as path from "path"; +import {v4 as uuidv4} from 'uuid'; +import saveFile from "../utils/file-processing"; + +const createPremiumAlbum = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const data = req.body; + if (!req.file) { + throw new StandardError(ErrorType.FILE_NOT_VALID); + } + // data.coverFilename = req.file.filename; + data.coverFilename = uuidv4() + path.extname(req.file.originalname); + const responseData = await PremiumAlbumService.createPremiumAlbum(data); + await phpClient( + process.env.PHP_URL + "upload", + "POST", + { + file : { + filename: data.coverFilename, + buffer: req.file.buffer + } + }, + true + ) + await saveFile(req.file, data.coverFilename) + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +}; + +const getPremiumAlbumById = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const premiumAlbumId = Number(req.params.premiumAlbumId); + const responseData = await PremiumAlbumService.getPremiumAlbumById( + premiumAlbumId, + ); + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +} + +const searchPremiumAlbum = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const allPremiumAlbum = await PremiumAlbumService.searchPremiumAlbum( + { + size: req.query.size ? Number(req.query.size) : undefined, + page: req.query.page ? Number(req.query.page) : undefined, + searchQuery: req.query.searchQuery ? String(req.query.searchQuery) : undefined, + } + ); + generateResponse(res, StatusCodes.OK, allPremiumAlbum); + } catch (err) { + next(err); + } +}; + +const searchPremiumAlbumOwned = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const allPremiumAlbum = await PremiumAlbumService.searchPremiumAlbumOwned( + { + size: req.query.size ? Number(req.query.size) : undefined, + page: req.query.page ? Number(req.query.page) : undefined, + searchQuery: req.query.searchQuery ? String(req.query.searchQuery) : undefined, + premiumAlbumIds: JSON.parse(<string>req.query.premiumAlbumIds, (key, value) => { + if (key === "") return value; + return typeof value === "string" ? Number(value) : value; + }), + }, + ); + generateResponse(res, StatusCodes.OK, allPremiumAlbum); + } catch (err) { + next(err); + } +} + +const updatePremiumAlbum = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const premiumAlbumId = Number(req.params.premiumAlbumId); + const data = req.body; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (req.files && req.files[0]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data.coverFilename = uuidv4() + path.extname(req.files[0].originalname); + } + + const updatedPremiumAlbum = await PremiumAlbumService.updatePremiumAlbum( + data, + premiumAlbumId, + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (req.files && req.files[0]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await phpClient( + process.env.PHP_URL + "upload", + "POST", + { + file : { + filename: data.coverFilename, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + buffer: req.files[0].buffer + } + }, + true + ) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await saveFile(req.files[0], data.coverFilename) + } + generateResponse(res, StatusCodes.OK, updatedPremiumAlbum); + } catch (err) { + next(err); + } +}; + +const deletePremiumAlbum = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const premiumAlbumId = Number(req.params.premiumAlbumId); + + const deletedPremiumAlbum = + await PremiumAlbumService.deletePremiumAlbum(premiumAlbumId); + generateResponse(res, StatusCodes.OK, deletedPremiumAlbum); + } catch (err) { + next(err); + } +}; + +export { + createPremiumAlbum, + getPremiumAlbumById, + searchPremiumAlbum, + searchPremiumAlbumOwned, + updatePremiumAlbum, + deletePremiumAlbum, +}; diff --git a/src/controllers/premium-song-controller.ts b/src/controllers/premium-song-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..104df9bbf89699afe78bdeb7233dbb89eb11d847 --- /dev/null +++ b/src/controllers/premium-song-controller.ts @@ -0,0 +1,137 @@ +import { NextFunction, Request, Response } from "express"; +import * as PremiumSongService from "../services/premium-song-services" +import { generateResponse } from "../utils/response"; +import { StatusCodes } from "http-status-codes"; +import {ErrorType, StandardError} from "../errors/standard-error"; +import phpClient from "../clients/php-client"; +import {v4 as uuidv4} from "uuid"; +import path from "path"; +import saveFile from "../utils/file-processing"; + +const addNewSong = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const data = req.body; + const premiumAlbumId = Number(req.params.premiumAlbumId); + if (!req.file) { + throw new StandardError(ErrorType.FILE_NOT_VALID); + } + data.audioFilename = uuidv4() + path.extname(req.file.originalname); + data.songNumber = Number(data.songNumber); + if (data.discNumber) { + data.discNumber = Number(data.discNumber); + } + data.duration = Number(data.duration); + const responseData = await PremiumSongService.addNewSong(data, premiumAlbumId); + await phpClient( + process.env.PHP_URL + "upload", + "POST", + { + file : { + filename: data.audioFilename, + buffer: req.file.buffer + } + }, + true + ) + await saveFile(req.file, data.audioFilename) + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +}; + +const getAllSongFromAlbum = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const premiumAlbumId = Number(req.params.premiumAlbumId); + const responseData = await PremiumSongService.getAllSongFromAlbum(premiumAlbumId); + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +}; + +const updatePremiumSong = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const data = req.body; + const premiumAlbumId = Number(req.params.premiumAlbumId); + const premiumSongId = Number(req.params.premiumSongId); + if (data.songNumber) { + data.songNumber = Number(data.songNumber); + } + if (data.discNumber) { + data.discNumber = Number(data.discNumber); + } + if (data.duration) { + data.duration = Number(data.duration); + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (req.files && req.files[0]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data.audioFilename = uuidv4() + path.extname(req.files[0].originalname); + } + const responseData = await PremiumSongService.updatePremiumSong(data, premiumAlbumId, premiumSongId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (req.files && req.files[0]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await phpClient( + process.env.PHP_URL + "upload", + "POST", + { + file : { + filename: data.audioFilename, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + buffer: req.files[0].buffer + } + }, + true + ) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await saveFile(req.files[0], data.audioFilename) + } + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +}; + +const deletePremiumSong = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const premiumAlbumId = Number(req.params.premiumAlbumId); + const premiumSongId = Number(req.params.premiumSongId); + const responseData = await PremiumSongService.deletePremiumSong(premiumAlbumId, premiumSongId); + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +}; + + +export { + addNewSong, + getAllSongFromAlbum, + updatePremiumSong, + deletePremiumSong, +}; diff --git a/src/controllers/subscription-controller.ts b/src/controllers/subscription-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8eafba279e916200814055139a0dfc275ee4c28 --- /dev/null +++ b/src/controllers/subscription-controller.ts @@ -0,0 +1,46 @@ +import { NextFunction, Request, Response } from "express"; +import * as SubscriptionService from "../services/subscription-service"; +import {generateResponse} from "../utils/response"; +import {StatusCodes} from "http-status-codes"; +import SubscriptionStatus from "../type/subscription-status"; + +const soapUrl = process.env.SOAP_URL + "subscription"; +const soapWSUrl = process.env.SOAP_WS_URL as string + +const updateSubscription = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const data = req.body; + const responseData = await SubscriptionService.updateSubscription(soapUrl, soapWSUrl, data); + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +} + +const searchSubscription = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise<void> => { + try { + const responseData = await SubscriptionService.searchSubscription(soapUrl, soapWSUrl, { + status: req.query.status as SubscriptionStatus, + searchInput: req.query.searchInput as string ?? "", + orderBy: req.query.orderBy as string ?? "", + page: req.query.page ? Number(req.query.page) : 1, + size: req.query.size ? Number(req.query.size) : 15, + }); + generateResponse(res, StatusCodes.OK, responseData); + } catch (err) { + next(err); + } +} + +export { + updateSubscription, + searchSubscription, +} \ No newline at end of file diff --git a/src/cores/app.ts b/src/cores/app.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a3946038c60fa74f52cb601bcfc754e2712b66e --- /dev/null +++ b/src/cores/app.ts @@ -0,0 +1,43 @@ +import express, { Express } from "express"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import dotenv from "dotenv"; +import apiRouter from "../routers/api"; + +dotenv.config(); + +export const app: Express = express(); +const port: string | undefined = process.env.EXPRESS_PORT; + +const allowedOrigin = [ + 'http://localhost:3000', + 'http://localhost:3001', + 'http://localhost:5173', + 'http://localhost:8000', + 'http://localhost:8888', +] + +app.use(cors({ + origin: function (origin, callback) { + if (!origin) return callback(null, true) + if (allowedOrigin.indexOf(origin) === -1) { + const msg = 'The CORS policy for this site does not ' + + 'allow access from the specified Origin.' + return callback(new Error(msg), false) + } + return callback(null, true) + }, + credentials: true +})); + +app.use(express.json()); + +app.use(cookieParser()); +app.use(apiRouter); + +// app.use(express.static(path.join(__dirname, '..', 'storage'))); +app.use(express.static('storage')); + +app.listen(port, () => { + return console.log(`Express is listening at port ${port}`); +}); diff --git a/src/cores/db.ts b/src/cores/db.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4e9782db9353445fc0ab001ca2a7c0c305efd95 --- /dev/null +++ b/src/cores/db.ts @@ -0,0 +1,41 @@ +import { PrismaClient } from "@prisma/client"; +import logger from "./logger"; + +const prismaClient = new PrismaClient({ + log: [ + { + emit: "event", + level: "query", + }, + { + emit: "event", + level: "info", + }, + { + emit: "event", + level: "warn", + }, + { + emit: "event", + level: "error", + }, + ], +}); + +prismaClient.$on("query", (e) => { + logger.info(e); +}); + +prismaClient.$on("info", (e) => { + logger.info(e); +}); + +prismaClient.$on("warn", (e) => { + logger.warn(e); +}); + +prismaClient.$on("error", (e) => { + logger.error(e); +}); + +export default prismaClient; diff --git a/src/cores/logger.ts b/src/cores/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..b280c518fe7cbfe98e29084b35d7738288780832 --- /dev/null +++ b/src/cores/logger.ts @@ -0,0 +1,9 @@ +import winston from "winston"; + +const logger = winston.createLogger({ + level: "info", + format: winston.format.json(), + transports: [new winston.transports.Console({})], +}); + +export default logger; diff --git a/src/errors/standard-error.ts b/src/errors/standard-error.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cbbacd9aaee0574c69ef9efe906bd7d9fc7461c --- /dev/null +++ b/src/errors/standard-error.ts @@ -0,0 +1,122 @@ +import { StatusCodes } from "http-status-codes"; + +enum ErrorType { + USERNAME_ALREADY_EXISTS, + USER_NOT_FOUND, + WRONG_PASSWORD, + PASSWORD_HASH_FAILURE, + PASSWORD_VERIFICATION_FAILURE, + ACCESS_TOKEN_GENERATION_FAILURE, + ACCESS_TOKEN_MISSING, + ACCESS_TOKEN_EXPIRED, + ACCESS_TOKEN_NOT_ACTIVE, + INVALID_SIGNATURE, + AUTHORIZATION_HEADER_NOT_SET, + FINGERPRINT_MISSING, + ALBUM_NOT_FOUND, + SONG_NOT_FOUND, + INPUT_DATA_NOT_VALID, + INVALID_API_KEY, + FILE_NOT_VALID, +} + +class StandardError { + title: string; + status: number; + + constructor(t: ErrorType) { + switch (t) { + + case ErrorType.USERNAME_ALREADY_EXISTS: + this.title = "Username already exists."; + this.status = StatusCodes.BAD_REQUEST; + break; + + case ErrorType.USER_NOT_FOUND: + this.title = "User not found."; + this.status = StatusCodes.BAD_REQUEST; + break; + + case ErrorType.WRONG_PASSWORD: + this.title = "Wrong password." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.PASSWORD_HASH_FAILURE: + this.title = "Failed to hash password." + this.status = StatusCodes.INTERNAL_SERVER_ERROR; + break; + + case ErrorType.PASSWORD_VERIFICATION_FAILURE: + this.title = "Failed to verify password." + this.status = StatusCodes.INTERNAL_SERVER_ERROR; + break; + + case ErrorType.ACCESS_TOKEN_GENERATION_FAILURE: + this.title = "Failed to generate access token." + this.status = StatusCodes.INTERNAL_SERVER_ERROR; + break; + + case ErrorType.ACCESS_TOKEN_MISSING: + this.title = "Your access token is missing." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.ACCESS_TOKEN_EXPIRED: + this.title = "Your access token is expired." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.ACCESS_TOKEN_NOT_ACTIVE: + this.title = "Your access token is not active yet." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.INVALID_SIGNATURE: + this.title = "Your access token is invalid." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.AUTHORIZATION_HEADER_NOT_SET: + this.title = "Authorization header not set." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.FINGERPRINT_MISSING: + this.title = "Fingerprint is missing." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.ALBUM_NOT_FOUND: + this.title = "Album not found." + this.status = StatusCodes.NOT_FOUND; + break; + + case ErrorType.SONG_NOT_FOUND: + this.title = "Song not found" + this.status = StatusCodes.NOT_FOUND; + break; + + case ErrorType.INPUT_DATA_NOT_VALID: + this.title = "Input data is not valid." + this.status = StatusCodes.BAD_REQUEST; + break; + + case ErrorType.INVALID_API_KEY: + this.title = "Your API key is invalid." + this.status = StatusCodes.UNAUTHORIZED; + break; + + case ErrorType.FILE_NOT_VALID: + this.title = "Your File is invalid." + this.status = StatusCodes.BAD_REQUEST; + break; + + default: + this.title = "Unknown error."; + this.status = StatusCodes.INTERNAL_SERVER_ERROR; + } + } +} + +export { StandardError, ErrorType }; diff --git a/src/middlewares/handle-standard-error.ts b/src/middlewares/handle-standard-error.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc0b4af0a7118de75e0f05f4fc3add1e1e463be5 --- /dev/null +++ b/src/middlewares/handle-standard-error.ts @@ -0,0 +1,18 @@ +import { NextFunction, Request, Response } from "express"; +import { StandardError } from "../errors/standard-error"; +import { generateStandardErrorResponse } from "../utils/response"; + +const handleStandardError = ( + err: StandardError | Error, + req: Request, + res: Response, + next: NextFunction, +): void => { + if (!(err instanceof StandardError)) { + next(err); // Pass to the default error-handling middleware (provided by Express) + } else { + generateStandardErrorResponse(res, err); + } +}; + +export { handleStandardError }; diff --git a/src/middlewares/verify-token.ts b/src/middlewares/verify-token.ts new file mode 100644 index 0000000000000000000000000000000000000000..68d69e311b83a454c252995c88c361a46ba554c7 --- /dev/null +++ b/src/middlewares/verify-token.ts @@ -0,0 +1,74 @@ +import { NextFunction, Request, Response } from "express"; +import jwt, {JsonWebTokenError, NotBeforeError, TokenExpiredError} from "jsonwebtoken"; +import { ErrorType, StandardError } from "../errors/standard-error"; +import { hashFingerprint } from "../utils/token"; + +interface TonalityPayload { + uid: number; + usr: string; + fgp: string; + exp: number; + nbf: number; + iss: string; +} + +// Compare fingerprint in the token payload with +// the fingerprint stored in the cookie +const verifyFingerprint = async ( + fingerprint: string, + d: TonalityPayload, +): Promise<boolean> => { + return (await hashFingerprint(fingerprint)) === d.fgp; +}; + +const verifyToken = async (req: Request, res: Response, next: NextFunction) => { + try { + // Authorization Bearer ${accessToken} + const authHeader = req.headers.authorization; + + if (!authHeader) { + throw new StandardError(ErrorType.AUTHORIZATION_HEADER_NOT_SET); + } + + const accessToken = authHeader.split(" ")[1]; + + if (!accessToken) { + throw new StandardError(ErrorType.ACCESS_TOKEN_MISSING); + } + + const fingerprint = req.cookies["Secure-fingerprint"]; + + if (!fingerprint) { + throw new StandardError(ErrorType.FINGERPRINT_MISSING); + } + + const decodedPayload = jwt.verify( + accessToken, + process.env.JWT_SHARED_SECRET as string, + { + algorithms: ["HS256"], + issuer: "Tonality REST Service", + }, + ); + + // Will generate run-time error when the decoded payload is not of the type TonalityPayload + // This is a wanted behavior because we want to validate the payload type + await verifyFingerprint(fingerprint, decodedPayload as TonalityPayload); + + next(); // The token is verified, pass to the next middleware + } catch (error) { + if (error instanceof TokenExpiredError) { + next(new StandardError(ErrorType.ACCESS_TOKEN_EXPIRED)); + } else if (error instanceof NotBeforeError) { + next(new StandardError(ErrorType.ACCESS_TOKEN_NOT_ACTIVE)); + } else if (error instanceof JsonWebTokenError) { + next(new StandardError(ErrorType.INVALID_SIGNATURE)); + } else if (error instanceof StandardError) { + next(error); + } + // unknown error + next(error); + } +}; + +export { verifyToken }; diff --git a/src/routers/api.ts b/src/routers/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6ec41aef6caac6bf2080abb448968fed34c4bbe --- /dev/null +++ b/src/routers/api.ts @@ -0,0 +1,14 @@ +import express from "express"; +import { premiumAlbumRouter } from "./premium-album-router"; +import { authRouter } from "./auth-router"; +import {subscriptionRouter} from "./subscription-router"; +import {premiumSongRouter} from "./premium-song-router"; + +const apiRouter = express.Router(); + +apiRouter.use(authRouter); +apiRouter.use(premiumAlbumRouter); +apiRouter.use(premiumSongRouter); +apiRouter.use(subscriptionRouter); + +export default apiRouter; diff --git a/src/routers/auth-router.ts b/src/routers/auth-router.ts new file mode 100644 index 0000000000000000000000000000000000000000..caead52e784b2ac7fc4e11c6809e5061fd486e0a --- /dev/null +++ b/src/routers/auth-router.ts @@ -0,0 +1,25 @@ +import express, { Router } from "express"; +import * as AuthController from "../controllers/auth-controller"; +import { handleStandardError } from "../middlewares/handle-standard-error"; + +const authRouter: Router = express.Router(); + +authRouter.post( + "/api/signup", + AuthController.signup, + handleStandardError +); + +authRouter.post( + "/api/login", + AuthController.login, + handleStandardError +); + +authRouter.post( + "/api/username-availability", + AuthController.isUsernameAvailable, + handleStandardError +); + +export { authRouter }; diff --git a/src/routers/premium-album-router.ts b/src/routers/premium-album-router.ts new file mode 100644 index 0000000000000000000000000000000000000000..4635ba376cacf0e907b02215d1970475b7356818 --- /dev/null +++ b/src/routers/premium-album-router.ts @@ -0,0 +1,54 @@ +import express, { Router } from "express"; +import * as PremiumAlbumController from "../controllers/premium-album-controller"; +import { handleStandardError } from "../middlewares/handle-standard-error"; +import { verifyToken } from "../middlewares/verify-token"; +import {uploadCover} from "../utils/multer"; + +const premiumAlbumRouter: Router = express.Router(); + +premiumAlbumRouter.post( + "/api/premium-album", + verifyToken, + uploadCover.single("coverFile"), + PremiumAlbumController.createPremiumAlbum, + handleStandardError +); + +premiumAlbumRouter.get( + "api/premium-album/:premiumAlbumId", + verifyToken, + PremiumAlbumController.getPremiumAlbumById, + handleStandardError, +); + + +premiumAlbumRouter.get( + "/api/premium-album", + verifyToken, + PremiumAlbumController.searchPremiumAlbum, + handleStandardError, +); + +premiumAlbumRouter.get( + "api/premium-album-owned", + verifyToken, + PremiumAlbumController.searchPremiumAlbumOwned, + handleStandardError, +) + +premiumAlbumRouter.patch( + "/api/premium-album/:premiumAlbumId", + verifyToken, + uploadCover.any(), + PremiumAlbumController.updatePremiumAlbum, + handleStandardError, +); + +premiumAlbumRouter.delete( + "/api/premium-album/:premiumAlbumId", + verifyToken, + PremiumAlbumController.deletePremiumAlbum, + handleStandardError, +); + +export { premiumAlbumRouter }; diff --git a/src/routers/premium-song-router.ts b/src/routers/premium-song-router.ts new file mode 100644 index 0000000000000000000000000000000000000000..c31161f478162bc16534cb51965bf126ff966d18 --- /dev/null +++ b/src/routers/premium-song-router.ts @@ -0,0 +1,39 @@ +import express, { Router } from "express"; +import * as PremiumSongController from "../controllers/premium-song-controller"; +import { handleStandardError } from "../middlewares/handle-standard-error"; +import { verifyToken } from "../middlewares/verify-token"; +import {uploadSong} from "../utils/multer"; + +const premiumSongRouter: Router = express.Router(); + +premiumSongRouter.post( + "/api/premium-album/:premiumAlbumId", + verifyToken, + uploadSong.single("audioFile"), + PremiumSongController.addNewSong, + handleStandardError, +); + +premiumSongRouter.get( + "/api/premium-album/:premiumAlbumId", + verifyToken, + PremiumSongController.getAllSongFromAlbum, + handleStandardError, +); + +premiumSongRouter.patch( + "/api/premium-album/:premiumAlbumId/:premiumSongId", + verifyToken, + uploadSong.any(), + PremiumSongController.updatePremiumSong, + handleStandardError, +); + +premiumSongRouter.delete( + "/api/premium-album/:premiumAlbumId/:premiumSongId", + verifyToken, + PremiumSongController.deletePremiumSong, + handleStandardError, +); + +export { premiumSongRouter }; diff --git a/src/routers/subscription-router.ts b/src/routers/subscription-router.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cee04ff668ffa4312930128d4eb6650f7450b15 --- /dev/null +++ b/src/routers/subscription-router.ts @@ -0,0 +1,22 @@ +import express, { Router } from "express"; +import * as SubscriptionController from "../controllers/subscription-controller"; +import { handleStandardError } from "../middlewares/handle-standard-error"; +import { verifyToken } from "../middlewares/verify-token"; + +const subscriptionRouter: Router = express.Router(); + +subscriptionRouter.post( + "/api/subscription", + verifyToken, + SubscriptionController.updateSubscription, + handleStandardError, +); + +subscriptionRouter.get( + "/api/subscription", + verifyToken, + SubscriptionController.searchSubscription, + handleStandardError, +); + +export { subscriptionRouter }; \ No newline at end of file diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..24c92585b2e2b6308af56a94fa5335f0872d1494 --- /dev/null +++ b/src/services/auth-service.ts @@ -0,0 +1,92 @@ +import { Prisma } from "@prisma/client"; +import prismaClient from "../cores/db"; + +import { ErrorType, StandardError } from "../errors/standard-error"; +import { hashPassword, isPasswordValid } from "../utils/password"; +import { generateAccessTokenAndFingerprint } from "../utils/token"; +import { validate } from "../validation/validation"; +import { loginSchema, signupSchema } from "../validation/auth-validation"; + +const signup = async ( + data: Prisma.UserCreateInput, +): Promise<{ userId: number; username: string }> => { + validate(signupSchema, data); + + // If username already exists throw error + if ( + (await prismaClient.user.findUnique({ + where: { + username: data.username, + }, + })) !== null + ) { + throw new StandardError(ErrorType.USERNAME_ALREADY_EXISTS); + } + + try { + // Hash the password + const hashedPassword: string = await hashPassword(data.password); + + return await prismaClient.user.create({ + data: { + username: data.username, + password: hashedPassword, + }, + select: { + userId: true, + username: true, + }, + }); + } catch (error) { + throw error; + } +}; + +const login = async (data: { username: string; password: string }) => { + validate(loginSchema, data); + + const user = await prismaClient.user.findUnique({ + where: { + username: data.username, + }, + }); + + // If username not found, throw error + if (user === null) { + throw new StandardError(ErrorType.USER_NOT_FOUND); + } + + // If wrong password, throw error + if (!(await isPasswordValid(user.password, data.password))) { + throw new StandardError(ErrorType.WRONG_PASSWORD); + } + + try { + return await generateAccessTokenAndFingerprint({ + userId: user.userId, + username: user.username, + }); + } catch (error) { + throw error; + } +}; + +const isUsernameAvailable = async (data: { username: string }) => { + const user = await prismaClient.user.findUnique({ + where: { + username: data.username, + } + }) + + if (user !== null) { + return { + usernameAvailable: "false", + } + } else { + return { + usernameAvailable: "true", + } + } +} + +export { signup, login, isUsernameAvailable }; diff --git a/src/services/premium-album-service.ts b/src/services/premium-album-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b0701b15601e05699fcb131d682286b514fe6b6 --- /dev/null +++ b/src/services/premium-album-service.ts @@ -0,0 +1,234 @@ +import {PremiumAlbum, Prisma} from "@prisma/client"; +import prismaClient from "../cores/db"; +import {ErrorType, StandardError} from "../errors/standard-error"; +import {validate} from "../validation/validation"; +import { + createPremiumAlbumSchema, + deletePremiumAlbumSchema, searchPremiumAlbumOwnSchema, + searchPremiumAlbumSchema, + updatePremiumAlbumSchema +} from "../validation/premium-album-validation"; + +const createPremiumAlbum = async ( + data: Prisma.PremiumAlbumCreateInput, +): Promise<PremiumAlbum> => { + validate(createPremiumAlbumSchema, data) + + return prismaClient.premiumAlbum.create({ + data: { + albumName: data.albumName, + releaseDate: data.releaseDate, + genre: data.genre, + artist: data.artist, + coverFilename: data.coverFilename, + }, + }); +}; + +const getPremiumAlbumById = async ( + premiumAlbumId: number, +): Promise<{ data: PremiumAlbum, duration: number }> => { + const album = await prismaClient.premiumAlbum.findUnique({ + where: { + albumId: premiumAlbumId, + }, + }); + + if (!album) { + throw new StandardError(ErrorType.ALBUM_NOT_FOUND); + } + + const albumDuration = await prismaClient.premiumSong.aggregate({ + where: { + albumId: premiumAlbumId + }, + _sum: { + duration: true + } + }) + + return { + data : album, + duration: albumDuration._sum.duration ?? 0 + }; +}; + +const searchPremiumAlbum = async (reqQuery: { + size: number | undefined; + page: number | undefined; + searchQuery: string | undefined; +}) => { + validate(searchPremiumAlbumSchema, reqQuery) + + const skip: number = ((reqQuery.page ?? 1) - 1) * (reqQuery.size ?? 10); + + const filters = []; + + if (reqQuery.searchQuery) { + filters.push({ + OR: [ + { + albumName: { + contains: reqQuery.searchQuery, + }, + }, + { + artist: { + contains: reqQuery.searchQuery, + }, + }, + ], + }); + } + + const albums = await prismaClient.premiumAlbum.findMany({ + where: { + AND: filters, + }, + take: reqQuery.size ?? 10, + skip: skip, + }); + + const totalAlbums = await prismaClient.premiumAlbum.count({ + where: { + AND: filters, + }, + }); + + return { + data: albums, + paging: { + page: reqQuery.page ?? 1, + totalAlbums: totalAlbums, + totalPages: Math.ceil(totalAlbums / (reqQuery.size ?? 10)), + }, + }; +}; + +const searchPremiumAlbumOwned = async (reqQuery: { + size: number | undefined; + page: number | undefined; + searchQuery: string | undefined; + premiumAlbumIds : number[] +}) => { + validate(searchPremiumAlbumOwnSchema, reqQuery) + + const skip: number = ((reqQuery.page ?? 1) - 1) * (reqQuery.size ?? 10); + + const filters = []; + + if (reqQuery.searchQuery) { + filters.push({ + OR: [ + { + albumName: { + contains: reqQuery.searchQuery, + }, + }, + { + artist: { + contains: reqQuery.searchQuery, + }, + }, + ], + }); + } + + + const albums = await prismaClient.premiumAlbum.findMany({ + where: { + AND: [ + { + albumId: { + in: reqQuery.premiumAlbumIds + } + }, + { + AND: filters, + } + ] + }, + take: reqQuery.size ?? 10, + skip: skip, + }); + + const totalAlbums = await prismaClient.premiumAlbum.count({ + where: { + AND: [ + { + albumId: { + in: reqQuery.premiumAlbumIds + } + }, + { + AND: filters, + } + ] + }, + }); + + return { + data: albums, + paging: { + page: reqQuery.page ?? 1, + totalAlbums: totalAlbums, + totalPages: Math.ceil(totalAlbums / (reqQuery.size ?? 10)), + }, + }; +}; + + +const updatePremiumAlbum = async ( + inputData: Prisma.PremiumAlbumUpdateInput, + premiumAlbumId: number, +): Promise<PremiumAlbum> => { + validate(updatePremiumAlbumSchema, { premiumAlbumId, ...inputData}) + + const albumCount = await prismaClient.premiumAlbum.count({ + where: { + albumId: premiumAlbumId, + }, + }); + + if (albumCount !== 1) { + throw new StandardError(ErrorType.ALBUM_NOT_FOUND); + } + + return prismaClient.premiumAlbum.update({ + where: { + albumId: premiumAlbumId, + }, + data: inputData, + }); +}; + +const deletePremiumAlbum = async ( + premiumAlbumId: number, +): Promise<PremiumAlbum> => { + validate(deletePremiumAlbumSchema, { premiumAlbumId }) + + const albumCount = await prismaClient.premiumAlbum.count({ + where: { + albumId: premiumAlbumId, + }, + }); + + if (albumCount !== 1) { + throw new StandardError(ErrorType.ALBUM_NOT_FOUND); + } + + return prismaClient.premiumAlbum.delete({ + where: { + albumId: premiumAlbumId, + }, + }); +}; + +export { + createPremiumAlbum, + getPremiumAlbumById, + searchPremiumAlbum, + searchPremiumAlbumOwned, + updatePremiumAlbum, + deletePremiumAlbum, +}; diff --git a/src/services/premium-song-services.ts b/src/services/premium-song-services.ts new file mode 100644 index 0000000000000000000000000000000000000000..41c8e221b4f197c8ebba883920e480a8188f17bc --- /dev/null +++ b/src/services/premium-song-services.ts @@ -0,0 +1,97 @@ +import { PremiumSong, Prisma } from "@prisma/client"; +import prismaClient from "../cores/db"; +import { ErrorType, StandardError } from "../errors/standard-error"; +import {validate} from "../validation/validation"; +import { + addNewSongSchema, deletePremiumSongSchema, + getAllSongFromAlbumSchema, updatePremiumSongSchema +} from "../validation/premium-song-validation"; + +const addNewSong = async ( + data: Prisma.PremiumSongCreateInput, + premiumAlbumId: number +): Promise<PremiumSong> => { + validate(addNewSongSchema, { premiumAlbumId, ...data}); + + return prismaClient.premiumSong.create({ + data: { + albumId: premiumAlbumId, + title: data.title, + artist: data.artist, + songNumber: data.songNumber, + discNumber: data.discNumber, + duration: data.duration, + audioFilename: data.audioFilename + } + }) +}; + +const getAllSongFromAlbum = async ( + premiumAlbumId: number +): Promise<PremiumSong[]> => { + validate(getAllSongFromAlbumSchema, { premiumAlbumId }) + return prismaClient.premiumSong.findMany({ + where: { + albumId: premiumAlbumId + } + }) +} + +const updatePremiumSong = async ( + inputData: Prisma.PremiumSongUpdateInput, + premiumAlbumId: number, + premiumSongId: number +): Promise<PremiumSong> => { + validate(updatePremiumSongSchema, { premiumAlbumId, premiumSongId, ...inputData }) + + const songCount = await prismaClient.premiumSong.count({ + where: { + songId: premiumSongId, + albumId: premiumAlbumId + } + }); + + if (songCount != 1) { + throw new StandardError(ErrorType.SONG_NOT_FOUND); + } + + return prismaClient.premiumSong.update({ + where: { + songId: premiumSongId, + albumId: premiumAlbumId + }, + data: inputData + }) +}; + +const deletePremiumSong = async ( + premiumAlbumId: number, + premiumSongId: number +): Promise<PremiumSong> => { + validate(deletePremiumSongSchema, { premiumAlbumId }) + + const songCount = await prismaClient.premiumSong.count({ + where: { + songId: premiumSongId, + albumId: premiumAlbumId + } + }) + + if (songCount != 1) { + throw new StandardError(ErrorType.SONG_NOT_FOUND); + } + + return prismaClient.premiumSong.delete({ + where: { + songId: premiumSongId, + albumId: premiumAlbumId + } + }) +}; + +export { + addNewSong, + getAllSongFromAlbum, + updatePremiumSong, + deletePremiumSong, +}; diff --git a/src/services/subscription-service.ts b/src/services/subscription-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7c579351dd124dd55ffabfd3e7dc4003f53e116 --- /dev/null +++ b/src/services/subscription-service.ts @@ -0,0 +1,37 @@ +import {soapClient} from "../clients/soap-client"; +import SubscriptionStatus from "../type/subscription-status"; +import {validate} from "../validation/validation"; +import {searchSubscriptionSchema, updateSubscriptionSchema} from "../validation/subscription-validation"; + +const updateSubscription = async ( + url : string, + ws_url : string, + data : { + userId: number; + albumId: number; + status: string; + }, +): Promise<object> => { + validate(updateSubscriptionSchema, data) + return soapClient(url, ws_url, "updateSubscription", data) +}; + +const searchSubscription = async ( + url : string, + ws_url : string, + data : { + status: SubscriptionStatus; + searchInput?: string; + orderBy?: string; + size?: number; + page?: number; + }, +): Promise<object> => { + validate(searchSubscriptionSchema, data) + return soapClient(url, ws_url, "searchSubscription", data) +} + +export { + updateSubscription, + searchSubscription, +} diff --git a/src/type/subscription-status.ts b/src/type/subscription-status.ts new file mode 100644 index 0000000000000000000000000000000000000000..44eb1178182834e55ab319f10714eb834aee480f --- /dev/null +++ b/src/type/subscription-status.ts @@ -0,0 +1,8 @@ +enum SubscriptionStatus { + PENDING = 'PENDING', + ACTIVE = 'ACTIVE', + CANCELLED = 'CANCELLED', + EXPIRED = 'EXPIRED', +} + +export default SubscriptionStatus; \ No newline at end of file diff --git a/src/utils/file-processing.ts b/src/utils/file-processing.ts new file mode 100644 index 0000000000000000000000000000000000000000..a459c42d2d2f86900ebcb0929fdd05220fad7624 --- /dev/null +++ b/src/utils/file-processing.ts @@ -0,0 +1,13 @@ +import fs from "fs"; + +const saveFile = async (file : Express.Multer.File, path : string) => { + // save to './storage' folder + fs.writeFile("./storage/" + path, file.buffer, (err) => { + if (err) { + console.log(err); + throw err; + } + }); +} + +export default saveFile; \ No newline at end of file diff --git a/src/utils/multer.ts b/src/utils/multer.ts new file mode 100644 index 0000000000000000000000000000000000000000..897cc537addedef78729138a493744817b47f73c --- /dev/null +++ b/src/utils/multer.ts @@ -0,0 +1,38 @@ +import multer from "multer"; + +// https://github.com/expressjs/multer +const storage = multer.memoryStorage(); + +const uploadCover = multer({ + storage: storage, + limits: { + fileSize: 1024 * 1024 * 10 // 10MB + }, + fileFilter: function (req, file, cb) { + if (file.mimetype === "image/png" || file.mimetype === "image/jpeg" || file.mimetype === "image/jpg") { + cb(null, true) + } else { + cb(null, false) + return cb(new Error('Only .png, .jpg and .jpeg format allowed!')) + } + } +}); + +const uploadSong = multer({ + storage: storage, + limits: { + fileSize: 1024 * 1024 * 10 // 10MB + }, + fileFilter: function (req, file, cb) { + if (file.mimetype === "audio/mpeg") { + cb(null, true) + } else { + return cb(new Error('Only .mp3 format allowed!')) + } + } +}); + +export { + uploadCover, + uploadSong, +} \ No newline at end of file diff --git a/src/utils/password.ts b/src/utils/password.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eda1b60ac08cad019de1794028f24a1f224987a --- /dev/null +++ b/src/utils/password.ts @@ -0,0 +1,23 @@ +import * as argon2 from "argon2"; +import { ErrorType, StandardError } from "../errors/standard-error"; + +const hashPassword = async (plainPassword: string): Promise<string> => { + try { + return await argon2.hash(plainPassword); + } catch (error) { + throw new StandardError(ErrorType.PASSWORD_HASH_FAILURE); + } +}; + +const isPasswordValid = async ( + hashedPassword: string, + plainPassword: string, +): Promise<boolean> => { + try { + return argon2.verify(hashedPassword, plainPassword); + } catch (error) { + throw new StandardError(ErrorType.PASSWORD_VERIFICATION_FAILURE); + } +}; + +export { hashPassword, isPasswordValid }; diff --git a/src/utils/response.ts b/src/utils/response.ts new file mode 100644 index 0000000000000000000000000000000000000000..e190bb9b74905d488801f39d798b06e7f607f8b2 --- /dev/null +++ b/src/utils/response.ts @@ -0,0 +1,24 @@ +import { Response } from "express"; +import { StatusCodes } from "http-status-codes"; +import { StandardError } from "../errors/standard-error"; + +const generateResponse = ( + res: Response, + status: StatusCodes, + data?: any, +): void => { + if (!data) { + res.status(status).json(); + return; + } + res.status(status).json(data); +}; + +const generateStandardErrorResponse = ( + res: Response, + e: StandardError, +): void => { + res.status(e.status).json(e); +}; + +export { generateResponse, generateStandardErrorResponse }; diff --git a/src/utils/token.ts b/src/utils/token.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6162c1a7e5bd0550077278f84329b0b6d5dee7e --- /dev/null +++ b/src/utils/token.ts @@ -0,0 +1,46 @@ +import jwt from "jsonwebtoken"; +import { ErrorType, StandardError } from "../errors/standard-error"; +import * as crypto from "crypto"; + +const generateFingerprint = async (numberOfBytes: number): Promise<string> => { + const randomBytes: Buffer = crypto.randomBytes(numberOfBytes); + return randomBytes.toString("hex"); +}; + +const hashFingerprint = async (fgp: string): Promise<string> => { + const hash: crypto.Hash = crypto.createHash("sha256"); + hash.update(fgp, "utf8"); + return hash.digest("hex"); +}; + +const generateAccessTokenAndFingerprint = async (data: { + userId: number, + username: string, +}) => { + try { + const fingerprint: string = await generateFingerprint(64); + const hashedFingerprint: string = await hashFingerprint(fingerprint); + + return { + accessToken: jwt.sign( + { + uid: data.userId, + usr: data.username, + fgp: hashedFingerprint, // User hashed fingerprint + }, + process.env.JWT_SHARED_SECRET as string, + { + algorithm: "HS256", // Only use HS256 to generate JWTs + expiresIn: "60m", // The token expires 60 minutes + notBefore: "0ms", // The token is valid right away + issuer: "Tonality REST Service", + }, + ), + fingerprint: fingerprint, + }; + } catch (error) { + throw new StandardError(ErrorType.ACCESS_TOKEN_GENERATION_FAILURE); + } +}; + +export { generateAccessTokenAndFingerprint, hashFingerprint }; diff --git a/src/validation/auth-validation.ts b/src/validation/auth-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..46900552460aa1eb670c67392c04d9566eadb3ed --- /dev/null +++ b/src/validation/auth-validation.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +const signupSchema = z.object({ + username: z.string().min(1).max(255), + password: z.string().min(1).max(255), +}); + +const loginSchema = z.object({ + username: z.string().min(1).max(255), + password: z.string().min(1).max(255), +}); + +export { + signupSchema, + loginSchema, +} \ No newline at end of file diff --git a/src/validation/premium-album-validation.ts b/src/validation/premium-album-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..173ac5d8bfb77908e81f1b41643224c0549f9474 --- /dev/null +++ b/src/validation/premium-album-validation.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +const createPremiumAlbumSchema = z.object({ + albumName: z.string().min(1).max(255), + releaseDate: z.coerce.date(), + genre: z.string().min(1).max(255), + artist: z.string().min(1).max(255), +}); + +const searchPremiumAlbumSchema = z.object({ + size: z.optional(z.number().int().min(10).max(100)), + page: z.optional(z.number().int().min(1)), + searchQuery: z.optional(z.string().min(1).max(255)), +}); + +const searchPremiumAlbumOwnSchema = z.object({ + size: z.optional(z.number().int().min(10).max(100)), + page: z.optional(z.number().int().min(1)), + searchQuery: z.optional(z.string().min(1).max(255)), + premiumAlbumIds : z.array(z.number().int().min(1)), +}); + +const updatePremiumAlbumSchema = z.object({ + premiumAlbumId: z.number().int().min(1), + albumName: z.optional(z.string().min(1).max(255)), + releaseDate: z.optional(z.coerce.date()), + genre: z.optional(z.string().min(1).max(255)), + artist: z.optional(z.string().min(1).max(255)), +}); + +const deletePremiumAlbumSchema = z.object({ + premiumAlbumId: z.number().int().min(1), +}); + +export { + createPremiumAlbumSchema, + searchPremiumAlbumSchema, + searchPremiumAlbumOwnSchema, + updatePremiumAlbumSchema, + deletePremiumAlbumSchema, +} diff --git a/src/validation/premium-song-validation.ts b/src/validation/premium-song-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..53cf28a424a9148fc868404d4f9c0692ff4e2c5e --- /dev/null +++ b/src/validation/premium-song-validation.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +const addNewSongSchema = z.object({ + premiumAlbumId: z.number().int().min(1), + title: z.string().min(1).max(255), + artist: z.string().min(1).max(255), + discNumber: z.optional(z.number().int().min(1)).nullable(), + songNumber: z.optional(z.number().int().min(1)), + duration: z.optional(z.number().int().min(1)), +}); + +const getAllSongFromAlbumSchema = z.object({ + premiumAlbumId: z.number().int().min(1), +}); + +const updatePremiumSongSchema = z.object({ + premiumAlbumId: z.number().int().min(1), + premiumSongId: z.number().int().min(1), + title: z.optional(z.string().min(1).max(255)), + artist: z.optional(z.string().min(1).max(255)), + discNumber: z.optional(z.number().int().min(1)).nullable(), + songNumber: z.optional(z.number().int().min(1)), + duration: z.optional(z.number().int().min(1)), +}); + +const deletePremiumSongSchema = z.object({ + premiumAlbumId: z.number().int().min(1), + songId: z.number().int().min(1), +}); + +export { + addNewSongSchema, + getAllSongFromAlbumSchema, + updatePremiumSongSchema, + deletePremiumSongSchema, +} \ No newline at end of file diff --git a/src/validation/subscription-validation.ts b/src/validation/subscription-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..20302f5ad82f573e11489b0c444f915803ecc461 --- /dev/null +++ b/src/validation/subscription-validation.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +const updateSubscriptionSchema = z.object({ + userId: z.number().int().min(1), + albumId: z.number().int().min(1), + status: z.string().min(1).max(255), +}); + +const searchSubscriptionSchema = z.object({ + status: z.optional(z.string().min(1).max(255)), + searchInput: z.optional(z.string()), + orderBy: z.optional(z.string()), + page: z.number().int().min(1), + size: z.number().int().min(1), +}); + +export { + updateSubscriptionSchema, + searchSubscriptionSchema, +} \ No newline at end of file diff --git a/src/validation/validation.ts b/src/validation/validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c8d60e9aca6cae4ad226046da3e2e2e82a159b8 --- /dev/null +++ b/src/validation/validation.ts @@ -0,0 +1,10 @@ +import {ZodObject} from "zod"; +import {ErrorType, StandardError} from "../errors/standard-error"; + +export const validate = (schema : ZodObject<any>, data : any) => { + const {error} :any = schema.safeParse(data); + if (error) { + // console.log(error); + throw new StandardError(ErrorType.INPUT_DATA_NOT_VALID); + } +} diff --git a/test/auth.test.ts b/test/auth.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..58d65f40361a3b033ccfdd33ee0f08a18a9459f4 --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,50 @@ +import supertest from "supertest"; +import { app } from "../src/cores/app"; +import { deleteAllUserTest } from "./test-util"; + +describe("POST /api/signup", () => { + afterEach(async () => { + await deleteAllUserTest(); + }); + + it("Should be able to create a new user.", async () => { + const result = await supertest(app) + .post("/api/signup") + .send({ + username: "test", + password: "test", + }); + + expect(result.status).toEqual(200); + expect(result.body.username).toBe("test"); + }); +}); + +describe("POST /api/login", () => { + beforeEach(async () => { + await deleteAllUserTest(); + await supertest(app) + .post("/api/signup") + .send({ + username: "test", + password: "test", + }); + }); + + afterEach(async () => { + await deleteAllUserTest(); + }); + + it("Should be able to login.", async () => { + const result = await supertest(app) + .post("/api/login") + .send({ + username: "test", + password: "test", + }); + + expect(result.status).toEqual(200); + expect(result.body.accessToken).toBeDefined(); + expect(result.header["set-cookie"][0]).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/test/premium-album.test.ts b/test/premium-album.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc6afe16d554b18201bda30c8d033261f58bb9bc --- /dev/null +++ b/test/premium-album.test.ts @@ -0,0 +1,102 @@ +import supertest from "supertest"; +import { app } from "../src/cores/app"; +import { + addManyPremiumAlbumTest, + deleteAllPremiumAlbumTest, + deleteUsersTest, +} from "./test-util"; + +const signup = async () => { +await supertest(app) + .post("/api/signup") + .send({ + username: "test", + password: "test", + }); +} + +describe("POST /api/premium-album", () => { + beforeEach(async () => { + await signup(); + }); + + afterEach(async () => { + await deleteAllPremiumAlbumTest(); + await deleteUsersTest(); + }); + + it("Should be able to create a new premium album.", async () => { + // login first + const loginResult = await supertest(app) + .post("/api/login") + .send({ + username: "test", + password: "test", + }); + + const token = loginResult.body.accessToken; + const fingerprintCookie = loginResult.header["set-cookie"][0]; + + // create new premium album + const result = await supertest(app) + .post("/api/premium-album") + .set("Authorization", `Bearer ${token}`) + .set("Cookie", fingerprintCookie) + .send({ + albumName: "test", + releaseDate: new Date("2021-01-01"), + genre: "test", + artist: "test", + coverFilename: "test", + }); + + + expect(result.status).toEqual(200); + expect(result.body.albumName).toBe("test"); + expect(result.body.artist).toBe("test"); + expect(result.body.releaseDate.toString()).toBe( + "2021-01-01T00:00:00.000Z", + ); + expect(result.body.genre).toBe("test"); + expect(result.body.coverFilename).toBe("test"); + }); +}); + +describe("GET /api/premium-albums", () => { + beforeEach(async () => { + await signup(); + await deleteAllPremiumAlbumTest(); + await addManyPremiumAlbumTest(); + }); + + afterEach(async () => { + await deleteAllPremiumAlbumTest(); + await deleteUsersTest(); + }); + + it("Should be able to get all premium albums.", async () => { + // login first + const loginResult = await supertest(app) + .post("/api/login") + .send({ + username: "test", + password: "test", + }); + + const token = loginResult.body.accessToken; + const fingerprintCookie = loginResult.header["set-cookie"][0]; + + const result = await supertest(app) + .get("/api/premium-albums") + .set("Authorization", `Bearer ${token}`) + .set("Cookie", fingerprintCookie) + .query({ + size: 10, + page: 1, + searchQuery: "", + }); + + expect(result.status).toEqual(200); + expect(result.body.paging.totalAlbums).toBe(20); + }); +}); diff --git a/test/test-util.ts b/test/test-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..10cc73f2fe66debc11541f7519e7760ea3dbbf38 --- /dev/null +++ b/test/test-util.ts @@ -0,0 +1,31 @@ +import prismaClient from "../src/cores/db"; + +export const deleteAllUserTest = async () => { + await prismaClient.user.deleteMany(); +} + +export const deleteAllPremiumAlbumTest = async () => { + await prismaClient.premiumAlbum.deleteMany(); +} + +export const addManyPremiumAlbumTest = async () => { + for (let i = 0; i < 20; i++) { + await prismaClient.premiumAlbum.create({ + data: { + albumName: `test ${i}`, + artist: "test", + releaseDate: new Date(`2021-01-0${i+1}`), + genre: "test", + coverFilename: "test", + } + }); + } +}; + +export const deleteUsersTest = async () => { + await prismaClient.user.delete({ + where: { + username: "test", + } + }) +}