diff --git a/.eslintrc b/.eslintrc index 5c36732..7253747 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,7 @@ { "extends": ["react-important-stuff", "plugin:prettier/recommended"], - "parser": "babel-eslint" + "parser": "babel-eslint", + "env": { + "node": true + } } diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1ca87ab --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": false +} diff --git a/README.md b/README.md index b621404..e958100 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,19 @@ This is a [single-spa](https://single-spa.js.org/) example React microapp. ## NPM Commands -Command | Description ---------------------- | ----------------------------------------------------------------- -`npm start` | Run server which serves production ready build from `dist` folder -`npm run dev` | Run app in the development mode -`npm run dev-https` | Run app in the development mode using HTTPS protocol -`npm run build` | Build app for production and puts files to the `dist` folder -`npm run analyze` | Analyze dependencies sizes and opens report in the browser -`npm run lint` | Check code for lint errors -`npm run format` | Format code using prettier -`npm run test` | Run unit tests -`npm run watch-tests` | Watch for file changes and run unit tests on changes -`npm run coverage` | Generate test code coverage report +| Command | Description | +| --------------------- | ----------------------------------------------------------------- | +| `npm start` | Run server which serves production ready build from `dist` folder | +| `npm run dev` | Run app in the development mode | +| `npm run dev-https` | Run app in the development mode using HTTPS protocol | +| `npm run build` | Build app for production and puts files to the `dist` folder | +| `npm run analyze` | Analyze dependencies sizes and opens report in the browser | +| `npm run lint` | Check code for lint errors | +| `npm run format` | Format code using prettier | +| `npm run test` | Run unit tests | +| `npm run watch-tests` | Watch for file changes and run unit tests on changes | +| `npm run coverage` | Generate test code coverage report | +| `npm run mock-api` | Start the mock api which mocks Recruit api | ## Local Deployment @@ -58,3 +59,20 @@ Make sure you have [Heroky CLI](https://devcenter.heroku.com/articles/heroku-cli - Now you have to configure frame app to use the URL provided by Heroku like `https://.herokuapp.com/earn-app/topcoder-micro-frontends-earn-app.js` to load this microapp. +### Aggregator API + +Please refer to [Swagger Doc](./src/api/docs/swagger.yaml) for Aggregator API endpoints + +#### Aggregator API Configuration + +In the `micro-frontends-earn-app` root directory create `.env` file with the next environment variables. + + ```bash + # Auth0 config + AUTH_SECRET= + AUTH0_URL= + AUTH0_AUDIENCE= + AUTH0_CLIENT_ID= + AUTH0_CLIENT_SECRET= + ``` +Once the earn app is started, the aggregator api will work as well \ No newline at end of file diff --git a/config/default.js b/config/default.js index 3b7344b..1c882d8 100644 --- a/config/default.js +++ b/config/default.js @@ -1,3 +1,4 @@ +require("dotenv").config(); module.exports = { GUIKIT: { DEBOUNCE_ON_CHANGE_TIME: 150, @@ -9,5 +10,46 @@ module.exports = { URL: { BASE: "https://www.topcoder-dev.com", COMMUNITY_APP: "https://community-app.topcoder-dev.com", + PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com", }, + RECRUIT_API: process.env.RECRUIT_API || "https://www.topcoder-dev.com", + // the server api base path + API_BASE_PATH: process.env.API_BASE_PATH || "/earn-app/api/my-gigs", + // the log level, default is 'debug' + LOG_LEVEL: process.env.LOG_LEVEL || "debug", + // The authorization secret used during token verification. + AUTH_SECRET: + process.env.AUTH_SECRET || + "UgL4(SEAM*~yc7L~vWrKKN&GHrwyc9N[@nVxm,X?#b4}7:xbzM", + // The valid issuer of tokens, a json array contains valid issuer. + VALID_ISSUERS: + process.env.VALID_ISSUERS || + '["https://api.topcoder-dev.com", "https://api.topcoder.com", "https://topcoder-dev.auth0.com/", "https://auth.topcoder-dev.com/"]', + // Auth0 URL, used to get TC M2M token + AUTH0_URL: + process.env.AUTH0_URL || "https://topcoder-dev.auth0.com/oauth/token", + // Auth0 audience, used to get TC M2M token + AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || "https://m2m.topcoder-dev.com/", + // Auth0 client id, used to get TC M2M token + AUTH0_CLIENT_ID: + process.env.AUTH0_CLIENT_ID || "gZ6jt50HYHLBf4vhxjUhXPZOR7Q5lk4k", + // Auth0 client secret, used to get TC M2M token + AUTH0_CLIENT_SECRET: + process.env.AUTH0_CLIENT_SECRET || + "zb-OV1Rl3QpUkt4BexJ-Rs58jYMazCre1_97aU4PJIvQdVB-DmQIs61W3gCfPyP4", + // Proxy Auth0 URL, used to get TC M2M token + AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, + m2m: { + M2M_AUDIT_USER_ID: + process.env.M2M_AUDIT_USER_ID || "00000000-0000-0000-0000-000000000000", + M2M_AUDIT_HANDLE: process.env.M2M_AUDIT_HANDLE || "TopcoderService", + }, + MOCK_API_PORT: process.env.MOCK_API_PORT || 4000, + ALLOWED_FILE_TYPES: process.env.ALLOWED_FILE_TYPES || [ + "pdf", + "doc", + "docx", + "txt", + ], + MAX_ALLOWED_FILE_SIZE_MB: process.env.MAX_ALLOWED_FILE_SIZE_MB || 10, }; diff --git a/config/development.js b/config/development.js index 3b7344b..19b4604 100644 --- a/config/development.js +++ b/config/development.js @@ -9,5 +9,6 @@ module.exports = { URL: { BASE: "https://www.topcoder-dev.com", COMMUNITY_APP: "https://community-app.topcoder-dev.com", + PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com", }, }; diff --git a/config/production.js b/config/production.js index bc4ca05..8aa83ad 100644 --- a/config/production.js +++ b/config/production.js @@ -9,5 +9,6 @@ module.exports = { URL: { BASE: "https://www.topcoder.com", COMMUNITY_APP: "https://community-app.topcoder.com", + PLATFORM_WEBSITE_URL: "https://platform.topcoder.com", }, }; diff --git a/jest.config.js b/jest.config.js index 116f278..551fa00 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { transform: { "^.+\\.(j|t)sx?$": "babel-jest", }, + transformIgnorePatterns: ["node_modules/?!(react-dropzone)"], moduleNameMapper: { "\\.(css|scss)$": "identity-obj-proxy", "\\.(png|eot|otf|ttf|woff|woff2|svg)$": "/__mocks__/fileMock.js", diff --git a/package-lock.json b/package-lock.json index ce03f4e..e29fdd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1250,6 +1250,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@bedrock-layout/use-forwarded-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@bedrock-layout/use-forwarded-ref/-/use-forwarded-ref-1.1.4.tgz", + "integrity": "sha512-DKzkLlCObn9z33YjoMgdKko0dBNtoLagxZzqFnTkG3/SfqPweUa5KayEtnhQmHo7T+w0wqdMBmTC1v6ygM/g1A==", + "requires": { + "@bedrock-layout/use-stateful-ref": "^1.1.4" + } + }, + "@bedrock-layout/use-stateful-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@bedrock-layout/use-stateful-ref/-/use-stateful-ref-1.1.4.tgz", + "integrity": "sha512-OHcQWfdtYcfGbeCSmRDBOraAgZ9fOEFDiZtrGxa/r194tzVWy14EOlZ/46+N0kM1n0+5QCPrJfZiyWz9+mjBVA==" + }, "@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -1260,6 +1273,16 @@ "minimist": "^1.2.0" } }, + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@hapi/hoek": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz", @@ -2799,6 +2822,11 @@ "@types/testing-library__react": "^9.1.2" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + }, "@trysound/sax": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.1.1.tgz", @@ -2852,6 +2880,61 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz", + "integrity": "sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "requires": { + "@types/express": "*" + } + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -2932,6 +3015,11 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -2941,8 +3029,7 @@ "@types/node": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.1.tgz", - "integrity": "sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA==", - "dev": true + "integrity": "sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -2967,6 +3054,16 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, + "@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, "@types/react": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz", @@ -3002,6 +3099,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" }, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -3424,11 +3530,33 @@ } } }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3630,7 +3758,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -3685,8 +3812,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -3736,8 +3862,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -3745,6 +3870,91 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, + "auth0-js": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.16.2.tgz", + "integrity": "sha512-cF1nRjmMDezmhJ+ZwwYp23F0gPqU0zNmF/VvTpcwvCrEMl9lAvkCd4iburN1I7G8SYaaIYEfcGedCphpDZw6OQ==", + "requires": { + "base64-js": "^1.3.0", + "idtoken-verifier": "^2.1.2", + "js-cookie": "^2.2.0", + "qs": "^6.7.0", + "superagent": "^5.3.1", + "url-join": "^4.0.1", + "winchan": "^0.2.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "superagent": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", + "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + } + } + } + }, "autoprefixer": { "version": "8.6.5", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.6.5.tgz", @@ -3761,14 +3971,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axe-core": { "version": "4.2.0", @@ -3776,6 +3984,25 @@ "integrity": "sha512-1uIESzroqpaTzt9uX48HO+6gfnKu3RwvWdCcWSrX4csMInJfCo1yvKPNXCwXFRpJqRW25tiASb6No0YH57PXqg==", "dev": true }, + "axios": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", + "requires": { + "follow-redirects": "0.0.7" + }, + "dependencies": { + "follow-redirects": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", + "requires": { + "debug": "^2.2.0", + "stream-consume": "^0.1.0" + } + } + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -4017,11 +4244,18 @@ } } }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base": { "version": "0.11.2", @@ -4081,8 +4315,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "batch": { "version": "0.6.1", @@ -4094,7 +4327,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -4170,6 +4402,11 @@ } } }, + "body-scroll-lock": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", + "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==" + }, "bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -4202,7 +4439,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4393,6 +4629,11 @@ "isarray": "^1.0.0" } }, + "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -4417,6 +4658,25 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -4551,8 +4811,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { "version": "2.4.2", @@ -4787,6 +5046,21 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "codependency": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/codependency/-/codependency-0.1.4.tgz", + "integrity": "sha1-0XY6tyZL1wyR2WJumIYtN5K/jUo=", + "requires": { + "semver": "5.0.1" + }, + "dependencies": { + "semver": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.1.tgz", + "integrity": "sha1-n7P0AE+QDYPEeWj+QvdYPgWDLMk=" + } + } + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -4803,6 +5077,15 @@ "object-visit": "^1.0.0" } }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4816,17 +5099,39 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, "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, "requires": { "delayed-stream": "~1.0.0" } @@ -4846,8 +5151,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "compose-function": { "version": "3.0.3", @@ -4893,8 +5197,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -5012,6 +5315,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -5043,8 +5351,7 @@ "core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-js-compat": { "version": "3.11.1", @@ -5086,8 +5393,16 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } }, "cosmiconfig": { "version": "7.0.0", @@ -5122,6 +5437,11 @@ } } }, + "country-calling-code": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/country-calling-code/-/country-calling-code-0.0.3.tgz", + "integrity": "sha512-DNLyidwJPrEZyDkILYFbgPRlg/tFkpP+uk6JWG+du41l7qoH5guePxSA2YlIjl6wUfp7vWdjCUJeMRuAQPKpRg==" + }, "create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -5268,6 +5588,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", @@ -5598,7 +5923,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -5757,8 +6081,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -5804,6 +6127,19 @@ "integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==", "dev": true }, + "diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E=" + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, "diff-sequences": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", @@ -5945,6 +6281,20 @@ "tslib": "^2.0.3" } }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -5967,12 +6317,19 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, + "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==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6030,6 +6387,11 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6162,6 +6524,11 @@ "es6-symbol": "^3.1.1" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -6730,6 +7097,22 @@ } } }, + "express-fileupload": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.2.1.tgz", + "integrity": "sha512-fWPNAkBj+Azt9Itmcz/Reqdg3LeBfaXptDEev2JM8bCC0yDptglCnlizhf0YZauyU5X/g6v7v4Xxqhg8tmEfEA==", + "requires": { + "busboy": "^0.3.1" + } + }, + "express-interceptor": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-interceptor/-/express-interceptor-1.2.0.tgz", + "integrity": "sha1-M0YKjhHc5+WgIsr1VdN35F3bgio=", + "requires": { + "debug": "^2.2.0" + } + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -6750,8 +7133,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -6853,8 +7235,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "3.1.3", @@ -6870,8 +7251,7 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -6879,6 +7259,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "fast-shallow-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", @@ -6913,6 +7298,11 @@ "bser": "2.1.1" } }, + "fecha": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", + "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -6971,6 +7361,14 @@ } } }, + "file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "requires": { + "tslib": "^2.0.3" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7098,11 +7496,15 @@ "readable-stream": "^2.3.6" } }, + "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==" + }, "follow-redirects": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.0.tgz", - "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==", - "dev": true + "integrity": "sha512-0vRwd7RKQBTt+mgu87mtYeofLFZpTas2S9zY+jIeuLJMNvudIgF52nr19q40HOwH5RrhWIPuj9puybzSJiRrVg==" }, "for-each": { "version": "0.3.3", @@ -7122,20 +7524,23 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -7298,6 +7703,11 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-parameter-names": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/get-parameter-names/-/get-parameter-names-0.3.0.tgz", + "integrity": "sha1-LSI3zVkubFuFmrLv2rQ18Ajlu5c=" + }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -7323,7 +7733,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -7631,14 +8040,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -8023,6 +8430,31 @@ "requires-port": "^1.0.0" } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "http-proxy-middleware": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", @@ -8039,30 +8471,66 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, + "http-status": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.5.0.tgz", + "integrity": "sha512-wcGvY31MpFNHIkUcXHHnvrE4IKYlpvitJw5P/1u892gMBAM46muQ+RH7UN1d+Ntnfx5apnOnVY6vcLmrWHOLwg==" + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true }, "hyphenate-style-name": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "i18n-iso-countries": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-6.7.0.tgz", + "integrity": "sha512-34JlFLXlDLq93N6bzEqTEuzW4ehDJ8Hbu4PyHHlO3bMrERNLO6NjQD/OfcjtObO5ZMwu4Lrr5FQlaCYtE2wAsQ==", + "requires": { + "diacritics": "1.3.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8117,6 +8585,26 @@ "harmony-reflect": "^1.4.6" } }, + "idtoken-verifier": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-2.1.2.tgz", + "integrity": "sha512-YMHiP9zAMjB+pWreV4EHnIj3XCQ168+InWirVRFeRtlsMQIK61S+LLnyLGI8EL0wtlk/v7ya69Gjfio3P9/7Gw==", + "requires": { + "base64-js": "^1.3.0", + "crypto-js": "3.3.0", + "es6-promise": "^4.2.8", + "jsbn": "^1.1.0", + "unfetch": "^4.1.0", + "url-join": "^4.0.1" + }, + "dependencies": { + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" + } + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8177,7 +8665,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -8633,8 +9120,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", @@ -8661,8 +9147,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -8678,8 +9163,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-lib-coverage": { "version": "2.0.5", @@ -11806,8 +12290,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { "version": "15.2.1", @@ -11864,14 +12347,12 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -11882,8 +12363,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json3": { "version": "3.3.3", @@ -11900,11 +12380,39 @@ "minimist": "^1.2.5" } }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "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": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -11927,6 +12435,72 @@ "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.1.0.tgz", "integrity": "sha512-mjzgSOFzlrurlURaHVjnQodyPNvrHrf1TbQP2XU9NSqBtHQPuHZ+Eb6TAJP7ASeJN9h9K0KXoRTs8u6ouHBKvg==" }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.3.tgz", + "integrity": "sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==", + "requires": { + "@types/express-jwt": "0.0.42", + "axios": "^0.21.1", + "debug": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^8.5.1", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2", + "ms": "^2.1.2", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -11951,6 +12525,11 @@ "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", "dev": true }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", @@ -11966,6 +12545,39 @@ "language-subtag-registry": "~0.3.2" } }, + "le_node": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", + "integrity": "sha512-NXzjxBskZ4QawTNwlGdRG05jYU0LhV2nxxmP3x7sRMHyROV0jPdyyikO9at+uYrWX3VFt0Y/am11oKITedx0iw==", + "requires": { + "babel-runtime": "6.6.1", + "codependency": "0.1.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.11", + "reconnect-core": "1.3.0", + "semver": "5.1.0" + }, + "dependencies": { + "babel-runtime": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", + "integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=", + "requires": { + "core-js": "^2.1.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "semver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11982,6 +12594,11 @@ "type-check": "~0.3.2" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -12076,6 +12693,11 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12094,6 +12716,11 @@ "lodash.isarray": "^3.0.0" } }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -12106,11 +12733,30 @@ "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "dev": true }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, "lodash.keys": { "version": "3.1.2", @@ -12123,12 +12769,36 @@ "lodash.isarray": "^3.0.0" } }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "loglevel": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", @@ -12175,11 +12845,35 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -12447,6 +13141,11 @@ } } }, + "millisecond": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/millisecond/-/millisecond-0.1.2.tgz", + "integrity": "sha1-bMWtOGJByrjniv+WT4cCjuyS2sU=" + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -12493,7 +13192,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -12501,8 +13199,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "3.1.3", @@ -12566,7 +13263,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -12641,11 +13337,45 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "dev": true + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, "nano-css": { "version": "5.3.1", @@ -12693,6 +13423,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -13041,8 +13777,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -13166,11 +13901,18 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -13402,8 +14144,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -13825,6 +14566,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -14047,8 +14793,7 @@ "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==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -14091,17 +14836,26 @@ "ipaddr.js": "1.9.1" } }, + "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==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "dev": true }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "public-encrypt": { "version": "4.0.3", @@ -14161,8 +14915,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.10.1", @@ -14320,6 +15073,16 @@ "scheduler": "^0.19.1" } }, + "react-dropzone": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.3.2.tgz", + "integrity": "sha512-Z0l/YHcrNK1r85o6RT77Z5XgTARmlZZGfEKBl3tqTXL9fZNQDuIdRx/J0QjvR60X+yYu26dnHeaG2pWU+1HHvw==", + "requires": { + "attr-accept": "^2.2.1", + "file-selector": "^0.2.2", + "prop-types": "^15.7.2" + } + }, "react-input-autosize": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", @@ -14359,6 +15122,16 @@ "react-is": "^16.13.1" } }, + "react-responsive-modal": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-responsive-modal/-/react-responsive-modal-6.1.0.tgz", + "integrity": "sha512-A0HUi0VCkLucfKblljLytnPXJ0FTnEOwYh/Yg3HSU0pT6Uevkc5bjDc+ai7RTZ6ENnXS+rR55i62tWpsW7B5EQ==", + "requires": { + "@bedrock-layout/use-forwarded-ref": "^1.1.4", + "body-scroll-lock": "^3.1.5", + "classnames": "^2.2.6" + } + }, "react-select": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", @@ -14420,7 +15193,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -14450,6 +15222,14 @@ "util.promisify": "^1.0.0" } }, + "reconnect-core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.3.0.tgz", + "integrity": "sha1-+65SkZp4d9hE4yRtAaLyZwHIM8g=", + "requires": { + "backoff": "~2.5.0" + } + }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -14709,7 +15489,6 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -14736,14 +15515,12 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -15081,6 +15858,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -15431,6 +16214,21 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "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==" + } + } + }, "single-spa-react": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/single-spa-react/-/single-spa-react-2.14.0.tgz", @@ -15805,7 +16603,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -15841,6 +16638,11 @@ "stackframe": "^1.1.1" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, "stack-utils": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", @@ -15946,6 +16748,11 @@ "readable-stream": "^2.0.2" } }, + "stream-consume": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", + "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==" + }, "stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", @@ -15975,6 +16782,11 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -16060,7 +16872,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -16154,6 +16965,72 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz", "integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==" }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -16307,6 +17184,21 @@ } } }, + "tc-core-library-js": { + "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", + "from": "github:appirio-tech/tc-core-library-js#v2.6", + "requires": { + "auth0-js": "^9.4.2", + "axios": "^0.12.0", + "bunyan": "^1.8.12", + "jsonwebtoken": "^8.3.0", + "jwks-rsa": "^1.3.0", + "le_node": "^1.3.1", + "lodash": "^4.17.10", + "millisecond": "^0.1.2", + "request": "^2.88.0" + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -16426,6 +17318,11 @@ "require-main-filename": "^2.0.0" } }, + "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==" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -16606,6 +17503,11 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", @@ -16641,7 +17543,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -16649,8 +17550,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type": { "version": "1.2.0", @@ -16715,6 +17615,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -16858,7 +17763,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -16887,6 +17791,11 @@ } } }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, "url-parse": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", @@ -16915,8 +17824,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.1.1", @@ -16945,8 +17853,7 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-compile-cache": { "version": "2.3.0", @@ -16992,7 +17899,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -17735,6 +18641,58 @@ } } }, + "winchan": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", + "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" + }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "dependencies": { + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -17781,8 +18739,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", @@ -17837,8 +18794,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 2224c09..41a5d87 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "prettier --write \"./**\"", "test": "cross-env BABEL_ENV=test jest", "watch-tests": "cross-env BABEL_ENV=test jest --watch", - "coverage": "cross-env BABEL_ENV=test jest --coverage" + "coverage": "cross-env BABEL_ENV=test jest --coverage", + "mock-api": "node ./src/api/mock-api/mock-api.js" }, "devDependencies": { "@babel/core": "^7.7.5", @@ -61,7 +62,15 @@ "@babel/runtime": "^7.13.10", "@reach/router": "^1.3.4", "autoprefixer": "^8.6.5", + "cors": "^2.8.5", + "country-calling-code": "0.0.3", + "dotenv": "^10.0.0", "express": "^4.17.1", + "express-fileupload": "^1.2.1", + "express-interceptor": "^1.2.0", + "get-parameter-names": "^0.3.0", + "http-status": "^1.5.0", + "i18n-iso-countries": "^6.7.0", "joi": "^17.4.0", "lodash": "^4.17.21", "moment": "^2.29.1", @@ -71,12 +80,17 @@ "react": "^16.12.0", "react-date-range": "^1.1.3", "react-dom": "^16.12.0", + "react-dropzone": "^11.3.2", "react-redux": "^7.2.3", + "react-responsive-modal": "^6.1.0", "react-select": "^1.3.0", "react-use": "^15.3.4", "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-logger": "^3.0.6", - "redux-promise-middleware": "^6.1.2" + "redux-promise-middleware": "^6.1.2", + "superagent": "^6.1.0", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6", + "winston": "^3.3.3" } } diff --git a/server.js b/server.js index 405ac8b..0ceb79e 100644 --- a/server.js +++ b/server.js @@ -1,10 +1,14 @@ /* global process */ - +require("./src/api/bootstrap"); const express = require("express"); const app = express(); -app.use('/earn-app', +// Register routes +require("./src/api/app-routes")(app); + +app.use( + "/earn-app", express.static("./dist", { setHeaders: function setHeaders(res) { res.header("Access-Control-Allow-Origin", "*"); @@ -16,10 +20,9 @@ app.use('/earn-app', }, }) ); - -app.get('/', function (req, res) { - res.send('alive') -}) +app.get("/", function (req, res) { + res.send("alive"); +}); const PORT = process.env.PORT || 8008; app.listen(PORT); diff --git a/src/App.jsx b/src/App.jsx index 2869c97..e645a77 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,6 +20,7 @@ import { useSelector } from "react-redux"; import "react-date-range/dist/theme/default.css"; import "react-date-range/dist/styles.css"; import "rc-tooltip/assets/bootstrap.css"; +import "react-responsive-modal/styles.css"; import "./styles/main.scss"; diff --git a/src/actions/lookup.js b/src/actions/lookup.js index 01a7811..e7fe54a 100644 --- a/src/actions/lookup.js +++ b/src/actions/lookup.js @@ -13,13 +13,13 @@ async function checkIsLoggedIn() { return service.checkIsLoggedIn(); } -async function getGigPhases() { - return service.getGigPhases(); +async function getGigStatuses() { + return service.getGigStatuses(); } export default createActions({ GET_TAGS: getTags, GET_COMMUNITY_LIST: getCommunityList, CHECK_IS_LOGGED_IN: checkIsLoggedIn, - GET_GIG_PHASES: getGigPhases, + GET_GIG_STATUSES: getGigStatuses, }); diff --git a/src/actions/myGigs.js b/src/actions/myGigs.js index ad3bf11..d045469 100644 --- a/src/actions/myGigs.js +++ b/src/actions/myGigs.js @@ -1,15 +1,38 @@ import { createActions } from "redux-actions"; +import { PER_PAGE } from "../constants"; import service from "../services/myGigs"; -async function getMyGigs() { - return service.getMyGigs(); +/** + * Action to get my gigs. + * @param {number} page page to fetch + * @param {number} perPage items per page. by default is 10. + * @returns + */ +async function getMyGigs(page = 1, perPage = PER_PAGE) { + return service.getMyGigs(page, perPage); } -async function loadMoreMyGigs() { - return service.loadMoreMyGigs(); +/** + * Action to load more pages of my gigs + * @param {number} nextPage page to fetch + * @param {*} perPage items per page. by default is 10 + * @returns + */ +async function loadMoreMyGigs(nextPage, perPage = PER_PAGE) { + return service.getMyGigs(nextPage, perPage); +} + +async function getProfile() { + return service.getProfile(); +} + +async function updateProfile(profile) { + return service.updateProfile(profile); } export default createActions({ GET_MY_GIGS: getMyGigs, LOAD_MORE_MY_GIGS: loadMoreMyGigs, + GET_PROFILE: getProfile, + UPDATE_PROFILE: updateProfile, }); diff --git a/src/api/app-constants.js b/src/api/app-constants.js new file mode 100644 index 0000000..c9ca380 --- /dev/null +++ b/src/api/app-constants.js @@ -0,0 +1,15 @@ +/** + * App constants + */ + +const Scopes = { + // JobApplication + READ_JOBAPPLICATION: "read:earn-jobApplications", + READ_PROFILE: "read:earn-profile", + WRITE_PROFILE: "write:earn-profile", + ALL_PROFILE: "all:earn-profile", +}; + +module.exports = { + Scopes, +}; diff --git a/src/api/app-routes.js b/src/api/app-routes.js new file mode 100644 index 0000000..a192fc9 --- /dev/null +++ b/src/api/app-routes.js @@ -0,0 +1,90 @@ +/** + * Configure all routes for express app + */ + +const _ = require("lodash"); +const config = require("config"); +const express = require("express"); +const cors = require("cors"); +const fileUpload = require("express-fileupload"); +const helper = require("./common/helper"); +const errors = require("./common/errors"); +const routes = require("./routes"); +const authenticator = require("tc-core-library-js").middleware.jwtAuthenticator; + +/** + * Configure all routes for express app + * @param app the express app + */ +module.exports = (app) => { + app.use(express.json()); + app.use(cors()); + app.use( + fileUpload({ + limits: { + fields: 20, + fileSize: config.MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024, + files: 1, + }, + debug: config.get("LOG_LEVEL") === "debug", + }) + ); + // intercept the response body from jwtAuthenticator + app.use(helper.interceptor); + // Load all routes + _.each(routes, (verbs, path) => { + _.each(verbs, (def, verb) => { + const controllerPath = `./controllers/${def.controller}`; + const method = require(controllerPath)[def.method]; + if (!method) { + throw new Error(`${def.method} is undefined`); + } + + const actions = []; + actions.push((req, res, next) => { + req.signature = `${def.controller}#${def.method}`; + next(); + }); + + // add Authenticator check if route has auth + if (def.auth) { + actions.push((req, res, next) => { + authenticator(_.pick(config, ["AUTH_SECRET", "VALID_ISSUERS"]))( + req, + res, + next + ); + }); + + actions.push((req, res, next) => { + if (req.authUser.isMachine) { + // M2M + if ( + !req.authUser.scopes || + !helper.checkIfExists(def.scopes, req.authUser.scopes) + ) { + next( + new errors.ForbiddenError( + "You are not allowed to perform this action!" + ) + ); + } else { + req.authUser.userId = config.m2m.M2M_AUDIT_USER_ID; + req.authUser.handle = config.m2m.M2M_AUDIT_HANDLE; + next(); + } + } else { + req.authUser.jwtToken = req.headers.authorization; + next(); + } + }); + } + + actions.push(method); + const fullPath = config.get("API_BASE_PATH") + path; + app[verb](fullPath, helper.autoWrapExpress(actions)); + }); + }); + // handle api errors + app.use(helper.errorHandler); +}; diff --git a/src/api/bootstrap.js b/src/api/bootstrap.js new file mode 100644 index 0000000..f62eaa9 --- /dev/null +++ b/src/api/bootstrap.js @@ -0,0 +1,26 @@ +const fs = require("fs"); +const Joi = require("joi"); +const path = require("path"); +const logger = require("./common/logger"); + +Joi.page = () => Joi.number().integer().min(1).default(1); +Joi.perPage = () => Joi.number().integer().min(1).default(20); + +function buildServices(dir) { + const files = fs.readdirSync(dir); + + files.forEach((file) => { + const curPath = path.join(dir, file); + fs.stat(curPath, (err, stats) => { + if (err) return; + if (stats.isDirectory()) { + buildServices(curPath); + } else if (path.extname(file) === ".js") { + const serviceName = path.basename(file, ".js"); + logger.buildService(require(curPath), serviceName); + } + }); + }); +} + +buildServices(path.join(__dirname, "services")); diff --git a/src/api/common/errors.js b/src/api/common/errors.js new file mode 100644 index 0000000..9160c9f --- /dev/null +++ b/src/api/common/errors.js @@ -0,0 +1,40 @@ +/** + * This file defines application errors + */ +const util = require("util"); + +/** + * Helper function to create generic error object with http status code + * @param {String} name the error name + * @param {Number} statusCode the http status code + * @returns {Function} the error constructor + * @private + */ +function createError(name, statusCode) { + /** + * The error constructor + * @param {String} message the error message + * @param {String} [cause] the error cause + * @constructor + */ + function ErrorCtor(message, cause) { + Error.call(this); + Error.captureStackTrace(this); + this.message = message || name; + this.cause = cause; + this.httpStatus = statusCode; + } + + util.inherits(ErrorCtor, Error); + ErrorCtor.prototype.name = name; + return ErrorCtor; +} + +module.exports = { + BadRequestError: createError("BadRequestError", 400), + UnauthorizedError: createError("UnauthorizedError", 401), + ForbiddenError: createError("ForbiddenError", 403), + NotFoundError: createError("NotFoundError", 404), + ConflictError: createError("ConflictError", 409), + InternalServerError: createError("InternalServerError", 500), +}; diff --git a/src/api/common/helper.js b/src/api/common/helper.js new file mode 100644 index 0000000..af86ba0 --- /dev/null +++ b/src/api/common/helper.js @@ -0,0 +1,418 @@ +/** + * This file defines helper methods + */ + +const _ = require("lodash"); +const config = require("config"); +const logger = require("./logger"); +const httpStatus = require("http-status"); +const Interceptor = require("express-interceptor"); +const m2mAuth = require("tc-core-library-js").auth.m2m; +const request = require("superagent"); +const querystring = require("querystring"); + +const localLogger = { + debug: (message) => + logger.debug({ + component: "helper", + context: message.context, + message: message.message, + }), + error: (message) => + logger.error({ + component: "helper", + context: message.context, + message: message.message, + }), + info: (message) => + logger.info({ + component: "helper", + context: message.context, + message: message.message, + }), +}; + +const m2m = m2mAuth( + _.pick(config, [ + "AUTH0_URL", + "AUTH0_AUDIENCE", + "AUTH0_CLIENT_ID", + "AUTH0_CLIENT_SECRET", + "AUTH0_PROXY_SERVER_URL", + ]) +); + +/** + * Gracefully handle errors thrown from the app + * @param {Object} err the error object + * @param {Object} req the request object + * @param {Object} res the response object + * @param {Function} next the next middleware + */ +function errorHandler(err, req, res, next) { + logger.logFullError(err, { + component: "app", + signature: req.signature || `${req.method}_${req.url}`, + }); + const errorResponse = {}; + const status = err.isJoi + ? httpStatus.BAD_REQUEST + : err.status || err.httpStatus || httpStatus.INTERNAL_SERVER_ERROR; + + if (_.isArray(err.details)) { + if (err.isJoi) { + _.map(err.details, (e) => { + if (e.message) { + if (_.isUndefined(errorResponse.message)) { + errorResponse.message = e.message; + } else { + errorResponse.message += `, ${e.message}`; + } + } + }); + } + } + if (err.response) { + // extract error message from V3/V5 API + errorResponse.message = + _.get(err, "response.body.result.content.message") || + _.get(err, "response.body.message"); + } + if (_.isUndefined(errorResponse.message)) { + if ( + err.message && + (err.httpStatus || status !== httpStatus.INTERNAL_SERVER_ERROR) + ) { + errorResponse.message = err.message; + } else { + errorResponse.message = "Internal server error"; + } + } + res.status(status).json(errorResponse); +} + +// intercepts the response body from jwtAuthenticator +const interceptor = Interceptor((req, res) => { + return { + isInterceptable: () => { + return res.statusCode === 403; + }, + + intercept: (body, send) => { + let obj; + if (body.length > 0) { + try { + obj = JSON.parse(body); + } catch (e) { + logger.error("Invalid response body."); + } + } + if (obj && _.get(obj, "result.content.message")) { + const ret = { message: obj.result.content.message }; + res.statusCode = 401; + send(JSON.stringify(ret)); + } else { + send(body); + } + }, + }; +}); + +/** + * Check if exists. + * + * @param {Array} source the array in which to search for the term + * @param {Array | String} term the term to search + */ +function checkIfExists(source, term) { + let terms; + + if (!_.isArray(source)) { + throw new Error("Source argument should be an array"); + } + + source = source.map((s) => s.toLowerCase()); + + if (_.isString(term)) { + terms = term.toLowerCase().split(" "); + } else if (_.isArray(term)) { + terms = term.map((t) => t.toLowerCase()); + } else { + throw new Error("Term argument should be either a string or an array"); + } + + for (let i = 0; i < terms.length; i++) { + if (source.includes(terms[i])) { + return true; + } + } + + return false; +} + +/** + * Wrap async function to standard express function + * @param {Function} fn the async function + * @returns {Function} the wrapped function + */ +function wrapExpress(fn) { + return function (req, res, next) { + fn(req, res, next).catch(next); + }; +} + +/** + * Wrap all functions from object + * @param obj the object (controller exports) + * @returns {Object|Array} the wrapped object + */ +function autoWrapExpress(obj) { + if (_.isArray(obj)) { + return obj.map(autoWrapExpress); + } + if (_.isFunction(obj)) { + if (obj.constructor.name === "AsyncFunction") { + return wrapExpress(obj); + } + return obj; + } + _.each(obj, (value, key) => { + obj[key] = autoWrapExpress(value); + }); + return obj; +} + +/** + * Function to get M2M token + * @returns {Promise} + */ +const getM2MToken = async () => { + return await m2m.getMachineToken( + config.AUTH0_CLIENT_ID, + config.AUTH0_CLIENT_SECRET + ); +}; + +/** + * Get link for a given page. + * @param {Object} req the HTTP request + * @param {Number} page the page number + * @returns {String} link for the page + */ +function getPageLink(req, page) { + const q = _.assignIn({}, req.query, { page }); + return `${req.protocol}://${req.get("Host")}${req.baseUrl}${ + req.path + }?${querystring.stringify(q)}`; +} + +/** + * Set HTTP response headers from result. + * @param {Object} req the HTTP request + * @param {Object} res the HTTP response + * @param {Object} result the operation result + */ +function setResHeaders(req, res, result) { + const totalPages = Math.ceil(result.total / result.perPage); + if (result.page > 1) { + res.set("X-Prev-Page", result.page - 1); + } + if (result.page < totalPages) { + res.set("X-Next-Page", result.page + 1); + } + res.set("X-Page", result.page); + res.set("X-Per-Page", result.perPage); + res.set("X-Total", result.total); + res.set("X-Total-Pages", totalPages); + // set Link header + if (totalPages > 0) { + let link = `<${getPageLink(req, 1)}>; rel="first", <${getPageLink( + req, + totalPages + )}>; rel="last"`; + if (result.page > 1) { + link += `, <${getPageLink(req, result.page - 1)}>; rel="prev"`; + } + if (result.page < totalPages) { + link += `, <${getPageLink(req, result.page + 1)}>; rel="next"`; + } + res.set("Link", link); + } +} + +/** + * Return details about the current user. + * @param {string} token the current user's token + * @return {Object} details about the user + */ +async function getCurrentUserDetails(token) { + const url = `${config.API.V5}/taas-teams/me`; + const res = await request + .get(url) + .set("Authorization", token) + .set("Accept", "application/json"); + localLogger.debug({ + context: "getCurrentUserDetails", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; +} + +/** + * Return job candidates by given criteria + * @param {string} criteria the search criteria + * @return {Object} the list of job candidates with pagination headers + */ +async function getJobCandidates(criteria) { + const token = await getM2MToken(); + const url = `${config.API.V5}/jobCandidates`; + const res = await request + .get(url) + .query(criteria) + .set("Authorization", `Bearer ${token}`) + .set("Accept", "application/json"); + localLogger.debug({ + context: "getJobCandidates", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return { + total: Number(_.get(res.headers, "x-total")), + page: Number(_.get(res.headers, "x-page")), + perPage: Number(_.get(res.headers, "x-per-page")), + result: res.body, + }; +} + +/** + * Return jobs by given criteria + * @param {string} criteria the search criteria + * @return {Object} the list of jobs with pagination headers + */ +async function getJobs(criteria) { + let jobIds = []; + if (criteria.jobIds) { + jobIds = criteria.jobIds; + criteria = _.omit(criteria, "jobIds"); + } + const token = await getM2MToken(); + const url = `${config.API.V5}/jobs`; + const res = await request + .get(url) + .query(criteria) + .set("Authorization", `Bearer ${token}`) + .set("Accept", "application/json") + .send({ jobIds }); + localLogger.debug({ + context: "getJobs", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return { + total: Number(_.get(res.headers, "x-total")), + page: Number(_.get(res.headers, "x-page")), + perPage: Number(_.get(res.headers, "x-per-page")), + result: res.body, + }; +} + +/** + * Get member details + * @param {string} handle the handle of the user + * @param {string} query the query criteria + * @return {Object} the object of member details + */ +async function getMember(handle, query) { + const token = await getM2MToken(); + const url = `${config.API.V5}/members/${handle}`; + const res = await request + .get(url) + .query(query) + .set("Authorization", `Bearer ${token}`) + .set("Accept", "application/json"); + localLogger.debug({ + context: "getMember", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; +} + +/** + * Update member details + * @param {string} handle the handle of the user + * @param {object} data the data to be updated + * @return {object} the object of updated member details + */ +async function updateMember(currentUser, data) { + const token = currentUser.jwtToken; + const url = `${config.API.V5}/members/${currentUser.handle}`; + const res = await request + .put(url) + .set("Authorization", token) + .set("Content-Type", "application/json") + .set("Accept", "application/json") + .send(data); + localLogger.debug({ + context: "updateMember", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; +} + +/** + * Get Recruit CRM profile details + * @param {object} currentUser the user who performs the operation + * @return {object} the object of profile details + */ +async function getRCRMProfile(currentUser) { + const token = currentUser.jwtToken; + const url = `${config.RECRUIT_API}/api/recruit/profile`; + const res = await request + .get(url) + .set("Authorization", token) + .set("Accept", "application/json"); + localLogger.debug({ + context: "getRCRMProfile", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; +} + +/** + * Update Recruit CRM profile details + * @param {object} currentUser the user who performs the operation + * @param {object} file the resume file + * @param {object} data the data to be updated + * @return {object} the returned object + */ +async function updateRCRMProfile(currentUser, file, data) { + const token = currentUser.jwtToken; + const url = `${config.RECRUIT_API}/api/recruit/profile`; + const res = await request + .post(url) + .set("Authorization", token) + .set("Content-Type", "multipart/form-data") + .set("Accept", "application/json") + .field("phone", data.phone) + .field("availability", data.availability) + .attach("resume", file.data, file.name); + localLogger.debug({ + context: "updateRCRMProfile", + message: `response body: ${JSON.stringify(res.body)}`, + }); + return res.body; +} + +module.exports = { + errorHandler, + interceptor, + checkIfExists, + autoWrapExpress, + setResHeaders, + getM2MToken, + getCurrentUserDetails, + getJobCandidates, + getJobs, + getMember, + updateMember, + getRCRMProfile, + updateRCRMProfile, +}; diff --git a/src/api/common/logger.js b/src/api/common/logger.js new file mode 100644 index 0000000..c096c40 --- /dev/null +++ b/src/api/common/logger.js @@ -0,0 +1,186 @@ +/** + * This module contains the winston logger configuration. + */ + +const _ = require("lodash"); +const Joi = require("joi"); +const util = require("util"); +const config = require("config"); +const getParams = require("get-parameter-names"); +const winston = require("winston"); + +const { combine, timestamp, colorize, printf } = winston.format; + +const basicFormat = printf((info) => { + const location = `${info.component}${info.context ? ` ${info.context}` : ""}`; + return `[${info.timestamp}] ${location} ${info.level} : ${info.message}`; +}); + +const transports = []; +if (!config.DISABLE_LOGGING) { + transports.push(new winston.transports.Console({ level: config.LOG_LEVEL })); +} + +const logger = winston.createLogger({ + transports, + format: combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + colorize(), + timestamp(), + basicFormat + ), +}); + +logger.config = config; + +/** + * Log error details + * @param {Object} err the error + * @param {Object} context contains extra info about errors + */ +logger.logFullError = (err, context = {}) => { + if (!err) { + return; + } + if (err.logged) { + return; + } + const signature = context.signature ? `${context.signature} : ` : ""; + let errMessage; + if (err.response && err.response.error) { + errMessage = err.response.error.message; + } else { + errMessage = err.message || util.inspect(err).split("\n")[0]; + } + logger.error({ + ..._.pick(context, ["component", "context"]), + message: `${signature}${errMessage}`, + }); + err.logged = true; +}; + +/** + * Remove invalid properties from the object and hide long arrays + * @param {Object} obj the object + * @returns {Object} the new object with removed properties + * @private + */ +const _sanitizeObject = (obj) => { + const hideFields = ["auth"]; + try { + return JSON.parse( + JSON.stringify(obj, (k, v) => { + return _.includes(hideFields, k) ? "" : v; + }) + ); + } catch (e) { + return obj; + } +}; + +/** + * Convert array with arguments to object + * @param {Array} params the name of parameters + * @param {Array} arr the array with values + * @private + */ +const _combineObject = (params, arr) => { + const ret = {}; + _.each(arr, (arg, i) => { + ret[params[i]] = arg; + }); + return ret; +}; + +/** + * Decorate all functions of a service and log debug information if DEBUG is enabled + * @param {Object} service the service + * @param {String} serviceName the service name + */ +logger.decorateWithLogging = (service, serviceName) => { + if (logger.config.LOG_LEVEL !== "debug") { + return; + } + _.each(service, (method, name) => { + const params = method.params || getParams(method); + service[name] = async function () { + const args = Array.prototype.slice.call(arguments); + logger.debug({ + component: serviceName, + context: name, + message: `input arguments: ${util.inspect( + _sanitizeObject(_combineObject(params, args)), + { compact: true, breakLength: Infinity } + )}`, + }); + try { + const result = await method.apply(this, arguments); + logger.debug({ + component: serviceName, + context: name, + message: `output arguments: ${ + result !== null && result !== undefined + ? util.inspect(_sanitizeObject(result), { + compact: true, + breakLength: Infinity, + depth: null, + }) + : undefined + }`, + }); + return result; + } catch (err) { + logger.logFullError(err, { + component: serviceName, + context: name, + }); + throw err; + } + }; + }); +}; + +/** + * Decorate all functions of a service and validate input values + * and replace input arguments with sanitized result form Joi + * Service method must have a `schema` property with Joi schema + * @param {Object} service the service + */ +logger.decorateWithValidators = function (service) { + _.each(service, (method, name) => { + if (!method.schema) { + return; + } + const params = getParams(method); + service[name] = async function () { + const args = Array.prototype.slice.call(arguments); + const value = _combineObject(params, args); + const normalized = Joi.attempt(value, method.schema); + + const newArgs = []; + // Joi will normalize values + // for example string number '1' to 1 + // if schema type is number + _.each(params, (param) => { + newArgs.push(normalized[param]); + }); + return method.apply(this, newArgs); + }; + service[name].params = params; + }); +}; + +/** + * Apply logger and validation decorators + * @param {Object} service the service to wrap + * @param {String} serviceName the service name + */ +logger.buildService = (service, serviceName) => { + logger.decorateWithValidators(service); + logger.decorateWithLogging(service, serviceName); +}; + +module.exports = logger; diff --git a/src/api/controllers/JobApplicationController.js b/src/api/controllers/JobApplicationController.js new file mode 100644 index 0000000..3d1d92d --- /dev/null +++ b/src/api/controllers/JobApplicationController.js @@ -0,0 +1,20 @@ +/** + * Controller for JobApplication endpoints + */ +const service = require("../services/JobApplicationService"); +const helper = require("../common/helper"); + +/** + * Get current user's job applications + * @param req the request + * @param res the response + */ +async function getMyJobApplications(req, res) { + const result = await service.getMyJobApplications(req.authUser, req.query); + helper.setResHeaders(req, res, result); + res.send(result.result); +} + +module.exports = { + getMyJobApplications, +}; diff --git a/src/api/controllers/ProfileController.js b/src/api/controllers/ProfileController.js new file mode 100644 index 0000000..ca824b4 --- /dev/null +++ b/src/api/controllers/ProfileController.js @@ -0,0 +1,29 @@ +/** + * Controller for Profile endpoints + */ +const service = require("../services/ProfileService"); +const helper = require("../common/helper"); + +/** + * Get current user's profile + * @param req the request + * @param res the response + */ +async function getMyProfile(req, res) { + res.send(await service.getMyProfile(req.authUser)); +} + +/** + * Update current user's profile + * @param req the request + * @param res the response + */ +async function updateMyProfile(req, res) { + await service.updateMyProfile(req.authUser, req.files, req.body); + res.status(204).end(); +} + +module.exports = { + getMyProfile, + updateMyProfile, +}; diff --git a/src/api/data/resume.invalid b/src/api/data/resume.invalid new file mode 100644 index 0000000..3409059 --- /dev/null +++ b/src/api/data/resume.invalid @@ -0,0 +1 @@ +My resume \ No newline at end of file diff --git a/src/api/data/resume.txt b/src/api/data/resume.txt new file mode 100644 index 0000000..3409059 --- /dev/null +++ b/src/api/data/resume.txt @@ -0,0 +1 @@ +My resume \ No newline at end of file diff --git a/src/api/docs/micro-frontends-earn-app.postman_collection.json b/src/api/docs/micro-frontends-earn-app.postman_collection.json new file mode 100644 index 0000000..552b176 --- /dev/null +++ b/src/api/docs/micro-frontends-earn-app.postman_collection.json @@ -0,0 +1,1805 @@ +{ + "info": { + "_postman_id": "2e63b877-2392-4487-a330-cbdad46c784b", + "name": "micro-frontends-earn-app", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Job Applications", + "item": [ + { + "name": "get job applications successfully", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications?page=1&perPage=10&sortBy=status&sortOrder=desc", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "perPage", + "value": "10" + }, + { + "key": "sortBy", + "value": "status" + }, + { + "key": "sortOrder", + "value": "desc" + } + ] + } + }, + "response": [] + }, + { + "name": "get job applications with m2m", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + " const response = pm.response.json()\r", + " pm.expect(response).to.deep.eq([])\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_read_jobApplications}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications with invalid token 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_1}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications with invalid token 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_2}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications with invalid token 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"No token provided.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_3}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications with invalid token 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_4}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications with invalid m2m scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_invalid}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ] + } + }, + "response": [] + }, + { + "name": "get job applications by invalid page parameter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"criteria.page\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications?page=one", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ], + "query": [ + { + "key": "page", + "value": "one" + } + ] + } + }, + "response": [] + }, + { + "name": "get job applications by invalid perPage parameter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"criteria.perPage\\\" must be a number\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications?perPage=one", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ], + "query": [ + { + "key": "perPage", + "value": "one" + } + ] + } + }, + "response": [] + }, + { + "name": "get job applications by invalid sortBy parameter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"criteria.sortBy\\\" must be one of [id, status]\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications?sortBy=remark", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ], + "query": [ + { + "key": "sortBy", + "value": "remark" + } + ] + } + }, + "response": [] + }, + { + "name": "get job applications by not allowed parameter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"criteria.userId\\\" is not allowed\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/myJobApplications?userId=40d7c759-2213-458e-88f7-0a3e458343ea", + "host": [ + "{{URL}}" + ], + "path": [ + "myJobApplications" + ], + "query": [ + { + "key": "userId", + "value": "40d7c759-2213-458e-88f7-0a3e458343ea" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Profile", + "item": [ + { + "name": "get profile successfully", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with m2m", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_read_profile}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with invalid token 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_1}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with invalid token 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_2}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with invalid token 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"No token provided.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_3}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with invalid token 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_4}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "get profile successfully with invalid m2m scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_invalid}}" + } + ], + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile successfully", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with m2m", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 204', function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_write_profile}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with invalid token 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_1}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with invalid token 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_2}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with invalid token 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"No token provided.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_3}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with invalid token 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Failed to authenticate token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{invalid_token_4}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile with invalid m2m scope", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_invalid}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by invalid field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"data.availability\\\" must be a boolean\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.txt" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "bool", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by invalid field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"files\\\" must be of type object\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "value": "resume", + "type": "text" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "bool", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by invalid file format", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Allowed file types are: pdf,doc,docx,txt\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.invalid" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by missing field 1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"files\\\" must be of type object\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by missing field 2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"data.city\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.invalid" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by missing field 3", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"data.country\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.invalid" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by missing field 4", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"data.phone\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.invalid" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "availability", + "value": "true", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "update profile by missing field 5", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"\\\"data.availability\\\" is required\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_user}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "resume", + "type": "file", + "src": "./src/api/data/resume.invalid" + }, + { + "key": "city", + "value": "Ankara", + "type": "text" + }, + { + "key": "country", + "value": "TR", + "type": "text" + }, + { + "key": "phone", + "value": "555-333-55-55", + "type": "text" + } + ], + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{URL}}/profile", + "host": [ + "{{URL}}" + ], + "path": [ + "profile" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/api/docs/micro-frontends-earn-app.postman_environment.json b/src/api/docs/micro-frontends-earn-app.postman_environment.json new file mode 100644 index 0000000..7547a55 --- /dev/null +++ b/src/api/docs/micro-frontends-earn-app.postman_environment.json @@ -0,0 +1,59 @@ +{ + "id": "fca17786-e71e-470a-8f03-daede0d2daa4", + "name": "micro-frontends-earn-app", + "values": [ + { + "key": "URL", + "value": "http://local.topcoder-dev.com:8008/api", + "enabled": true + }, + { + "key": "token_user", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik5VSkZORGd4UlRVME5EWTBOVVkzTlRkR05qTXlRamxETmpOQk5UYzVRVUV3UlRFeU56TTJRUSJ9.eyJodHRwczovL3RvcGNvZGVyLWRldi5jb20vcm9sZXMiOlsiVG9wY29kZXIgVXNlciJdLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdXNlcklkIjoiODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vaGFuZGxlIjoiaXNiaWxpciIsImh0dHBzOi8vdG9wY29kZXItZGV2LmNvbS91c2VyX2lkIjoiYXV0aDB8ODg3NzQ2MzQiLCJodHRwczovL3RvcGNvZGVyLWRldi5jb20vdGNzc28iOiI4ODc3NDYzNHw0ZDczMWRjODNiZTk5OThkOWE1MmUyOTA2OThmNGIwNGZiMmVjNjE1OTliODIxZmYxNjRjYWEzYzhhNmU3IiwiaHR0cHM6Ly90b3Bjb2Rlci1kZXYuY29tL2FjdGl2ZSI6dHJ1ZSwibmlja25hbWUiOiJpc2JpbGlyIiwibmFtZSI6ImVtcmUuaXNiaWxpckBnbWFpbC5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvODE3NjNjMzE0ZGU0Y2ZiNGUxNDRhYzU3M2U1NmMxZjY_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZlbS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wNi0xOFQxNzowMDo1MS45MTNaIiwiZW1haWwiOiJlbXJlLmlzYmlsaXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8vYXV0aC50b3Bjb2Rlci1kZXYuY29tLyIsInN1YiI6ImF1dGgwfDg4Nzc0NjM0IiwiYXVkIjoiQlhXWFVXbmlsVlVQZE4wMXQyU2UyOVR3MlpZTkdadkgiLCJpYXQiOjE2MjQwMzg2MDQsImV4cCI6MTYyNDA0MTYwNCwibm9uY2UiOiJka05RWlVKMFNtMTJOMGxRV1VWVWEwZGhValJ3ZVRsdU4zbFpUME5TWTA1YWIzZDBXbjVUZVcxT053PT0ifQ.KPSBCmAdl46bF6uScCwwgQ83oRXuDY8i05YNPzBC_52g6xzgHQ-ORobWQxhYKEhZr6U5QqAqdJ3iOoEvaqwGNOzMMPbt9r6neZ8i4TMZTDcDnPaAaKRBbe8Xv7rHOaG68ZnU9qUfw78W9x9b0g6TBIgNJwd5S9qlSJxSR6cfneyVk5aWCvh_g4Ue7H7uraO4-AJ1cBG31GmljjyizzRrfo4l78wU9wzI0w9s8D6J6VOuRX53JQlGkEZLG8tJLbwLjNbcwtWQtosB23xmOgjba6vEAdCZSCXpEUkTF0f5MMi1z_dQKUTkU2AnUffMuGGfhDW8zWTwotOik50hjKlSPg", + "enabled": true + }, + { + "key": "token_m2m_read_jobApplications", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOmVhcm4tam9iQXBwbGljYXRpb25zIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.25VaSlf0nTJcQaIsbK0IANpZ6efgg5M3xawJEIxz6Xw", + "enabled": true + }, + { + "key": "token_m2m_read_profile", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOmVhcm4tcHJvZmlsZSIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.z3pyT4aeR-ASsDl9YL_QldGi9LP8yCO9lyZS5tS6dm8", + "enabled": true + }, + { + "key": "token_m2m_write_profile", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ3cml0ZTplYXJuLXByb2ZpbGUiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ULvTAKD7HriBImE7B9P1chrhDg3FtFUpmCtoGqsH7yY", + "enabled": true + }, + { + "key": "invalid_token_1", + "value": "eyJ", + "enabled": true + }, + { + "key": "invalid_token_2", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU", + "enabled": true + }, + { + "key": "invalid_token_3", + "value": "", + "enabled": true + }, + { + "key": "invalid_token_4", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU", + "enabled": true + }, + { + "key": "token_m2m_invalid", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJub25lOmVhcm4tbm9uZSIsImd0eSI6ImNsaWVudC1jcmVkZW50aWFscyJ9.art8aiAOoEMSl91zjvI7UMnCYiiS4mlzgZ_vOSxr4PM", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2021-06-18T18:28:27.337Z", + "_postman_exported_using": "Postman/8.6.2" +} \ No newline at end of file diff --git a/src/api/docs/swagger.yaml b/src/api/docs/swagger.yaml new file mode 100644 index 0000000..38c337d --- /dev/null +++ b/src/api/docs/swagger.yaml @@ -0,0 +1,461 @@ +openapi: 3.0.0 +info: + title: Micro Frontends Earn App + description: Micro Frontends Earn App + version: 1.0.0 +servers: + - url: http://local.topcoder-dev.com:8008/earn-app/api/my-gigs +tags: + - name: JobApplications + - name: Profile +paths: + /myJobApplications: + get: + tags: + - JobApplications + description: | + Get Job Applications of current user + + **Authorization** All topcoder members are allowed. M2M token with "read:earn-jobApplications" is allowed + security: + - bearerAuth: [] + parameters: + - in: query + name: page + required: false + schema: + type: integer + default: 1 + description: The page number. + - in: query + name: perPage + required: false + schema: + type: integer + default: 20 + description: The number of items to list per page. + - in: query + name: sortBy + required: false + schema: + type: string + default: id + enum: ["id", "status"] + description: The sort by column. + - in: query + name: sortOrder + required: false + schema: + type: string + default: desc + enum: ["desc", "asc"] + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/JobApplication" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /profile: + get: + tags: + - Profile + description: | + Get Profile details of current user + **Authorization** All topcoder members are allowed. M2M token with "read:earn-profile" is allowed + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Profile" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + tags: + - Profile + description: | + Update Profile details of current user + **Authorization** All topcoder members are allowed. M2M token with "write:earn-profile" is allowed + security: + - bearerAuth: [] + requestBody: + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/ProfileUpdate" + responses: + "204": + description: OK + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + JobApplication: + required: + - title + - payment + - hoursPerWeek + - location + - workingHours + - duration + - status + - remark + - interview + properties: + title: + type: string + example: "Dummy title" + description: "The title." + maxLength: 64 + payment: + $ref: "#/components/schemas/Payment" + hoursPerWeek: + type: integer + example: 20 + description: "the amount working hours per week" + location: + type: string + example: "Any location" + description: "the location of job" + workingHours: + type: string + example: "GMT" + description: "the timezone of job" + duration: + type: integer + example: 1 + description: "The duration in weeks" + status: + type: string + enum: + [ + "open", + "placed", + "selected", + "client rejected - screening", + "client rejected - interview", + "rejected - other", + "cancelled", + "interview", + "topcoder-rejected", + ] + description: "The job candidate status." + remark: + type: string + example: "excellent" + description: "The remark of candidate" + interview: + $ref: "#/components/schemas/Interview" + Payment: + required: + - min + - max + - currency + description: "The payment information" + properties: + min: + type: integer + example: 1000 + description: "the amount of minimum salary" + max: + type: integer + example: 3000 + description: "the amount of maximum salary" + frequency: + type: string + enum: ["hourly", "daily", "weekly", "monthly"] + description: "The frequency of the job." + currency: + type: string + example: "USD" + description: "the currency of job" + Interview: + required: + - id + - jobCandidateId + - templateUrl + - round + - status + - createdAt + - createdBy + properties: + id: + type: string + format: uuid + description: "The interview id." + xaiId: + type: string + description: "The x.ai id." + jobCandidateId: + type: string + format: uuid + description: "The job candidate id." + calendarEventId: + type: string + example: "dummyId" + description: "The calendar event id." + templateUrl: + type: string + example: "interview-30" + enum: ["interview-30", "interview-60"] + description: "The x.ai template name" + templateId: + type: string + format: uuid + description: "The x.ai template id" + templateType: + type: string + description: "The x.ai template type" + title: + type: string + description: "The x.ai template title" + locationDetails: + type: string + example: "Location TBD." + description: "The x.ai meeting location." + round: + type: integer + example: 1 + description: "The interview round." + duration: + type: integer + example: 30 + description: "The interview duration (in minutes)." + hostEmail: + type: string + format: email + description: "The interview host email." + hostName: + type: string + description: "The interview host name." + guestEmails: + type: array + description: "Attendee list for this interview." + items: + type: string + format: email + guestNames: + type: array + description: "Names of guests." + items: + type: string + startTimestamp: + type: string + format: date-time + description: "Interview start time." + endTimestamp: + type: string + format: date-time + description: "Interview end time." + status: + type: string + enum: + [ + "Scheduling", + "Scheduled", + "Requested for reschedule", + "Rescheduled", + "Completed", + "Cancelled", + ] + description: "The interview status." + rescheduleUrl: + type: string + format: uri + description: "x.ai reschedule url." + createdAt: + type: string + format: date-time + description: "The interview created date." + createdBy: + type: string + format: uuid + description: "The user who created the interview." + updatedAt: + type: string + format: date-time + description: "The interview last updated at." + updatedBy: + type: string + format: uuid + description: "The user who updated the interview last time." + Profile: + required: + - country + - availability + properties: + profilePhoto: + type: string + format: uri + description: "The url of the profile photo" + example: "http://photos.topcoder.com/123" + firstName: + type: string + description: "First name of the user" + example: "Jane" + lastName: + type: string + description: "First name of the user" + example: "Doe" + handle: + type: string + description: "The topcoder handle of the user" + example: "janedoe" + email: + type: string + format: email + description: "The email address of the user" + example: "example@topcoder.com" + city: + type: string + description: "The city of the user" + example: "New York" + country: + type: string + description: "The country of the user" + example: "USA" + phone: + type: string + description: "The phone number of the user" + example: "(123) 456-7890" + resume: + type: string + format: uri + description: "The resume url of the user" + example: "http://resumes.topcoder.com/123" + availability: + type: boolean + description: "The availability of the user" + default: true + example: true + ProfileUpdate: + required: + - city + - country + - phone + - resume + - availability + properties: + city: + type: string + description: "The city of the user" + example: "New York" + country: + type: string + description: "The country of the user" + example: "USA" + phone: + type: string + description: "The phone number of the user" + example: "(123) 456-7890" + resume: + type: string + format: binary + description: "The resume file of the user" + availability: + type: boolean + description: "The availability of the user" + example: true + Error: + required: + - message + properties: + message: + type: string diff --git a/src/api/mock-api/mock-api.js b/src/api/mock-api/mock-api.js new file mode 100644 index 0000000..810237f --- /dev/null +++ b/src/api/mock-api/mock-api.js @@ -0,0 +1,54 @@ +/** + * The mock APIs. + */ + +const config = require("config"); +const express = require("express"); +const cors = require("cors"); +const fileUpload = require("express-fileupload"); +const logger = require("../common/logger"); +const _ = require("lodash"); + +const app = express(); +app.set("port", config.MOCK_API_PORT || 4000); +app.use(express.json()); +app.use(cors()); +app.use(fileUpload()); +app.use((req, res, next) => { + logger.info({ component: "Mock Api", message: `${req.method} ${req.url}` }); + next(); +}); + +app.get("/api/recruit/profile", (req, res) => { + const result = { + phone: "555-555-55-55", + resume: "https://resume.topcoder.com/1234567", + availibility: true, + }; + res.status(200).json(result); +}); + +app.post("/api/recruit/profile", (req, res) => { + res.status(204).end(); +}); + +app.use((req, res) => { + res.status(404).json({ error: "route not found" }); +}); + +app.use((err, req, res, next) => { + logger.logFullError(err, { + component: "Mock Api", + signature: `${req.method}_${req.url}`, + }); + res.status(500).json({ + error: err.message, + }); +}); + +app.listen(app.get("port"), "0.0.0.0", () => { + logger.info({ + component: "Mock Api", + message: `Mock Api listening on port ${app.get("port")}`, + }); +}); diff --git a/src/api/routes/JobApplicationRoutes.js b/src/api/routes/JobApplicationRoutes.js new file mode 100644 index 0000000..d52fb44 --- /dev/null +++ b/src/api/routes/JobApplicationRoutes.js @@ -0,0 +1,15 @@ +/** + * Contains JobApplication routes + */ +const constants = require("../app-constants"); + +module.exports = { + "/myJobApplications": { + get: { + controller: "JobApplicationController", + method: "getMyJobApplications", + auth: "jwt", + scopes: [constants.Scopes.READ_JOBAPPLICATION], + }, + }, +}; diff --git a/src/api/routes/ProfileRoutes.js b/src/api/routes/ProfileRoutes.js new file mode 100644 index 0000000..c589113 --- /dev/null +++ b/src/api/routes/ProfileRoutes.js @@ -0,0 +1,21 @@ +/** + * Contains Profile routes + */ +const constants = require("../app-constants"); + +module.exports = { + "/profile": { + get: { + controller: "ProfileController", + method: "getMyProfile", + auth: "jwt", + scopes: [constants.Scopes.READ_PROFILE, constants.Scopes.ALL_PROFILE], + }, + post: { + controller: "ProfileController", + method: "updateMyProfile", + auth: "jwt", + scopes: [constants.Scopes.WRITE_PROFILE, constants.Scopes.ALL_PROFILE], + }, + }, +}; diff --git a/src/api/routes/index.js b/src/api/routes/index.js new file mode 100644 index 0000000..33fa814 --- /dev/null +++ b/src/api/routes/index.js @@ -0,0 +1,22 @@ +/** + * Defines the API routes + */ + +const fs = require("fs"); +const path = require("path"); + +const modules = {}; + +fs.readdirSync(__dirname) + .filter( + (file) => + file.indexOf(".") !== 0 && + file !== path.basename(module.filename) && + file.slice(-3) === ".js" + ) + .forEach((file) => { + const moduleName = file.slice(0, -3); + modules[moduleName] = require(path.join(__dirname, file)); + }); + +module.exports = Object.assign({}, ...Object.values(modules)); diff --git a/src/api/services/JobApplicationService.js b/src/api/services/JobApplicationService.js new file mode 100644 index 0000000..aee95b6 --- /dev/null +++ b/src/api/services/JobApplicationService.js @@ -0,0 +1,101 @@ +/** + * This service provides operations of JobApplications. + */ + +const _ = require("lodash"); +const Joi = require("joi"); +const helper = require("../common/helper"); +const errors = require("../common/errors"); + +/** + * Get Job Applications of current user + * @param {Object} currentUser the user who perform this operation. + * @param {Object} criteria the search criteria + * @returns {Array} the JobApplications + */ +async function getMyJobApplications(currentUser, criteria) { + const page = criteria.page; + const perPage = criteria.perPage; + const sortBy = criteria.sortBy; + const sortOrder = criteria.sortOrder; + const emptyResult = { + total: 0, + page, + perPage, + result: [], + }; + // we expect logged-in users + if (currentUser.isMachine) { + return emptyResult; + } + // get user id by calling taas-api with current user's token + const { id: userId } = await helper.getCurrentUserDetails( + currentUser.jwtToken + ); + if (!userId) { + throw new errors.NotFoundError( + `Id for user: ${currentUser.userId} not found` + ); + } + // get jobCandidates of current user by calling taas-api + const jobCandidates = await helper.getJobCandidates({ + userId, + page, + perPage, + sortBy, + sortOrder, + }); + // if no candidates found then return empty result + if (jobCandidates.result.length === 0) { + return emptyResult; + } + const jobIds = _.map(jobCandidates.result, "jobId"); + // get jobs of current user by calling taas-api + const { result: jobs } = await helper.getJobs({ jobIds, page: 1, perPage }); + // apply desired structure + const jobApplications = _.map(jobCandidates.result, (jobCandidate) => { + const job = _.find(jobs, ["id", jobCandidate.jobId]); + return { + title: job.title, + payment: { + min: job.minSalary, + max: job.maxSalary, + frequency: job.rateType, + currency: job.currency, + }, + hoursPerWeek: job.hoursPerWeek, + location: job.jobLocation, + workingHours: job.jobTimezone, + status: jobCandidate.status, + interview: !_.isEmpty(jobCandidate.interviews) + ? _.maxBy(jobCandidate.interviews, "round") + : null, + remark: jobCandidate.remark, + duration: job.duration, + }; + }); + return { + total: jobCandidates.total, + page: jobCandidates.page, + perPage: jobCandidates.perPage, + result: jobApplications, + }; +} + +getMyJobApplications.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + criteria: Joi.object() + .keys({ + page: Joi.page(), + perPage: Joi.perPage(), + sortBy: Joi.string().valid("id", "status").default("id"), + sortOrder: Joi.string().valid("desc", "asc").default("desc"), + }) + .required(), + }) + .required(); + +module.exports = { + getMyJobApplications, +}; diff --git a/src/api/services/ProfileService.js b/src/api/services/ProfileService.js new file mode 100644 index 0000000..5867620 --- /dev/null +++ b/src/api/services/ProfileService.js @@ -0,0 +1,132 @@ +/** + * This service provides operations of Profile. + */ + +const _ = require("lodash"); +const Joi = require("joi"); +const config = require("config"); +const helper = require("../common/helper"); +const errors = require("../common/errors"); + +/** + * Get user profile details + * @param {object} currentUser the user who perform this operation. + * @returns {object} the user profile details + */ +async function getMyProfile(currentUser) { + // we expect logged-in users + if (currentUser.isMachine) { + return {}; + } + const member = await helper.getMember( + currentUser.handle, + `fields=photoURL,firstName,lastName,handle,email,addresses,competitionCountryCode` + ); + const recruitProfile = await helper.getRCRMProfile(currentUser); + return { + profilePhoto: _.get(member, "photoURL", null), + firstName: _.get(member, "firstName", null), + lastName: _.get(member, "lastName", null), + handle: _.get(member, "handle", null), + email: _.get(member, "email", null), + city: _.get(member, "addresses[0].city", null), + country: _.get(member, "competitionCountryCode", null), + phone: _.get(recruitProfile, "phone", null), + resume: _.get(recruitProfile, "resume", null), + availability: _.get(recruitProfile, "availibility", true), + }; +} + +getMyProfile.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + }) + .required(); + +/** + * Update user profile details + * @param {object} currentUser the user who perform this operation. + * @param {object} data the data to be updated + */ +async function updateMyProfile(currentUser, files, data) { + // we expect logged-in users + if (currentUser.isMachine) { + return; + } + // check if file was truncated + if (files.resume.truncated) { + throw new errors.BadRequestError( + `Maximum allowed file size is ${config.MAX_ALLOWED_FILE_SIZE_MB} MB` + ); + } + // validate file extension + const regex = new RegExp( + `^.*\.(${_.join(config.ALLOWED_FILE_TYPES, "|")})$`, + "i" + ); + if (!regex.test(files.resume.name)) { + throw new errors.BadRequestError( + `Allowed file types are: ${_.join(config.ALLOWED_FILE_TYPES, ",")}` + ); + } + // get member's current address data + const member = await helper.getMember( + currentUser.handle, + "fields=addresses,competitionCountryCode,homeCountryCode" + ); + const update = {}; + // update member data if city is different from existing one + if (_.get(member, "addresses[0].city") !== data.city) { + update.addresses = _.cloneDeep(member.addresses); + if (!_.isEmpty(update.addresses)) { + update.addresses[0].city = data.city; + delete update.addresses[0].createdAt; + delete update.addresses[0].updatedAt; + } else { + update.addresses = [ + { + city: data.city, + }, + ]; + } + } + // update member data if competitionCountryCode is different from existing one + if (_.get(member, "competitionCountryCode") !== data.country) { + update.competitionCountryCode = data.country; + } + if (_.get(member, "homeCountryCode") !== data.country) { + update.homeCountryCode = data.country; + } + // avoid unnecessary api calls + if (!_.isEmpty(update)) { + await helper.updateMember(currentUser, update); + } + await helper.updateRCRMProfile(currentUser, files.resume, { + phone: data.phone, + availability: data.availability, + }); +} + +updateMyProfile.schema = Joi.object() + .keys({ + currentUser: Joi.object().required(), + files: Joi.object() + .keys({ + resume: Joi.object().required(), + }) + .required(), + data: Joi.object() + .keys({ + city: Joi.string().required(), + country: Joi.string().required(), + phone: Joi.string().required(), + availability: Joi.boolean().required(), + }) + .required(), + }) + .required(); + +module.exports = { + getMyProfile, + updateMyProfile, +}; diff --git a/src/assets/data/my-gigs.json b/src/assets/data/my-gigs.json index f77e85a..973854d 100644 --- a/src/assets/data/my-gigs.json +++ b/src/assets/data/my-gigs.json @@ -1,745 +1,27 @@ { - "phases":[ - "Applied", - "Phone Screen", - "Screen Pass", - "Interview Process", - "Selected", - "Offered", - "Placed" - ], - "phaseActions":[ - "check email", - "stand by", - "round", - "follow-up by email" - ], - "phaseTooltips":{ - "Applied":"They clicked submit on an application and no one has looked at their resume yet.", - "Phone Screen":"You either need to or have scheduled a phone screen with a Topcoder Screener.", - "Screen Pass":"You are selected by the client and an offer letter should follow shortly.", - "Interview Process":"You need to schedule or an interview has been scheduled with the client.", - "Selected":"You are selected by the client and an offer letter should follow shortly.", - "Offered":"An offer letter was sent.", - "Placed":"An offer was accepted and onboarding will begin soon." + "gigProfile":{ + "handle":"handle", + "photoURL":"", + "firstName":"FirstName", + "lastName":"LastName", + "email":"fnamelname@gmail.com", + "city":"Goa", + "country":"India", + "phone":"+91123456789", + "file":{ + "name":"resume.pdf" + }, + "uploadTime":"5/9/2021", + "status":"Available", + "gigStatus":"PLACED" }, - "phaseStatuses": [ - "Passed", - "Active" + "gigStatuses":[ + "Available", + "Unavailable" ], - "myGigs":[ - { - "label":"INTERVIEW PROCESS", - "title":"Google voice assistance architect - part time", - "paymentRangeFrom":800, - "paymentRangeTo":1000, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":7, - "hours":40, - "workingHours":"US hours", - "note":"The interview has not been scheduled yet.", - "phase": "Interview Process", - "phaseNote":"You need to schedule or an interview has been scheduled with the client", - "phaseAction":"check email", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phasenterviewRoundStartsIn":null, - "previous":"Screen Pass", - "previousNote":"You had your resume reviewed and passed the phone screen.", - "next":"Interview Process", - "nextNote":"You need to schedule or an interview has been scheduled with the client." - }, - { - "label":"SCREEN PASS", - "title":"Databricks and Python engineer", - "paymentRangeFrom":820, - "paymentRangeTo":1020, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Congratulation! You passed the phone screening. We’ll keep you update on the next steps.", - "phase": "Screen Pass", - "phaseNote":"You are selected by the client and an offer letter should follow shortly", - "phaseAction":"stand by", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous": "Phone Screen", - "previousNote":"You had your resume reviewed.", - "next":"Screen Pass", - "nextNote":"You are selected by the client and an offer letter should follow shortly." - }, - { - "label":"PHONE SCREEN", - "title":"Senior Flutter professional", - "paymentRangeFrom":1250, - "paymentRangeTo":1650, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":12, - "hours":40, - "workingHours":"US hours", - "note":"The phone screen has not been scheduled yet.", - "phase": "Phone Screen", - "phaseNote":"You either need to or have scheduled a phone screen with a Topcoder Screener", - "phaseAction":"check email", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous": "Applied", - "previousNote":"Your application was sent to the client.", - "next":"Phone Screen", - "nextNote":"You either need to or have scheduled a phone screen with a Topcoder Screener." - }, - { - "label":"APPLIED", - "title":"Back end developer -Spring Boot & microservices", - "paymentRangeFrom":820, - "paymentRangeTo":1020, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":12, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":"stand by", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous": "", - "previousNote":"", - "next":"APPLIED", - "nextNote":"Your application was sent to the client. If we find a match, we will contact you for the next steps." - }, - { - "label":"INTERVIEW PROCESS", - "title":"Google voice assistance architect - part time", - "paymentRangeFrom":800, - "paymentRangeTo":1000, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":7, - "hours":40, - "workingHours":"US hours", - "note":"Round 1 scheduled for May 20, 2021 11:00 EST.", - "phase": "Interview Process", - "phaseNote":"", - "phaseAction":"round", - "phaseStatus":"Active", - "phaseInterviewRound":1, - "phaseInterviewRoundStartsIn":"2d:6h:30m", - "previous":"Screen Pass", - "previousNote":"You had your resume reviewed and passed the phone screen.", - "next":"Interview Process", - "nextNote":"" - }, - { - "label":"SELECTED", - "title":"Google voice assistance architect - part time", - "paymentRangeFrom":800, - "paymentRangeTo":1000, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":7, - "hours":40, - "workingHours":"US hours", - "note":"Congratulations! You are selected by the client and an offer letter should follow shortly.", - "phase": "Selected", - "phaseNote":"", - "phaseAction":"follow-up by email", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"Interview Process", - "previousNote":"", - "next":"Selected", - "nextNote":"" - }, - { - "label":"OFFERED", - "title":"Google voice assistance architect - part time", - "paymentRangeFrom":800, - "paymentRangeTo":1000, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":7, - "hours":40, - "workingHours":"US hours", - "note":"An offer letter was sent! Please check your email.", - "phase": "Offered", - "phaseNote":"", - "phaseAction":"check email", - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"Selected", - "previousNote":"", - "next":"Offered", - "nextNote":"" - }, - { - "label":"PLACED", - "title":"Google voice assistance architect - part time", - "paymentRangeFrom":800, - "paymentRangeTo":1000, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":7, - "hours":40, - "workingHours":"US hours", - "note":"An offer was accepted and onboarding will begin soon!", - "phase": "Placed", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"Offered", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"NOT SELECTED", - "title":"Senior Tableau designer/developer", - "paymentRangeFrom":1650, - "paymentRangeTo":2050, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":52, - "hours":40, - "workingHours":"US hours", - "note":"Thank you for your interest in this gig. The client has now selected his preferred candidate for this position.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":"Active", - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"Applied", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"NOT SELECTED", - "title":"Google cloud data engineer", - "paymentRangeFrom":1650, - "paymentRangeTo":2050, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":52, - "hours":40, - "workingHours":"US hours", - "note":"Thank you for your interest in this gig. The client has now selected his preferred candidate for this position.", - "phase": "Interview Process", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"Screen Pass", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Kafka developer", - "paymentRangeFrom":1250, - "paymentRangeTo":1650, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Full stack Java developer", - "paymentRangeFrom":1250, - "paymentRangeTo":1650, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":12, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Engineer - Fullstack", - "paymentRangeFrom":2550, - "paymentRangeTo":2950, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"United States Only", - "duration":52, - "hours":40, - "workingHours":"PST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Cloud Dev-Ops engineer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Handson architect", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Senior backend developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Senior fullstack developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Backend developer - Google Cloud", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Frontend developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Senior Angular developer", - "paymentRangeFrom":1250, - "paymentRangeTo":1650, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":26, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Front End developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India or Southeast Asia", - "duration":32, - "hours":40, - "workingHours":"Any", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Senior Springboot developer", - "paymentRangeFrom":1250, - "paymentRangeTo":1650, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":26, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Backend developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India or Southeast Asia", - "duration":31, - "hours":40, - "workingHours":"Any", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Backend developer", - "paymentRangeFrom":820, - "paymentRangeTo":1020, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":26, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Senior software development engineer – Java", - "paymentRangeFrom":1650, - "paymentRangeTo":2050, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"Any Location", - "duration":20, - "hours":40, - "workingHours":"IST or US time Zone", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"UI developer [Core Java + Spring Boot + GraphQL]", - "paymentRangeFrom":820, - "paymentRangeTo":1020, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":13, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Fullstack developer(Angular)", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Fullstack Developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Frontend developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Java developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - }, - { - "label":"APPLIED", - "title":"Backend developer", - "paymentRangeFrom":660, - "paymentRangeTo":860, - "paymentRangeRateType":"week", - "paymentRangeCurrency":"USD", - "location":"India Only", - "duration":52, - "hours":40, - "workingHours":"IST", - "note":"Your application was sent to the client. If we find a match, we will contact you for the next steps.", - "phase": "Applied", - "phaseNote":"", - "phaseAction":null, - "phaseStatus":null, - "phaseInterviewRound":null, - "phaseInterviewRoundStartsIn":null, - "previous":"", - "previousNote":"", - "next":"", - "nextNote":"" - } - ] + "gigStatusTooltip":{ + "Available":"You’re open to take on new jobs.", + "Unavailable":"You’re not open to take on new jobs.", + "Placed":"You’re on a topcoder gig already." + } } diff --git a/src/assets/icons/checkpoint-small.png b/src/assets/icons/checkpoint-small.png deleted file mode 100644 index ebdccec..0000000 Binary files a/src/assets/icons/checkpoint-small.png and /dev/null differ diff --git a/src/assets/icons/checkpoint-small.svg b/src/assets/icons/checkpoint-small.svg new file mode 100644 index 0000000..81f1073 --- /dev/null +++ b/src/assets/icons/checkpoint-small.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/checkpoint.png b/src/assets/icons/checkpoint.png deleted file mode 100644 index 59bd14b..0000000 Binary files a/src/assets/icons/checkpoint.png and /dev/null differ diff --git a/src/assets/icons/checkpoint.svg b/src/assets/icons/checkpoint.svg new file mode 100644 index 0000000..5f748e8 --- /dev/null +++ b/src/assets/icons/checkpoint.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..26c8dfb --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 0000000..9378441 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/icons/note.png b/src/assets/icons/note.png deleted file mode 100644 index d5e89cb..0000000 Binary files a/src/assets/icons/note.png and /dev/null differ diff --git a/src/assets/icons/note.svg b/src/assets/icons/note.svg new file mode 100644 index 0000000..5edeea9 --- /dev/null +++ b/src/assets/icons/note.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/ribbon-icon.svg b/src/assets/icons/ribbon-icon.svg new file mode 100644 index 0000000..92bef26 --- /dev/null +++ b/src/assets/icons/ribbon-icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/update-success.svg b/src/assets/icons/update-success.svg new file mode 100644 index 0000000..a721c4f --- /dev/null +++ b/src/assets/icons/update-success.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/celebrate.png b/src/assets/images/celebrate.png deleted file mode 100644 index df84165..0000000 Binary files a/src/assets/images/celebrate.png and /dev/null differ diff --git a/src/assets/images/celebrate.svg b/src/assets/images/celebrate.svg new file mode 100644 index 0000000..be86c1d --- /dev/null +++ b/src/assets/images/celebrate.svg @@ -0,0 +1,9 @@ + + + Combined Shape + + + + + + \ No newline at end of file diff --git a/src/assets/images/update-success-big.svg b/src/assets/images/update-success-big.svg new file mode 100644 index 0000000..ba202e1 --- /dev/null +++ b/src/assets/images/update-success-big.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx index 0e689b5..16f95e3 100644 --- a/src/components/Button/index.jsx +++ b/src/components/Button/index.jsx @@ -3,7 +3,7 @@ import PT from "prop-types"; import "./styles.scss"; -const Button = ({ children, onClick, isPrimary, isText, size }) => ( +const Button = ({ children, onClick, isPrimary, isText, size, disabled }) => ( diff --git a/src/components/Dropdown/index.jsx b/src/components/Dropdown/index.jsx index d279c3e..6dfe1f9 100644 --- a/src/components/Dropdown/index.jsx +++ b/src/components/Dropdown/index.jsx @@ -72,7 +72,11 @@ function Dropdown({ {required ?  * : null} ) : null} - {errorMsg ? {errorMsg} : null} + {errorMsg ? ( + + {errorMsg} + + ) : null} ); } diff --git a/src/components/Dropdown/styles.scss b/src/components/Dropdown/styles.scss index 4c8d666..1c2c6c7 100644 --- a/src/components/Dropdown/styles.scss +++ b/src/components/Dropdown/styles.scss @@ -56,11 +56,11 @@ .Select-control { margin: 0; padding: 0; - border: 1px solid $gui-kit-gray-30 !important; + border: none; border-radius: 6px !important; height: 52px; outline: none !important; - box-shadow: none !important; + box-shadow: inset 0 0 0 1px $gui-kit-gray-30 !important; } .Select-input { @@ -167,7 +167,7 @@ &.haveError { :global { .Select-control { - border: 2px solid $gui-kit-level-5 !important; + box-shadow: inset 0 0 0 2px $gui-kit-level-5 !important; } } } diff --git a/src/components/FilePicker/index.jsx b/src/components/FilePicker/index.jsx new file mode 100644 index 0000000..d9ebfa5 --- /dev/null +++ b/src/components/FilePicker/index.jsx @@ -0,0 +1,121 @@ +import React from "react"; +import PT from "prop-types"; +import Dropzone from "react-dropzone"; +import _ from "lodash"; +import Button from "../Button"; + +import "./styles.scss"; + +const FilePicker = ({ + file, + required, + label, + uploadTime, + onFilePick, + errorMsg, + accept, + maxSize, +}) => { + const fileName = file ? file.name : ""; + + let stage; + if (file && uploadTime) { + stage = "uploaded"; + } else if (file) { + stage = "selected"; + } else { + stage = "select"; + } + + const stageSelect = ( + {`${label} ${required ? "*" : ""}`} + ); + + const stageSelected = ( +
+
{fileName}
+
+ ); + + const stageUploaded = ( +
+
{fileName}
+

Uploaded on {uploadTime}

+
+ ); + + const onClick = (event) => { + if (stage !== "select") { + event.stopPropagation(); + } + + switch (stage) { + case "select": + break; + case "selected": + case "uploaded": { + onFilePick(null); + break; + } + } + }; + + return ( +
+ { + if (acceptedFiles.length > 0) { + onFilePick(acceptedFiles[0]); + } + }} + maxSize={maxSize} + accept={accept} + onDropRejected={(event) => { + onFilePick( + _.get(event, "[0].file"), + _.get(event, "[0].errors[0].message") + ); + }} + > + {({ getRootProps, getInputProps }) => ( +
+ + + {stage === "select" && stageSelect} + {stage === "selected" && stageSelected} + {stage === "uploaded" && stageUploaded} + + +
+ )} +
+ {errorMsg ? ( + + {errorMsg} + + ) : null} +
+ ); +}; + +FilePicker.defaultProps = { + maxSize: 8 * 1024 * 1024, +}; + +FilePicker.propTypes = { + file: PT.any, + required: PT.bool, + label: PT.string, + uploadTime: PT.string, + onFilePick: PT.func, + errorMsg: PT.string, + accept: PT.string, + maxSize: PT.number, +}; + +export default FilePicker; diff --git a/src/components/FilePicker/styles.scss b/src/components/FilePicker/styles.scss new file mode 100644 index 0000000..830351e --- /dev/null +++ b/src/components/FilePicker/styles.scss @@ -0,0 +1,66 @@ +@import "styles/variables"; +@import "styles/mixins"; +@import 'styles/GUIKit/default'; + +.file-picker { + position: relative; +} + +.container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: #fff; + border-radius: $border-radius; + min-height: 164px; + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='gray' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + outline: none !important; + padding: 0; + text-align: center; + + &.hasError { + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='red' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + } +} + +.select-a-file { + max-width: 242px; +} + +.select-a-file { + margin-bottom: 16px; + font-size: $font-size-sm; + color: $tc-gray3; + line-height: 26px; +} + +.selected, +.uploaded { + .title { + @include barlow-semibold; + + margin-bottom: 6px; + font-size: $font-size-base; + line-height: 20px; + text-transform: uppercase; + } + + .text { + min-height: 2 * $line-height-base; + line-height: $line-height-base; + } +} + +.selected { + .title { + margin-bottom: 16px; + } +} + +.errorMessage { + display: block; + + @include errorMessage; +} diff --git a/src/components/Modal/index.jsx b/src/components/Modal/index.jsx new file mode 100644 index 0000000..a1dfa9a --- /dev/null +++ b/src/components/Modal/index.jsx @@ -0,0 +1,36 @@ +import React from "react"; +import PT from "prop-types"; +import { Modal as ReactModal } from "react-responsive-modal"; + +import styles from "./styles.scss"; + +const Modal = ({ children, open, center, onClose }) => { + return ( + + {children} + + ); +}; + +Modal.defaultProps = { + center: true, + onClose() {}, +}; + +Modal.propTypes = { + children: PT.node, + open: PT.bool, + center: PT.bool, + onClose: PT.func, +}; + +export default Modal; diff --git a/src/components/Modal/styles.scss b/src/components/Modal/styles.scss new file mode 100644 index 0000000..d6fbf48 --- /dev/null +++ b/src/components/Modal/styles.scss @@ -0,0 +1,6 @@ +.modal.content { + max-width: 100%; + padding: 0; + margin: 0; + background: none; +} diff --git a/src/components/Ribbon/index.jsx b/src/components/Ribbon/index.jsx index dd83b0a..05e2bbc 100644 --- a/src/components/Ribbon/index.jsx +++ b/src/components/Ribbon/index.jsx @@ -1,6 +1,6 @@ import React from "react"; import PT from "prop-types"; - +import IconInfo from "assets/icons/ribbon-icon.svg"; import "./styles.scss"; const Ribbon = ({ text, tooltip }) => { @@ -10,7 +10,9 @@ const Ribbon = ({ text, tooltip }) => { {text} - i + + + ); diff --git a/src/components/Ribbon/styles.scss b/src/components/Ribbon/styles.scss index 915d1cb..e6336c9 100644 --- a/src/components/Ribbon/styles.scss +++ b/src/components/Ribbon/styles.scss @@ -26,13 +26,12 @@ justify-content: center; width: 14px; height: 14px; - padding-top: 1px; - padding-left: 0.5px; margin: 4px; - font-weight: bold; - font-size: 10.5px; - border: 2px solid currentColor; - border-radius: 50%; cursor: default; - user-select: none; + + svg { + path { + fill: currentColor; + } + } } diff --git a/src/components/TextInput/index.jsx b/src/components/TextInput/index.jsx index a01ebb5..be848aa 100644 --- a/src/components/TextInput/index.jsx +++ b/src/components/TextInput/index.jsx @@ -64,7 +64,11 @@ function TextInput({ {required ?  * : null} ) : null} - {errorMsg ? {errorMsg} : null} + {errorMsg ? ( + + {errorMsg} + + ) : null} ); } diff --git a/src/components/UserPhoto/index.jsx b/src/components/UserPhoto/index.jsx new file mode 100644 index 0000000..903b850 --- /dev/null +++ b/src/components/UserPhoto/index.jsx @@ -0,0 +1,29 @@ +import React, { useMemo } from "react"; +import PT from "prop-types"; +import * as util from "../../utils/myGig"; + +import "./styles.scss"; + +const UserPhoto = ({ handle, photoURL }) => { + const imgSrc = useMemo(() => { + if (!photoURL) { + const text = handle || ""; + return util.createTextImage(text.toUpperCase().slice(0, 2)); + } + + return photoURL; + }, [handle, photoURL]); + + return ( +
+ user +
+ ); +}; + +UserPhoto.propTypes = { + handle: PT.string, + photoURL: PT.string, +}; + +export default UserPhoto; diff --git a/src/components/UserPhoto/styles.scss b/src/components/UserPhoto/styles.scss new file mode 100644 index 0000000..e78ba62 --- /dev/null +++ b/src/components/UserPhoto/styles.scss @@ -0,0 +1,15 @@ +.user-photo { + display: inline-flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + vertical-align: middle; + overflow: hidden; + border-radius: 50%; + + img { + width: auto; + height: 100%; + } +} diff --git a/src/constants/index.js b/src/constants/index.js index d35c7b3..4759717 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -101,16 +101,20 @@ export const CURRENCY_SYMBOL = { export const MY_GIG_PHASE = { APPLIED: "Applied", + SKILLS_TEST: "Skills Test", PHONE_SCREEN: "Phone Screen", SCREEN_PASS: "Screen Pass", INTERVIEW_PROCESS: "Interview Process", SELECTED: "Selected", OFFERED: "Offered", PLACED: "Placed", + NOT_SELECTED: "Not Selected", + JOB_CLOSED: "Job Closed", }; export const MY_GIG_PHASE_LABEL = { APPLIED: "APPLIED", + SKILLS_TEST: "SKILLS TEST", PHONE_SCREEN: "PHONE SCREEN", SCREEN_PASS: "SCREEN PASS", INTERVIEW_PROCESS: "INTERVIEW PROCESS", @@ -118,6 +122,7 @@ export const MY_GIG_PHASE_LABEL = { OFFERED: "OFFERED", PLACED: "PLACED", NOT_SELECTED: "NOT SELECTED", + JOB_CLOSED: "JOB CLOSED", }; export const MY_GIG_PHASE_STATUS = { @@ -128,6 +133,218 @@ export const MY_GIG_PHASE_STATUS = { export const MY_GIG_PHASE_ACTION = { CHECK_EMAIL: "check email", STAND_BY: "stand by", - ROUND: "round", - FOLLOW_UP_BY_EMAIL: "follow-up by email", }; + +export const MY_GIGS_JOB_STATUS = { + APPLIED: "applied", + SKILLS_TEST: "skills-test", + PHONE_SCREEN: "phone-screen", + SCREEN_PASS: "open", + INTERVIEW: "interview", + SELECTED: "selected", + OFFERED: "offered", + PLACED: "placed", + REJECTED_OTHER: "rejected - other", + REJECTED_PRE_SCREEN: "rejected-pre-screen", + CLIENT_REJECTED_INTERVIEW: "client rejected - interview", + CLIENT_REJECTED_SCREENING: "client rejected - screening", + JOB_CLOSED: "job-closed", +}; +/** + * Maps the status from API to gig status + */ +export const JOB_STATUS_MAPPER = { + [MY_GIGS_JOB_STATUS.APPLIED]: MY_GIG_PHASE.APPLIED, + [MY_GIGS_JOB_STATUS.SKILLS_TEST]: MY_GIG_PHASE.SKILLS_TEST, + [MY_GIGS_JOB_STATUS.PHONE_SCREEN]: MY_GIG_PHASE.PHONE_SCREEN, + [MY_GIGS_JOB_STATUS.SCREEN_PASS]: MY_GIG_PHASE.SCREEN_PASS, + [MY_GIGS_JOB_STATUS.INTERVIEW]: MY_GIG_PHASE.INTERVIEW_PROCESS, + [MY_GIGS_JOB_STATUS.SELECTED]: MY_GIG_PHASE.SELECTED, + [MY_GIGS_JOB_STATUS.OFFERED]: MY_GIG_PHASE.OFFERED, + [MY_GIGS_JOB_STATUS.PLACED]: MY_GIG_PHASE.PLACED, + [MY_GIGS_JOB_STATUS.REJECTED_OTHER]: MY_GIG_PHASE.NOT_SELECTED, + [MY_GIGS_JOB_STATUS.REJECTED_PRE_SCREEN]: MY_GIG_PHASE.NOT_SELECTED, + [MY_GIGS_JOB_STATUS.CLIENT_REJECTED_INTERVIEW]: MY_GIG_PHASE.NOT_SELECTED, + [MY_GIGS_JOB_STATUS.CLIENT_REJECTED_SCREENING]: MY_GIG_PHASE.NOT_SELECTED, + [MY_GIGS_JOB_STATUS.JOB_CLOSED]: MY_GIG_PHASE.JOB_CLOSED, +}; + +/** + * messages to be shown in each phase/status + */ +export const JOB_STATUS_MESSAGE_MAPPER = { + [MY_GIG_PHASE.APPLIED]: + "Thank you for Applying. We will be reviewing your profile shortly.", + [MY_GIG_PHASE.SKILLS_TEST]: "You are requested to complete a skills test", + [MY_GIG_PHASE.PHONE_SCREEN]: + "You need to schedule a phone screen or a phone screen has already been scheduled", + [MY_GIG_PHASE.SCREEN_PASS]: + "You have passed our initial crtieria and we are pushing your profile to our client", + [MY_GIG_PHASE.INTERVIEW_PROCESS]: + "You are currently in the interview process. Please check your email for updates.", + [MY_GIG_PHASE.SELECTED]: + "The client has selected you for this position! Please stand by for an offer Letter.", + [MY_GIG_PHASE.OFFERED]: + "An offer letter was sent to your email! Please review and Accept", + [MY_GIG_PHASE.PLACED]: + "Congrats on the placement! Please follow onboarding instructions from the Client and Topcoder Teams.", + [MY_GIG_PHASE.NOT_SELECTED]: "You were not selected for this position.", + [MY_GIG_PHASE.JOB_CLOSED]: + "This position is no longer active. Please apply to other open gigs.", +}; + +export const ACTIONS_AVAILABLE_FOR_MY_GIG_PHASE = { + [MY_GIG_PHASE_ACTION.CHECK_EMAIL]: [ + MY_GIG_PHASE.SKILLS_TEST, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.OFFERED, + ], + [MY_GIG_PHASE_ACTION.STAND_BY]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.SELECTED, + ], +}; +/** + * jobs can have different flows (progress bar) dependending on the status. + * here it's where it's defined the flow + */ +export const PHASES_FOR_JOB_STATUS = { + [MY_GIGS_JOB_STATUS.APPLIED]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.SKILLS_TEST]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.SKILLS_TEST, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.PHONE_SCREEN]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.SCREEN_PASS]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.INTERVIEW]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.SELECTED]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.OFFERED]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.PLACED]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.PLACED, + ], + [MY_GIGS_JOB_STATUS.REJECTED_OTHER]: [MY_GIG_PHASE.NOT_SELECTED], + [MY_GIGS_JOB_STATUS.REJECTED_PRE_SCREEN]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.NOT_SELECTED, + ], + [MY_GIGS_JOB_STATUS.CLIENT_REJECTED_INTERVIEW]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.NOT_SELECTED, + ], + [MY_GIGS_JOB_STATUS.CLIENT_REJECTED_SCREENING]: [ + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.NOT_SELECTED, + ], + [MY_GIGS_JOB_STATUS.JOB_CLOSED]: [MY_GIG_PHASE.JOB_CLOSED], +}; + +/** + * definition of how the sort is made on status. the order in the array defined the + * priority. + */ +export const SORT_STATUS_ORDER = [ + MY_GIG_PHASE.PLACED, + MY_GIG_PHASE.OFFERED, + MY_GIG_PHASE.SELECTED, + MY_GIG_PHASE.INTERVIEW_PROCESS, + MY_GIG_PHASE.SCREEN_PASS, + MY_GIG_PHASE.PHONE_SCREEN, + MY_GIG_PHASE.SKILLS_TEST, + MY_GIG_PHASE.APPLIED, + MY_GIG_PHASE.JOB_CLOSED, + MY_GIG_PHASE.NOT_SELECTED, +]; + +export const PER_PAGE = 10; + +/** + * defines which status can show remarks + */ +export const AVAILABLE_REMARK_BY_JOB_STATUS = [ + MY_GIGS_JOB_STATUS.SKILLS_TEST, + MY_GIGS_JOB_STATUS.PHONE_SCREEN, + MY_GIGS_JOB_STATUS.SCREEN_PASS, + MY_GIGS_JOB_STATUS.OFFERED, + MY_GIGS_JOB_STATUS.PLACED, + MY_GIGS_JOB_STATUS.REJECTED_OTHER, + MY_GIGS_JOB_STATUS.REJECTED_PRE_SCREEN, + MY_GIGS_JOB_STATUS.JOB_CLOSED, +]; +export const MY_GIG_STATUS_PLACED = "PLACED"; + +export const GIG_STATUS = { + AVAILABLE: "Available", + UNAVAILABLE: "Unavailable", + PLACED: "Placed", +}; + +export const EMPTY_GIGS_TEXT = + "Looks like you haven't applied to any gig opportunities yet."; diff --git a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/index.jsx b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/index.jsx index d0e3ad6..bc56628 100644 --- a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/index.jsx +++ b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/index.jsx @@ -1,5 +1,6 @@ import React from "react"; import PT from "prop-types"; +import IconCheck from "assets/icons/checkpoint.svg"; import "./styles.scss"; @@ -10,7 +11,9 @@ const PhasePoint = ({ text, passed, active }) => { active ? "active" : "" }`} > -
+
+ +
{text}
); diff --git a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/styles.scss b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/styles.scss index 218cfd7..92cc503 100644 --- a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/styles.scss +++ b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/PhasePoint/styles.scss @@ -37,7 +37,10 @@ left: 2px; width: 20px; height: 20px; - background: url("../../../../../../assets/icons/checkpoint.png") no-repeat center / 100%; + + .check { + display: block; + } } } } @@ -51,6 +54,10 @@ background-color: $gray3; border-radius: 50%; box-shadow: 0 0 0 2px $white; + + .check { + display: none; + } } .text { diff --git a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/index.jsx b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/index.jsx index f64b138..97e25bf 100644 --- a/src/containers/MyGigs/JobListing/JobCard/ProgressBar/index.jsx +++ b/src/containers/MyGigs/JobListing/JobCard/ProgressBar/index.jsx @@ -21,7 +21,7 @@ const ProgressBar = ({ phases, currentPhase, currentPhaseStatus, note }) => {
- {phases.map((phase, index) => ( + {(phases || []).map((phase, index) => ( { +const JobCard = ({ job }) => { const [expanded, setExpanded] = useState(false); const [footerHeight, setFooterHeight] = useState(0); const footerRef = useRef({}); @@ -27,17 +33,11 @@ const JobCard = ({ job, phases }) => { return (
{
Payment Range
- {utils.formatMoneyValue( - job.paymentRangeFrom, - constants.CURRENCY_SYMBOL[job.paymentRangeCurrency] - )} - {" - "} - {utils.formatMoneyValue( - job.paymentRangeTo, - constants.CURRENCY_SYMBOL[job.paymentRangeCurrency] - )} - {" / "} - {job.paymentRangeRateType} + {job.paymentRangeFrom && + job.paymentRangeTo && + job.currency && ( + <> + {job.currency}{" "} + {formatMoneyValue(job.paymentRangeFrom, "")} + {" - "} + {formatMoneyValue(job.paymentRangeTo, "")} + {" / "} + {job.paymentRangeRateType} + + )}
@@ -84,82 +85,84 @@ const JobCard = ({ job, phases }) => {
  • Duration
    -
    {job.duration} Weeks
    +
    + {job.duration && `${job.duration} Weeks`} +
  • Hours
    -
    {job.hours} hours / week
    +
    + {job.hours && `${job.hours} hours / week`} +
  • Working Hours
    -
    {job.workingHours}
    +
    + {job.workingHours && `${job.workingHours} hours`} +
  • - {job.phaseAction !== constants.MY_GIG_PHASE_ACTION.ROUND ? ( - - ) : ( -
    - - Round {job.phaseInterviewRound} starts in - -
    - {job.phaseInterviewRoundStartsIn} -
    -
    - )} + {job.phaseAction && }
    - - - - {job.note} - - - -
    -
    - + {job.remark && ( + + + + + + )} + {job.remark} + {![ + MY_GIGS_JOB_STATUS.JOB_CLOSED, + MY_GIGS_JOB_STATUS.REJECTED_OTHER, + ].includes(job.status) && ( + + + + )}
    + {![ + MY_GIGS_JOB_STATUS.JOB_CLOSED, + MY_GIGS_JOB_STATUS.REJECTED_OTHER, + ].includes(job.status) && ( +
    + +
    + )}
    @@ -168,7 +171,6 @@ const JobCard = ({ job, phases }) => { JobCard.propTypes = { job: PT.shape(), - phases: PT.arrayOf(PT.string), }; export default JobCard; diff --git a/src/containers/MyGigs/JobListing/JobCard/styles.scss b/src/containers/MyGigs/JobListing/JobCard/styles.scss index b04bdcf..5feba2f 100644 --- a/src/containers/MyGigs/JobListing/JobCard/styles.scss +++ b/src/containers/MyGigs/JobListing/JobCard/styles.scss @@ -82,7 +82,7 @@ &.label-placed { .card-image { - background: url('../../../../assets/images/celebrate.png') no-repeat right 24px center / auto, linear-gradient(101.95deg, #8B41B0 0%, #EF476F 100%); + background: url('../../../../assets/images/celebrate.svg') no-repeat right 24px center / auto, linear-gradient(101.95deg, #8B41B0 0%, #EF476F 100%); } } @@ -203,7 +203,6 @@ flex: none; width: 16px; height: 16px; - background: url("../../../../assets/icons/note.png") no-repeat center / 100%; } .note { diff --git a/src/containers/MyGigs/JobListing/JobCard/tooltips/PhasePointTooltip/index.jsx b/src/containers/MyGigs/JobListing/JobCard/tooltips/PhasePointTooltip/index.jsx index 790e41d..4953e1a 100644 --- a/src/containers/MyGigs/JobListing/JobCard/tooltips/PhasePointTooltip/index.jsx +++ b/src/containers/MyGigs/JobListing/JobCard/tooltips/PhasePointTooltip/index.jsx @@ -1,15 +1,14 @@ import React from "react"; import PT from "prop-types"; import Tooltip from "../../../../../../components/Tooltip"; -import { phaseTooltips } from "../../../../../../assets/data/my-gigs.json"; - +import { JOB_STATUS_MESSAGE_MAPPER } from "../../../../../../constants"; import "./styles.scss"; const PhasePointTooltip = ({ phase, children, placement, width }) => { const Content = () => (
    {phase}
    -

    {phaseTooltips[phase]}

    +

    {JOB_STATUS_MESSAGE_MAPPER[phase]}

    ); diff --git a/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/index.jsx b/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/index.jsx index fc6ac44..0160948 100644 --- a/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/index.jsx +++ b/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/index.jsx @@ -1,6 +1,7 @@ import React from "react"; import PT from "prop-types"; -import Tooltip from "../../../../../../components/Tooltip"; +import Tooltip from "components/Tooltip"; +import IconCheck from "assets/icons/checkpoint-small.svg"; import "./styles.scss"; @@ -13,19 +14,34 @@ const ProgressTooltip = ({ job, children }) => { !job.previous ? "hidden" : "" }`} > - + + + + + +
    - + + + + + +
    {job.previous}

    {job.previousNote}

    - + + + + + +
    NEXT: {job.next}

    {job.nextNote}

    diff --git a/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/styles.scss b/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/styles.scss index 1bbd770..a0d635b 100644 --- a/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/styles.scss +++ b/src/containers/MyGigs/JobListing/JobCard/tooltips/ProgressTooltip/styles.scss @@ -40,11 +40,11 @@ padding-top: 18px; background-color: $lightGreen; - &::before { + .before { display: block; } - &::after { + .after { display: none; } } @@ -67,11 +67,11 @@ padding-top: 11px; background-image: linear-gradient(180deg, $tc-gray-70 0%, rgba($tc-gray-70, 0.25) 100%);; - &::before { + .before { display: none; } - &::after { + .after { display: block; } } @@ -97,9 +97,8 @@ padding-right: 5.5px; background-clip: content-box; - &::before, - &::after { - content: ''; + .before, + .after { position: absolute; top: 0; left: 0; @@ -109,11 +108,7 @@ border-radius: 16px; } - &::before { - background: url('../../../../../../assets/icons/checkpoint-small.png') no-repeat center / 100%; - } - - &::after { + .after { border: 5px solid $white; border-radius: 50%; } @@ -121,4 +116,4 @@ &.hidden { background: none !important; } -} \ No newline at end of file +} diff --git a/src/containers/MyGigs/JobListing/index.jsx b/src/containers/MyGigs/JobListing/index.jsx index d110e90..5f19c75 100644 --- a/src/containers/MyGigs/JobListing/index.jsx +++ b/src/containers/MyGigs/JobListing/index.jsx @@ -6,12 +6,20 @@ import { useScrollLock } from "../../../utils/hooks"; import "./styles.scss"; -const JobListing = ({ jobs, phases, loadMore, total, numLoaded }) => { +const JobListing = ({ jobs, loadMore, total, numLoaded }) => { const scrollLock = useScrollLock(); + const [page, setPage] = useState(1); const varsRef = useRef(); varsRef.current = { scrollLock }; + const handleLoadMoreClick = () => { + const nextPage = page + 1; + scrollLock(true); + setPage(nextPage); + loadMore(nextPage); + }; + useEffect(() => { varsRef.current.scrollLock(false); }, [jobs]); @@ -20,20 +28,13 @@ const JobListing = ({ jobs, phases, loadMore, total, numLoaded }) => {
    {jobs.map((job, index) => (
    - +
    ))} {numLoaded < total && (
    - +
    )}
    @@ -42,7 +43,6 @@ const JobListing = ({ jobs, phases, loadMore, total, numLoaded }) => { JobListing.propTypes = { jobs: PT.arrayOf(PT.shape()), - phases: PT.arrayOf(PT.string), loadMore: PT.func, total: PT.number, numLoaded: PT.number, diff --git a/src/containers/MyGigs/index.jsx b/src/containers/MyGigs/index.jsx index b18b825..39c68f3 100644 --- a/src/containers/MyGigs/index.jsx +++ b/src/containers/MyGigs/index.jsx @@ -1,69 +1,129 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import PT from "prop-types"; import { connect } from "react-redux"; +import Modal from "../../components/Modal"; import Button from "../../components/Button"; import JobListing from "./JobListing"; import actions from "../../actions"; +import { EMPTY_GIGS_TEXT } from "../../constants"; + +import UpdateGigProfile from "./modals/UpdateGigProfile"; +import UpdateSuccess from "./modals/UpdateSuccess"; import "./styles.scss"; const MyGigs = ({ myGigs, - phases, - getPhases, getMyGigs, loadMore, total, numLoaded, + profile, + statuses, + getProfile, + getStatuses, + updateProfile, + updateProfileSuccess, }) => { const propsRef = useRef(); - propsRef.current = { getMyGigs, getPhases }; + propsRef.current = { getMyGigs, getProfile, getStatuses }; useEffect(() => { propsRef.current.getMyGigs(); - propsRef.current.getPhases(); + propsRef.current.getProfile(); + propsRef.current.getStatuses(); }, []); + const [openUpdateProfile, setOpenUpdateProfile] = useState(false); + const [openUpdateSuccess, setOpenUpdateSuccess] = useState(false); + + useEffect(() => { + if (updateProfileSuccess) { + setOpenUpdateSuccess(true); + } + }, [updateProfileSuccess]); + return ( -
    -

    - MY GIGS - -

    - -
    + <> +
    +

    + MY GIGS + +

    + {myGigs && myGigs.length == 0 && ( +

    {EMPTY_GIGS_TEXT}

    + )} + {myGigs && myGigs.length > 0 && ( + + )} +
    + + { + updateProfile(profileEdit); + setOpenUpdateProfile(false); + }} + onClose={() => { + setOpenUpdateProfile(false); + }} + /> + + + { + setOpenUpdateSuccess(false); + }} + /> + + ); }; MyGigs.propTypes = { myGigs: PT.arrayOf(PT.shape()), - phases: PT.arrayOf(PT.string), - getPhases: PT.func, getMyGigs: PT.func, loadMore: PT.func, total: PT.number, numLoaded: PT.number, + profile: PT.shape(), + statuses: PT.arrayOf(PT.string), + getProfile: PT.func, + getStatuses: PT.func, + updateProfile: PT.func, + updateProfileSuccess: PT.bool, }; const mapStateToProps = (state) => ({ myGigs: state.myGigs.myGigs, total: state.myGigs.total, numLoaded: state.myGigs.numLoaded, - phases: state.lookup.gigPhases, + profile: state.myGigs.profile, + statuses: state.lookup.gigStatuses, + updateProfileSuccess: state.myGigs.updatingProfileSucess, }); const mapDispatchToProps = { getMyGigs: actions.myGigs.getMyGigs, loadMore: actions.myGigs.loadMoreMyGigs, - getPhases: actions.lookup.getGigPhases, + getProfile: actions.myGigs.getProfile, + getStatuses: actions.lookup.getGigStatuses, + updateProfile: actions.myGigs.updateProfile, }; export default connect(mapStateToProps, mapDispatchToProps)(MyGigs); diff --git a/src/containers/MyGigs/modals/UpdateGigProfile/index.jsx b/src/containers/MyGigs/modals/UpdateGigProfile/index.jsx new file mode 100644 index 0000000..fcdae42 --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateGigProfile/index.jsx @@ -0,0 +1,264 @@ +/* global process */ +import React, { useEffect, useMemo, useState, useRef } from "react"; +import PT from "prop-types"; +import Button from "components/Button"; +import FilePicker from "components/FilePicker"; +import TextInput from "components/TextInput"; +import Dropdown from "components/Dropdown"; +import UserPhoto from "components/UserPhoto"; +import IconClose from "assets/icons/close.svg"; +import IconInfo from "assets/icons/info.svg"; +import StatusTooltip from "./tooltips/StatusTooltip"; +import _ from "lodash"; + +import * as constants from "constants"; +import * as utils from "utils"; + +import "./styles.scss"; + +const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => { + const countryOptions = useMemo(() => { + const countryMap = utils.myGig.countries.getNames("en"); + const options = Object.keys(countryMap).map((key) => countryMap[key]); + const selected = profile.country; + return utils.createDropdownOptions(options, selected); + }, [profile]); + + const statusOptions = useMemo(() => { + const selected = profile.status; + const options = + profile.gigStatus === constants.MY_GIG_STATUS_PLACED + ? statuses + : statuses.filter((s) => s !== constants.GIG_STATUS.PLACED); + return utils.createDropdownOptions(options, selected); + }, [profile, statuses]); + + const [profileEdit, setProfileEdit] = useState( + profile ? _.clone(profile) : null + ); + const [validation, setValidation] = useState(null); + const [pristine, setPristine] = useState(true); + + useEffect(() => { + setProfileEdit(_.clone(profile)); + }, [profile]); + + useEffect(() => { + let validation = null; + let error; + + if (profileEdit == null) { + return; + } + + if (!profileEdit.status) { + validation = validation || {}; + validation.status = "Please, select your status"; + } + + if (profileEdit.fileError) { + validation = validation || {}; + validation.file = profileEdit.fileError; + } + + if (!profileEdit.file) { + validation = validation || {}; + validation.file = "Please, pick your CV file for uploading"; + } + + if ((error = utils.myGig.validateCity(profileEdit.city))) { + validation = validation || {}; + validation.city = error; + } + + if (!profileEdit.country) { + validation = validation || {}; + validation.country = "Please, select your country"; + } + + if ( + (error = utils.myGig.validatePhone( + profileEdit.phone, + profileEdit.country + )) + ) { + validation = validation || {}; + validation.phone = error; + } + + setValidation(validation); + }, [profileEdit]); + + const submitEnabled = + validation === null && + !profileEdit.fileError && + (profile.city !== profileEdit.city || + profile.country !== profileEdit.country || + profile.phone !== profileEdit.phone || + profile.status !== profileEdit.status || + profile.file !== profileEdit.file); + + const varsRef = useRef(); + varsRef.current = { profileEdit, validation }; + + const onSubmitProfile = () => { + const update = varsRef.current.profileEdit; + delete update.fileError; + onSubmit(update); + }; + + return ( +
    + +

    UPDATE RESUME

    +

    + Uploading a resume will change your resume for all jobs that you apply + to. +

    +
    +
    +
    + +
    + +
    + {profile.firstName} {profile.lastName} +
    +
    {profile.email}
    +
    +
    + { + const selectedOption = utils.getSelectedDropdownOption( + newOptions + ); + setProfileEdit({ + ...varsRef.current.profileEdit, + status: selectedOption.label, + }); + setPristine(false); + }} + errorMsg={(!pristine && validation && validation.status) || ""} + /> +
    + + + + + +
    +
    +
    +
    + { + if (error) { + setProfileEdit({ + ...varsRef.current.profileEdit, + file, + fileError: error, + uploadTime: null, + }); + } else { + setProfileEdit({ + ...varsRef.current.profileEdit, + file, + fileError: null, + uploadTime: null, + }); + } + setPristine(false); + }} + /> +
    +
    + { + setProfileEdit({ ...varsRef.current.profileEdit, city: value }); + setPristine(false); + }} + errorMsg={(!pristine && validation && validation.city) || ""} + /> +
    +
    + { + const selectedOption = utils.getSelectedDropdownOption( + newOptions + ); + setProfileEdit({ + ...varsRef.current.profileEdit, + country: selectedOption.label, + }); + setPristine(false); + }} + errorMsg={(!pristine && validation && validation.country) || ""} + /> +
    +
    + { + setProfileEdit({ + ...varsRef.current.profileEdit, + phone: value, + }); + setPristine(false); + }} + errorMsg={(!pristine && validation && validation.phone) || ""} + /> +
    +
    +
    + +
    + ); +}; + +UpdateGigProfile.propTypes = { + profile: PT.shape(), + statuses: PT.arrayOf(PT.string), + onSubmit: PT.func, + onClose: PT.func, +}; + +export default UpdateGigProfile; diff --git a/src/containers/MyGigs/modals/UpdateGigProfile/styles.scss b/src/containers/MyGigs/modals/UpdateGigProfile/styles.scss new file mode 100644 index 0000000..0fcbc6e --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateGigProfile/styles.scss @@ -0,0 +1,182 @@ +@import "styles/variables"; +@import "styles/mixins"; + +.update-resume { + position: relative; + width: 860px; + padding: 30px 40px; + background-color: $white; + border-radius: 10px; +} + +.close { + position: absolute; + top: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + margin-top: 30px; + margin-right: 30px; + outline: none; + background: none; + border: none; +} + +.title, +.text { + text-align: center +} + +.title { + @include barlow-condensed-medium; + + margin: 40px 0 16px; + font-size: 34px; + line-height: 38px; +} + +.text { + @include roboto-medium; + + font-size: $font-size-sm; + line-height: $line-height-base; + + &.warnings { + color: $tc-red; + } +} + +.profile { + display: flex; + margin: 40px 0; + + .member { + flex: none; + display: flex; + flex-direction: column; + align-items: center; + width: 260px; + min-height: 330px; + padding: 20px 15px; + margin-right: 40px; + line-height: $line-height-base; + text-align: center; + background-color: $tc-blue-light4; + border-radius: $border-radius; + + .photo { + margin-top: 20px; + margin-bottom: 20px; + } + + .handle { + @include roboto-medium; + + font-size: $font-size-sm; + color: $tc-link-blue; + } + + .name { + @include roboto-medium; + + margin-bottom: 4px; + line-height: 26px; + } + + .email { + margin-bottom: 26px; + font-size: $font-size-sm; + word-break: break-all; + } + + .status { + display: flex; + align-items: center; + margin-top: auto; + margin-bottom: 30px; + + .dropdown { + position: relative; + width: 128px; + margin-right: 7px; + + @include error-msg-not-change-container-height; + @include dropdown-show-label; + } + + .icon { + width: 16px; + height: 16px; + } + } + } + + .details { + display: flex; + flex-wrap: wrap; + + .resume { + align-self: flex-start; + } + .city, + .country + { + align-self: center; + } + .phone { + align-self: flex-end; + } + + .resume, + .phone { + width: 100%; + } + .city, + .country { + width: 50%; + } + + .city { + padding-right: 10px; + } + .country { + padding-left: 10px; + } + + + .resume, + .city, + .country, + .phone { + @include error-msg-not-change-container-height; + @include dropdown-show-label; + @include textinput-show-label; + } + + .country { + :global(.Select-menu) { + max-height: 180px; + } + } + } +} + +.footer { + display: flex; + align-items: center; + padding-top: 24px; + border-top: 1px solid $tc-gray5; + + .link { + font-size: 13px; + color: $tc-link-blue; + } + + button { + margin-left: auto; + } +} diff --git a/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/index.jsx b/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/index.jsx new file mode 100644 index 0000000..573918b --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/index.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import PT from "prop-types"; +import Tooltip from "components/Tooltip"; +import { gigStatusTooltip } from "assets/data/my-gigs.json"; + +import "./styles.scss"; + +const StatusTooltip = ({ children }) => { + const Content = () => ( +
    +
      + {Object.keys(gigStatusTooltip).map((status) => ( +
    • +
      +
      {status}
      +
      {gigStatusTooltip[status]}
      +
      +
    • + ))} +
    +
    + ); + + return ( + } placement="bottom"> + {children} + + ); +}; + +StatusTooltip.propTypes = { + children: PT.node, +}; + +export default StatusTooltip; diff --git a/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/styles.scss b/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/styles.scss new file mode 100644 index 0000000..705123b --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/styles.scss @@ -0,0 +1,28 @@ +@import "styles/variables"; +@import "styles/mixins"; + +.status-tooltip { + width: 220px; + padding: 14px 12px 12px 14px; +} + +.caption, +.text { + font-size: $font-size-xs; + line-height: $line-height-xs; +} + +.caption { + @include roboto-medium; + + margin-bottom: 2px; + text-transform: uppercase; +} + +.text { + color: $tc-gray3; +} + +.item + .item { + margin-top: 16px; +} diff --git a/src/containers/MyGigs/modals/UpdateSuccess/index.jsx b/src/containers/MyGigs/modals/UpdateSuccess/index.jsx new file mode 100644 index 0000000..4d1fb96 --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateSuccess/index.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import PT from "prop-types"; +import Button from "components/Button"; +import IconUpdateSuccess from "assets/icons/update-success.svg"; + +import "./styles.scss"; + +const UpdateSuccess = ({ onClose }) => { + return ( +
    +

    + + RESUME UPDATED! +

    +

    Your resume has been successfuly updated.

    + +
    + ); +}; + +UpdateSuccess.propTypes = { + onClose: PT.func, +}; + +export default UpdateSuccess; diff --git a/src/containers/MyGigs/modals/UpdateSuccess/styles.scss b/src/containers/MyGigs/modals/UpdateSuccess/styles.scss new file mode 100644 index 0000000..8092d87 --- /dev/null +++ b/src/containers/MyGigs/modals/UpdateSuccess/styles.scss @@ -0,0 +1,42 @@ +@import "styles/variables"; +@import "styles/mixins"; + +.update-success { + display: flex; + flex-direction: column; + align-items: center; + width: 460px; + min-height: 420px; + padding: 60px; + background: url('../../../../assets/images/update-success-big.svg') no-repeat top right / auto, $white; + border-radius: 10px; + + .title { + @include barlow-condensed-medium; + + margin-bottom: 16px; + font-size: 34px; + color: $tc-turquoise-dark1; + line-height: 38px; + + .icon { + display: block; + margin: 12px auto 16px; + fill: currentColor; + } + } + + .text { + color: $tc-gray1; + line-height: 26px; + } + + button { + margin-top: auto; + margin-bottom: 4px; + color: $tc-turquoise-dark3; + border-color: $tc-turquoise-dark3; + } +} + + diff --git a/src/containers/MyGigs/styles.scss b/src/containers/MyGigs/styles.scss index 18ab2f0..ad8c50a 100644 --- a/src/containers/MyGigs/styles.scss +++ b/src/containers/MyGigs/styles.scss @@ -20,3 +20,8 @@ margin-left: auto; } } + +.empty-label { + text-align: center; + margin-top: 100px; +} diff --git a/src/reducers/lookup.js b/src/reducers/lookup.js index 5e9d2ef..d45e8db 100644 --- a/src/reducers/lookup.js +++ b/src/reducers/lookup.js @@ -8,7 +8,7 @@ const defaultState = { tags: [], subCommunities: [], isLoggedIn: null, - gigPhases: [], + gigStatuses: [], }; function onGetTagsDone(state, { payload }) { @@ -23,8 +23,8 @@ function onCheckIsLoggedInDone(state, { payload }) { return { ...state, isLoggedIn: payload }; } -function onGetGigPhasesDone(state, { payload }) { - return { ...state, gigPhases: payload }; +function onGetGigStatusesDone(state, { payload }) { + return { ...state, gigStatuses: payload }; } export default handleActions( @@ -32,7 +32,7 @@ export default handleActions( GET_TAGS_DONE: onGetTagsDone, GET_COMMUNITY_LIST_DONE: onGetCommunityListDone, CHECK_IS_LOGGED_IN_DONE: onCheckIsLoggedInDone, - GET_GIG_PHASES_DONE: onGetGigPhasesDone, + GET_GIG_STATUSES_DONE: onGetGigStatusesDone, }, defaultState ); diff --git a/src/reducers/myGigs.js b/src/reducers/myGigs.js index d4ff86b..a0759f3 100644 --- a/src/reducers/myGigs.js +++ b/src/reducers/myGigs.js @@ -1,13 +1,20 @@ +import { size, sortBy } from "lodash"; import { handleActions } from "redux-actions"; const defaultState = { loadingMyGigs: false, loadingMyGigsError: null, - myGigs: [], + myGigs: null, total: 0, numLoaded: 0, loadingMore: false, loadingMoreError: null, + profile: {}, + loadingProfile: false, + loadingProfileError: null, + updatingProfile: false, + updatingProfileError: null, + updatingProfileSucess: null, }; function onGetMyGigsInit(state) { @@ -17,7 +24,7 @@ function onGetMyGigsInit(state) { function onGetMyGigsDone(state, { payload }) { return { ...state, - myGigs: payload.myGigs, + myGigs: sortBy(payload.myGigs, ["sortPrio"]), total: payload.total, numLoaded: payload.myGigs.length, loadingMyGigs: false, @@ -30,7 +37,7 @@ function onGetMyGigsFailure(state, { payload }) { ...state, loadingMyGigs: false, loadingMyGigsError: payload, - myGigs: [], + myGigs: null, total: 0, numLoaded: 0, }; @@ -40,11 +47,11 @@ function onLoadMoreMyGigsInit(state) { return { ...state, loadingMore: true, loadingMoreError: null }; } -function onLoadMoreMyGigsDone(state, { payload }) { +function onLoadMoreMyGigsDone(state, { payload: { myGigs } }) { return { ...state, - myGigs: state.myGigs.concat(payload), - numLoaded: state.numLoaded + payload.length, + myGigs: sortBy(state.myGigs.concat(myGigs), ["sortPrio"]), + numLoaded: state.numLoaded + size(myGigs), loadingMore: false, loadingMoreError: null, }; @@ -54,6 +61,56 @@ function onLoadMoreMyGigsFailure(state, { payload }) { return { ...state, loadingMore: false, loadingMoreError: payload }; } +function onGetProfileInit(state) { + return { ...state, loadingProfile: true, loadingProfileError: null }; +} + +function onGetProfileDone(state, { payload }) { + return { + ...state, + profile: { ...payload }, + loadingProfile: false, + loadingProfileError: null, + }; +} + +function onGetProfileFailure(state, { payload }) { + return { + ...state, + loadingProfile: false, + loadingProfileError: payload, + profile: {}, + }; +} + +function onUpdateProfileInit(state) { + return { + ...state, + updatingProfile: true, + updatingProfileError: null, + updatingProfileSucess: null, + }; +} + +function onUpdateProfileDone(state, { payload }) { + return { + ...state, + profile: { ...payload }, + loadingProfile: false, + updatingProfileError: null, + updatingProfileSucess: true, + }; +} + +function onUpdateProfileFailure(state, { payload }) { + return { + ...state, + loadingProfile: false, + updatingProfileError: payload, + updatingProfileSucess: false, + }; +} + export default handleActions( { GET_MY_GIGS_INIT: onGetMyGigsInit, @@ -62,6 +119,12 @@ export default handleActions( LOAD_MORE_MY_GIGS_INIT: onLoadMoreMyGigsInit, LOAD_MORE_MY_GIGS_DONE: onLoadMoreMyGigsDone, LOAD_MORE_MY_GIGS_FAILURE: onLoadMoreMyGigsFailure, + GET_PROFILE_INIT: onGetProfileInit, + GET_PROFILE_DONE: onGetProfileDone, + GET_PROFILE_FAILURE: onGetProfileFailure, + UPDATE_PROFILE_INIT: onUpdateProfileInit, + UPDATE_PROFILE_DONE: onUpdateProfileDone, + UPDATE_PROFILE_FAILURE: onUpdateProfileFailure, }, defaultState ); diff --git a/src/services/api.js b/src/services/api.js index 1c2d73f..3eb2acd 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -27,8 +27,8 @@ async function doFetch(endpoint, options = {}, v3, baseUrl) { }); } -async function get(endpoint) { - const response = await doFetch(endpoint); +async function get(endpoint, baseUrl) { + const response = await doFetch(endpoint, undefined, undefined, baseUrl); const meta = utils.pagination.getResponseHeaders(response); const result = await response.json(); result.meta = meta; diff --git a/src/services/lookup.js b/src/services/lookup.js index 2db9c0b..002c7aa 100644 --- a/src/services/lookup.js +++ b/src/services/lookup.js @@ -54,13 +54,13 @@ async function getCommunityList() { ); } -async function getGigPhases() { - return Promise.resolve(myGigsData.phases); +async function getGigStatuses() { + return Promise.resolve(myGigsData.gigStatuses); } export default { getTags, getCommunityList, checkIsLoggedIn, - getGigPhases, + getGigStatuses, }; diff --git a/src/services/myGigs.js b/src/services/myGigs.js index cd35b94..d4db3d3 100644 --- a/src/services/myGigs.js +++ b/src/services/myGigs.js @@ -1,19 +1,108 @@ +import api from "./api"; +import { get, keys, size, sortBy, values } from "lodash"; +import { + ACTIONS_AVAILABLE_FOR_MY_GIG_PHASE, + AVAILABLE_REMARK_BY_JOB_STATUS, + JOB_STATUS_MAPPER, + JOB_STATUS_MESSAGE_MAPPER, + SORT_STATUS_ORDER, + PHASES_FOR_JOB_STATUS, + MY_GIG_PHASE, +} from "../constants"; import data from "../assets/data/my-gigs.json"; -let i = 0; +/** + * Maps the data from api to data to be used by application + * @param {Object} serverResponse data returned by the api + * @returns + */ +const mapMyGigsData = (serverResponse) => { + const phaseActionKeys = keys(ACTIONS_AVAILABLE_FOR_MY_GIG_PHASE); -async function getMyGigs() { - return Promise.resolve({ - myGigs: data.myGigs.slice(0, (i += 10)), - total: data.myGigs.length, - }); + return ( + serverResponse + .map((myGig) => { + const gigPhase = JOB_STATUS_MAPPER[myGig.status]; + const action = phaseActionKeys.find((key) => + ACTIONS_AVAILABLE_FOR_MY_GIG_PHASE[key].includes(gigPhase) + ); + const phases = PHASES_FOR_JOB_STATUS[myGig.status]; + const statusIndex = phases.findIndex((key) => key === gigPhase); + let previousStatus = null; + if (statusIndex >= 1) { + previousStatus = phases[statusIndex - 1]; + } + const sortPrio = SORT_STATUS_ORDER.findIndex( + (status) => status === gigPhase + ); + + return { + label: (gigPhase || "").toUpperCase(), + title: myGig.title, + paymentRangeFrom: myGig.payment.min, + paymentRangeTo: myGig.payment.max, + paymentRangeRateType: myGig.payment.frequency, + currency: myGig.payment.currency, + location: myGig.location, + duration: myGig.duration, + hours: myGig.hoursPerWeek, + workingHours: myGig.workingHours, + note: JOB_STATUS_MESSAGE_MAPPER[gigPhase], + phase: gigPhase, + phaseNote: JOB_STATUS_MESSAGE_MAPPER[gigPhase], + phaseAction: action, + phaseStatus: "Active", + remark: AVAILABLE_REMARK_BY_JOB_STATUS.includes(myGig.status) + ? myGig.remark + : "", + previous: + gigPhase == MY_GIG_PHASE.APPLIED ? gigPhase : previousStatus, + next: gigPhase == MY_GIG_PHASE.APPLIED ? null : gigPhase, + previousNote: + gigPhase == MY_GIG_PHASE.APPLIED + ? JOB_STATUS_MESSAGE_MAPPER[gigPhase] + : JOB_STATUS_MESSAGE_MAPPER[previousStatus], + nextNote: + gigPhase == MY_GIG_PHASE.APPLIED + ? null + : JOB_STATUS_MESSAGE_MAPPER[gigPhase], + status: myGig.status, + // in case there's some status not taken in account, it will show last. + sortPrio: sortPrio === -1 ? SORT_STATUS_ORDER.length + 1 : sortPrio, + }; + }) + // enforce that all have a valid status. Those without a status should be ignored. + .filter((gig) => gig.status) + ); +}; +/** + * Fetches MyGigs data from service + * @param {*} page page number to request + * @param {*} perPage item per page to request + * @returns + */ +async function getMyGigs(page, perPage) { + const response = await api.get( + `/earn-app/api/my-gigs/myJobApplications?page=${page}&perPage=${perPage}`, + process.env.URL.PLATFORM_WEBSITE_URL + ); + + return { + myGigs: mapMyGigsData(response), + total: response.meta.total, + }; +} + +async function getProfile() { + return Promise.resolve(data.gigProfile); } -async function loadMoreMyGigs() { - return Promise.resolve(data.myGigs.slice(i, (i += 10))); +async function updateProfile(profile) { + return Promise.resolve(profile); } export default { getMyGigs, - loadMoreMyGigs, + getProfile, + updateProfile, }; diff --git a/src/styles/GUIKit/Includes/_mixin-utils.scss b/src/styles/GUIKit/Includes/_mixin-utils.scss index cf96c7b..02fc6cd 100644 --- a/src/styles/GUIKit/Includes/_mixin-utils.scss +++ b/src/styles/GUIKit/Includes/_mixin-utils.scss @@ -28,3 +28,13 @@ } } } + +@mixin error-msg-not-change-container-height { + :global { + .errorMsg { + position: absolute; + top: 100%; + margin-top: 0; + } + } +} diff --git a/src/styles/_utils.scss b/src/styles/_utils.scss index 0b46dc8..9bbd60f 100644 --- a/src/styles/_utils.scss +++ b/src/styles/_utils.scss @@ -60,6 +60,6 @@ left: 0; width: 0; height: 0; - z-index: 1000; + z-index: 1001; } } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index f2ebb38..99b937f 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -5,7 +5,6 @@ * colors in your stylesheets. */ /* Grayscale colors. */ -$tc-black: #151516; $tc-gray-90: #2a2a2b; $tc-gray-80: #404041; $tc-gray-70: #555; @@ -19,8 +18,25 @@ $tc-gray-10: #d5d5d5; $tc-gray-05: #e0e0e0; $tc-gray-neutral-dark: #ebebeb; $tc-gray-neutral-light: #fafafb; +$tc-black: #2A2A2A; $tc-white: #fff; +$tc-gray1: #555555; +$tc-gray2: #7f7f7f; +$tc-gray3: #aaaaaa; +$tc-gray4: #d4d4d4; +$tc-gray5: #e9e9e9; +$tc-gray6: #f4f4f4; +$tc-gray7: #fbfbfb; + +$tc-blue: #2c95d7; +$tc-blue-light1: #50ade8; +$tc-blue-light2: #83c5ee; +$tc-blue-light3: #c6e6ea; +$tc-blue-light4: #eaf6fd; +$tc-blue-dark1: #2984bd; +$tc-blue-dark2: #16679a; + /* Accents & Shades. */ $tc-dark-blue-110: #006ad7; $tc-dark-blue-105: #006feb; @@ -49,6 +65,12 @@ $tc-red-100: #f52c14; $tc-red-70: #ff5a4c; $tc-red-30: #ffd4d0; $tc-red-10: #fff4f4; +$tc-red: #ef476f; +$tc-red-light1: #f37593; +$tc-red-light2: #f7a3b7; +$tc-red-light3: #fbd1db; +$tc-red-dark1: #be405e; +$tc-red-dark2: #8c384c; /* Yellow. */ $tc-yellow-110: #f2c900; @@ -64,6 +86,22 @@ $tc-green-100: #5cc900; $tc-green-70: #94db4e; $tc-green-30: #cef0af; $tc-green-10: #effae4; +$tc-green: #63f963; +$tc-green-light1: #8afb8a; +$tc-green-light2: #b0fcb1; +$tc-green-light3: #d8fdd8; +$tc-green-dark1: #4cc94c; +$tc-green-dark2: #35ac35; + +/* Turquoise */ +$tc-turquoise: #06d6a0; +$tc-turquoise-light1: #45e1b8; +$tc-turquoise-light2: #82eacf; +$tc-turquoise-light3: #c1f5e7; +$tc-turquoise-light4: #e0faf3; +$tc-turquoise-dark1: #0ab88a; +$tc-turquoise-dark2: #219174; +$tc-turquoise-dark3: #137d60; /* Purples. */ $tc-purple-120: #8231a9; @@ -120,6 +158,10 @@ $tc-level-3: $tc-pastel-blue; $tc-level-4: $tc-pastel-yellow; $tc-level-5: $tc-pastel-crimson; +/* Links */ +$tc-link-blue: #0d61bf; +$tc-link-blue-light: #5fb7ee; + /// FONTS $font-size-base: 16px; @@ -137,10 +179,10 @@ $body-color: $tc-gray-90; $white: $tc-white; $green: #229174; $green2: #287d61; -$green3: #06D6A0; -$darkGreen: #137d60; -$lightGreen: #0ab88a; -$lightGreen2: #06d6a0; +$green3: $tc-turquoise; +$darkGreen: $tc-turquoise-dark3; +$lightGreen: $tc-turquoise-dark1; +$lightGreen2: $tc-turquoise; $blue: #2c95d7; $gray1: #fbfbfb; $gray2: #f4f4f4; diff --git a/src/utils/myGig.js b/src/utils/myGig.js index 6929ce5..89f270b 100644 --- a/src/utils/myGig.js +++ b/src/utils/myGig.js @@ -1,5 +1,12 @@ +import codes from "country-calling-code"; +import countries from "i18n-iso-countries"; +import enLocale from "i18n-iso-countries/langs/en.json"; import * as constants from "../constants"; +countries.registerLocale(enLocale); + +export { countries }; + export function isPassedPhase(allPhases, currentPhase, checkPhase) { const currentPhaseIndex = allPhases.indexOf(currentPhase); const checkPhaseIndex = allPhases.indexOf(checkPhase); @@ -14,3 +21,75 @@ export function isFirstPhase(checkPhase) { export function isLastPhase(checkPhase) { return checkPhase === constants.MY_GIG_PHASE.PLACED; } + +export function createTextImage(text) { + const canvas = document.createElement("canvas"); + canvas.width = 80; + canvas.height = 80; + + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#2C95D7"; + ctx.fillRect(0, 0, 80, 80); + ctx.fillStyle = "#fff"; + ctx.font = "500 24px Roboto san-serif"; + ctx.textAlign = "center"; + ctx.fillText(text, 40, 48); + + return canvas.toDataURL(); +} + +function validateTextRequired(value) { + value = (value || "").trim(); + + if (!value) return "Required field"; + if (value.length < 2) return "Must be at least 2 characters"; + if (value.length > 50) return "Must be max 50 characters"; + + return null; +} + +export function validateCity(value) { + return validateTextRequired(value); +} + +export function validatePhone(phoneNumber, country) { + const countryCode = countries.getAlpha2Code(country, "en") || "US"; + let error = validateTextRequired(phoneNumber); + if (error) { + return error; + } + + phoneNumber = phoneNumber.trim(); + + const code = codes.find((i) => i.isoCode2 === countryCode); + const regionCode = `+${code.countryCodes[0]}`; + + error = !phoneNumber.startsWith(regionCode) && "Invalid country code"; + + if (!error) { + const regexValidCharacters = /[\s0-9+-\.()]/g; + error = + phoneNumber.replace(regexValidCharacters, "") !== "" && + "Invalid phone number"; + } + + if (!error) { + const regexSeparateCharacters = /[\s-\.()]/g; + const numberLen = phoneNumber + .replace(regionCode, "") + .replace(regexSeparateCharacters, "").length; + error = (numberLen < 9 || numberLen > 11) && "Invalid phone number"; + } + + if (!error) { + const regexGroupCharacters = /[-\.()]/g; + const groups = phoneNumber + .replace(regionCode, "") + .replace(regexGroupCharacters, " ") + .trim() + .split(/\s+/); + error = groups.length > 3 && "Invalid phone format"; + } + + return error ? error : null; +} diff --git a/webpack.config.js b/webpack.config.js index 81b9285..35e18ae 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,12 +1,12 @@ process.env.NODE_ENV = process.env.APPMODE; -const config = require('config'); -const _ = require('lodash'); +const config = require("config"); +const _ = require("lodash"); const webpackMerge = require("webpack-merge"); const singleSpaDefaults = require("webpack-config-single-spa-react"); -const autoprefixer = require('autoprefixer'); -const path = require('path') -const webpack = require('webpack'); +const autoprefixer = require("autoprefixer"); +const path = require("path"); +const webpack = require("webpack"); module.exports = (webpackConfigEnv) => { const defaultConfig = singleSpaDefaults({ @@ -16,14 +16,19 @@ module.exports = (webpackConfigEnv) => { disableHtmlGeneration: true, }); - const unusedFilesWebpackPlugin = defaultConfig.plugins.find(p => p.constructor.name === "UnusedFilesWebpackPlugin"); - unusedFilesWebpackPlugin.globOptions.ignore.push("**/assets/icons/*.svg", "**/__mocks__/**"); + const unusedFilesWebpackPlugin = defaultConfig.plugins.find( + (p) => p.constructor.name === "UnusedFilesWebpackPlugin" + ); + unusedFilesWebpackPlugin.globOptions.ignore.push( + "**/assets/icons/*.svg", + "**/__mocks__/**" + ); let cssLocalIdent; - if (process.env.APPMODE == 'development') { - cssLocalIdent = 'earn_[path][name]___[local]___[hash:base64:6]'; + if (process.env.APPMODE == "development") { + cssLocalIdent = "earn_[path][name]___[local]___[hash:base64:6]"; } else { - cssLocalIdent = '[hash:base64:6]'; + cssLocalIdent = "[hash:base64:6]"; } // modify the webpack config however you'd like to by adding to this object @@ -31,7 +36,8 @@ module.exports = (webpackConfigEnv) => { // we have to list here all the microapps which we would like to use in imports // so webpack doesn't tries to import them externals: { - "@topcoder/micro-frontends-navbar-app": "@topcoder/micro-frontends-navbar-app", + "@topcoder/micro-frontends-navbar-app": + "@topcoder/micro-frontends-navbar-app", }, module: { rules: [ @@ -43,66 +49,62 @@ module.exports = (webpackConfigEnv) => { /[/\\]assets[/\\]fonts/, /[/\\]assets[/\\]images/, ], - loader: 'babel-loader', + loader: "babel-loader", }, { /* Loads images */ test: /\.(svg|gif|jpe?g|png)$/, - exclude: [ - /[/\\]assets[/\\]fonts/ - ], - loader: 'file-loader', + exclude: [/[/\\]assets[/\\]fonts/], + loader: "file-loader", options: { - outputPath: 'images', - } + outputPath: "images", + }, }, { /* Loads fonts */ test: /\.(eot|otf|svg|ttf|woff2?)$/, - exclude: [ - /[/\\]assets[/\\]images/ - ], - loader: 'file-loader', + exclude: [/[/\\]assets[/\\]images/], + loader: "file-loader", options: { - outputPath: 'fonts', - } + outputPath: "fonts", + }, }, { /* Loads scss stylesheets. */ test: /\.scss$/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - modules: { - localIdentName: cssLocalIdent, - mode: 'local', - } + use: [ + "style-loader", + { + loader: "css-loader", + options: { + modules: { + localIdentName: cssLocalIdent, + mode: "local", }, }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: [autoprefixer], - } + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [autoprefixer], }, }, - 'resolve-url-loader', - { - loader: 'sass-loader', - options: { - sourceMap: true, - }, - } - ] - } + }, + "resolve-url-loader", + { + loader: "sass-loader", + options: { + sourceMap: true, + }, + }, + ], + }, ], }, plugins: [ new webpack.DefinePlugin({ - 'process.env': { + "process.env": { ..._.mapValues(config, (value) => JSON.stringify(value)), APPENV: JSON.stringify(process.env.APPENV), APPMODE: JSON.stringify(process.env.APPMODE), @@ -113,11 +115,17 @@ module.exports = (webpackConfigEnv) => { alias: { styles: path.resolve(__dirname, "src/styles"), assets: path.resolve(__dirname, "src/assets"), - } + }, }, devServer: { + clientLogLevel: config.LOG_LEVEL, + before: function (app) { + require("./src/api/bootstrap"); + // Register routes + require("./src/api/app-routes")(app); + }, port: 8008, - host: '0.0.0.0' - } + host: "0.0.0.0", + }, }); };