├── .github └── workflows-1 │ └── node.js.yml ├── .vscode └── settings.json ├── Banner.png ├── README.md ├── backend ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dist │ ├── config │ │ └── env.js │ ├── constants │ │ └── responseMessages.js │ ├── db │ │ └── index.js │ ├── index.js │ ├── middlewares │ │ └── authenticated.js │ ├── modules │ │ ├── urls │ │ │ ├── controller.js │ │ │ ├── model.js │ │ │ ├── routes.js │ │ │ ├── services.js │ │ │ └── validators.js │ │ └── users │ │ │ ├── controller.js │ │ │ ├── model.js │ │ │ ├── routes.js │ │ │ ├── services.js │ │ │ └── validators.js │ ├── routes │ │ ├── index.js │ │ └── v1 │ │ │ └── index.js │ ├── service │ │ └── email.js │ ├── swagger.json │ └── utils │ │ ├── encryption.js │ │ ├── handleCustomErrors.js │ │ ├── hashPayload.js │ │ ├── httpStatusCode.js │ │ ├── index.js │ │ ├── sendResponse.js │ │ └── url.js ├── jest.config.js ├── nodemon.json ├── package.json ├── src │ ├── config │ │ └── env.ts │ ├── constants │ │ └── responseMessages.ts │ ├── db │ │ └── index.ts │ ├── index.spec.ts │ ├── index.ts │ ├── middlewares │ │ └── authenticated.ts │ ├── modules │ │ ├── urls │ │ │ ├── controller.ts │ │ │ ├── model.ts │ │ │ ├── routes.ts │ │ │ ├── services.ts │ │ │ └── validators.ts │ │ └── users │ │ │ ├── controller.ts │ │ │ ├── model.ts │ │ │ ├── routes.ts │ │ │ ├── services.ts │ │ │ └── validators.ts │ ├── routes │ │ ├── index.ts │ │ └── v1 │ │ │ └── index.ts │ ├── service │ │ └── email.ts │ ├── swagger.json │ └── utils │ │ ├── encryption.ts │ │ ├── handleCustomErrors.ts │ │ ├── hashPayload.ts │ │ ├── httpStatusCode.ts │ │ ├── index.ts │ │ ├── sendResponse.ts │ │ └── url.ts ├── tsconfig.json ├── typings.d.ts ├── vercel.json └── yarn.lock └── frontend ├── .env ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── package.json ├── public └── vite.svg ├── src ├── App.tsx ├── components │ ├── Dropzone │ │ └── Dropzone.tsx │ ├── ErrorBoundary.tsx │ ├── FallbackPageWrapper.tsx │ ├── FilesList │ │ └── FilesList.tsx │ ├── Layout.tsx │ ├── NavBar │ │ ├── ActionAvatar.tsx │ │ └── NavBar.tsx │ ├── UploadForm.tsx │ ├── UploadProgressIndicator.tsx │ ├── UrlForm.tsx │ └── auth │ │ ├── SignInForm.tsx │ │ └── SignUpForm.tsx ├── config │ ├── env.ts │ ├── images.ts │ ├── routes.ts │ └── routes_config.ts ├── hooks │ ├── useAuth.ts │ └── usePageRoute.ts ├── index.css ├── main.tsx ├── pages │ ├── Home.tsx │ ├── NotFound.tsx │ ├── SignIn.tsx │ ├── SignUp.tsx │ ├── Upload.tsx │ └── UrlLinks.tsx ├── providers │ └── FallbackProvider.tsx ├── routes │ ├── PrivateRoute.tsx │ ├── PublicRoute.tsx │ ├── Routes.tsx │ └── index.tsx ├── services │ ├── HttpService.ts │ └── Storage.ts ├── styles │ └── global.style.tsx ├── utils │ ├── DeviceDetect.ts │ ├── Toast.tsx │ └── commonFunction.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.ts └── yarn.lock /.github/workflows-1/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - name: Check out repository 24 | uses: actions/checkout@v3 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'yarn' 30 | cache-dependency-path: backend/yarn.lock 31 | - name: Deploy to Heroku 32 | uses: akhileshns/heroku-deploy@v3.12.12 # This is the action 33 | with: 34 | heroku_email: ${{secrets.HEROKU_EMAIL}} 35 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 36 | heroku_app_name: ${{secrets.HEROKU_APP_NAME}} 37 | # HEROKU_EMAIL, HEROKU_API_KEY and HEROKU_APP_NAME will need to be set in repository secrets -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | "./frontend", 4 | ] 5 | } -------------------------------------------------------------------------------- /Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asif-simform/MERN-Stack/34602efea44daf47754ab62eb2ef71e02730abe2/Banner.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN-Stack 2 | 3 | Web app built using MongoDB, ExpressJS, Node.js, and Reactjs (often called as MERN stack). 4 | 5 | MongoDB, ExpressJS, Node.js, and Reactjs are used together to build web applications. In this, Node.js and Express bind together to serve the backend, MongoDB provides a NoSQL database to store the data and frontend is built using React that a user interacts with. All four of these technologies are open source, cross-platform and JavaScript based. Since they are JavaScript based, one of the main reasons why they are often used together. 6 | 7 | ![](./Banner.png) 8 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | PORT= 2 | DB_NAME= 3 | DB_USERNAME= 4 | DB_PASSWORD= 5 | CLUSTER_URL= 6 | EMAIL_HOST= 7 | EMAIL_PORT= 8 | EMAIL_USERNAME= 9 | EMAIL_API_KEY= 10 | TOKEN_SECRET= 11 | ACCESS_TOKEN_EXPIRY= 12 | ACCESS_TOKEN_ALGO= 13 | REFRESH_TOKEN_EXPIRY= 14 | REFRESH_TOKEN_ALGO= -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | build 5 | dist 6 | # don't lint nyc coverage output 7 | coverage -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier", "jest"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "no-console": 1, 12 | "prettier/prettier": 2 13 | }, 14 | "env": { 15 | "browser": true, 16 | "node": true, 17 | "jest/globals": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.local 74 | .env.development.local 75 | .env.test.local 76 | .env.production.local 77 | # .env.test 78 | 79 | # parcel-bundler cache 80 | .cache 81 | 82 | # build / generate output 83 | build/ 84 | 85 | # Stores VSCode versions used for testing VSCode extensions 86 | .vscode-test -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /backend/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute? 2 | - 1 Fork the project. 3 | - 2 Make required changes and commit. 4 | - 3 Generate pull request. Mention all the required description regarding changes you made. 5 | 6 | Happy coding. 🙂 -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Asif Vora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Node Express Server with TypeScript 2 | 3 | For help getting started with Node, Express and MongoDB view online documentation 4 | 5 | - [Node.js](https://nodejs.org/en/docs/) 6 | - [Express.js](https://expressjs.com/) 7 | - [MongoDB](https://docs.mongodb.com/) 8 | 9 | #### 1. [Setup Node](https://nodejs.org/en/) 10 | 11 | #### 2. Clone the repo 12 | 13 | ```sh 14 | $ https://https://github.com/asif-simform/MERN-Stack.git 15 | $ cd MERN-Stack/backend 16 | ``` 17 | 18 | #### 3. Install dependency 19 | 20 | ```sh 21 | $ npm install 22 | ``` 23 | 24 | #### 4. Starts the application in development using `nodemon` and `ts-node` to do hot reloading. 25 | ```bash 26 | npm run dev 27 | ``` 28 | 29 | #### 5. Starts the app in production by first building the project with `npm run build`, and then executing the compiled JavaScript at `dist/index.js`. 30 | ```bash 31 | npm run start 32 | ``` 33 | 34 | #### 6. Builds the app at `build`, cleaning the folder first. 35 | ```bash 36 | npm run build 37 | ``` 38 | 39 | #### 7. Runs the `jest` tests once. 40 | ```bash 41 | npm run test 42 | ``` 43 | 44 | #### 8. Run the `jest` tests in watch mode, waiting for file changes. 45 | ```bash 46 | npm run test:dev 47 | ``` 48 | 49 | #### 9. Format your code. 50 | ```bash 51 | npm run prettier-format 52 | ``` 53 | 54 | #### 10. Format your code in watch mode, waiting for file changes. 55 | ```bash 56 | npm run prettier-watch 57 | ``` 58 | 59 | # 👨‍💻 Demo 60 | Check out the API's : https://mern-stack-cyan.vercel.app 61 | 62 | # 📖 App architecture 63 | - src 64 | - constants 65 | - db 66 | - middlewares 67 | - modules 68 | - users 69 | - routes 70 | - utils 71 | # 💻 Built With 72 | - [NodeJS](https://nodejs.org/en/) 73 | - [ExpressJS](https://expressjs.com/) 74 | - [TypeScript](https://www.typescriptlang.org/) 75 | 76 | # 🎉 Features 77 | 78 | - Minimal 79 | - TypeScript v4 80 | - Testing with Jest 81 | - Linting with Eslint and Prettier 82 | - Pre-commit hooks with Husky 83 | - VS Code debugger scripts 84 | - Local development with Nodemon 85 | 86 | # 🛡️ License 87 | 88 | This project is licensed under the MIT License - see the [`LICENSE`](LICENSE) file for details. 89 | 90 | # 👨‍💻 Author 91 | ### 👤 Asif Vora 92 | - Github: [@asif-simform](https://github.com/asif-simform) 93 | - LinkedIn: [@asif-vora](https://www.linkedin.com/in/asif-vora/) 94 | - Twitter: [@007_dark_shadow](https://twitter.com/007_dark_shadow) 95 | - Instagram: [@007_dark_shadow](https://www.instagram.com/007_dark_shadow/) 96 | 97 | # 🍰 Contributing 98 | 99 | - Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/asif-simform/MERN-Stack/compare). 100 | 101 | - Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details. 102 | 103 | # 🙏 Support 104 | This project needs a ⭐️ from you. Don't forget to leave a star ⭐️ -------------------------------------------------------------------------------- /backend/dist/config/env.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | Object.defineProperty(exports, "__esModule", { value: true }); 26 | exports.REFRESH_TOKEN_ALGO = exports.REFRESH_TOKEN_EXPIRY = exports.ACCESS_TOKEN_ALGO = exports.ACCESS_TOKEN_EXPIRY = exports.TOKEN_SECRET = exports.EMAIL_API_KEY = exports.EMAIL_USERNAME = exports.EMAIL_PORT = exports.EMAIL_HOST = exports.CLUSTER_URL = exports.DB_NAME = exports.DB_PASSWORD = exports.DB_USERNAME = exports.IS_DEVELOPMENT = exports.PORT = void 0; 27 | var dotenv = __importStar(require("dotenv")); 28 | dotenv.config(); 29 | exports.PORT = process.env.PORT || 8080; 30 | exports.IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 31 | exports.DB_USERNAME = process.env.DB_USERNAME; 32 | exports.DB_PASSWORD = process.env.DB_PASSWORD; 33 | exports.DB_NAME = process.env.DB_NAME; 34 | exports.CLUSTER_URL = process.env.CLUSTER_URL; 35 | exports.EMAIL_HOST = process.env.EMAIL_HOST; 36 | exports.EMAIL_PORT = process.env.EMAIL_PORT; 37 | exports.EMAIL_USERNAME = process.env.EMAIL_USERNAME; 38 | exports.EMAIL_API_KEY = process.env.EMAIL_API_KEY; 39 | exports.TOKEN_SECRET = process.env.TOKEN_SECRET; 40 | exports.ACCESS_TOKEN_EXPIRY = process.env.ACCESS_TOKEN_EXPIRY; 41 | exports.ACCESS_TOKEN_ALGO = process.env.ACCESS_TOKEN_ALGO; 42 | exports.REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY; 43 | exports.REFRESH_TOKEN_ALGO = process.env.REFRESH_TOKEN_ALGO; 44 | -------------------------------------------------------------------------------- /backend/dist/constants/responseMessages.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.reponseMessages = void 0; 4 | exports.reponseMessages = { 5 | genericError: 'Something went wrong! Please try again later.', 6 | genericSuccess: 'Request successful.', 7 | }; 8 | -------------------------------------------------------------------------------- /backend/dist/db/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var mongodb_1 = require("mongodb"); 4 | var env_1 = require("../config/env"); 5 | // Replace the uri string with your connection string. 6 | // example : mongodb+srv://:@?retryWrites=true&w=majority 7 | var URI = "mongodb+srv://".concat(env_1.DB_USERNAME, ":").concat(env_1.DB_PASSWORD, "@").concat(env_1.CLUSTER_URL, "?retryWrites=true&w=majority"); 8 | var client = new mongodb_1.MongoClient(env_1.IS_DEVELOPMENT ? "mongodb://localhost:27017/".concat(env_1.DB_NAME) : URI); 9 | console.log("\u26A1\uFE0F[App runing in]: ".concat(env_1.IS_DEVELOPMENT ? 'Development' : ' PROD', " mode")); 10 | client.connect(function (error) { 11 | if (error) { 12 | console.log("\u26A1\uFE0F[DB]: Could not connected to database!"); 13 | console.log(error); 14 | } 15 | else { 16 | console.log("\u26A1\uFE0F[DB]: Connected to database!"); 17 | } 18 | }); 19 | var db = client.db(env_1.DB_NAME); 20 | exports.default = db; 21 | -------------------------------------------------------------------------------- /backend/dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var env_1 = require("./config/env"); 7 | var express_1 = __importDefault(require("express")); 8 | var body_parser_1 = __importDefault(require("body-parser")); 9 | var multer_1 = __importDefault(require("multer")); 10 | var cors_1 = __importDefault(require("cors")); 11 | var helmet_1 = __importDefault(require("helmet")); 12 | var swagger_ui_express_1 = __importDefault(require("swagger-ui-express")); 13 | var swagger_json_1 = __importDefault(require("./swagger.json")); 14 | // custom modules 15 | var routes_1 = __importDefault(require("./routes")); 16 | require("./db"); 17 | var app = (0, express_1.default)(); 18 | // parse application/json 19 | app.use(body_parser_1.default.json()); 20 | // parse application/x-www-form-urlencoded 21 | app.use(body_parser_1.default.urlencoded({ extended: true })); 22 | app.use((0, multer_1.default)().array()); 23 | app.use((0, helmet_1.default)()); 24 | app.use((0, cors_1.default)()); 25 | app.disable('x-powered-by'); 26 | app.get('/', function (req, res) { 27 | res.status(200).json({ 28 | message: 'Welcome to Node API\'s by 007', 29 | }); 30 | }); 31 | app.use(routes_1.default); 32 | app.use('/api-docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(swagger_json_1.default, { 33 | customCssUrl: 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.0/swagger-ui.min.css' 34 | })); 35 | app.listen(env_1.PORT, function () { 36 | console.log("\u26A1\uFE0F[server]: Server is running at ".concat(env_1.IS_DEVELOPMENT ? 'http://localhost:' : '').concat(env_1.PORT)); 37 | }); 38 | -------------------------------------------------------------------------------- /backend/dist/middlewares/authenticated.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.isAuthenticated = void 0; 40 | var utils_1 = require("../utils"); 41 | var whiteListEndpoints = ["/urls/short"]; 42 | var isAuthenticated = function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 43 | var token, decoded; 44 | return __generator(this, function (_a) { 45 | token = req.header('Authorization'); 46 | try { 47 | if (!token) { 48 | if (whiteListEndpoints.includes(req.url)) { 49 | return [2 /*return*/, next()]; 50 | } 51 | return [2 /*return*/, (0, utils_1.sendResponse)(res, utils_1.HttpStatusCode.Unauthorized, { tokenExpired: 0 }, 'Failed to Authenticate')]; 52 | } 53 | decoded = (0, utils_1.decryptAccessToken)(token); 54 | // if everything is good, save to request for use in other routes 55 | req.user = decoded; 56 | next(); 57 | } 58 | catch (err) { 59 | if ((err === null || err === void 0 ? void 0 : err['name']) === 'TokenExpiredError') { 60 | return [2 /*return*/, (0, utils_1.sendResponse)(res, utils_1.HttpStatusCode.Unauthorized, { tokenExpired: 1 }, 'Token Expired')]; 61 | } 62 | if ((err === null || err === void 0 ? void 0 : err['name']) === 'JsonWebTokenError') { 63 | return [2 /*return*/, (0, utils_1.sendResponse)(res, utils_1.HttpStatusCode.Unauthorized, { tokenExpired: 0 }, 'Corrupt Token')]; 64 | } 65 | } 66 | return [2 /*return*/, 0]; 67 | }); 68 | }); }; 69 | exports.isAuthenticated = isAuthenticated; 70 | -------------------------------------------------------------------------------- /backend/dist/modules/urls/controller.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.getAllShortUrlsByUser = exports.getShortUrl = exports.createShortUrl = void 0; 40 | var express_validator_1 = require("express-validator"); 41 | var services_1 = require("./services"); 42 | var responseMessages_1 = require("../../constants/responseMessages"); 43 | var sendResponse_1 = require("../../utils/sendResponse"); 44 | var handleCustomErrors_1 = require("../../utils/handleCustomErrors"); 45 | var createShortUrl = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 46 | var errors, extractedErrors, originalUrl, baseURL, userId, data, err_1; 47 | var _a; 48 | return __generator(this, function (_b) { 49 | switch (_b.label) { 50 | case 0: 51 | _b.trys.push([0, 2, , 3]); 52 | errors = (0, express_validator_1.validationResult)(req); 53 | if (!errors.isEmpty()) { 54 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 55 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 56 | } 57 | originalUrl = req.body.originalUrl; 58 | baseURL = req.get('origin'); 59 | userId = ((_a = req === null || req === void 0 ? void 0 : req.user) === null || _a === void 0 ? void 0 : _a.id) || ''; 60 | return [4 /*yield*/, (0, services_1.create)({ originalUrl: originalUrl, baseURL: baseURL, userId: userId })]; 61 | case 1: 62 | data = _b.sent(); 63 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 201, data, responseMessages_1.reponseMessages.genericSuccess)]; 64 | case 2: 65 | err_1 = _b.sent(); 66 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_1)]; 67 | case 3: return [2 /*return*/]; 68 | } 69 | }); 70 | }); }; 71 | exports.createShortUrl = createShortUrl; 72 | var getShortUrl = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 73 | var errors, extractedErrors, urlId, originalUrl, err_2; 74 | return __generator(this, function (_a) { 75 | switch (_a.label) { 76 | case 0: 77 | _a.trys.push([0, 2, , 3]); 78 | errors = (0, express_validator_1.validationResult)(req); 79 | if (!errors.isEmpty()) { 80 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 81 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 82 | } 83 | urlId = req.params.urlId; 84 | return [4 /*yield*/, (0, services_1.get)(urlId)]; 85 | case 1: 86 | originalUrl = _a.sent(); 87 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 201, { originalUrl: originalUrl }, responseMessages_1.reponseMessages.genericSuccess)]; 88 | case 2: 89 | err_2 = _a.sent(); 90 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_2)]; 91 | case 3: return [2 /*return*/]; 92 | } 93 | }); 94 | }); }; 95 | exports.getShortUrl = getShortUrl; 96 | var getAllShortUrlsByUser = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 97 | var errors, extractedErrors, userId, data, err_3; 98 | var _a; 99 | return __generator(this, function (_b) { 100 | switch (_b.label) { 101 | case 0: 102 | _b.trys.push([0, 2, , 3]); 103 | errors = (0, express_validator_1.validationResult)(req); 104 | if (!errors.isEmpty()) { 105 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 106 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 107 | } 108 | userId = ((_a = req === null || req === void 0 ? void 0 : req.user) === null || _a === void 0 ? void 0 : _a.id) || ''; 109 | return [4 /*yield*/, (0, services_1.getAll)(userId)]; 110 | case 1: 111 | data = _b.sent(); 112 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 201, { data: data }, responseMessages_1.reponseMessages.genericSuccess)]; 113 | case 2: 114 | err_3 = _b.sent(); 115 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_3)]; 116 | case 3: return [2 /*return*/]; 117 | } 118 | }); 119 | }); }; 120 | exports.getAllShortUrlsByUser = getAllShortUrlsByUser; 121 | -------------------------------------------------------------------------------- /backend/dist/modules/urls/model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var mongoose_1 = __importDefault(require("mongoose")); 7 | var Schema = mongoose_1.default.Schema; 8 | var urlSchema = new Schema({ 9 | urlId: { 10 | type: String, 11 | required: true 12 | }, 13 | originalUrl: { 14 | type: String, 15 | required: true, 16 | }, 17 | shortUrl: { 18 | type: String, 19 | required: true, 20 | unique: true, 21 | }, 22 | clicks: { 23 | type: Number, 24 | required: true, 25 | default: 0, 26 | }, 27 | date: { 28 | type: String, 29 | default: Date.now, 30 | }, 31 | userId: { 32 | type: mongoose_1.default.Schema.Types.ObjectId, 33 | required: false, 34 | default: null 35 | } 36 | }); 37 | var Urls = mongoose_1.default.model('Urls', urlSchema); 38 | exports.default = Urls; 39 | -------------------------------------------------------------------------------- /backend/dist/modules/urls/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var controller_1 = require("./controller"); 8 | var validators_1 = require("./validators"); 9 | var authenticated_1 = require("../../middlewares/authenticated"); 10 | var router = express_1.default.Router(); 11 | router.get('/urls/list', authenticated_1.isAuthenticated, controller_1.getAllShortUrlsByUser); 12 | router.post('/urls/short', authenticated_1.isAuthenticated, (0, validators_1.validateCreateShortUrlRequest)(), controller_1.createShortUrl); 13 | router.get('/urls/:urlId', controller_1.getShortUrl); 14 | exports.default = router; 15 | -------------------------------------------------------------------------------- /backend/dist/modules/urls/services.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | exports.getAll = exports.get = exports.create = void 0; 43 | var shortid_1 = __importDefault(require("shortid")); 44 | var model_1 = __importDefault(require("./model")); 45 | var db_1 = __importDefault(require("../../db")); 46 | var utils_1 = require("../../utils"); 47 | var urls = db_1.default.collection('urls'); 48 | var create = function (_a) { 49 | var originalUrl = _a.originalUrl, baseURL = _a.baseURL, userId = _a.userId; 50 | return __awaiter(void 0, void 0, void 0, function () { 51 | var msg, error, urlId, shortUrl, url; 52 | return __generator(this, function (_b) { 53 | switch (_b.label) { 54 | case 0: 55 | if (!(0, utils_1.validateUrl)(originalUrl)) { 56 | msg = 'Invalid URL'; 57 | error = new Error(msg); 58 | error['code'] = 422; 59 | error['message'] = msg; 60 | throw error; 61 | } 62 | urlId = shortid_1.default.generate(); 63 | shortUrl = "".concat(baseURL, "/").concat(urlId); 64 | url = new model_1.default({ 65 | urlId: urlId, 66 | originalUrl: originalUrl, 67 | shortUrl: shortUrl, 68 | date: new Date(), 69 | userId: userId 70 | }); 71 | return [4 /*yield*/, urls.insertOne(url)]; 72 | case 1: 73 | _b.sent(); 74 | return [2 /*return*/, { 75 | shortUrl: shortUrl 76 | }]; 77 | } 78 | }); 79 | }); 80 | }; 81 | exports.create = create; 82 | var get = function (urlId) { return __awaiter(void 0, void 0, void 0, function () { 83 | var res, msg, error; 84 | return __generator(this, function (_a) { 85 | switch (_a.label) { 86 | case 0: return [4 /*yield*/, urls.findOne({ urlId: urlId })]; 87 | case 1: 88 | res = _a.sent(); 89 | if (!res) { 90 | msg = 'Short URL not found in records'; 91 | error = new Error(msg); 92 | error['code'] = 404; 93 | error['message'] = msg; 94 | throw error; 95 | } 96 | return [4 /*yield*/, urls.findOneAndUpdate({ urlId: urlId }, { $inc: { clicks: 1 } })]; 97 | case 2: 98 | _a.sent(); 99 | return [2 /*return*/, res.originalUrl]; 100 | } 101 | }); 102 | }); }; 103 | exports.get = get; 104 | var getAll = function (userId) { return __awaiter(void 0, void 0, void 0, function () { 105 | var res, msg, error; 106 | return __generator(this, function (_a) { 107 | switch (_a.label) { 108 | case 0: return [4 /*yield*/, urls.find({ userId: userId })]; 109 | case 1: 110 | res = _a.sent(); 111 | if (!res) { 112 | msg = 'No data available'; 113 | error = new Error(msg); 114 | error['code'] = 404; 115 | error['message'] = msg; 116 | throw error; 117 | } 118 | return [2 /*return*/, res]; 119 | } 120 | }); 121 | }); }; 122 | exports.getAll = getAll; 123 | -------------------------------------------------------------------------------- /backend/dist/modules/urls/validators.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.validateCreateShortUrlRequest = void 0; 4 | var express_validator_1 = require("express-validator"); 5 | var validateCreateShortUrlRequest = function () { 6 | return [ 7 | (0, express_validator_1.body)('originalUrl', 'Original Url is required').isString() 8 | ]; 9 | }; 10 | exports.validateCreateShortUrlRequest = validateCreateShortUrlRequest; 11 | -------------------------------------------------------------------------------- /backend/dist/modules/users/controller.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | exports.getUser = exports.loginUser = exports.createNewUser = void 0; 51 | var express_validator_1 = require("express-validator"); 52 | var services_1 = require("./services"); 53 | var responseMessages_1 = require("../../constants/responseMessages"); 54 | var sendResponse_1 = require("../../utils/sendResponse"); 55 | var handleCustomErrors_1 = require("../../utils/handleCustomErrors"); 56 | var createNewUser = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 57 | var errors, extractedErrors, _a, email, firstName, lastName, password, data, err_1; 58 | return __generator(this, function (_b) { 59 | switch (_b.label) { 60 | case 0: 61 | _b.trys.push([0, 2, , 3]); 62 | errors = (0, express_validator_1.validationResult)(req); 63 | if (!errors.isEmpty()) { 64 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 65 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 66 | } 67 | _a = req.body, email = _a.email, firstName = _a.firstName, lastName = _a.lastName, password = _a.password; 68 | return [4 /*yield*/, (0, services_1.create)({ 69 | email: email, 70 | firstName: firstName, 71 | lastName: lastName, 72 | password: password, 73 | })]; 74 | case 1: 75 | data = _b.sent(); 76 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 201, __assign({}, data), responseMessages_1.reponseMessages.genericSuccess)]; 77 | case 2: 78 | err_1 = _b.sent(); 79 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_1)]; 80 | case 3: return [2 /*return*/]; 81 | } 82 | }); 83 | }); }; 84 | exports.createNewUser = createNewUser; 85 | var loginUser = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 86 | var errors, extractedErrors, _a, email, password, data, err_2; 87 | return __generator(this, function (_b) { 88 | switch (_b.label) { 89 | case 0: 90 | _b.trys.push([0, 2, , 3]); 91 | errors = (0, express_validator_1.validationResult)(req); 92 | if (!errors.isEmpty()) { 93 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 94 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 95 | } 96 | _a = req.body, email = _a.email, password = _a.password; 97 | return [4 /*yield*/, (0, services_1.login)({ email: email, password: password })]; 98 | case 1: 99 | data = _b.sent(); 100 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 200, __assign({}, data), responseMessages_1.reponseMessages.genericSuccess)]; 101 | case 2: 102 | err_2 = _b.sent(); 103 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_2)]; 104 | case 3: return [2 /*return*/]; 105 | } 106 | }); 107 | }); }; 108 | exports.loginUser = loginUser; 109 | var getUser = function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 110 | var errors, extractedErrors, userId, data, err_3; 111 | return __generator(this, function (_a) { 112 | switch (_a.label) { 113 | case 0: 114 | _a.trys.push([0, 2, , 3]); 115 | errors = (0, express_validator_1.validationResult)(req); 116 | if (!errors.isEmpty()) { 117 | extractedErrors = (0, handleCustomErrors_1.extractErrors)(errors); 118 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 422, {}, { errors: extractedErrors })]; 119 | } 120 | userId = req.params.userId; 121 | console.log("userId", userId); 122 | return [4 /*yield*/, (0, services_1.user)(userId)]; 123 | case 1: 124 | data = _a.sent(); 125 | return [2 /*return*/, (0, sendResponse_1.sendResponse)(res, 200, __assign({}, data), responseMessages_1.reponseMessages.genericSuccess)]; 126 | case 2: 127 | err_3 = _a.sent(); 128 | return [2 /*return*/, (0, handleCustomErrors_1.handleCustomError)(res, err_3)]; 129 | case 3: return [2 /*return*/]; 130 | } 131 | }); 132 | }); }; 133 | exports.getUser = getUser; 134 | -------------------------------------------------------------------------------- /backend/dist/modules/users/model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var mongoose_1 = __importDefault(require("mongoose")); 7 | var Schema = mongoose_1.default.Schema; 8 | var usersSchema = new Schema({ 9 | firstName: { 10 | type: String, 11 | required: true, 12 | }, 13 | lastName: { 14 | type: String, 15 | required: true, 16 | }, 17 | email: { 18 | type: String, 19 | required: true, 20 | unique: true, 21 | }, 22 | password: { 23 | type: String, 24 | required: true, 25 | }, 26 | }); 27 | var Users = mongoose_1.default.model('Users', usersSchema); 28 | exports.default = Users; 29 | -------------------------------------------------------------------------------- /backend/dist/modules/users/routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var controller_1 = require("./controller"); 8 | var validators_1 = require("./validators"); 9 | var authenticated_1 = require("../../middlewares/authenticated"); 10 | var router = express_1.default.Router(); 11 | router.post('/users/signup', (0, validators_1.validateCreateUserRequest)(), controller_1.createNewUser); 12 | router.post('/users/signin', (0, validators_1.validateSignInUserRequest)(), controller_1.loginUser); 13 | router.get('/users/:userId', authenticated_1.isAuthenticated, controller_1.getUser); 14 | exports.default = router; 15 | -------------------------------------------------------------------------------- /backend/dist/modules/users/services.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | var __importDefault = (this && this.__importDefault) || function (mod) { 50 | return (mod && mod.__esModule) ? mod : { "default": mod }; 51 | }; 52 | Object.defineProperty(exports, "__esModule", { value: true }); 53 | exports.user = exports.login = exports.create = void 0; 54 | var model_1 = __importDefault(require("./model")); 55 | var db_1 = __importDefault(require("../../db")); 56 | var utils_1 = require("../../utils"); 57 | var mongodb_1 = require("mongodb"); 58 | var users = db_1.default.collection('users'); 59 | var create = function (_a) { 60 | var email = _a.email, password = _a.password, firstName = _a.firstName, lastName = _a.lastName; 61 | return __awaiter(void 0, void 0, void 0, function () { 62 | var res, msg, error, hashedPassword, user, newUser; 63 | return __generator(this, function (_b) { 64 | switch (_b.label) { 65 | case 0: return [4 /*yield*/, users.findOne({ email: email })]; 66 | case 1: 67 | res = _b.sent(); 68 | if (res) { 69 | msg = 'Email already exits.'; 70 | error = new Error(msg); 71 | error['code'] = 409; 72 | error['message'] = msg; 73 | throw error; 74 | } 75 | return [4 /*yield*/, (0, utils_1.generateHash)(password)]; 76 | case 2: 77 | hashedPassword = _b.sent(); 78 | user = new model_1.default({ 79 | email: email, 80 | firstName: firstName, 81 | lastName: lastName, 82 | password: hashedPassword, 83 | }); 84 | return [4 /*yield*/, users.insertOne(user)]; 85 | case 3: 86 | newUser = _b.sent(); 87 | return [2 /*return*/, { 88 | user: { 89 | id: newUser.insertedId, 90 | firstName: firstName, 91 | lastName: lastName, 92 | email: email, 93 | }, 94 | }]; 95 | } 96 | }); 97 | }); 98 | }; 99 | exports.create = create; 100 | var login = function (_a) { 101 | var email = _a.email, password = _a.password; 102 | return __awaiter(void 0, void 0, void 0, function () { 103 | var hashedPassword, res, msg, error, user, accessToken; 104 | return __generator(this, function (_b) { 105 | switch (_b.label) { 106 | case 0: return [4 /*yield*/, (0, utils_1.generateHash)(password)]; 107 | case 1: 108 | hashedPassword = _b.sent(); 109 | return [4 /*yield*/, users.findOne({ email: email, password: hashedPassword })]; 110 | case 2: 111 | res = _b.sent(); 112 | if (!res) { 113 | msg = 'Invalid email or password.'; 114 | error = new Error(msg); 115 | error['code'] = 404; 116 | error['message'] = msg; 117 | throw error; 118 | } 119 | user = { 120 | id: res._id, 121 | email: res.email, 122 | firstName: res.firstName, 123 | lastName: res.lastName, 124 | }; 125 | accessToken = (0, utils_1.createAccessToken)(__assign(__assign({}, user), { tokenType: 'LoginToken' })); 126 | return [2 /*return*/, { 127 | user: user, 128 | token: accessToken, 129 | }]; 130 | } 131 | }); 132 | }); 133 | }; 134 | exports.login = login; 135 | var user = function (id) { return __awaiter(void 0, void 0, void 0, function () { 136 | var o_id, res, msg, error; 137 | return __generator(this, function (_a) { 138 | switch (_a.label) { 139 | case 0: 140 | o_id = new mongodb_1.ObjectId(id); 141 | return [4 /*yield*/, users.findOne({ _id: o_id })]; 142 | case 1: 143 | res = _a.sent(); 144 | if (!res) { 145 | msg = 'User not found in records'; 146 | error = new Error(msg); 147 | error['code'] = 404; 148 | error['message'] = msg; 149 | throw error; 150 | } 151 | delete res['password']; 152 | return [2 /*return*/, { user: res }]; 153 | } 154 | }); 155 | }); }; 156 | exports.user = user; 157 | -------------------------------------------------------------------------------- /backend/dist/modules/users/validators.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.validateSignInUserRequest = exports.validateCreateUserRequest = void 0; 4 | var express_validator_1 = require("express-validator"); 5 | var validateCreateUserRequest = function () { 6 | return [ 7 | (0, express_validator_1.body)('firstName', 'FirstName is required') 8 | .isString() 9 | .isLength({ min: 3 }) 10 | .withMessage('FirstName must be at least 3 chars long'), 11 | (0, express_validator_1.body)('lastName', 'LastName is required') 12 | .isString() 13 | .isLength({ min: 3 }) 14 | .withMessage('LastName must be at least 3 chars long'), 15 | (0, express_validator_1.body)('email', 'Email is required/invalid').isEmail(), 16 | (0, express_validator_1.body)('password', 'Password is required') 17 | .isLength({ min: 6 }) 18 | .withMessage('Password must be at least 6 chars long'), 19 | ]; 20 | }; 21 | exports.validateCreateUserRequest = validateCreateUserRequest; 22 | var validateSignInUserRequest = function () { 23 | return [ 24 | (0, express_validator_1.body)('email', 'Email is required/invalid').isEmail(), 25 | (0, express_validator_1.body)('password', 'Password is required') 26 | .isLength({ min: 6 }) 27 | .withMessage('Password must be at least 6 chars long'), 28 | ]; 29 | }; 30 | exports.validateSignInUserRequest = validateSignInUserRequest; 31 | -------------------------------------------------------------------------------- /backend/dist/routes/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | var express = __importStar(require("express")); 30 | var v1_1 = __importDefault(require("./v1")); 31 | var router = express.Router(); 32 | router.use('/api/v1', v1_1.default); 33 | exports.default = router; 34 | -------------------------------------------------------------------------------- /backend/dist/routes/v1/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __importDefault = (this && this.__importDefault) || function (mod) { 26 | return (mod && mod.__esModule) ? mod : { "default": mod }; 27 | }; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | var express = __importStar(require("express")); 30 | var routes_1 = __importDefault(require("../../modules/users/routes")); 31 | var routes_2 = __importDefault(require("../../modules/urls/routes")); 32 | var router = express.Router(); 33 | router.use(routes_1.default, routes_2.default); 34 | exports.default = router; 35 | -------------------------------------------------------------------------------- /backend/dist/service/email.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.sendMail = void 0; 40 | var cloudmailin_1 = require("cloudmailin"); 41 | var env_1 = require("../config/env"); 42 | var client = new cloudmailin_1.MessageClient({ 43 | username: env_1.EMAIL_USERNAME, 44 | apiKey: env_1.EMAIL_API_KEY, 45 | }); 46 | var sendMail = function (_a) { 47 | var to = _a.to, from = _a.from, plain = _a.plain, html = _a.html, subject = _a.subject; 48 | return __awaiter(void 0, void 0, void 0, function () { 49 | var response; 50 | return __generator(this, function (_b) { 51 | switch (_b.label) { 52 | case 0: return [4 /*yield*/, client.sendMessage({ 53 | to: to, 54 | from: from, 55 | plain: plain, 56 | html: html, 57 | subject: subject, 58 | })]; 59 | case 1: 60 | response = _b.sent(); 61 | console.log("\u26A1\uFE0F[Send Mail]: response ".concat(JSON.stringify(response, null, 2))); 62 | return [2 /*return*/, response]; 63 | } 64 | }); 65 | }); 66 | }; 67 | exports.sendMail = sendMail; 68 | -------------------------------------------------------------------------------- /backend/dist/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Node.js API" 6 | }, 7 | "basePath": "/api/v1", 8 | "tags": [ 9 | { 10 | "name": "Users", 11 | "description": "API for users in the system" 12 | }, 13 | { 14 | "name": "Urls", 15 | "description": "API for Urls in the system" 16 | } 17 | ], 18 | "schemes": [ 19 | "http", 20 | "https" 21 | ], 22 | "consumes": [ 23 | "application/json", 24 | "multipart/form-data", 25 | "application/x-www-form-urlencoded" 26 | ], 27 | "produces": [ 28 | "application/json", 29 | "multipart/form-data", 30 | "application/x-www-form-urlencoded" 31 | ], 32 | "paths": { 33 | "/users/signup": { 34 | "post": { 35 | "tags": [ 36 | "Users" 37 | ], 38 | "description": "Create new user in system", 39 | "parameters": [ 40 | { 41 | "name": "user", 42 | "in": "body", 43 | "description": "User that we want to create", 44 | "schema": { 45 | "$ref": "#/definitions/User" 46 | } 47 | } 48 | ], 49 | "produces": [ 50 | "application/json" 51 | ], 52 | "responses": { 53 | "200": { 54 | "description": "New user is created", 55 | "schema": { 56 | "$ref": "#/definitions/User" 57 | } 58 | } 59 | } 60 | } 61 | }, 62 | "/users/signin": { 63 | "post": { 64 | "tags": [ 65 | "Users" 66 | ], 67 | "description": "Signin user in system", 68 | "parameters": [ 69 | { 70 | "name": "user", 71 | "in": "body", 72 | "description": "User that we want to signin", 73 | "schema": { 74 | "$ref": "#/definitions/SignInUser" 75 | } 76 | } 77 | ], 78 | "produces": [ 79 | "application/json" 80 | ], 81 | "responses": { 82 | "200": { 83 | "description": "Signin successfully", 84 | "schema": { 85 | "$ref": "#/definitions/SignInUser" 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "/users/{userId}": { 92 | "parameters": [ 93 | { 94 | "name": "userId", 95 | "in": "path", 96 | "required": true, 97 | "description": "ID of user that we want to find", 98 | "type": "string" 99 | } 100 | ], 101 | "get": { 102 | "summary": "Get user by given ID", 103 | "tags": [ 104 | "Users" 105 | ], 106 | "responses": {} 107 | } 108 | }, 109 | "/urls/short": { 110 | "post": { 111 | "tags": [ 112 | "Urls" 113 | ], 114 | "description": "Create new short url in system", 115 | "parameters": [ 116 | { 117 | "name": "url", 118 | "in": "body", 119 | "description": "Url that we want to short", 120 | "schema": { 121 | "$ref": "#/definitions/Url" 122 | } 123 | } 124 | ], 125 | "produces": [ 126 | "application/json" 127 | ], 128 | "responses": { 129 | "200": { 130 | "description": "New short Url is created", 131 | "schema": { 132 | "$ref": "#/definitions/Url" 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | "/urls/{urlId}": { 139 | "parameters": [ 140 | { 141 | "name": "urlId", 142 | "in": "path", 143 | "required": true, 144 | "description": "ID of url that we want to find", 145 | "type": "string" 146 | } 147 | ], 148 | "get": { 149 | "summary": "Get short by given ID", 150 | "tags": [ 151 | "Urls" 152 | ], 153 | "responses": {} 154 | } 155 | }, 156 | "/urls/list": { 157 | "parameters": [ 158 | { 159 | "name": "urlId", 160 | "in": "path", 161 | "required": true, 162 | "description": "ID of url that we want to find", 163 | "type": "string" 164 | } 165 | ], 166 | "get": { 167 | "summary": "Get all short url list by user", 168 | "tags": [ 169 | "Urls" 170 | ], 171 | "responses": {} 172 | } 173 | } 174 | }, 175 | "definitions": { 176 | "User": { 177 | "required": [ 178 | "firstName", 179 | "lastName", 180 | "email", 181 | "password" 182 | ], 183 | "properties": { 184 | "firstName": { 185 | "type": "string", 186 | "required": true 187 | }, 188 | "lastName": { 189 | "type": "string", 190 | "required": true 191 | }, 192 | "email": { 193 | "type": "string", 194 | "required": true 195 | }, 196 | "password": { 197 | "type": "string", 198 | "required": true 199 | } 200 | } 201 | }, 202 | "SignInUser": { 203 | "required": [ 204 | "email", 205 | "password" 206 | ], 207 | "properties": { 208 | "email": { 209 | "type": "string", 210 | "required": true 211 | }, 212 | "password": { 213 | "type": "string", 214 | "required": true 215 | } 216 | } 217 | }, 218 | "Url": { 219 | "required": [ 220 | "originalUrl" 221 | ], 222 | "properties": { 223 | "originalUrl": { 224 | "type": "string", 225 | "required": true 226 | } 227 | } 228 | } 229 | }, 230 | "securityDefinitions": { 231 | "token": { 232 | "type": "apiKey", 233 | "name": "token", 234 | "description": "API key to authorize requests", 235 | "in": "header" 236 | } 237 | }, 238 | "security": [ 239 | { 240 | "token": [] 241 | } 242 | ] 243 | } 244 | -------------------------------------------------------------------------------- /backend/dist/utils/encryption.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.decryptRefreshToken = exports.createaRefreshToken = exports.decryptAccessToken = exports.createAccessToken = void 0; 4 | var jsonwebtoken_1 = require("jsonwebtoken"); 5 | var env_1 = require("../config/env"); 6 | if (!env_1.TOKEN_SECRET || 7 | !env_1.ACCESS_TOKEN_EXPIRY || 8 | !env_1.ACCESS_TOKEN_ALGO || 9 | !env_1.REFRESH_TOKEN_EXPIRY || 10 | !env_1.REFRESH_TOKEN_ALGO) { 11 | console.log('Please set JWT ENV variables'); 12 | process.exit(-1); 13 | } 14 | var createAccessToken = function (data) { 15 | return (0, jsonwebtoken_1.sign)(data, process.env.TOKEN_SECRET, { 16 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 17 | algorithm: process.env.ACCESS_TOKEN_ALGO, 18 | }); 19 | }; 20 | exports.createAccessToken = createAccessToken; 21 | var decryptAccessToken = function (token) { 22 | return (0, jsonwebtoken_1.verify)(token, process.env.TOKEN_SECRET, { 23 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 24 | algorithm: process.env.ACCESS_TOKEN_ALGO, 25 | }); 26 | }; 27 | exports.decryptAccessToken = decryptAccessToken; 28 | var createaRefreshToken = function (data) { 29 | return (0, jsonwebtoken_1.sign)(data, process.env.TOKEN_SECRET, { 30 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 31 | algorithm: process.env.REFRESH_TOKEN_ALGO, 32 | }); 33 | }; 34 | exports.createaRefreshToken = createaRefreshToken; 35 | var decryptRefreshToken = function (token) { 36 | return (0, jsonwebtoken_1.verify)(token, process.env.TOKEN_SECRET, { 37 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 38 | algorithm: process.env.REFRESH_TOKEN_ALGO, 39 | }); 40 | }; 41 | exports.decryptRefreshToken = decryptRefreshToken; 42 | -------------------------------------------------------------------------------- /backend/dist/utils/handleCustomErrors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.extractErrors = exports.handleCustomError = void 0; 4 | var responseMessages_1 = require("../constants/responseMessages"); 5 | var sendResponse_1 = require("./sendResponse"); 6 | var httpStatusCode_1 = require("./httpStatusCode"); 7 | /** 8 | * @function handleCustomError 9 | * 10 | * This function is a response sending wrapper for custom error handling 11 | * Instead of writing extra repetitive lines 12 | * use this wrapper 13 | * 14 | * @param {object} res the response object 15 | * 16 | * @returns {object} error the error object 17 | */ 18 | var handleCustomError = function (res, error) { 19 | if (error.code === httpStatusCode_1.HttpStatusCode.NotFound) { 20 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 21 | } 22 | if (error.code === httpStatusCode_1.HttpStatusCode.Unauthorized) { 23 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 24 | } 25 | if (error.code === httpStatusCode_1.HttpStatusCode.Forbidden) { 26 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 27 | } 28 | if (error.code === httpStatusCode_1.HttpStatusCode.NotFound) { 29 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 30 | } 31 | if (error.code === httpStatusCode_1.HttpStatusCode.Conflict) { 32 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 33 | } 34 | if (error.code === httpStatusCode_1.HttpStatusCode.UnprocessableEntity) { 35 | return (0, sendResponse_1.sendResponse)(res, error.code, {}, error.message); 36 | } 37 | return (0, sendResponse_1.sendResponse)(res, httpStatusCode_1.HttpStatusCode.InternalServerError, error, responseMessages_1.reponseMessages.genericError); 38 | }; 39 | exports.handleCustomError = handleCustomError; 40 | /** 41 | * @function extractErrors 42 | * 43 | * This function is a extract error from errors array 44 | * 45 | * @param {Array} errors the errors array of object 46 | * 47 | * @returns {Array} errorMessages returns validation error messages 48 | */ 49 | var extractErrors = function (errors) { 50 | return errors.array({ onlyFirstError: true }).map(function (err) { 51 | var _a; 52 | return (_a = {}, _a[err.param] = err.msg, _a); 53 | }); 54 | }; 55 | exports.extractErrors = extractErrors; 56 | -------------------------------------------------------------------------------- /backend/dist/utils/hashPayload.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | Object.defineProperty(exports, "__esModule", { value: true }); 42 | exports.generateHash = void 0; 43 | var crypto_1 = __importDefault(require("crypto")); 44 | var generateHash = function (payload) { return __awaiter(void 0, void 0, void 0, function () { 45 | var hash; 46 | return __generator(this, function (_a) { 47 | switch (_a.label) { 48 | case 0: return [4 /*yield*/, crypto_1.default.createHash('sha512').update(payload).digest('hex')]; 49 | case 1: 50 | hash = _a.sent(); 51 | return [2 /*return*/, hash]; 52 | } 53 | }); 54 | }); }; 55 | exports.generateHash = generateHash; 56 | -------------------------------------------------------------------------------- /backend/dist/utils/httpStatusCode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.HttpStatusCode = void 0; 4 | var HttpStatusCode; 5 | (function (HttpStatusCode) { 6 | // 1XX Informational 7 | HttpStatusCode[HttpStatusCode["Continue"] = 100] = "Continue"; 8 | HttpStatusCode[HttpStatusCode["SwitchingProtocols"] = 101] = "SwitchingProtocols"; 9 | HttpStatusCode[HttpStatusCode["Processing"] = 102] = "Processing"; 10 | // 2XX Success: 11 | HttpStatusCode[HttpStatusCode["OK"] = 200] = "OK"; 12 | HttpStatusCode[HttpStatusCode["Created"] = 201] = "Created"; 13 | HttpStatusCode[HttpStatusCode["Accepted"] = 202] = "Accepted"; 14 | HttpStatusCode[HttpStatusCode["NonAuthoritativeInformation"] = 203] = "NonAuthoritativeInformation"; 15 | HttpStatusCode[HttpStatusCode["NoContent"] = 204] = "NoContent"; 16 | HttpStatusCode[HttpStatusCode["ResetContent"] = 205] = "ResetContent"; 17 | HttpStatusCode[HttpStatusCode["PartialContent"] = 206] = "PartialContent"; 18 | HttpStatusCode[HttpStatusCode["MultiStatus"] = 207] = "MultiStatus"; 19 | HttpStatusCode[HttpStatusCode["AlreadyReported"] = 208] = "AlreadyReported"; 20 | HttpStatusCode[HttpStatusCode["IMUsed"] = 226] = "IMUsed"; 21 | // 3XX Redirectional: 22 | HttpStatusCode[HttpStatusCode["MultipleChoices"] = 300] = "MultipleChoices"; 23 | HttpStatusCode[HttpStatusCode["MovedPermanently"] = 301] = "MovedPermanently"; 24 | HttpStatusCode[HttpStatusCode["Found"] = 302] = "Found"; 25 | HttpStatusCode[HttpStatusCode["SeeOther"] = 303] = "SeeOther"; 26 | HttpStatusCode[HttpStatusCode["NotModified"] = 304] = "NotModified"; 27 | HttpStatusCode[HttpStatusCode["UseProxy"] = 305] = "UseProxy"; 28 | HttpStatusCode[HttpStatusCode["TemporaryRedirect"] = 307] = "TemporaryRedirect"; 29 | HttpStatusCode[HttpStatusCode["PermanentRedirect"] = 308] = "PermanentRedirect"; 30 | // 4XX Client Error: 31 | HttpStatusCode[HttpStatusCode["BadRequest"] = 400] = "BadRequest"; 32 | HttpStatusCode[HttpStatusCode["Unauthorized"] = 401] = "Unauthorized"; 33 | HttpStatusCode[HttpStatusCode["PaymentRequired"] = 402] = "PaymentRequired"; 34 | HttpStatusCode[HttpStatusCode["Forbidden"] = 403] = "Forbidden"; 35 | HttpStatusCode[HttpStatusCode["NotFound"] = 404] = "NotFound"; 36 | HttpStatusCode[HttpStatusCode["MethodNotAllowed"] = 405] = "MethodNotAllowed"; 37 | HttpStatusCode[HttpStatusCode["NotAcceptable"] = 406] = "NotAcceptable"; 38 | HttpStatusCode[HttpStatusCode["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; 39 | HttpStatusCode[HttpStatusCode["RequestTimeout"] = 408] = "RequestTimeout"; 40 | HttpStatusCode[HttpStatusCode["Conflict"] = 409] = "Conflict"; 41 | HttpStatusCode[HttpStatusCode["Gone"] = 410] = "Gone"; 42 | HttpStatusCode[HttpStatusCode["LengthRequired"] = 411] = "LengthRequired"; 43 | HttpStatusCode[HttpStatusCode["PreconditionFailed"] = 412] = "PreconditionFailed"; 44 | HttpStatusCode[HttpStatusCode["PayloadTooLarge"] = 413] = "PayloadTooLarge"; 45 | HttpStatusCode[HttpStatusCode["RequestURITooLong"] = 414] = "RequestURITooLong"; 46 | HttpStatusCode[HttpStatusCode["UnsupportedMediaType"] = 415] = "UnsupportedMediaType"; 47 | HttpStatusCode[HttpStatusCode["RequestedRangeNotSatisfiable"] = 416] = "RequestedRangeNotSatisfiable"; 48 | HttpStatusCode[HttpStatusCode["ExpectationFailed"] = 417] = "ExpectationFailed"; 49 | HttpStatusCode[HttpStatusCode["ImATeapot"] = 418] = "ImATeapot"; 50 | HttpStatusCode[HttpStatusCode["MisdirectedRequest"] = 421] = "MisdirectedRequest"; 51 | HttpStatusCode[HttpStatusCode["UnprocessableEntity"] = 422] = "UnprocessableEntity"; 52 | HttpStatusCode[HttpStatusCode["Locked"] = 423] = "Locked"; 53 | HttpStatusCode[HttpStatusCode["FailedDependency"] = 424] = "FailedDependency"; 54 | HttpStatusCode[HttpStatusCode["UpgradeRequired"] = 426] = "UpgradeRequired"; 55 | HttpStatusCode[HttpStatusCode["PreconditionRequired"] = 428] = "PreconditionRequired"; 56 | HttpStatusCode[HttpStatusCode["TooManyRequests"] = 429] = "TooManyRequests"; 57 | HttpStatusCode[HttpStatusCode["RequestHeaderFieldsTooLarge"] = 431] = "RequestHeaderFieldsTooLarge"; 58 | HttpStatusCode[HttpStatusCode["ConnectionClosedWithoutResponse"] = 444] = "ConnectionClosedWithoutResponse"; 59 | HttpStatusCode[HttpStatusCode["UnavailableForLegalReasons"] = 451] = "UnavailableForLegalReasons"; 60 | HttpStatusCode[HttpStatusCode["ClientClosedRequest"] = 499] = "ClientClosedRequest"; 61 | // 5XX Server Error: 62 | HttpStatusCode[HttpStatusCode["InternalServerError"] = 500] = "InternalServerError"; 63 | HttpStatusCode[HttpStatusCode["NotImplemented"] = 501] = "NotImplemented"; 64 | HttpStatusCode[HttpStatusCode["BadGateway"] = 502] = "BadGateway"; 65 | HttpStatusCode[HttpStatusCode["ServiceUnavailable"] = 503] = "ServiceUnavailable"; 66 | HttpStatusCode[HttpStatusCode["GatewayTimeout"] = 504] = "GatewayTimeout"; 67 | HttpStatusCode[HttpStatusCode["HttpVersionNotSupported"] = 505] = "HttpVersionNotSupported"; 68 | HttpStatusCode[HttpStatusCode["VariantAlsoNegotiates"] = 506] = "VariantAlsoNegotiates"; 69 | HttpStatusCode[HttpStatusCode["InsufficientStorage"] = 507] = "InsufficientStorage"; 70 | HttpStatusCode[HttpStatusCode["LoopDetected"] = 508] = "LoopDetected"; 71 | HttpStatusCode[HttpStatusCode["NotExtended"] = 510] = "NotExtended"; 72 | HttpStatusCode[HttpStatusCode["NetworkAuthenticationRequired"] = 511] = "NetworkAuthenticationRequired"; 73 | HttpStatusCode[HttpStatusCode["NetworkConnectTimeoutError"] = 599] = "NetworkConnectTimeoutError"; 74 | })(HttpStatusCode = exports.HttpStatusCode || (exports.HttpStatusCode = {})); 75 | -------------------------------------------------------------------------------- /backend/dist/utils/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.HttpStatusCode = exports.validateUrl = exports.decryptRefreshToken = exports.createaRefreshToken = exports.decryptAccessToken = exports.createAccessToken = exports.extractErrors = exports.handleCustomError = exports.sendResponse = exports.generateHash = void 0; 4 | var hashPayload_1 = require("./hashPayload"); 5 | Object.defineProperty(exports, "generateHash", { enumerable: true, get: function () { return hashPayload_1.generateHash; } }); 6 | var sendResponse_1 = require("./sendResponse"); 7 | Object.defineProperty(exports, "sendResponse", { enumerable: true, get: function () { return sendResponse_1.sendResponse; } }); 8 | var handleCustomErrors_1 = require("./handleCustomErrors"); 9 | Object.defineProperty(exports, "handleCustomError", { enumerable: true, get: function () { return handleCustomErrors_1.handleCustomError; } }); 10 | Object.defineProperty(exports, "extractErrors", { enumerable: true, get: function () { return handleCustomErrors_1.extractErrors; } }); 11 | var url_1 = require("./url"); 12 | Object.defineProperty(exports, "validateUrl", { enumerable: true, get: function () { return url_1.validateUrl; } }); 13 | var httpStatusCode_1 = require("./httpStatusCode"); 14 | Object.defineProperty(exports, "HttpStatusCode", { enumerable: true, get: function () { return httpStatusCode_1.HttpStatusCode; } }); 15 | var encryption_1 = require("./encryption"); 16 | Object.defineProperty(exports, "createAccessToken", { enumerable: true, get: function () { return encryption_1.createAccessToken; } }); 17 | Object.defineProperty(exports, "decryptAccessToken", { enumerable: true, get: function () { return encryption_1.decryptAccessToken; } }); 18 | Object.defineProperty(exports, "createaRefreshToken", { enumerable: true, get: function () { return encryption_1.createaRefreshToken; } }); 19 | Object.defineProperty(exports, "decryptRefreshToken", { enumerable: true, get: function () { return encryption_1.decryptRefreshToken; } }); 20 | -------------------------------------------------------------------------------- /backend/dist/utils/sendResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.sendResponse = void 0; 4 | /** 5 | * @function sendResponse 6 | * 7 | * This function is a response sending wrapper 8 | * Instead of writing extra repetitive lines 9 | * use this wrapper 10 | * 11 | * @param {object} res the response object 12 | * @param {number} statusCode the http status code 13 | * @param {array | object } data the data you want to send with the response 14 | * @param {string} message the message you want to send for success/failure 15 | * 16 | * @returns {object} res the res object 17 | */ 18 | var sendResponse = function (res, statusCode, data, message) { 19 | if (data === void 0) { data = {}; } 20 | if (typeof statusCode !== 'number') { 21 | throw new Error('statusCode should be a number'); 22 | } 23 | // status variable to store the status of the response either success or failed 24 | var status = null; 25 | // regex pattern to validate that the status code is always 3 digits in length 26 | var lengthPattern = /^[0-9]{3}$/; 27 | // check for the length of the status code, if its 3 then set default value for status as 28 | // failed 29 | // else throw an error 30 | if (!lengthPattern.test(statusCode)) { 31 | throw new Error('Invalid Status Code'); 32 | } 33 | // regex to test that status code start with 2 or 3 and should me 3 digits in length 34 | var pattern = /^(2|3)\d{2}$/; 35 | // if the status code starts with 2, set status variable as success 36 | // eslint-disable-next-line no-unused-expressions 37 | pattern.test(statusCode) ? (status = 'success') : (status = 'failed'); 38 | return res.status(statusCode).json({ 39 | status: status, 40 | data: data, 41 | message: message, 42 | }); 43 | }; 44 | exports.sendResponse = sendResponse; 45 | -------------------------------------------------------------------------------- /backend/dist/utils/url.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.validateUrl = void 0; 4 | /** 5 | * @function validateUrl 6 | * 7 | * This function is a validate the URL 8 | * 9 | * @param {value} url 10 | * 11 | * @returns {boolean} true if the url is valid, otherwise false 12 | */ 13 | var validateUrl = function (value) { 14 | var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol 15 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name 16 | '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address 17 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path 18 | '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string 19 | '(\\#[-a-z\\d_]*)?$', 'i'); 20 | return !!urlPattern.test(value); 21 | }; 22 | exports.validateUrl = validateUrl; 23 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.ts?$': 'ts-jest', 4 | }, 5 | testEnvironment: 'node', 6 | testRegex: './src/.*\\.(test|spec)?\\.(ts|ts)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | roots: ['/src'], 9 | }; 10 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": ".ts,.js", 6 | "ignore": [], 7 | "exec": "ts-node ./src/index.ts" 8 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "MERN App Backend", 5 | "main": "src/index.ts", 6 | "private": false, 7 | "author": "Asif Vora ", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/asif-simform/MERN-Stack.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/asif-simform/MERN-Stack/issues" 15 | }, 16 | "homepage": "https://github.com/asif-simform/MERN-Stack", 17 | "scripts": { 18 | "typesync": "typesync", 19 | "dev": "NODE_ENV=development nodemon src/index.ts", 20 | "build": "rm -rf ./dist && tsc", 21 | "start": "NODE_ENV=production node dist/index.js", 22 | "add-build": "git add dist", 23 | "lint": "eslint . --ext .ts", 24 | "prettier-format": "run-script-os", 25 | "prettier-format:win32": "prettier --config .prettierrc \"./src/**/*.ts\" --write", 26 | "prettier-format:darwin:linux": "prettier --config .prettierrc 'src/**/*.ts' --write", 27 | "prettier-format:default": "prettier --config .prettierrc 'src/**/*.ts' --write", 28 | "prettier-watch": "run-script-os", 29 | "prettier-watch:win32": "onchange \"src/**/*.ts\" -- prettier --write {{changed}}", 30 | "prettier-watch:darwin:linux": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 31 | "prettier-watch:default": "onchange 'src/**/*.ts' -- prettier --write {{changed}}", 32 | "test": "jest", 33 | "test:dev": "jest --watchAll" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "npm run test && npm run prettier-format && npm run lint" 38 | } 39 | }, 40 | "pre-commit": [ 41 | "build", 42 | "add-build" 43 | ], 44 | "keywords": [ 45 | "node", 46 | "MERN" 47 | ], 48 | "dependencies": { 49 | "@types/node": "^18.11.7", 50 | "body-parser": "^1.20.1", 51 | "cloudmailin": "^0.0.3", 52 | "cors": "^2.8.5", 53 | "crypto": "^1.0.1", 54 | "dotenv": "^16.0.3", 55 | "express": "^4.18.2", 56 | "express-validator": "^6.14.2", 57 | "helmet": "^6.0.0", 58 | "jsonwebtoken": "^8.5.1", 59 | "mongodb": "^4.11.0", 60 | "mongoose": "^6.7.0", 61 | "multer": "^1.4.5-lts.1", 62 | "shortid": "^2.2.16", 63 | "swagger-ui-express": "^4.5.0", 64 | "typescript": "^4.8.4" 65 | }, 66 | "devDependencies": { 67 | "@types/eslint": "^8.4.6", 68 | "@types/eslint-plugin-prettier": "^3.1.0", 69 | "@types/express": "^4.17.14", 70 | "@types/jest": "^29.2.0", 71 | "@types/mocha": "^10.0.0", 72 | "@types/nodemon": "^1.19.2", 73 | "@types/prettier": "^2.7.1", 74 | "eslint": "^8.25.0", 75 | "eslint-config-prettier": "^8.5.0", 76 | "eslint-plugin-jest": "^27.1.3", 77 | "eslint-plugin-prettier": "^4.2.1", 78 | "husky": "^8.0.1", 79 | "jest": "^29.2.1", 80 | "nodemon": "^2.0.20", 81 | "onchange": "^7.1.0", 82 | "prettier": "^2.7.1", 83 | "run-script-os": "^1.1.6", 84 | "ts-jest": "^29.0.3", 85 | "ts-node": "^10.9.1", 86 | "typesync": "^0.9.2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /backend/src/config/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | export const PORT = process.env.PORT || 8080; 6 | export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 7 | export const DB_USERNAME = process.env.DB_USERNAME; 8 | export const DB_PASSWORD = process.env.DB_PASSWORD; 9 | export const DB_NAME = process.env.DB_NAME; 10 | export const CLUSTER_URL = process.env.CLUSTER_URL; 11 | export const EMAIL_HOST = process.env.EMAIL_HOST; 12 | export const EMAIL_PORT = process.env.EMAIL_PORT; 13 | export const EMAIL_USERNAME = process.env.EMAIL_USERNAME; 14 | export const EMAIL_API_KEY = process.env.EMAIL_API_KEY; 15 | export const TOKEN_SECRET = process.env.TOKEN_SECRET; 16 | export const ACCESS_TOKEN_EXPIRY = process.env.ACCESS_TOKEN_EXPIRY; 17 | export const ACCESS_TOKEN_ALGO = process.env.ACCESS_TOKEN_ALGO; 18 | export const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY; 19 | export const REFRESH_TOKEN_ALGO = process.env.REFRESH_TOKEN_ALGO; -------------------------------------------------------------------------------- /backend/src/constants/responseMessages.ts: -------------------------------------------------------------------------------- 1 | export const reponseMessages = { 2 | genericError: 'Something went wrong! Please try again later.', 3 | genericSuccess: 'Request successful.', 4 | }; 5 | -------------------------------------------------------------------------------- /backend/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { IS_DEVELOPMENT, DB_USERNAME, DB_PASSWORD, DB_NAME, CLUSTER_URL } from '../config/env' 3 | 4 | // Replace the uri string with your connection string. 5 | // example : mongodb+srv://:@?retryWrites=true&w=majority 6 | const URI = `mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@${CLUSTER_URL}?retryWrites=true&w=majority`; 7 | 8 | const client = new MongoClient(IS_DEVELOPMENT ? `mongodb://localhost:27017/${DB_NAME}` : URI); 9 | 10 | console.log(`⚡️[App runing in]: ${IS_DEVELOPMENT ? 'Development' : ' PROD'} mode`); 11 | 12 | client.connect((error) => { 13 | if (error) { 14 | console.log(`⚡️[DB]: Could not connected to database!`); 15 | console.log(error) 16 | } else { 17 | console.log(`⚡️[DB]: Connected to database!`); 18 | } 19 | }); 20 | 21 | const db = client.db(DB_NAME); 22 | 23 | export default db; -------------------------------------------------------------------------------- /backend/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | test('add', async () => { 3 | expect(1 + 1).toEqual(2); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PORT, IS_DEVELOPMENT } from './config/env'; 2 | 3 | import express, { Application, Request, Response } from 'express'; 4 | import bodyParser from'body-parser'; 5 | import multer from 'multer'; 6 | import cors from 'cors'; 7 | import helmet from 'helmet'; 8 | import swaggerUi from 'swagger-ui-express' 9 | 10 | import swaggerDocument from './swagger.json' 11 | 12 | // custom modules 13 | import allRoutes from './routes'; 14 | 15 | import './db' 16 | 17 | const app: Application = express(); 18 | 19 | // parse application/json 20 | app.use(bodyParser.json()) 21 | // parse application/x-www-form-urlencoded 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | app.use(multer().array()) 24 | app.use(helmet()); 25 | app.use(cors()); 26 | app.disable('x-powered-by'); 27 | 28 | app.get('/', (req: Request, res: Response) => { 29 | res.status(200).json({ 30 | message: 'Welcome to Node API\'s by 007', 31 | }); 32 | }); 33 | 34 | 35 | app.use(allRoutes); 36 | 37 | app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, { 38 | customCssUrl: 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.0/swagger-ui.min.css' 39 | })); 40 | 41 | app.listen(PORT, () => { 42 | console.log(`⚡️[server]: Server is running at ${IS_DEVELOPMENT ? 'http://localhost:' : ''}${PORT}`); 43 | }); -------------------------------------------------------------------------------- /backend/src/middlewares/authenticated.ts: -------------------------------------------------------------------------------- 1 | import { sendResponse, decryptAccessToken, HttpStatusCode } from '../utils' 2 | 3 | const whiteListEndpoints = ["/urls/short"] 4 | 5 | export const isAuthenticated = async (req, res, next) => { 6 | const token = req.header('Authorization'); 7 | 8 | try { 9 | if (!token) { 10 | if(whiteListEndpoints.includes(req.url)) { 11 | return next() 12 | } 13 | return sendResponse(res, HttpStatusCode.Unauthorized, { tokenExpired: 0 }, 'Failed to Authenticate'); 14 | } 15 | 16 | const decoded = decryptAccessToken(token); 17 | 18 | // if everything is good, save to request for use in other routes 19 | req.user = decoded; 20 | 21 | next(); 22 | } catch (err) { 23 | if (err?.['name'] === 'TokenExpiredError') { 24 | return sendResponse(res, HttpStatusCode.Unauthorized, { tokenExpired: 1 }, 'Token Expired'); 25 | } 26 | if (err?.['name'] === 'JsonWebTokenError') { 27 | return sendResponse(res, HttpStatusCode.Unauthorized, { tokenExpired: 0 }, 'Corrupt Token'); 28 | } 29 | } 30 | return 0; 31 | }; -------------------------------------------------------------------------------- /backend/src/modules/urls/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { validationResult } from 'express-validator'; 3 | 4 | import { create, get, getAll } from './services'; 5 | import { reponseMessages } from '../../constants/responseMessages'; 6 | import { sendResponse } from '../../utils/sendResponse'; 7 | import { 8 | handleCustomError, 9 | extractErrors, 10 | } from '../../utils/handleCustomErrors'; 11 | 12 | interface IUserRequest extends Request { 13 | user?: Record 14 | } 15 | 16 | export const createShortUrl = async (req: IUserRequest, res: Response) => { 17 | try { 18 | const errors = validationResult(req); 19 | 20 | if (!errors.isEmpty()) { 21 | const extractedErrors = extractErrors(errors); 22 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 23 | } 24 | 25 | const { originalUrl } = req.body; 26 | const baseURL = req.get('origin'); 27 | const userId = req?.user?.id || ''; 28 | 29 | const data = await create({ originalUrl, baseURL, userId }); 30 | 31 | return sendResponse(res, 201, data, reponseMessages.genericSuccess); 32 | } catch (err) { 33 | return handleCustomError(res, err); 34 | } 35 | }; 36 | 37 | export const getShortUrl = async (req: Request, res: Response) => { 38 | try { 39 | const errors = validationResult(req); 40 | 41 | if (!errors.isEmpty()) { 42 | const extractedErrors = extractErrors(errors); 43 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 44 | } 45 | 46 | const { urlId } = req.params; 47 | 48 | const originalUrl = await get(urlId); 49 | 50 | return sendResponse(res, 201, { originalUrl }, reponseMessages.genericSuccess); 51 | } catch (err) { 52 | return handleCustomError(res, err); 53 | } 54 | }; 55 | 56 | export const getAllShortUrlsByUser = async (req: IUserRequest, res: Response) => { 57 | try { 58 | const errors = validationResult(req); 59 | 60 | if (!errors.isEmpty()) { 61 | const extractedErrors = extractErrors(errors); 62 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 63 | } 64 | 65 | const userId = req?.user?.id || ''; 66 | 67 | const data = await getAll(userId); 68 | 69 | return sendResponse(res, 201, { data }, reponseMessages.genericSuccess); 70 | } catch (err) { 71 | return handleCustomError(res, err); 72 | } 73 | }; -------------------------------------------------------------------------------- /backend/src/modules/urls/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const urlSchema = new Schema({ 6 | urlId: { 7 | type: String, 8 | required: true 9 | }, 10 | originalUrl: { 11 | type: String, 12 | required: true, 13 | }, 14 | shortUrl: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | }, 19 | clicks: { 20 | type: Number, 21 | required: true, 22 | default: 0, 23 | }, 24 | date: { 25 | type: String, 26 | default: Date.now, 27 | }, 28 | userId: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | required: false, 31 | default: null 32 | } 33 | }); 34 | 35 | const Urls = mongoose.model('Urls', urlSchema); 36 | 37 | export default Urls; -------------------------------------------------------------------------------- /backend/src/modules/urls/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { createShortUrl, getShortUrl, getAllShortUrlsByUser } from './controller'; 4 | import { validateCreateShortUrlRequest } from './validators'; 5 | import { isAuthenticated } from '../../middlewares/authenticated'; 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/urls/list',isAuthenticated, getAllShortUrlsByUser); 10 | router.post('/urls/short', isAuthenticated, validateCreateShortUrlRequest(), createShortUrl); 11 | router.get('/urls/:urlId', getShortUrl); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /backend/src/modules/urls/services.ts: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | import Urls from './model'; 3 | import db from '../../db'; 4 | import { validateUrl } from '../../utils'; 5 | 6 | const urls = db.collection('urls'); 7 | 8 | const create = async ({ originalUrl, baseURL, userId }) => { 9 | 10 | if (!validateUrl(originalUrl)) { 11 | const msg = 'Invalid URL'; 12 | const error = new Error(msg); 13 | error['code'] = 422; 14 | error['message'] = msg; 15 | throw error; 16 | } 17 | 18 | const urlId = shortid.generate(); 19 | 20 | const shortUrl = `${baseURL}/${urlId}`; 21 | 22 | const url = new Urls({ 23 | urlId, 24 | originalUrl, 25 | shortUrl, 26 | date: new Date(), 27 | userId 28 | }); 29 | 30 | await urls.insertOne(url); 31 | 32 | return { 33 | shortUrl 34 | } 35 | }; 36 | 37 | const get = async (urlId: string) => { 38 | const res = await urls.findOne({ urlId }); 39 | 40 | if (!res) { 41 | const msg = 'Short URL not found in records'; 42 | const error = new Error(msg); 43 | error['code'] = 404; 44 | error['message'] = msg; 45 | throw error; 46 | } 47 | 48 | await urls.findOneAndUpdate({ urlId }, { $inc: { clicks: 1 } } ); 49 | 50 | return res.originalUrl; 51 | }; 52 | 53 | const getAll = async (userId: string) => { 54 | const res = await urls.find({ userId }); 55 | 56 | if (!res) { 57 | const msg = 'No data available'; 58 | const error = new Error(msg); 59 | error['code'] = 404; 60 | error['message'] = msg; 61 | throw error; 62 | } 63 | 64 | return res; 65 | }; 66 | 67 | export { create, get, getAll }; 68 | -------------------------------------------------------------------------------- /backend/src/modules/urls/validators.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const validateCreateShortUrlRequest = () => { 4 | return [ 5 | body('originalUrl', 'Original Url is required').isString() 6 | ]; 7 | }; 8 | 9 | export { validateCreateShortUrlRequest }; 10 | -------------------------------------------------------------------------------- /backend/src/modules/users/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { validationResult } from 'express-validator'; 3 | 4 | import { create, login, user } from './services'; 5 | import { reponseMessages } from '../../constants/responseMessages'; 6 | import { sendResponse } from '../../utils/sendResponse'; 7 | import { 8 | handleCustomError, 9 | extractErrors, 10 | } from '../../utils/handleCustomErrors'; 11 | 12 | 13 | export const createNewUser = async (req: Request, res: Response) => { 14 | try { 15 | const errors = validationResult(req); 16 | 17 | if (!errors.isEmpty()) { 18 | const extractedErrors = extractErrors(errors); 19 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 20 | } 21 | 22 | const { email, firstName, lastName, password } = req.body; 23 | const data = await create({ 24 | email, 25 | firstName, 26 | lastName, 27 | password, 28 | }); 29 | return sendResponse(res, 201, { ...data }, reponseMessages.genericSuccess); 30 | } catch (err) { 31 | return handleCustomError(res, err); 32 | } 33 | }; 34 | 35 | export const loginUser = async (req: Request, res: Response) => { 36 | try { 37 | const errors = validationResult(req); 38 | 39 | if (!errors.isEmpty()) { 40 | const extractedErrors = extractErrors(errors); 41 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 42 | } 43 | 44 | const { email, password } = req.body; 45 | const data = await login({ email, password }); 46 | 47 | return sendResponse(res, 200, { ...data }, reponseMessages.genericSuccess); 48 | } catch (err) { 49 | return handleCustomError(res, err); 50 | } 51 | }; 52 | 53 | export const getUser = async (req, res) => { 54 | try { 55 | const errors = validationResult(req); 56 | 57 | if (!errors.isEmpty()) { 58 | const extractedErrors = extractErrors(errors); 59 | return sendResponse(res, 422, {}, { errors: extractedErrors }); 60 | } 61 | 62 | const { userId } = req.params; 63 | console.log("userId", userId) 64 | 65 | const data = await user(userId); 66 | 67 | return sendResponse(res, 200, { ...data }, reponseMessages.genericSuccess); 68 | } catch (err) { 69 | return handleCustomError(res, err); 70 | } 71 | }; -------------------------------------------------------------------------------- /backend/src/modules/users/model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const usersSchema = new Schema({ 6 | firstName: { 7 | type: String, 8 | required: true, 9 | }, 10 | lastName: { 11 | type: String, 12 | required: true, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | }, 19 | password: { 20 | type: String, 21 | required: true, 22 | }, 23 | }); 24 | 25 | const Users = mongoose.model('Users', usersSchema); 26 | 27 | export default Users; -------------------------------------------------------------------------------- /backend/src/modules/users/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { createNewUser, loginUser, getUser } from './controller'; 4 | import { validateCreateUserRequest, validateSignInUserRequest } from './validators'; 5 | import { isAuthenticated } from '../../middlewares/authenticated'; 6 | 7 | const router = express.Router(); 8 | 9 | router.post('/users/signup', validateCreateUserRequest(), createNewUser); 10 | router.post('/users/signin', validateSignInUserRequest(), loginUser); 11 | router.get('/users/:userId', isAuthenticated, getUser); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /backend/src/modules/users/services.ts: -------------------------------------------------------------------------------- 1 | import Users from './model'; 2 | import db from '../../db'; 3 | import { generateHash, createAccessToken } from '../../utils'; 4 | import { ObjectId } from 'mongodb'; 5 | 6 | const users = db.collection('users'); 7 | 8 | const create = async ({ email, password, firstName, lastName }) => { 9 | const res = await users.findOne({ email }); 10 | 11 | if (res) { 12 | const msg = 'Email already exits.'; 13 | const error = new Error(msg); 14 | error['code'] = 409; 15 | error['message'] = msg; 16 | throw error; 17 | } 18 | 19 | const hashedPassword = await generateHash(password); 20 | const user = new Users({ 21 | email, 22 | firstName, 23 | lastName, 24 | password: hashedPassword, 25 | }); 26 | const newUser = await users.insertOne(user); 27 | 28 | return { 29 | user: { 30 | id: newUser.insertedId, 31 | firstName, 32 | lastName, 33 | email, 34 | }, 35 | }; 36 | }; 37 | 38 | const login = async ({ email, password }) => { 39 | const hashedPassword = await generateHash(password); 40 | const res = await users.findOne({ email, password: hashedPassword }); 41 | 42 | if (!res) { 43 | const msg = 'Invalid email or password.'; 44 | const error = new Error(msg); 45 | error['code'] = 404; 46 | error['message'] = msg; 47 | throw error; 48 | } 49 | 50 | const user = { 51 | id: res._id, 52 | email: res.email, 53 | firstName: res.firstName, 54 | lastName: res.lastName, 55 | }; 56 | const accessToken = createAccessToken({ ...user, tokenType: 'LoginToken' }); 57 | 58 | return { 59 | user, 60 | token: accessToken, 61 | }; 62 | }; 63 | 64 | const user = async (id: string) => { 65 | const o_id = new ObjectId(id); 66 | const res = await users.findOne({ _id: o_id }); 67 | 68 | if (!res) { 69 | const msg = 'User not found in records'; 70 | const error = new Error(msg); 71 | error['code'] = 404; 72 | error['message'] = msg; 73 | throw error; 74 | } 75 | delete res['password']; 76 | 77 | return { user: res }; 78 | }; 79 | 80 | export { create, login, user }; 81 | -------------------------------------------------------------------------------- /backend/src/modules/users/validators.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const validateCreateUserRequest = () => { 4 | return [ 5 | body('firstName', 'FirstName is required') 6 | .isString() 7 | .isLength({ min: 3 }) 8 | .withMessage('FirstName must be at least 3 chars long'), 9 | body('lastName', 'LastName is required') 10 | .isString() 11 | .isLength({ min: 3 }) 12 | .withMessage('LastName must be at least 3 chars long'), 13 | body('email', 'Email is required/invalid').isEmail(), 14 | body('password', 'Password is required') 15 | .isLength({ min: 6 }) 16 | .withMessage('Password must be at least 6 chars long'), 17 | ]; 18 | }; 19 | 20 | const validateSignInUserRequest = () => { 21 | return [ 22 | body('email', 'Email is required/invalid').isEmail(), 23 | body('password', 'Password is required') 24 | .isLength({ min: 6 }) 25 | .withMessage('Password must be at least 6 chars long'), 26 | ]; 27 | }; 28 | 29 | export { validateCreateUserRequest, validateSignInUserRequest }; 30 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import v1Routes from './v1'; 3 | 4 | const router = express.Router(); 5 | 6 | router.use('/api/v1', v1Routes); 7 | 8 | export default router; -------------------------------------------------------------------------------- /backend/src/routes/v1/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import userRoutes from '../../modules/users/routes'; 3 | import urlsRoutes from '../../modules/urls/routes'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use(userRoutes, urlsRoutes); 8 | 9 | export default router; -------------------------------------------------------------------------------- /backend/src/service/email.ts: -------------------------------------------------------------------------------- 1 | import { MessageClient } from 'cloudmailin'; 2 | import { EMAIL_USERNAME, EMAIL_API_KEY } from '../config/env'; 3 | 4 | const client = new MessageClient({ 5 | username: EMAIL_USERNAME, 6 | apiKey: EMAIL_API_KEY, 7 | }); 8 | 9 | export const sendMail = async ({ 10 | to, 11 | from, 12 | plain, 13 | html, 14 | subject, 15 | }) => { 16 | const response = await client.sendMessage({ 17 | to: to, 18 | from: from, 19 | plain: plain, 20 | html: html, 21 | subject: subject, 22 | }); 23 | 24 | console.log(`⚡️[Send Mail]: response ${JSON.stringify(response, null, 2)}`); 25 | 26 | return response; 27 | }; 28 | -------------------------------------------------------------------------------- /backend/src/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Node.js API" 6 | }, 7 | "basePath": "/api/v1", 8 | "tags": [ 9 | { 10 | "name": "Users", 11 | "description": "API for users in the system" 12 | }, 13 | { 14 | "name": "Urls", 15 | "description": "API for Urls in the system" 16 | } 17 | ], 18 | "schemes": [ 19 | "http", 20 | "https" 21 | ], 22 | "consumes": [ 23 | "application/json", 24 | "multipart/form-data", 25 | "application/x-www-form-urlencoded" 26 | ], 27 | "produces": [ 28 | "application/json", 29 | "multipart/form-data", 30 | "application/x-www-form-urlencoded" 31 | ], 32 | "paths": { 33 | "/users/signup": { 34 | "post": { 35 | "tags": [ 36 | "Users" 37 | ], 38 | "description": "Create new user in system", 39 | "parameters": [ 40 | { 41 | "name": "user", 42 | "in": "body", 43 | "description": "User that we want to create", 44 | "schema": { 45 | "$ref": "#/definitions/User" 46 | } 47 | } 48 | ], 49 | "produces": [ 50 | "application/json" 51 | ], 52 | "responses": { 53 | "200": { 54 | "description": "New user is created", 55 | "schema": { 56 | "$ref": "#/definitions/User" 57 | } 58 | } 59 | } 60 | } 61 | }, 62 | "/users/signin": { 63 | "post": { 64 | "tags": [ 65 | "Users" 66 | ], 67 | "description": "Signin user in system", 68 | "parameters": [ 69 | { 70 | "name": "user", 71 | "in": "body", 72 | "description": "User that we want to signin", 73 | "schema": { 74 | "$ref": "#/definitions/SignInUser" 75 | } 76 | } 77 | ], 78 | "produces": [ 79 | "application/json" 80 | ], 81 | "responses": { 82 | "200": { 83 | "description": "Signin successfully", 84 | "schema": { 85 | "$ref": "#/definitions/SignInUser" 86 | } 87 | } 88 | } 89 | } 90 | }, 91 | "/users/{userId}": { 92 | "parameters": [ 93 | { 94 | "name": "userId", 95 | "in": "path", 96 | "required": true, 97 | "description": "ID of user that we want to find", 98 | "type": "string" 99 | } 100 | ], 101 | "get": { 102 | "summary": "Get user by given ID", 103 | "tags": [ 104 | "Users" 105 | ], 106 | "responses": {} 107 | } 108 | }, 109 | "/urls/short": { 110 | "post": { 111 | "tags": [ 112 | "Urls" 113 | ], 114 | "description": "Create new short url in system", 115 | "parameters": [ 116 | { 117 | "name": "url", 118 | "in": "body", 119 | "description": "Url that we want to short", 120 | "schema": { 121 | "$ref": "#/definitions/Url" 122 | } 123 | } 124 | ], 125 | "produces": [ 126 | "application/json" 127 | ], 128 | "responses": { 129 | "200": { 130 | "description": "New short Url is created", 131 | "schema": { 132 | "$ref": "#/definitions/Url" 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | "/urls/{urlId}": { 139 | "parameters": [ 140 | { 141 | "name": "urlId", 142 | "in": "path", 143 | "required": true, 144 | "description": "ID of url that we want to find", 145 | "type": "string" 146 | } 147 | ], 148 | "get": { 149 | "summary": "Get short by given ID", 150 | "tags": [ 151 | "Urls" 152 | ], 153 | "responses": {} 154 | } 155 | }, 156 | "/urls/list": { 157 | "parameters": [ 158 | { 159 | "name": "urlId", 160 | "in": "path", 161 | "required": true, 162 | "description": "ID of url that we want to find", 163 | "type": "string" 164 | } 165 | ], 166 | "get": { 167 | "summary": "Get all short url list by user", 168 | "tags": [ 169 | "Urls" 170 | ], 171 | "responses": {} 172 | } 173 | } 174 | }, 175 | "definitions": { 176 | "User": { 177 | "required": [ 178 | "firstName", 179 | "lastName", 180 | "email", 181 | "password" 182 | ], 183 | "properties": { 184 | "firstName": { 185 | "type": "string", 186 | "required": true 187 | }, 188 | "lastName": { 189 | "type": "string", 190 | "required": true 191 | }, 192 | "email": { 193 | "type": "string", 194 | "required": true 195 | }, 196 | "password": { 197 | "type": "string", 198 | "required": true 199 | } 200 | } 201 | }, 202 | "SignInUser": { 203 | "required": [ 204 | "email", 205 | "password" 206 | ], 207 | "properties": { 208 | "email": { 209 | "type": "string", 210 | "required": true 211 | }, 212 | "password": { 213 | "type": "string", 214 | "required": true 215 | } 216 | } 217 | }, 218 | "Url":{ 219 | "required": [ 220 | "originalUrl" 221 | ], 222 | "properties": { 223 | "originalUrl": { 224 | "type": "string", 225 | "required": true 226 | } 227 | } 228 | } 229 | }, 230 | "securityDefinitions": { 231 | "token": { 232 | "type": "apiKey", 233 | "name": "token", 234 | "description": "API key to authorize requests", 235 | "in": "header" 236 | } 237 | }, 238 | "security": [ 239 | { 240 | "token": [] 241 | } 242 | ] 243 | } -------------------------------------------------------------------------------- /backend/src/utils/encryption.ts: -------------------------------------------------------------------------------- 1 | import { sign, verify } from 'jsonwebtoken'; 2 | 3 | import { 4 | TOKEN_SECRET, 5 | ACCESS_TOKEN_EXPIRY, 6 | ACCESS_TOKEN_ALGO, 7 | REFRESH_TOKEN_EXPIRY, 8 | REFRESH_TOKEN_ALGO, 9 | } from '../config/env'; 10 | 11 | if ( 12 | !TOKEN_SECRET || 13 | !ACCESS_TOKEN_EXPIRY || 14 | !ACCESS_TOKEN_ALGO || 15 | !REFRESH_TOKEN_EXPIRY || 16 | !REFRESH_TOKEN_ALGO 17 | ) { 18 | console.log('Please set JWT ENV variables'); 19 | process.exit(-1); 20 | } 21 | 22 | export const createAccessToken = (data) => 23 | sign(data, process.env.TOKEN_SECRET, { 24 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 25 | algorithm: process.env.ACCESS_TOKEN_ALGO, 26 | }); 27 | 28 | export const decryptAccessToken = (token) => 29 | verify(token, process.env.TOKEN_SECRET, { 30 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 31 | algorithm: process.env.ACCESS_TOKEN_ALGO, 32 | }); 33 | 34 | export const createaRefreshToken = (data) => 35 | sign(data, process.env.TOKEN_SECRET, { 36 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 37 | algorithm: process.env.REFRESH_TOKEN_ALGO, 38 | }); 39 | 40 | export const decryptRefreshToken = (token) => 41 | verify(token, process.env.TOKEN_SECRET, { 42 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 43 | algorithm: process.env.REFRESH_TOKEN_ALGO, 44 | }); 45 | -------------------------------------------------------------------------------- /backend/src/utils/handleCustomErrors.ts: -------------------------------------------------------------------------------- 1 | import { reponseMessages } from '../constants/responseMessages'; 2 | import { sendResponse } from './sendResponse'; 3 | import { HttpStatusCode } from './httpStatusCode'; 4 | 5 | 6 | /** 7 | * @function handleCustomError 8 | * 9 | * This function is a response sending wrapper for custom error handling 10 | * Instead of writing extra repetitive lines 11 | * use this wrapper 12 | * 13 | * @param {object} res the response object 14 | * 15 | * @returns {object} error the error object 16 | */ 17 | export const handleCustomError = (res, error) => { 18 | if (error.code === HttpStatusCode.NotFound) { 19 | return sendResponse(res, error.code, {}, error.message); 20 | } 21 | if (error.code === HttpStatusCode.Unauthorized) { 22 | return sendResponse(res, error.code, {}, error.message); 23 | } 24 | if (error.code === HttpStatusCode.Forbidden) { 25 | return sendResponse(res, error.code, {}, error.message); 26 | } 27 | if (error.code === HttpStatusCode.NotFound) { 28 | return sendResponse(res, error.code, {}, error.message); 29 | } 30 | if (error.code === HttpStatusCode.Conflict) { 31 | return sendResponse(res, error.code, {}, error.message); 32 | } 33 | if (error.code === HttpStatusCode.UnprocessableEntity) { 34 | return sendResponse(res, error.code, {}, error.message); 35 | } 36 | return sendResponse(res, HttpStatusCode.InternalServerError, error, reponseMessages.genericError); 37 | }; 38 | 39 | /** 40 | * @function extractErrors 41 | * 42 | * This function is a extract error from errors array 43 | * 44 | * @param {Array} errors the errors array of object 45 | * 46 | * @returns {Array} errorMessages returns validation error messages 47 | */ 48 | export const extractErrors = (errors : any) => { 49 | return errors.array({ onlyFirstError: true }).map(err => ({ [err.param]: err.msg })) 50 | }; -------------------------------------------------------------------------------- /backend/src/utils/hashPayload.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export const generateHash = async (payload) => { 4 | const hash = await crypto.createHash('sha512').update(payload).digest('hex'); 5 | return hash; 6 | }; -------------------------------------------------------------------------------- /backend/src/utils/httpStatusCode.ts: -------------------------------------------------------------------------------- 1 | export enum HttpStatusCode { 2 | // 1XX Informational 3 | Continue = 100, 4 | SwitchingProtocols = 101, 5 | Processing = 102, 6 | 7 | // 2XX Success: 8 | OK = 200, 9 | Created = 201, 10 | Accepted = 202, 11 | NonAuthoritativeInformation = 203, 12 | NoContent = 204, 13 | ResetContent = 205, 14 | PartialContent = 206, 15 | MultiStatus = 207, 16 | AlreadyReported = 208, 17 | IMUsed = 226, 18 | 19 | // 3XX Redirectional: 20 | MultipleChoices = 300, 21 | MovedPermanently = 301, 22 | Found = 302, 23 | SeeOther = 303, 24 | NotModified = 304, 25 | UseProxy = 305, 26 | TemporaryRedirect = 307, 27 | PermanentRedirect = 308, 28 | 29 | // 4XX Client Error: 30 | BadRequest = 400, 31 | Unauthorized = 401, 32 | PaymentRequired = 402, 33 | Forbidden = 403, 34 | NotFound = 404, 35 | MethodNotAllowed = 405, 36 | NotAcceptable = 406, 37 | ProxyAuthenticationRequired = 407, 38 | RequestTimeout = 408, 39 | Conflict = 409, 40 | Gone = 410, 41 | LengthRequired = 411, 42 | PreconditionFailed = 412, 43 | PayloadTooLarge = 413, 44 | RequestURITooLong = 414, 45 | UnsupportedMediaType = 415, 46 | RequestedRangeNotSatisfiable = 416, 47 | ExpectationFailed = 417, 48 | ImATeapot = 418, 49 | MisdirectedRequest = 421, 50 | UnprocessableEntity = 422, 51 | Locked = 423, 52 | FailedDependency = 424, 53 | UpgradeRequired = 426, 54 | PreconditionRequired = 428, 55 | TooManyRequests = 429, 56 | RequestHeaderFieldsTooLarge = 431, 57 | ConnectionClosedWithoutResponse = 444, 58 | UnavailableForLegalReasons = 451, 59 | ClientClosedRequest = 499, 60 | 61 | // 5XX Server Error: 62 | InternalServerError = 500, 63 | NotImplemented = 501, 64 | BadGateway = 502, 65 | ServiceUnavailable = 503, 66 | GatewayTimeout = 504, 67 | HttpVersionNotSupported = 505, 68 | VariantAlsoNegotiates = 506, 69 | InsufficientStorage = 507, 70 | LoopDetected = 508, 71 | NotExtended = 510, 72 | NetworkAuthenticationRequired = 511, 73 | NetworkConnectTimeoutError = 599 74 | } -------------------------------------------------------------------------------- /backend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { generateHash } from './hashPayload'; 2 | import { sendResponse } from './sendResponse'; 3 | import { handleCustomError, extractErrors } from './handleCustomErrors'; 4 | import { validateUrl } from './url'; 5 | import { HttpStatusCode } from './httpStatusCode'; 6 | import { 7 | createAccessToken, 8 | decryptAccessToken, 9 | createaRefreshToken, 10 | decryptRefreshToken, 11 | } from './encryption'; 12 | 13 | export { 14 | generateHash, 15 | sendResponse, 16 | handleCustomError, 17 | extractErrors, 18 | createAccessToken, 19 | decryptAccessToken, 20 | createaRefreshToken, 21 | decryptRefreshToken, 22 | validateUrl, 23 | HttpStatusCode, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/utils/sendResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | 3 | /** 4 | * @function sendResponse 5 | * 6 | * This function is a response sending wrapper 7 | * Instead of writing extra repetitive lines 8 | * use this wrapper 9 | * 10 | * @param {object} res the response object 11 | * @param {number} statusCode the http status code 12 | * @param {array | object } data the data you want to send with the response 13 | * @param {string} message the message you want to send for success/failure 14 | * 15 | * @returns {object} res the res object 16 | */ 17 | export const sendResponse = ( 18 | res: Response, 19 | statusCode: number, 20 | data = {}, 21 | message: string | any, 22 | ) => { 23 | if (typeof statusCode !== 'number') { 24 | throw new Error('statusCode should be a number'); 25 | } 26 | 27 | // status variable to store the status of the response either success or failed 28 | let status = null; 29 | 30 | // regex pattern to validate that the status code is always 3 digits in length 31 | const lengthPattern = /^[0-9]{3}$/; 32 | 33 | // check for the length of the status code, if its 3 then set default value for status as 34 | // failed 35 | // else throw an error 36 | if (!lengthPattern.test(statusCode as any)) { 37 | throw new Error('Invalid Status Code'); 38 | } 39 | 40 | // regex to test that status code start with 2 or 3 and should me 3 digits in length 41 | const pattern = /^(2|3)\d{2}$/; 42 | 43 | // if the status code starts with 2, set status variable as success 44 | // eslint-disable-next-line no-unused-expressions 45 | pattern.test(statusCode as any) ? (status = 'success') : (status = 'failed'); 46 | 47 | return res.status(statusCode).json({ 48 | status, 49 | data, 50 | message, 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /backend/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @function validateUrl 3 | * 4 | * This function is a validate the URL 5 | * 6 | * @param {value} url 7 | * 8 | * @returns {boolean} true if the url is valid, otherwise false 9 | */ 10 | export const validateUrl = (value: string) => { 11 | const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol 12 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name 13 | '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address 14 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path 15 | '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string 16 | '(\\#[-a-z\\d_]*)?$','i'); 17 | 18 | return !!urlPattern.test(value); 19 | }; -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "skipLibCheck": true, 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "es6", 10 | "DOM" 11 | ] /* Specify library files to be included in the compilation. */, 12 | "allowJs": true /* Allow javascript files to be compiled. */, 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | "outDir": "dist" /* Redirect output structure to the directory. */, 20 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 32 | "strictNullChecks": false, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": { 49 | // "@config/*": ["./src/config/*"], 50 | // }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | "typeRoots": ["node_modules/@types","@types"], 54 | "types": ["node"] /* Type declaration files to be included in compilation. */, 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "resolveJsonModule": true /* Include modules imported with '.json' extension */ 72 | }, 73 | "include": ["./src/**/*.ts"], 74 | "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] 75 | } 76 | -------------------------------------------------------------------------------- /backend/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'node/globals' -------------------------------------------------------------------------------- /backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "dist/index.js", 6 | "use": "@vercel/node", 7 | "config": { "includeFiles": ["dist/**"] } 8 | } 9 | ], 10 | "routes": [ 11 | { 12 | "src": "/(.*)", 13 | "dest": "dist/index.js" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_PROD_API_URL=https://mern-stack-cyan.vercel.app/api/v1 2 | VITE_APP_DEV_API_URL=http://localhost:6060/api/v1 -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_PROD_API_URL= 2 | VITE_APP_DEV_API_URL= -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .eslintrc.cjs -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: ['./tsconfig.json', './tsconfig.node.json'], 6 | sourceType: "module", 7 | }, 8 | // ignorePatterns: [ 9 | // ".eslintrc.cjs" 10 | // ], 11 | } -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "settings": { 7 | "import/resolver": { 8 | "node": { 9 | "typescript": {}, 10 | "extensions": [ 11 | ".js", 12 | ".ts", 13 | ".tsx", 14 | ".jsx" 15 | ] 16 | } 17 | } 18 | }, 19 | "extends": [ 20 | "airbnb", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:css-import-order/recommended", 23 | "prettier" 24 | ], 25 | "parser": "@typescript-eslint/parser", 26 | "parserOptions": { 27 | "ecmaFeatures": { 28 | "jsx": true 29 | }, 30 | "sourceType": "module", 31 | "paths": "/src", 32 | "ecmaVersion": 2020, 33 | "project": "./tsconfig.json" 34 | }, 35 | "plugins": [ 36 | "react", 37 | "deprecation", 38 | "css-import-order", 39 | "no-relative-import-paths" 40 | ], 41 | "rules": { 42 | "import/no-unresolved": [ 43 | 0, 44 | { 45 | "caseSensitive": false 46 | } 47 | ], 48 | "semi": [ 49 | "error", 50 | "always" 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single" 55 | ], 56 | "no-use-before-define": "off", 57 | "no-console": "error", 58 | "no-shadow": "off", 59 | "@typescript-eslint/no-shadow": [ 60 | "error" 61 | ], 62 | "@typescript-eslint/explicit-module-boundary-types": "off", 63 | "import/extensions": [ 64 | "error", 65 | "ignorePackages", 66 | { 67 | "js": "never", 68 | "ts": "never", 69 | "jsx": "never", 70 | "tsx": "never" 71 | } 72 | ], 73 | "react/jsx-filename-extension": [ 74 | 0, 75 | { 76 | "extensions": [ 77 | ".tsx" 78 | ] 79 | } 80 | ], 81 | "react/react-in-jsx-scope" : [0], 82 | "react/destructuring-assignment": [ 83 | 0, 84 | "never", 85 | { 86 | "ignoreClassFields": true 87 | } 88 | ], 89 | "click-events-have-key-events": "off", 90 | "jsx-a11y/no-static-element-interactions": [ 91 | "off", 92 | { 93 | "handlers": [ 94 | "onClick", 95 | "onMouseDown", 96 | "onMouseUp", 97 | "onKeyPress", 98 | "onKeyDown", 99 | "onKeyUp" 100 | ] 101 | } 102 | ], 103 | "no-array-constructor": "off", 104 | "no-empty-function": "off", 105 | "operator-assignment": [ 106 | "error", 107 | "never" 108 | ], 109 | "class-methods-use-this": "off", 110 | "@typescript-eslint/explicit-member-accessibility": "off", 111 | "@typescript-eslint/member-ordering": "off", 112 | "react/sort-comp": "off", 113 | "sort-imports": [ 114 | "error", 115 | { 116 | "ignoreCase": true, 117 | "ignoreDeclarationSort": true 118 | } 119 | ], 120 | "import/order": [ 121 | 1, 122 | { 123 | "groups": [ 124 | "external", 125 | "builtin", 126 | "internal", 127 | "sibling", 128 | "parent", 129 | "index", 130 | "object", 131 | "type" 132 | ], 133 | "pathGroups": [ 134 | { 135 | "pattern": "react", 136 | "group": "external", 137 | "position": "before" 138 | }, 139 | { 140 | "pattern": "src/pages/**", 141 | "group": "internal" 142 | }, 143 | { 144 | "pattern": "src/components/**", 145 | "group": "internal" 146 | }, 147 | { 148 | "pattern": "src/config/**", 149 | "group": "internal" 150 | }, 151 | { 152 | "pattern": "src/utils/**", 153 | "group": "internal" 154 | } 155 | ], 156 | "pathGroupsExcludedImportTypes": [ 157 | "react", 158 | "**/*.scss" 159 | ], 160 | "newlines-between": "always", 161 | "alphabetize": { 162 | "order": "asc", 163 | "caseInsensitive": true 164 | } 165 | } 166 | ], 167 | "no-relative-import-paths/no-relative-import-paths": [ 168 | "warn", 169 | { 170 | "allowSameFolder": false 171 | } 172 | ] 173 | }, 174 | "overrides": [ 175 | { 176 | "files": [ 177 | "*.ts", 178 | "*.tsx" 179 | ], 180 | "rules": { 181 | "lines-between-class-members": [ 182 | "error", 183 | "always", 184 | { 185 | "exceptAfterSingleLine": true 186 | } 187 | ], 188 | "no-param-reassign": 0, 189 | "id-length": [ 190 | 2, 191 | { 192 | "exceptions": [ 193 | "i", 194 | "j", 195 | "id", 196 | "t", 197 | "e", 198 | "x", 199 | "y" 200 | ], 201 | "properties": "never", 202 | "min": 3 203 | } 204 | ], 205 | "no-plusplus": [ 206 | "error", 207 | { 208 | "allowForLoopAfterthoughts": true 209 | } 210 | ], 211 | "deprecation/deprecation": "warn", 212 | "@typescript-eslint/explicit-module-boundary-types": [ 213 | "error" 214 | ], 215 | "no-unused-vars": "off", 216 | "@typescript-eslint/no-unused-vars": [ 217 | "error" 218 | ], 219 | "@typescript-eslint/no-non-null-assertion": "error", 220 | "react/function-component-definition": [ 221 | 2, 222 | { 223 | "namedComponents": "arrow-function" 224 | } 225 | ], 226 | "func-names": [ 227 | "error", 228 | "always", 229 | { 230 | "generators": "never" 231 | } 232 | ], 233 | "radix": [ 234 | 2, 235 | "as-needed" 236 | ], 237 | "curly": [ 238 | "error", 239 | "all" 240 | ], 241 | "prefer-destructuring": [ 242 | "error", 243 | { 244 | "array": false, 245 | "object": false 246 | }, 247 | { 248 | "enforceForRenamedProperties": true 249 | } 250 | ], 251 | "@typescript-eslint/no-array-constructor": [ 252 | "off" 253 | ], 254 | "@typescript-eslint/no-empty-function": [ 255 | "off" 256 | ], 257 | "react/prefer-stateless-function": "off", 258 | "react/jsx-boolean-value": [ 259 | "error", 260 | "always" 261 | ], 262 | "@typescript-eslint/explicit-member-accessibility": [ 263 | "error", 264 | { 265 | "accessibility": "explicit", 266 | "overrides": { 267 | "accessors": "explicit", 268 | "constructors": "no-public", 269 | "methods": "explicit", 270 | "properties": "explicit", 271 | "parameterProperties": "explicit" 272 | } 273 | } 274 | ], 275 | "react/require-default-props": [ 276 | 0, 277 | { 278 | "forbidDefaultForRequired": false, 279 | "classes": "ignore", 280 | "functions": "ignore" 281 | } 282 | ], 283 | "react/jsx-props-no-spreading": [ 284 | 0, 285 | { 286 | "html": "ignore", 287 | "custom": "ignore", 288 | "explicitSpread": "ignore" 289 | } 290 | ], 291 | "react/no-unused-prop-types": [ 292 | 0 293 | ], 294 | "react/no-danger": [ 295 | 0 296 | ], 297 | "react/no-unknown-property": [ 298 | 0 299 | ], 300 | "prefer-const": [ 301 | 0 302 | ] 303 | } 304 | } 305 | ] 306 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: true, 5 | singleQuote: true, 6 | printWidth: 160, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute? 2 | - 1 Fork the project. 3 | - 2 Make required changes and commit. 4 | - 3 Generate pull request. Mention all the required description regarding changes you made. 5 | 6 | Happy coding. 🙂 -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Asif Vora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 |

MERN Frontend App ✨️

2 | 3 | [![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://reactjs.org/) 4 | [![Redux](https://img.shields.io/badge/Redux-593D88?style=for-the-badge&logo=redux&logoColor=white)](https://redux.js.org/) 5 | [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 6 | [![ReactRouter](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)](https://reactrouter.com/) 7 | 8 | 9 | # 💻 Built With 10 | - [React](https://reactjs.org/) 11 | - [Redux](https://redux.js.org/) 12 | - [TypeScript](https://www.typescriptlang.org/) 13 | - [Router](https://reactrouter.com/) 14 | - Some services and utils 15 | 16 | # 🛠️ Installation Steps 17 | 18 | ### Clone the repository ⎘ 19 | ```bash 20 | git clone https://github.com/asif-simform/MERN-Stack.git 21 | 22 | ``` 23 | 24 | ### Change the working directory 📂 25 | ```bash 26 | cd frontend 27 | ``` 28 | 29 | ### Install dependency 🚚 30 | ```bash 31 | yarn 32 | ``` 33 | 34 | ### Start app 🚀 35 | ```bash 36 | yarn dev 37 | ``` 38 | 39 | ### Build app 🔨 40 | ```bash 41 | yarn build 42 | ``` 43 | 44 | You are all set! Open [localhost:3000](http://localhost:3000/) to see the app. 45 | 46 | # 🛡️ License 47 | 48 | This project is licensed under the MIT License - see the [`LICENSE`](LICENSE) file for details. 49 | 50 | # 👨‍💻 Author 51 | ### 👤 Asif Vora 52 | - Github: [@asif-simform](https://github.com/asif-simform) 53 | - LinkedIn: [@asif-vora](https://www.linkedin.com/in/asif-vora/) 54 | - Twitter: [@007_dark_shadow](https://twitter.com/007_dark_shadow) 55 | - Instagram: [@007_dark_shadow](https://www.instagram.com/007_dark_shadow/) 56 | 57 | # 🍰 Contributing 58 | 59 | - Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/asif-simform/MERN-Stack/compare). 60 | 61 | - Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details. 62 | 63 | # 🙏 Support 64 | This project needs a ⭐️ from you. Don't forget to leave a star ⭐️ -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MERN App 8 | 9 | 10 |
11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "dependencies": { 7 | "@emotion/react": "^11.10.5", 8 | "@mantine/core": "^5.9.2", 9 | "@mantine/dropzone": "^5.9.5", 10 | "@mantine/form": "^5.9.2", 11 | "@mantine/hooks": "^5.9.2", 12 | "@mantine/notifications": "^5.9.2", 13 | "axios": "^1.2.1", 14 | "clipboard": "^2.0.11", 15 | "history": "^5.3.0", 16 | "npm-install-peers": "^1.2.2", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-icons": "^4.7.1", 20 | "react-router-dom": "^6.4.5", 21 | "react-toastify": "^9.1.1", 22 | "react-topbar-progress-indicator": "^4.1.1", 23 | "yup": "^0.32.11" 24 | }, 25 | "devDependencies": { 26 | "@rollup/plugin-commonjs": "^24.0.0", 27 | "@rollup/plugin-node-resolve": "^15.0.1", 28 | "@types/eslint": "^8.37.0", 29 | "@types/node": "^18.15.11", 30 | "@types/prettier": "^2.7.2", 31 | "@types/react": "^18.0.26", 32 | "@types/react-dom": "^18.0.9", 33 | "@types/yup": "^0.32.0", 34 | "@typescript-eslint/eslint-plugin": "^5.4.0", 35 | "@typescript-eslint/parser": "^5.4.0", 36 | "@vitejs/plugin-react": "^3.0.0", 37 | "@vitejs/plugin-react-swc": "^3.0.0", 38 | "eslint": "^8.38.0", 39 | "eslint-config-airbnb": "^19.0.4", 40 | "eslint-config-prettier": "^8.8.0", 41 | "eslint-plugin-css-import-order": "^1.1.0", 42 | "eslint-plugin-deprecation": "1.3.3", 43 | "eslint-plugin-import": "^2.25.2", 44 | "eslint-plugin-jsx-a11y": "^6.4.1", 45 | "eslint-plugin-no-relative-import-paths": "^1.4.0", 46 | "eslint-plugin-react": "^7.26.1", 47 | "eslint-plugin-react-hooks": "^4.2.0", 48 | "path": "^0.12.7", 49 | "prettier": "^2.4.1", 50 | "typescript": "^4.9.3", 51 | "typesync": "^0.9.2", 52 | "vite": "^4.0.0", 53 | "vite-tsconfig-paths": "^4.0.8" 54 | }, 55 | "scripts": { 56 | "typesync": "typesync", 57 | "dev": "vite", 58 | "build": "tsc && vite build", 59 | "preview": "vite preview", 60 | "check-types": "./node_modules/.bin/tsc --noemit", 61 | "eslint": "./node_modules/.bin/eslint src/**.ts", 62 | "lint": "npm run eslint && npm run check-types", 63 | "lint:fix": "eslint . --ext .ts,.tsx --fix" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { MantineProvider } from '@mantine/core'; 4 | import { NotificationsProvider } from '@mantine/notifications'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import { FallbackProvider } from 'src/providers/FallbackProvider'; 7 | import { Routes } from 'src/routes'; 8 | import GlobalStyle from 'src/styles/global.style'; 9 | 10 | import ErrorBoundary from 'src/components/ErrorBoundary'; 11 | 12 | export const App: FC = () => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /frontend/src/components/Dropzone/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | import React, { ForwardedRef, useRef } from 'react'; 2 | 3 | import { Button, Center, createStyles, Group, Text } from '@mantine/core'; 4 | import { Dropzone as MantineDropzone } from '@mantine/dropzone'; 5 | import { TbCloudUpload, TbUpload } from 'react-icons/tb'; 6 | 7 | import toast from 'src/utils/Toast'; 8 | 9 | const useStyles = createStyles((theme) => ({ 10 | wrapper: { 11 | position: 'relative', 12 | marginBottom: 30, 13 | }, 14 | 15 | dropzone: { 16 | borderWidth: 1, 17 | paddingBottom: 50, 18 | }, 19 | 20 | icon: { 21 | color: 22 | theme.colorScheme === 'dark' 23 | ? theme.colors.dark[3] 24 | : theme.colors.gray[4], 25 | }, 26 | 27 | control: { 28 | position: 'absolute', 29 | bottom: -20, 30 | }, 31 | })); 32 | 33 | interface IDropzone { 34 | isUploading: boolean; 35 | setFiles: (files: File[]) => void; 36 | } 37 | 38 | const Dropzone = ({ isUploading, setFiles }: IDropzone) => { 39 | const { classes } = useStyles(); 40 | const openRef = useRef<() => void>(); 41 | return ( 42 |
43 | { 46 | toast.error(e[0].errors[0].message); 47 | }} 48 | disabled={isUploading} 49 | openRef={openRef as ForwardedRef<() => void>} 50 | onDrop={(files: File[]) => { 51 | const newFiles = files.map((file: File) => file); 52 | setFiles(newFiles); 53 | }} 54 | className={classes.dropzone} 55 | radius="md" 56 | > 57 |
58 | 59 | 60 | 61 | 62 | Upload files 63 | 64 | 65 | Drag'n'drop files here. 66 | 67 |
68 |
69 |
70 | 80 |
81 |
82 | ); 83 | }; 84 | export default Dropzone; 85 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | interface IState { 4 | error: any; 5 | errorInfo: any; 6 | } 7 | 8 | class ErrorBoundary extends Component { 9 | constructor(props: any) { 10 | super(props); 11 | this.state = { error: null, errorInfo: null }; 12 | } 13 | 14 | componentDidCatch(error: any, errorInfo: any) { 15 | // Catch errors in any components below and re-render with error message 16 | this.setState({ 17 | error, 18 | errorInfo 19 | }); 20 | // You can also log error messages to an error reporting service here 21 | } 22 | 23 | render() { 24 | if (this.state.errorInfo) { 25 | // Error path 26 | return ( 27 |
28 |

Something went wrong.

29 |
30 | {this.state.error && this.state.error.toString()} 31 |
32 | {this.state.errorInfo.componentStack} 33 |
34 |
35 | ); 36 | } 37 | // Normally, just render children 38 | return this.props.children; 39 | } 40 | } 41 | 42 | export default ErrorBoundary; -------------------------------------------------------------------------------- /frontend/src/components/FallbackPageWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react'; 2 | 3 | import { usePageRoute } from 'src/hooks/usePageRoute'; 4 | 5 | interface PageWrapperProps { 6 | children?: React.ReactNode | React.ReactElement | any 7 | } 8 | 9 | export const FallbackPageWrapper: React.FC = ({ children }: PageWrapperProps) => { 10 | const { onLoad } = usePageRoute(); 11 | 12 | const render = useMemo(() => children, [children]); 13 | 14 | useEffect(() => { 15 | onLoad(render); 16 | }, [onLoad, render]); 17 | 18 | return render; 19 | }; 20 | 21 | export default FallbackPageWrapper; -------------------------------------------------------------------------------- /frontend/src/components/FilesList/FilesList.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Table } from '@mantine/core'; 2 | import { CheckboxGroup } from '@mantine/core/lib/Checkbox/CheckboxGroup/CheckboxGroup'; 3 | import { TbTrash } from 'react-icons/tb'; 4 | 5 | import { byteStringToHumanSizeString } from 'src/utils/commonFunction'; 6 | 7 | interface IFilesList { 8 | files: File[]; 9 | setFiles: (files: File[]) => void; 10 | } 11 | 12 | const FilesList = ({ files, setFiles }: IFilesList) => { 13 | const remove = (index: number) => { 14 | files.splice(index, 1); 15 | setFiles([...files]); 16 | }; 17 | 18 | const rows = files.map((file: File, i: number) => ( 19 | 20 | {file.name} 21 | {byteStringToHumanSizeString(file.size.toString())} 22 | 23 | remove(i)} 28 | > 29 | 30 | 31 | 32 | 33 | )); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {rows} 45 |
NameSizeAction
46 | ); 47 | }; 48 | 49 | export default FilesList; 50 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@mantine/core'; 2 | 3 | import Header from 'src/components/NavBar/NavBar'; 4 | 5 | const Layout = ({ children }: any) => ( 6 | <> 7 |
8 | {children} 9 | 10 | ); 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/ActionAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Avatar, Menu } from '@mantine/core'; 2 | import { TbDoorExit, TbUser } from 'react-icons/tb'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import Storage from 'src/services/Storage'; 5 | 6 | import { routes } from 'src/config/routes_config'; 7 | import toast from 'src/utils/Toast'; 8 | 9 | const ActionAvatar = () => { 10 | const navigate = useNavigate(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | { 22 | // TODO : Do redirect code 23 | }} 24 | icon={} 25 | > 26 | My account 27 | 28 | { 30 | Storage.clearAll(); 31 | toast.success('Logout successful'); 32 | navigate(routes.signIn.path_string()); 33 | }} 34 | icon={} 35 | > 36 | Sign out 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default ActionAvatar; 44 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | import { 4 | Box, 5 | Burger, 6 | Container, 7 | createStyles, 8 | Group, 9 | Header, 10 | Paper, 11 | Stack, 12 | Text, 13 | Transition, 14 | } from '@mantine/core'; 15 | import { useDisclosure } from '@mantine/hooks'; 16 | import { NavLink } from 'react-router-dom'; 17 | import { useAuth } from 'src/hooks/useAuth'; 18 | 19 | import ActionAvatar from 'src/components/NavBar/ActionAvatar'; 20 | import { routes as routesConfig } from 'src/config/routes_config'; 21 | 22 | 23 | 24 | const HEADER_HEIGHT = 60; 25 | 26 | type NavLink = { 27 | link?: string; 28 | label?: string; 29 | component?: React.ComponentType; 30 | action?: () => Promise; 31 | }; 32 | 33 | const useStyles = createStyles((theme) => ({ 34 | root: { 35 | position: 'relative', 36 | zIndex: 1, 37 | }, 38 | 39 | dropdown: { 40 | position: 'absolute', 41 | top: HEADER_HEIGHT, 42 | left: 0, 43 | right: 0, 44 | zIndex: 0, 45 | borderTopRightRadius: 0, 46 | borderTopLeftRadius: 0, 47 | borderTopWidth: 0, 48 | overflow: 'hidden', 49 | 50 | [theme.fn.largerThan('sm')]: { 51 | display: 'none', 52 | }, 53 | }, 54 | 55 | header: { 56 | display: 'flex', 57 | justifyContent: 'space-between', 58 | alignItems: 'center', 59 | height: '100%', 60 | }, 61 | 62 | links: { 63 | [theme.fn.smallerThan('sm')]: { 64 | display: 'none', 65 | }, 66 | }, 67 | 68 | burger: { 69 | [theme.fn.largerThan('sm')]: { 70 | display: 'none', 71 | }, 72 | }, 73 | 74 | link: { 75 | display: 'block', 76 | lineHeight: 1, 77 | padding: '8px 12px', 78 | borderRadius: theme.radius.sm, 79 | textDecoration: 'none', 80 | color: 81 | theme.colorScheme === 'dark' 82 | ? theme.colors.dark[0] 83 | : theme.colors.gray[7], 84 | fontSize: theme.fontSizes.sm, 85 | fontWeight: 500, 86 | 87 | '&:hover': { 88 | backgroundColor: 89 | theme.colorScheme === 'dark' 90 | ? theme.colors.dark[6] 91 | : theme.colors.gray[0], 92 | }, 93 | 94 | [theme.fn.smallerThan('sm')]: { 95 | borderRadius: 0, 96 | padding: theme.spacing.md, 97 | }, 98 | }, 99 | 100 | linkActive: { 101 | '&, &:hover': { 102 | backgroundColor: 103 | theme.colorScheme === 'dark' 104 | ? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25) 105 | : theme.colors[theme.primaryColor][0], 106 | color: 107 | theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 3 : 7], 108 | }, 109 | }, 110 | })); 111 | 112 | const NavBar = () => { 113 | const [opened, toggleOpened] = useDisclosure(false); 114 | const { classes, cx } = useStyles(); 115 | const auth = useAuth(); 116 | 117 | const authenticatedLinks = [ 118 | { 119 | link: routesConfig.home.path_string(), 120 | label: 'Home', 121 | id: 'home' 122 | }, 123 | { 124 | component: , 125 | id: 'action' 126 | }, 127 | ]; 128 | 129 | 130 | const unauthenticatedLinks = [ 131 | { 132 | link: routesConfig.home.path_string(), 133 | label: 'Home', 134 | component: null, 135 | id: 'home' 136 | }, 137 | { 138 | link: routesConfig.signIn.path_string(), 139 | label: 'Sign in', 140 | component: null, 141 | id: 'signIn' 142 | }, 143 | { 144 | link: routesConfig.signUp.path_string(), 145 | label: 'Sign up', 146 | component: null, 147 | id: 'signUp' 148 | }]; 149 | 150 | 151 | const items = ( 152 | 153 | (auth ? authenticatedLinks : unauthenticatedLinks)?.map((link) => { 154 | if (link?.component) { 155 | return ( 156 | 157 | {link.component} 158 | 159 | ); 160 | } 161 | return ( 162 | toggleOpened.toggle()} 166 | className={cx(classes.link, { 167 | [classes.linkActive]: window.location.pathname === link.link, 168 | })} 169 | > 170 | {link.label} 171 | 172 | ); 173 | }) 174 | ); 175 | 176 | return ( 177 |
178 | 179 | 180 | 181 | MERN App 182 | 183 | 184 | 185 | {items} 186 | 187 | toggleOpened.toggle()} 190 | className={classes.burger} 191 | size="sm" 192 | /> 193 | 194 | {(styles) => ( 195 | 196 | {items} 197 | 198 | )} 199 | 200 | 201 |
202 | ); 203 | }; 204 | 205 | export default NavBar; 206 | -------------------------------------------------------------------------------- /frontend/src/components/UploadForm.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | import { Button, Group } from '@mantine/core'; 4 | import { useForm, yupResolver } from '@mantine/form'; 5 | import * as yup from 'yup'; 6 | 7 | import Dropzone from 'src/components/Dropzone/Dropzone'; 8 | import FilesList from 'src/components/FilesList/FilesList'; 9 | 10 | const UploadForm = () => { 11 | const [isUploading] = useState(false); 12 | 13 | const validationSchema = yup.object().shape({ 14 | files: yup.mixed(), 15 | }); 16 | 17 | const form = useForm({ 18 | initialValues: { 19 | files: [], 20 | }, 21 | validate: yupResolver(validationSchema), 22 | }); 23 | 24 | const { values, setFieldValue } = form; 25 | 26 | const onSetFiles = useCallback( 27 | (newFiles: File[]) => { 28 | const allFiles = [...values.files, ...newFiles]; 29 | 30 | setFieldValue('files', allFiles as any); 31 | }, 32 | [values.files] 33 | ); 34 | 35 | const onRemoveFiles = useCallback((files:any) => { 36 | setFieldValue('files', files); 37 | }, []); 38 | 39 | const onSubmit = (values: File[]) => { 40 | console.log('onSubmit Files', values); 41 | }; 42 | 43 | return ( 44 | <> 45 |
onSubmit(values as any))}> 46 | 47 | 54 | 55 | 56 | 57 | {values?.files.length > 0 && ( 58 | 59 | )} 60 | 61 | ); 62 | }; 63 | 64 | export default UploadForm; 65 | -------------------------------------------------------------------------------- /frontend/src/components/UploadProgressIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { RingProgress } from '@mantine/core'; 2 | import { TbCircleCheck, TbCircleX } from 'react-icons/tb'; 3 | 4 | const UploadProgressIndicator = ({ progress }: { progress: number }) => { 5 | if (progress > 0 && progress < 100) { 6 | return ( 7 | 12 | ); 13 | } if (progress >= 100) { 14 | return ; 15 | } 16 | return ; 17 | 18 | }; 19 | 20 | export default UploadProgressIndicator; 21 | -------------------------------------------------------------------------------- /frontend/src/components/UrlForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useRef, useEffect } from 'react'; 2 | 3 | import { ActionIcon, TextInput, useMantineTheme } from '@mantine/core'; 4 | import { TbCopy } from 'react-icons/tb'; 5 | import { useForm, yupResolver } from '@mantine/form'; 6 | import ClipboardJS from 'clipboard'; 7 | 8 | import { POST } from 'src/services/HttpService'; 9 | import { isValidURL } from 'src/utils/commonFunction'; 10 | import * as yup from 'yup'; 11 | 12 | import { getApiErrorMessage } from 'src/utils/commonFunction'; 13 | import { isSafari } from 'src/utils/DeviceDetect'; 14 | import toast from 'src/utils/Toast'; 15 | 16 | export const UrlForm: React.FC = () => { 17 | const theme = useMantineTheme(); 18 | 19 | const [isLoading, setLoading] = useState(false); 20 | const isSafariBrowser: boolean = useMemo(() => isSafari(), []); 21 | const clipboard = useRef(null); 22 | 23 | const validationSchema = yup.object().shape({ 24 | originalUrl: yup.string() 25 | .test('validateWebsite', 'Please enter valid URL', (val) => { 26 | if (val && !isValidURL(val)) { 27 | return false; 28 | } 29 | return true; 30 | }).required('Please enter a URL'), 31 | }); 32 | 33 | const form = useForm({ 34 | initialValues: { 35 | originalUrl: '' 36 | }, 37 | validate: yupResolver(validationSchema), 38 | }); 39 | 40 | const onShortURL = async (values: any) => { 41 | setLoading(true); 42 | const { originalUrl } = values; 43 | try { 44 | const data = await POST({ 45 | subUrl: '/urls/short', 46 | data: { 47 | originalUrl, 48 | }, 49 | }); 50 | 51 | const shortUrl = data?.data?.data?.shortUrl; 52 | 53 | if (!isSafariBrowser) { 54 | navigator.clipboard 55 | .writeText(shortUrl) 56 | .then(() => { 57 | toast.success('Copied shortlink to clipboard!'); 58 | form.reset(); 59 | }).catch(() => { 60 | toast.error('Error in Copy shortlink'); 61 | }); 62 | } 63 | } catch (error) { 64 | const message = getApiErrorMessage(error, 'originalUrl'); 65 | if(message && typeof message ==='string') { 66 | toast.error(message); 67 | } 68 | } finally { 69 | setLoading(false); 70 | } 71 | }; 72 | 73 | useEffect(() => { 74 | if (isSafariBrowser && !clipboard.current) { 75 | window.console.log('set clipboard') 76 | clipboard.current = new ClipboardJS('.btn'); 77 | 78 | clipboard.current?.on('success', (e) => { 79 | console.info('Action:', e.action); 80 | console.info('Text:', e.text); 81 | console.info('Trigger:', e.trigger); 82 | toast.success('Copied shortlink to clipboard!'); 83 | }); 84 | 85 | clipboard.current?.on('error', (e) => { 86 | console.info('Action:', e.action); 87 | console.info('Text:', e.text); 88 | console.info('Trigger:', e.trigger); 89 | toast.error('Error in Copy clipboard safari'); 90 | }); 91 | window.console.log('ref clipboard in ', clipboard) 92 | } 93 | }, [isSafariBrowser]); 94 | 95 | useEffect(() => () => { 96 | clipboard?.current?.destroy(); 97 | }, []); 98 | 99 | return ( 100 |
onShortURL(values))}> 101 | 107 | 108 | 109 | } 110 | placeholder="Enter URL" 111 | rightSectionWidth={52} 112 | {...form.getInputProps('originalUrl')} 113 | /> 114 | 115 | ); 116 | }; 117 | 118 | export default UrlForm; -------------------------------------------------------------------------------- /frontend/src/components/auth/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { 4 | Button, 5 | Container, 6 | Paper, 7 | PasswordInput, 8 | Text, 9 | TextInput, 10 | Title, 11 | } from '@mantine/core'; 12 | import { useForm, yupResolver } from '@mantine/form'; 13 | import { NavLink, useNavigate } from 'react-router-dom'; 14 | import { POST } from 'src/services/HttpService'; 15 | import Storage from 'src/services/Storage'; 16 | import * as yup from 'yup'; 17 | 18 | import { routes , routes as routesConfig } from 'src/config/routes_config'; 19 | import { getApiErrorMessage } from 'src/utils/commonFunction'; 20 | import toast from 'src/utils/Toast'; 21 | 22 | const SignInForm = () => { 23 | const [isLoading, setLoading] = useState(false); 24 | const navigate = useNavigate(); 25 | 26 | const validationSchema = yup.object().shape({ 27 | email: yup.string().email().required(), 28 | password: yup.string().min(8).required(), 29 | }); 30 | 31 | const form = useForm({ 32 | initialValues: { 33 | email: '', 34 | password: '', 35 | }, 36 | validate: yupResolver(validationSchema), 37 | }); 38 | 39 | const signIn = async (values: any) => { 40 | setLoading(true); 41 | const { email, password } = values; 42 | try { 43 | const response = await POST({ 44 | subUrl: '/users/signin', 45 | data: { 46 | email, 47 | password, 48 | }, 49 | }); 50 | console.log('response',response.data?.data?.token); 51 | Storage.setItem('token', response.data?.data?.token); 52 | toast.success('Login successful'); 53 | navigate(routes.home.path_string()); 54 | } catch (error) { 55 | const message = getApiErrorMessage(error); 56 | message && toast.error(message); 57 | } finally { 58 | setLoading(false); 59 | } 60 | }; 61 | 62 | return ( 63 | 64 | ({ 67 | fontFamily: `Greycliff CF, ${theme.fontFamily}`, 68 | fontWeight: 900, 69 | })} 70 | > 71 | Welcome back 72 | 73 | 74 | You don't have an account yet?{' '} 75 | Sign up 76 | 77 | 78 |
signIn(values))}> 79 | 84 | 90 | 93 | 94 |
95 |
96 | ); 97 | }; 98 | 99 | export default SignInForm; 100 | -------------------------------------------------------------------------------- /frontend/src/components/auth/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { 4 | Button, 5 | Container, 6 | Paper, 7 | PasswordInput, 8 | Text, 9 | TextInput, 10 | Title, 11 | } from '@mantine/core'; 12 | import { useForm, yupResolver } from '@mantine/form'; 13 | import { NavLink } from 'react-router-dom'; 14 | import { POST } from 'src/services/HttpService'; 15 | import * as yup from 'yup'; 16 | 17 | import { routes as routesConfig } from 'src/config/routes_config'; 18 | import { getApiErrorMessage } from 'src/utils/commonFunction'; 19 | import toast from 'src/utils/Toast'; 20 | 21 | const SignUpForm = () => { 22 | const [isLoading, setLoading] = useState(false); 23 | 24 | const validationSchema = yup.object().shape({ 25 | firstName: yup.string().min(3).required(), 26 | lastName: yup.string().min(3).required(), 27 | email: yup.string().email().required(), 28 | password: yup.string().min(8).required(), 29 | confirmPassword: yup 30 | .string() 31 | .min(8) 32 | .oneOf([yup.ref('password')], 'Passwords must match') 33 | .required(), 34 | }); 35 | 36 | const form = useForm({ 37 | initialValues: { 38 | firstName: '', 39 | lastName: '', 40 | email: '', 41 | password: '', 42 | confirmPassword: '', 43 | }, 44 | validate: yupResolver(validationSchema), 45 | }); 46 | 47 | const signUp = async (values: any) => { 48 | setLoading(true); 49 | const { firstName, lastName, email, password } = values; 50 | try { 51 | await POST({ 52 | subUrl: '/users/signup', 53 | data: { 54 | firstName, 55 | lastName, 56 | email, 57 | password, 58 | }, 59 | }); 60 | toast.success('Registration successful'); 61 | } catch (error) { 62 | const message = getApiErrorMessage(error); 63 | message && toast.error(message); 64 | } finally { 65 | setLoading(false); 66 | } 67 | }; 68 | 69 | return ( 70 | 71 | ({ 74 | fontFamily: `Greycliff CF, ${theme.fontFamily}`, 75 | fontWeight: 900, 76 | })} 77 | > 78 | Sign up 79 | 80 | 81 | You have an account already? Sign in 82 | 83 | 84 |
signUp(values))}> 85 | 91 | 97 | 103 | 109 | 115 | 118 | 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default SignUpForm; 125 | -------------------------------------------------------------------------------- /frontend/src/config/env.ts: -------------------------------------------------------------------------------- 1 | export const isDevelopment = import.meta.env.DEV; 2 | export const DEV_API_URL = import.meta.env.VITE_APP_DEV_API_URL; 3 | export const PROD_API_URL = import.meta.env.VITE_APP_PROD_API_URL; 4 | export const API_URL = isDevelopment ? DEV_API_URL : PROD_API_URL; -------------------------------------------------------------------------------- /frontend/src/config/images.ts: -------------------------------------------------------------------------------- 1 | export const images = { 2 | // HeroBg: require('../assets/images/hero-bg.png'), 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/config/routes.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | import { createBrowserHistory } from 'history'; 4 | 5 | import { IRoutesConfig, routes as routesConfig } from 'src/config/routes_config'; 6 | 7 | export const history = createBrowserHistory(); 8 | 9 | export const routes: IRoutesConfig = { 10 | [routesConfig.signIn.id]: { 11 | ...routesConfig.signIn, 12 | component: lazy(() => import('../pages/SignIn')) 13 | }, 14 | [routesConfig.signUp.id]: { 15 | ...routesConfig.signUp, 16 | component: lazy(() => import('../pages/SignUp')) 17 | }, 18 | [routesConfig.home.id]: { 19 | ...routesConfig.home, 20 | component: lazy(() => import('../pages/Home')) 21 | }, 22 | [routesConfig.urlLink.id]: { 23 | ...routesConfig.urlLink, 24 | component: lazy(() => import('../pages/UrlLinks')) 25 | }, 26 | [routesConfig.notFound.id]: { 27 | ...routesConfig.notFound, 28 | component: lazy(() => import('../pages/NotFound')) 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/config/routes_config.ts: -------------------------------------------------------------------------------- 1 | import { ElementType } from 'react'; 2 | 3 | export type IRoutesConfig = { 4 | [key: string]: { 5 | id: string; 6 | name: string; 7 | description?: string; 8 | path: string; 9 | path_string: (params?: any) => string; 10 | exact: boolean; 11 | isPrivate: boolean; 12 | isStatic: boolean; 13 | component?: ElementType | ElementType | undefined; 14 | }; 15 | }; 16 | /** 17 | * @description the aim to create this config is to have 18 | * a single source of truth for the routes defination. 19 | * the reason we are not importing the components here 20 | * for the property `component` is to avoid circular 21 | * import dependencies error. 22 | * components will be assigned in config/routes.ts 23 | */ 24 | export const routes: IRoutesConfig = { 25 | home: { 26 | id: 'home', 27 | name: 'Home', 28 | description: 'Home', 29 | path: '/', 30 | path_string: () => '/', 31 | exact: true, 32 | isPrivate: false, 33 | isStatic: true, 34 | component: undefined 35 | }, 36 | signIn: { 37 | id: 'signIn', 38 | name: 'SignIn', 39 | description: 'SignIn', 40 | path: '/sign-in', 41 | path_string: () => '/sign-in', 42 | exact: true, 43 | isPrivate: false, 44 | isStatic: false, 45 | component: undefined 46 | }, 47 | signUp: { 48 | id: 'signUp', 49 | name: 'SignUp', 50 | description: 'SignUp', 51 | path: '/sign-up', 52 | path_string: () => '/sign-up', 53 | exact: true, 54 | isPrivate: false, 55 | isStatic: false, 56 | component: undefined 57 | }, 58 | urlLink: { 59 | id: 'urlLink', 60 | name: 'UrlLink', 61 | description: 'UrlLink', 62 | path: '/:urlId', 63 | path_string: (urlId) => `/${urlId}`, 64 | exact: true, 65 | isPrivate: false, 66 | isStatic: true, 67 | component: undefined 68 | }, 69 | notFound: { 70 | id: 'notFound', 71 | name: '404', 72 | description: 'Page not found', 73 | path: '/*', 74 | path_string: () => '/404', 75 | exact: false, 76 | isPrivate: false, 77 | isStatic: true, 78 | component: undefined 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import Storage from 'src/services/Storage'; 2 | 3 | export const useAuth = () => { 4 | const token = Storage.getItem('token'); 5 | if (token) { 6 | return true; 7 | } 8 | return false; 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/src/hooks/usePageRoute.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext } from 'react'; 2 | 3 | import { FallbackContext, FallbackType } from 'src/providers/FallbackProvider'; 4 | 5 | export const usePageRoute = (): any => { 6 | const { updateFallback } = useContext(FallbackContext); 7 | 8 | const onLoad = useCallback( 9 | (component: FallbackType | undefined) => { 10 | if (component === undefined) { 11 | component = null; 12 | } 13 | updateFallback(component); 14 | }, 15 | [updateFallback] 16 | ); 17 | 18 | return { onLoad }; 19 | }; 20 | 21 | export default usePageRoute; 22 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | 6 | :root { 7 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 8 | font-size: 16px; 9 | line-height: 24px; 10 | font-weight: 400; 11 | 12 | color-scheme: light dark; 13 | color: rgba(255, 255, 255, 0.87); 14 | background-color: #242424; 15 | 16 | font-synthesis: none; 17 | text-rendering: optimizeLegibility; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | -webkit-text-size-adjust: 100%; 21 | } 22 | 23 | a { 24 | font-weight: 500; 25 | color: #646cff; 26 | text-decoration: inherit; 27 | } 28 | a:hover { 29 | color: #535bf2; 30 | } 31 | 32 | body { 33 | margin: 0; 34 | display: flex; 35 | place-items: center; 36 | min-width: 320px; 37 | min-height: 100vh; 38 | } 39 | 40 | h1 { 41 | font-size: 3.2em; 42 | line-height: 1.1; 43 | } 44 | 45 | button { 46 | border-radius: 8px; 47 | border: 1px solid transparent; 48 | padding: 0.6em 1.2em; 49 | font-size: 1em; 50 | font-weight: 500; 51 | font-family: inherit; 52 | background-color: #1a1a1a; 53 | cursor: pointer; 54 | transition: border-color 0.25s; 55 | } 56 | button:hover { 57 | border-color: #646cff; 58 | } 59 | button:focus, 60 | button:focus-visible { 61 | outline: 4px auto -webkit-focus-ring-color; 62 | } 63 | 64 | @media (prefers-color-scheme: light) { 65 | :root { 66 | color: #213547; 67 | background-color: #ffffff; 68 | } 69 | a:hover { 70 | color: #747bff; 71 | } 72 | button { 73 | background-color: #f9f9f9; 74 | } 75 | } 76 | 77 | 78 | .link-find-loader { 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | height: 100vh; 83 | } -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | 3 | import ReactDOM from 'react-dom/client'; 4 | import App from 'src/App'; 5 | 6 | import 'src/index.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | , 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Container, 4 | createStyles, 5 | Group, 6 | List, 7 | Text, 8 | ThemeIcon, 9 | Title, 10 | } from '@mantine/core'; 11 | import { TbCheck } from 'react-icons/tb'; 12 | import { useNavigate } from 'react-router'; 13 | 14 | 15 | import { FallbackPageWrapper } from 'src/components/FallbackPageWrapper'; 16 | import { UrlForm } from 'src/components/UrlForm'; 17 | import Layout from 'src/components/Layout'; 18 | import { routes as routesConfig } from 'src/config/routes_config'; 19 | 20 | const useStyles = createStyles((theme) => ({ 21 | inner: { 22 | display: 'flex', 23 | justifyContent: 'space-between', 24 | paddingTop: `calc(${theme.spacing.md} * 4)`, 25 | paddingBottom: `calc(${theme.spacing.md} * 4)`, 26 | }, 27 | 28 | content: { 29 | maxWidth: 480, 30 | marginRight: `calc(${theme.spacing.md} * 3)`, 31 | 32 | [theme.fn.smallerThan('md')]: { 33 | maxWidth: '100%', 34 | marginRight: 0, 35 | }, 36 | }, 37 | 38 | title: { 39 | color: theme.colorScheme === 'dark' ? theme.white : theme.black, 40 | fontSize: 44, 41 | lineHeight: 1.2, 42 | fontWeight: 900, 43 | 44 | [theme.fn.smallerThan('xs')]: { 45 | fontSize: 28, 46 | }, 47 | }, 48 | 49 | control: { 50 | [theme.fn.smallerThan('xs')]: { 51 | flex: 1, 52 | }, 53 | }, 54 | 55 | image: { 56 | [theme.fn.smallerThan('md')]: { 57 | display: 'none', 58 | }, 59 | }, 60 | 61 | highlight: { 62 | position: 'relative', 63 | backgroundColor: 64 | theme.colorScheme === 'dark' 65 | ? theme.fn.rgba(theme.colors[theme.primaryColor][6], 0.55) 66 | : theme.colors[theme.primaryColor][0], 67 | borderRadius: theme.radius.sm, 68 | padding: '4px 12px', 69 | }, 70 | })); 71 | 72 | const Home = () => { 73 | const { classes } = useStyles(); 74 | const navigate = useNavigate(); 75 | 76 | return ( 77 | 78 | 79 | 80 |
81 |
82 | 83 | A <span className={classes.highlight}>self-hosted</span> <br />{' '} 84 | URL shortner platform. 85 | 86 | 87 | Shorten, personalize, and share fully branded short URLs. 88 | 89 | 95 | 96 | 97 | } 98 | > 99 | 100 |
101 | Self-Hosted - Host Share Link on your own machine. 102 |
103 |
104 | 105 |
106 | Privacy - Password protected link 107 |
108 |
109 |
110 | 111 | 119 | 133 | 134 |
135 | 136 | 137 | 138 |
139 |
140 |
141 |
142 | ); 143 | }; 144 | export default Home; 145 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { 4 | Container, 5 | createStyles, 6 | Group, 7 | Text, 8 | Title, 9 | } from '@mantine/core'; 10 | import { NavLink } from 'react-router-dom'; 11 | 12 | import { FallbackPageWrapper } from 'src/components/FallbackPageWrapper'; 13 | import Layout from 'src/components/Layout'; 14 | 15 | const useStyles = createStyles((theme) => ({ 16 | root: { 17 | paddingTop: 80, 18 | paddingBottom: 80, 19 | }, 20 | 21 | label: { 22 | textAlign: 'center', 23 | fontWeight: 900, 24 | fontSize: 220, 25 | lineHeight: 1, 26 | marginBottom: theme.spacing.xl * 1.5, 27 | color: theme.colors.gray[2], 28 | 29 | [theme.fn.smallerThan('sm')]: { 30 | fontSize: 120, 31 | }, 32 | }, 33 | 34 | description: { 35 | maxWidth: 500, 36 | margin: 'auto', 37 | marginBottom: theme.spacing.xl * 1.5, 38 | }, 39 | })); 40 | 41 | const ErrorNotFound : FC = () => { 42 | const { classes } = useStyles(); 43 | 44 | return ( 45 | 46 | 47 | 48 |
404
49 | 50 | Oops this page doesn't exist. 51 | 52 | 57 | 58 | 59 | Bring me back 60 | 61 | 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default ErrorNotFound; 69 | -------------------------------------------------------------------------------- /frontend/src/pages/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import SignInForm from 'src/components/auth/SignInForm'; 2 | import { FallbackPageWrapper } from 'src/components/FallbackPageWrapper'; 3 | import Layout from 'src/components/Layout'; 4 | 5 | const SignIn = () => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | export default SignIn; 13 | -------------------------------------------------------------------------------- /frontend/src/pages/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import SignUpForm from 'src/components/auth/SignUpForm'; 2 | import { FallbackPageWrapper } from 'src/components/FallbackPageWrapper'; 3 | import Layout from 'src/components/Layout'; 4 | 5 | const SignUp = () => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | export default SignUp; -------------------------------------------------------------------------------- /frontend/src/pages/Upload.tsx: -------------------------------------------------------------------------------- 1 | import { FallbackPageWrapper } from 'src/components/FallbackPageWrapper'; 2 | import Layout from 'src/components/Layout'; 3 | import UploadForm from 'src/components/UploadForm'; 4 | 5 | const Upload = () => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | export default Upload; 13 | -------------------------------------------------------------------------------- /frontend/src/pages/UrlLinks.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import { Loader } from '@mantine/core'; 4 | import { useParams } from "react-router-dom"; 5 | import { GET } from 'src/services/HttpService'; 6 | 7 | import { getApiErrorMessage } from 'src/utils/commonFunction'; 8 | import toast from 'src/utils/Toast'; 9 | 10 | const UrlLinks = () => { 11 | const { urlId = '' } = useParams(); 12 | const [isLoading, setLoading] = useState(false); 13 | 14 | useEffect(() => { 15 | fetchLink(urlId); 16 | }, [urlId]); 17 | 18 | const fetchLink = async (urlId: string) => { 19 | setLoading(true); 20 | try { 21 | const data = await GET({ subUrl: `/urls/${urlId}` }); 22 | const originalUrl = data.data.data.originalUrl; 23 | 24 | window.location.href = originalUrl; 25 | } catch (error) { 26 | const message = getApiErrorMessage(error); 27 | message && toast.error(message); 28 | } finally { 29 | setLoading(false); 30 | } 31 | }; 32 | 33 | return isLoading ? 34 |
35 | 36 |
: null; 37 | }; 38 | 39 | export default UrlLinks; -------------------------------------------------------------------------------- /frontend/src/providers/FallbackProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback, useMemo, useState } from 'react'; 2 | 3 | import ProgressBar from 'react-topbar-progress-indicator'; 4 | 5 | // Progress Bar -> Configuration 6 | ProgressBar.config({ 7 | barColors: { 8 | '0': '#fff86d', 9 | '1.0': '#fff86d', 10 | }, 11 | shadowBlur: 0, 12 | barThickness: 4, 13 | }); 14 | 15 | export type FallbackType = NonNullable | null; 16 | 17 | export interface FallbackContextType { 18 | updateFallback: (fallbackElement: FallbackType) => void; 19 | } 20 | 21 | export const FallbackContext = createContext({ 22 | updateFallback: () => { }, 23 | }); 24 | 25 | interface FallbackProviderProps { 26 | children: React.ReactNode | React.ReactElement | null 27 | } 28 | 29 | export const FallbackProvider: React.FC = ({ 30 | children, 31 | }) => { 32 | const [fallback, setFallback] = useState(null); 33 | 34 | const updateFallback = useCallback((fallbackElement: FallbackType) => { 35 | setFallback(() => fallbackElement); 36 | }, []); 37 | 38 | const renderChildren = useMemo(() => children, [children]); 39 | 40 | return ( 41 | 42 | {fallback}} 44 | > 45 | {renderChildren} 46 | 47 | 48 | ); 49 | }; -------------------------------------------------------------------------------- /frontend/src/routes/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from 'react'; 2 | 3 | import { Navigate } from 'react-router-dom'; 4 | import { useAuth } from 'src/hooks/useAuth'; 5 | 6 | import { routes } from 'src/config/routes_config'; 7 | 8 | interface IProps { 9 | children: any; 10 | } 11 | 12 | export const PrivateRoute: FC> = ({ 13 | children, 14 | }) => { 15 | const auth = useAuth(); 16 | 17 | /** 18 | * @description When user is not logged in and the 19 | * user access private routes like home page we 20 | * redirect the user to login page 21 | */ 22 | if (!auth) { 23 | return ; 24 | } 25 | 26 | return children; 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/routes/PublicRoute.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from 'react'; 2 | 3 | import { Navigate } from 'react-router-dom'; 4 | import { useAuth } from 'src/hooks/useAuth'; 5 | 6 | import { routes } from 'src/config/routes_config'; 7 | 8 | interface IProps { 9 | children: any; 10 | } 11 | 12 | export const PublicRoute: FC> = ({ 13 | children, 14 | }) => { 15 | const auth = useAuth(); 16 | 17 | /** 18 | * @description When user is logged in and the 19 | * user access public routes like login/signup we 20 | * redirect the user to home page 21 | */ 22 | if (auth) { 23 | return ; 24 | } 25 | 26 | return children; 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/routes/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, FC } from 'react'; 2 | 3 | import { Route, Routes } from 'react-router-dom'; 4 | import { PrivateRoute } from 'src/routes/PrivateRoute'; 5 | import { PublicRoute } from 'src/routes/PublicRoute'; 6 | 7 | import { routes } from 'src/config/routes'; 8 | 9 | export const AppRoutes: FC = () => ( 10 | 11 | {Object.keys(routes).map((key) => { 12 | const { 13 | component, 14 | path, 15 | id, 16 | isPrivate, 17 | isStatic, 18 | } = routes[key]; 19 | const Component = component as ElementType; 20 | 21 | if (isStatic) { 22 | return } />; 23 | } 24 | 25 | if (isPrivate) { 26 | return ( 27 | 32 | 33 | 34 | } 35 | /> 36 | ); 37 | } 38 | 39 | return ( 40 | 45 | 46 | 47 | } 48 | /> 49 | ); 50 | })} 51 | 52 | ); 53 | 54 | export default AppRoutes; 55 | -------------------------------------------------------------------------------- /frontend/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import Routes from 'src/routes/Routes'; 2 | 3 | export { Routes }; -------------------------------------------------------------------------------- /frontend/src/services/HttpService.ts: -------------------------------------------------------------------------------- 1 | import axios, { Method } from 'axios'; 2 | 3 | import { API_URL } from 'src/config/env'; 4 | // import { errorToast } from '../utils/Toast' 5 | import Storage from 'src/services/Storage'; 6 | 7 | const codes = { 8 | UNAUTHORIZED: 401, 9 | CUSTOM_TOKEN_EXPIRED: -2, 10 | REQUEST_TIMEOUT: 408, 11 | ECONNABORTED: 'ECONNABORTED' 12 | }; 13 | 14 | axios.interceptors.response.use( 15 | response => response, 16 | error => { 17 | if (error?.response?.data?.error?.code === codes.CUSTOM_TOKEN_EXPIRED) { 18 | console.log('token expired'); 19 | } 20 | 21 | if ( 22 | error?.response?.status === codes.REQUEST_TIMEOUT || 23 | error.code === codes.ECONNABORTED 24 | ) { 25 | // Looks like the server is taking to long to respond, please try again in sometime. 26 | console.log(`A timeout happend on url ${error.config.url}`); 27 | // errorToast({ content: 'Server request timed out. Please retry again.' }) 28 | } 29 | 30 | if (!error?.response?.data?.error) { 31 | console.log('Server error not found'); 32 | // Add something went wrong toast error here 33 | // statusText in toast maybe. 34 | } 35 | return Promise.reject(error); 36 | } 37 | ); 38 | 39 | const getFullUrl = (url: string) => `${API_URL}${url}`; 40 | 41 | export type Irequest = { 42 | subUrl: string; 43 | method?: Method; 44 | data?: object; 45 | params?: object; 46 | headers?: object; 47 | }; 48 | 49 | export const GET = (request: Irequest) => commonFetch({ method: 'get', ...request }); 50 | 51 | export const POST = (request: Irequest) => commonFetch({ method: 'post', ...request }); 52 | 53 | export const PUT = (request: Irequest) => commonFetch({ method: 'put', ...request }); 54 | 55 | export const PATCH = (request: Irequest) => commonFetch({ method: 'patch', ...request }); 56 | 57 | export const DELETE = (request: Irequest) => commonFetch({ method: 'delete', ...request }); 58 | 59 | const commonFetch = (request: Irequest) => { 60 | const { subUrl, method, data = {}, params, headers = {} } = request; 61 | const url = getFullUrl(subUrl); 62 | const commonHeaders = getCommonHeaders(); 63 | 64 | return axios({ 65 | method, 66 | url, 67 | params, 68 | data, 69 | headers: { ...commonHeaders, ...headers } 70 | }); 71 | }; 72 | 73 | export const contentTypes = { 74 | multipart: { 75 | 'Content-Type': 'multipart/form-data' 76 | }, 77 | json: { 78 | 'Content-Type': 'application/json' 79 | } 80 | }; 81 | 82 | const getCommonHeaders = () => { 83 | const authHeader : any = {}; 84 | const token = Storage.getItem('token'); 85 | 86 | if(token){ 87 | authHeader['Authorization'] = `${token}`; 88 | } 89 | 90 | return { 91 | ...contentTypes.json, 92 | ...authHeader 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /frontend/src/services/Storage.ts: -------------------------------------------------------------------------------- 1 | class StorageService { 2 | public setItem = (key: string, value: any): void => { 3 | localStorage.setItem(key, JSON.stringify(value)); 4 | }; 5 | 6 | public getItem = (key: string): T | null => { 7 | const data: string | null = localStorage.getItem(key); 8 | 9 | if (data !== null) { 10 | return JSON.parse(data); 11 | } 12 | 13 | return null; 14 | }; 15 | 16 | public removeItem = (key: string): void => { 17 | localStorage.removeItem(key); 18 | }; 19 | 20 | public clearAll = (): void => { 21 | localStorage.clear(); 22 | }; 23 | } 24 | 25 | export default new StorageService(); 26 | -------------------------------------------------------------------------------- /frontend/src/styles/global.style.tsx: -------------------------------------------------------------------------------- 1 | import { Global } from '@mantine/core'; 2 | 3 | const GlobalStyle = () => ( 4 | ({ 6 | a: { 7 | color: 'inherit', 8 | textDecoration: 'none', 9 | }, 10 | })} 11 | /> 12 | ); 13 | export default GlobalStyle; -------------------------------------------------------------------------------- /frontend/src/utils/DeviceDetect.ts: -------------------------------------------------------------------------------- 1 | export const isSafari = (): boolean => /^((?!chrome|android).)*safari/i.test(navigator.userAgent); -------------------------------------------------------------------------------- /frontend/src/utils/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { showNotification } from '@mantine/notifications'; 2 | import { TbCheck, TbX } from 'react-icons/tb'; 3 | 4 | const error = (message: string) => 5 | showNotification({ 6 | icon: , 7 | color: 'red', 8 | radius: 'md', 9 | title: 'Error', 10 | message, 11 | }); 12 | 13 | const axiosError = (axiosError: any) => 14 | error(axiosError?.response?.data?.message ?? 'An unknown error occured'); 15 | 16 | const success = (message: string) => 17 | showNotification({ 18 | icon: , 19 | color: 'green', 20 | radius: 'md', 21 | title: 'Success', 22 | message, 23 | }); 24 | 25 | const toast = { 26 | error, 27 | success, 28 | axiosError, 29 | }; 30 | export default toast; -------------------------------------------------------------------------------- /frontend/src/utils/commonFunction.ts: -------------------------------------------------------------------------------- 1 | export const getApiErrorMessage = (error: any | {[key: string]: string}, key?: string) : string => error?.response?.data?.message?.errors?.[0]?.[key as string] || error?.response?.data?.message; 2 | 3 | export const byteStringToHumanSizeString = (bytes: string): string => { 4 | const bytesNumber = parseInt(bytes); 5 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; 6 | if (bytesNumber === 0) {return '0 Byte';} 7 | const i = parseInt( 8 | Math.floor(Math.log(bytesNumber) / Math.log(1024)).toString() 9 | ); 10 | return ( 11 | `${(bytesNumber / 1024**i).toFixed(1).toString() } ${ sizes[i]}` 12 | ); 13 | }; 14 | 15 | export function isValidURL(url: string): boolean { 16 | const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol 17 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name 18 | '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address 19 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path 20 | '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string 21 | '(\\#[-a-z\\d_]*)?$','i'); 22 | 23 | return !!urlPattern.test(url); 24 | } -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // declare module 'react-current-page-fallback'; 3 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | // "rootDir": "src", 6 | "baseUrl": "./", 7 | "paths": { 8 | "src/*": [ "./src/*" ], 9 | }, 10 | // "paths": { 11 | // "react": ["node_modules/@types/react"], 12 | // "react-dom": ["node_modules/@types/react-dom"], 13 | // }, 14 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 15 | "allowJs": false, 16 | "skipLibCheck": true, 17 | "esModuleInterop": false, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "module": "ESNext", 22 | "moduleResolution": "Node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "noEmit": true, 26 | "jsx": "react-jsx", 27 | "typeRoots": ["node_modules/yup"] 28 | }, 29 | "include": ["src"], 30 | "exclude": ["node_modules", "src/**/*.spec.ts"], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "typeRoots": ["node_modules/yup"] 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import * as path from 'path' 5 | 6 | // import react from '@vitejs/plugin-react' 7 | // import commonjs from "@originjs/vite-plugin-commonjs"; 8 | // import commonjs from "@rollup/plugin-commonjs"; 9 | // import resolve from "@rollup/plugin-node-resolve"; 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | // build: { 14 | // minify: true, 15 | // sourcemap: true, 16 | // rollupOptions: { 17 | // external: ["react", "react-dom"], 18 | // output: { 19 | // globals: { 20 | // react: "React", 21 | // "react-dom": "ReactDOM", 22 | // }, 23 | // }, 24 | // }, 25 | // }, 26 | // optimizeDeps: { 27 | // include: ["react", "react-dom/client"], 28 | // }, 29 | resolve: { 30 | alias: [ 31 | { find: 'src', replacement: path.resolve(__dirname, 'src') }, 32 | ], 33 | }, 34 | 35 | plugins: [ 36 | // resolve({ 37 | // preferBuiltins: true, 38 | // browser: true, 39 | // }), 40 | // commonjs({ 41 | // include: /node_modules/, 42 | // requireReturnsDefault: "auto", // <---- this solves default issue 43 | // }), 44 | react(), 45 | tsconfigPaths() 46 | ], 47 | }); 48 | --------------------------------------------------------------------------------