├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assetsTransformer.ts ├── babel.config.js ├── build ├── bundle.js ├── bundle.js.LICENSE.txt ├── favicon.ico ├── index.html └── src │ └── assets │ └── images │ ├── rabbitpaw.jpg │ └── rabbitphoto.jpg ├── index.html ├── package-lock.json ├── package.json ├── rabbitmq └── consume.ts ├── server ├── __tests__ │ └── server.test.ts ├── controllers │ ├── authController.ts │ ├── messageController.ts │ └── userController.ts ├── models │ └── elephantsql.ts ├── routes │ ├── authRouter.ts │ ├── messageRouter.ts │ └── userRouter.ts └── server.ts ├── src ├── App.tsx ├── Components │ ├── AddProjectModal.tsx │ ├── DeadLetterMessage.tsx │ ├── ErrorPage │ │ └── ErrorPageMessage.tsx │ ├── Login.tsx │ ├── NavBar │ │ ├── NavAfterLoggedIn.tsx │ │ ├── NavLoginPage.tsx │ │ ├── NavMessagesPage.tsx │ │ └── NavSignupPage.tsx │ ├── Signup.tsx │ └── UserProjects.tsx ├── Containers │ ├── ErrorPageContainer.tsx │ ├── MessageContainer.tsx │ └── UserProjectsContainer.tsx ├── assets │ ├── images │ │ ├── RT_logo_wide.png │ │ ├── RabbitTracks-DB-Schema.png │ │ ├── favicon.ico │ │ ├── rabbitpaw.jpg │ │ └── rabbitphoto.jpg │ └── stylesheets │ │ └── global.scss ├── declaration.d.ts └── index.tsx ├── tsconfig.json ├── types.ts └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # RabbitTracks 4 | 5 | RabbitTracks is a full-stack application that developers use to monitor and debug failed messages in RabbitMQ. 6 | 7 | RabbitTracks is an open source devTool, accelerated under OSLabs. Please read our Medium article [here](https://medium.com/@jerikkoagatep/rabbittracks-tracking-your-rabbitmq-dead-letter-messages-ccee4cac65fa) for more background information. 8 | 9 | ## What You’ll Need to Get Started 10 | 11 | 1. A local Integrated Development Environment (ie - VSCode, NetBeans, Eclipse, etc.) 12 | 2. A working RabbitMQ publisher/consumer instance (more information on how to set this up can be found on their website - https://www.rabbitmq.com/) 13 | 3. A PostgresQL Database Hosting Service account (ie - ElephantSQL, ScaleGrid, Aiven, etc… it is free!) 14 | 15 | ## Database Set-Up Instructions 16 | 17 | Before installing the RabbitTracks application, it is vital you set up your Database. 18 | 19 | 1. Same as step 3 in the “What You’ll Need to Get Started” section. Set up a PostgresQL Database Hosting Service account (ie - ElephantSQL, ScaleGrid, Aiven, etc… it is free!) 20 | 2. Create Tables in SQL. Below is an image of the entity relationship diagrams that you’ll need for RabbitTracks. Also, below is code that you need to execute in your SQL’s query in order to replicate the diagram’s schema: 21 | 22 | 23 | 24 | A. Creating ‘users’ table for Authentication and security purposes: 25 | 26 | ```SQL 27 | CREATE TABLE users ( 28 | user_id integer SERIAL PRIMARY KEY, 29 | user_email text UNIQUE NOT NULL, 30 | user_password text NOT NULL, 31 | first_name text NOT NULL, 32 | last_name text NOT NULL, 33 | session_key text, 34 | session_start_time timestamp, 35 | created_at timestamp DEFAULT now() NOT NULL 36 | ); 37 | ``` 38 | 39 | B. Creating ‘projects’ table to help connect users' projects with messages: 40 | 41 | ```SQL 42 | CREATE TABLE projects ( 43 | project_id integer SERIAL PRIMARY KEY, 44 | project_url text NOT NULL, 45 | created_at timestamp DEFAULT now() NOT NULL 46 | ); 47 | ``` 48 | 49 | C. Creating ‘users_projects’ table for users to keep their projects organized: 50 | 51 | ```SQL 52 | CREATE TABLE users_projects ( 53 | user_project_id integer SERIAL PRIMARY KEY, 54 | user_id integer REFERENCES users(user_id), 55 | project_id integer REFERENCES projects(project_id), 56 | project_name text, 57 | created_at timestamp DEFAULT now() NOT NULL 58 | ); 59 | ``` 60 | 61 | D. Creating ‘messages’ table to obtain essential message data that RabbitMQ offers: 62 | 63 | ```SQL 64 | CREATE TABLE messages ( 65 | message_id integer SERIAL PRIMARY KEY, 66 | project_id integer REFERENCES projects(project_id), 67 | consumertag text, 68 | deliverytag integer, 69 | redelivered boolean, 70 | exchange text, 71 | routingkey text, 72 | contenttype text, 73 | contentencoding text, 74 | correlationid text, 75 | replyto text, 76 | expiration text, 77 | messageid text, 78 | timestamp bigint, 79 | type text, 80 | userid text, 81 | appid text, 82 | clusterid text, 83 | first_death_reason text, 84 | first_death_queue text, 85 | first_death_exchange text, 86 | deliverymode text, 87 | priority integer, 88 | created_at timestamp DEFAULT now() NOT NULL 89 | ); 90 | ``` 91 | 92 | ## Add code to your RabbitMQ instance 93 | 94 | A. IF YOU **DO** HAVE A DEAD LETTER EXCHANGE ALREADY SET UP: 95 | 96 | 1. Make sure that your existing Dead Letter Exchange type is “fanout” and keep the exchange name handy. 97 | 2. Skip ahead to the [Installation](#Installation) section below! \*NOTE: MAKE SURE YOU DON'T MISS INSTALLATION STEP 4! 98 | 99 | B. IF YOU **DO NOT** HAVE A DEAD LETTER EXCHANGE SET UP: 100 | 101 | 1. In your RabbitMQ instance, add the following **exact** lines of code within your channel before any messages are sent or consumed: 102 | 103 | ```Javascript 104 | const DLExchange = 'RabbitTracks-DLExchange' 105 | channel.assertExchange(DLExchange, 'fanout'); 106 | ``` 107 | 108 | 2. Add the RabbitTracks Dead Letter Exchange to your queue’s arguments, or configure it in your server as a policy. More information about establishing the Dead Letter Exchange via either of these methods is available in the [RabbitMQ documentation.](https://www.rabbitmq.com/dlx.html) 109 | 110 | Here is a simple example of how to add the exchange to your queue’s arguments: 111 | 112 | ```Javascript 113 | channel.assertQueue( queueName… , { 114 | additionalArgs… , 115 | deadLetterExchange: DLExchange 116 | }) 117 | ``` 118 | 119 | 120 | 121 | ## Installation 122 | 123 | 1. Fork and clone this repository! (step-by-step instructions found [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo) if you need) 124 | 125 | 2. Next, use the following command to install any new npm dependencies: 126 | 127 | ```bash 128 | npm install --legacy-peer-deps 129 | ``` 130 | 131 | \*Please note that you must add the flag “--legacy-peer-deps” in order to bypass potential React dependency version issues. 132 | 133 | 3. Create a “.env” file in your root directory that consists of the following PRECISE key-value pairs: 134 | 135 | - **PORT** = “\_\_\_” 136 | - Within quotes, please indicate IN QUOTES which port number you would like your server to run on. ie - “3000”. This will be used in server/server.ts 137 | - **JWT_SECRET** = “\_\_\_” 138 | - Within quotes, please insert IN QUOTES any string, buffer, or object containing either the secret for HMAC algorithms or the PEM encoded private key for RSA and ECDSA. Where this will be used in the application can be found in the /server/controllers/authController.ts file. 139 | - **SALT_WORK_FACTOR** = \_\_\_ 140 | - Please indicate WITHOUT QUOTES the amount of times you would like your password to be hashed (ie 12). This will be used in the /server/controllers/authController.ts file. 141 | - **JWT_EXPIRES_IN** = "\_\_\_" 142 | - Expressed in seconds or a string describing a time span vercel/ms (ie: 60, "2 days", "10h", "7d"). This can also be found in the /server/controllers/authController.ts file. 143 | - **SQL_URI** = “\_\_\_” 144 | - Within quotes, please indicate IN QUOTES the URI to your PostgresQL Database Instance on the Hosting Service you set up earlier. 145 | 146 | 4. ONLY if you are using an **EXISTING** Dead Letter Exchange, go to the rabbitmq/consume.ts file and **replace** “RabbitTracks-DLExchange” with your exchange name in the following line of code: 147 | 148 | ```javascript 149 | const DLExchange: string = "RabbitTracks-DLExchange"; 150 | ``` 151 | 152 | 5. Run build 153 | 154 | ```bash 155 | npm run build 156 | ``` 157 | 158 | 6. Start your application by running 159 | 160 | ```bash 161 | npm run start 162 | ``` 163 | 164 | 7. You are all done! Track your failed messages and enjoy using our application! :) 165 | 166 | ## Available Scripts 167 | 168 | In the project directory, you can also run: 169 | 170 | ### `npm run dev` 171 | 172 | Runs the app in the development mode.\ 173 | Your browser will open RabbitTracks on [http://localhost:8080](http://localhost:8080). 174 | 175 | The page will reload and update when you make changes in the codebase.\ 176 | You may also see lint errors in the console. 177 | 178 | ### `npm test` 179 | 180 | Runs all written frontend and backend tests... feel free to add your own as well! 181 | 182 | ### `npm run build` 183 | 184 | Builds the app for production to the `build` folder.\ 185 | It correctly bundles React in production mode and optimizes the build for the best performance. 186 | 187 | ## Authors 188 | 189 | Jerikko Agatep - [Github](https://github.com/jerikko) | [LinkedIn](https://www.linkedin.com/in/jerikko-agatep/)\ 190 | Evelyn Brown - [Github](https://github.com/elrjolliffe) | [LinkedIn](https://www.linkedin.com/in/elrjolliffe/)\ 191 | Glen Kasoff - [Github](https://github.com/gkasoff) | [LinkedIn](https://www.linkedin.com/in/glen-kasoff/)\ 192 | Jake Kazi - [Github](https://github.com/jakekazi) | [LinkedIn](https://www.linkedin.com/in/jakekazi/) 193 | 194 | ## Contributions Welcome! 195 | 196 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. We welcome and appreciate your contributions to this product! 197 | 198 | Here are some ways you can contribute: 199 | 200 | - Submitting any bugs or feature requests as [Github Issues](https://github.com/oslabs-beta/Rabbit-Tracks/issues) 201 | - Adding new features and fixing issues by opening Pull Requests: 202 | - Fork the Project 203 | - Create your Feature Branch (git checkout -b feature/FeatureName) 204 | - Commit your Changes (git commit -m 'Completed new FeatureName') 205 | - Push to the Branch (git push origin feature/FeatureName) 206 | - Open a Pull Request into the **dev branch** 207 | - Improving documentation 208 | 209 | ## License 210 | 211 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/oslabs-beta/Rabbit-Tracks/blob/main/LICENSE) file for details 212 | -------------------------------------------------------------------------------- /assetsTransformer.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | process(src, filename, config, options) { 5 | return "module.exports = " + JSON.stringify(path.basename(filename)) + ";"; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /build/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2015 Jed Watson. 3 | Based on code that is Copyright 2013-2015, Facebook, Inc. 4 | All rights reserved. 5 | */ 6 | 7 | /*! 8 | * Adapted from jQuery UI core 9 | * 10 | * http://jqueryui.com 11 | * 12 | * Copyright 2014 jQuery Foundation and other contributors 13 | * Released under the MIT license. 14 | * http://jquery.org/license 15 | * 16 | * http://api.jqueryui.com/category/ui-core/ 17 | */ 18 | 19 | /*! 20 | * The buffer module from node.js, for the browser. 21 | * 22 | * @author Feross Aboukhadijeh 23 | * @license MIT 24 | */ 25 | 26 | /*! 27 | * The buffer module from node.js, for the browser. 28 | * 29 | * @author Feross Aboukhadijeh 30 | * @license MIT 31 | */ 32 | 33 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 34 | 35 | /** 36 | * @license React 37 | * react-dom.production.min.js 38 | * 39 | * Copyright (c) Facebook, Inc. and its affiliates. 40 | * 41 | * This source code is licensed under the MIT license found in the 42 | * LICENSE file in the root directory of this source tree. 43 | */ 44 | 45 | /** 46 | * @license React 47 | * react-is.production.min.js 48 | * 49 | * Copyright (c) Facebook, Inc. and its affiliates. 50 | * 51 | * This source code is licensed under the MIT license found in the 52 | * LICENSE file in the root directory of this source tree. 53 | */ 54 | 55 | /** 56 | * @license React 57 | * react-jsx-runtime.production.min.js 58 | * 59 | * Copyright (c) Facebook, Inc. and its affiliates. 60 | * 61 | * This source code is licensed under the MIT license found in the 62 | * LICENSE file in the root directory of this source tree. 63 | */ 64 | 65 | /** 66 | * @license React 67 | * react.production.min.js 68 | * 69 | * Copyright (c) Facebook, Inc. and its affiliates. 70 | * 71 | * This source code is licensed under the MIT license found in the 72 | * LICENSE file in the root directory of this source tree. 73 | */ 74 | 75 | /** 76 | * @license React 77 | * scheduler.production.min.js 78 | * 79 | * Copyright (c) Facebook, Inc. and its affiliates. 80 | * 81 | * This source code is licensed under the MIT license found in the 82 | * LICENSE file in the root directory of this source tree. 83 | */ 84 | 85 | /** 86 | * @remix-run/router v1.0.1 87 | * 88 | * Copyright (c) Remix Software Inc. 89 | * 90 | * This source code is licensed under the MIT license found in the 91 | * LICENSE.md file in the root directory of this source tree. 92 | * 93 | * @license MIT 94 | */ 95 | 96 | /** 97 | * React Router DOM v6.4.1 98 | * 99 | * Copyright (c) Remix Software Inc. 100 | * 101 | * This source code is licensed under the MIT license found in the 102 | * LICENSE.md file in the root directory of this source tree. 103 | * 104 | * @license MIT 105 | */ 106 | 107 | /** 108 | * React Router v6.4.1 109 | * 110 | * Copyright (c) Remix Software Inc. 111 | * 112 | * This source code is licensed under the MIT license found in the 113 | * LICENSE.md file in the root directory of this source tree. 114 | * 115 | * @license MIT 116 | */ 117 | 118 | /** @license MUI v5.10.3 119 | * 120 | * This source code is licensed under the MIT license found in the 121 | * LICENSE file in the root directory of this source tree. 122 | */ 123 | 124 | /** @license MUI v5.17.0 125 | * 126 | * This source code is licensed under the MIT license found in the 127 | * LICENSE file in the root directory of this source tree. 128 | */ 129 | 130 | /** @license React v16.13.1 131 | * react-is.production.min.js 132 | * 133 | * Copyright (c) Facebook, Inc. and its affiliates. 134 | * 135 | * This source code is licensed under the MIT license found in the 136 | * LICENSE file in the root directory of this source tree. 137 | */ 138 | -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/build/favicon.ico -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | Rabbit Tracks
-------------------------------------------------------------------------------- /build/src/assets/images/rabbitpaw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/build/src/assets/images/rabbitpaw.jpg -------------------------------------------------------------------------------- /build/src/assets/images/rabbitphoto.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/build/src/assets/images/rabbitphoto.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 17 | Rabbit Tracks 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rabbittracks", 3 | "version": "1.0.0", 4 | "description": "UI for visualizing RabbitMQ Dead-Letter messages", 5 | "main": "src/index.js", 6 | "types": "src/index.tsx", 7 | "scripts": { 8 | "test": "jest", 9 | "start": "cross-env NODE_ENV=production ts-node server/server.ts", 10 | "build": "webpack", 11 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve --open\" \"cross-env NODE_ENV=development nodemon server/server.ts\"" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/oslabs-beta/RabbitTracks.git" 16 | }, 17 | "author": "cat-snake7", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/RabbitTracks/issues" 21 | }, 22 | "homepage": "https://github.com/oslabs-beta/RabbitTracks#readme", 23 | "dependencies": { 24 | "@mui/icons-material": "^5.10.6", 25 | "amqplib": "^0.10.2", 26 | "axios": "^0.27.2", 27 | "babel-loader": "^8.2.5", 28 | "bcryptjs": "^2.4.3", 29 | "concurrently": "^7.3.0", 30 | "cookie-parser": "^1.4.6", 31 | "cors": "^2.8.5", 32 | "css-loader": "^6.7.1", 33 | "dotenv": "^16.0.1", 34 | "express": "^4.18.1", 35 | "html-webpack-plugin": "^5.5.0", 36 | "jsonwebtoken": "^9.0.0", 37 | "node-polyfill-webpack-plugin": "^2.0.1", 38 | "pg": "^8.7.3", 39 | "pg-hstore": "^2.3.4", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "react-modal": "^3.15.1", 43 | "react-router": "^6.3.0", 44 | "react-router-dom": "^6.4.1", 45 | "sass": "^1.54.4", 46 | "sass-loader": "^13.0.2", 47 | "sequelize": "^6.21.4", 48 | "socket.io": "^4.5.2", 49 | "socket.io-client": "^4.5.2", 50 | "sqlite3": "^5.0.11", 51 | "style-loader": "^3.3.1", 52 | "ts-loader": "^9.4.1", 53 | "ts-node": "^10.9.1", 54 | "ws": "^8.9.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.19.3", 58 | "@babel/preset-env": "^7.19.4", 59 | "@babel/preset-react": "^7.18.6", 60 | "@babel/preset-typescript": "^7.18.6", 61 | "@emotion/react": "^11.10.4", 62 | "@emotion/styled": "^11.10.4", 63 | "@mui/material": "^5.10.3", 64 | "@mui/types": "^7.2.0", 65 | "@mui/x-data-grid": "^5.17.0", 66 | "@testing-library/react": "^13.4.0", 67 | "@types/amqplib": "^0.8.2", 68 | "@types/bcryptjs": "^2.4.2", 69 | "@types/express": "^4.17.14", 70 | "@types/jest": "^28.1.7", 71 | "@types/node": "^18.7.18", 72 | "@types/node-sass": "^4.11.3", 73 | "@types/react": "^18.0.20", 74 | "@types/react-dom": "^18.0.6", 75 | "@types/react-modal": "^3.13.1", 76 | "@types/react-router-dom": "^5.3.3", 77 | "babel-jest": "^29.1.2", 78 | "cross-env": "^7.0.3", 79 | "eslint": "^8.22.0", 80 | "file-loader": "^6.2.0", 81 | "html-webpack-plugin": "^5.5.0", 82 | "jest": "^28.1.3", 83 | "nodemon": "^2.0.19", 84 | "prettier": "^2.7.1", 85 | "supertest": "^6.3.1", 86 | "ts-jest": "^28.0.8", 87 | "typescript": "^4.8.3", 88 | "webpack": "^5.74.0", 89 | "webpack-cli": "^4.10.0", 90 | "webpack-dev-server": "^4.10.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rabbitmq/consume.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | import axios from "axios"; 4 | import amqp, { 5 | Connection, 6 | Channel, 7 | Message, 8 | MessageFields, 9 | MessageProperties, 10 | } from "amqplib/callback_api"; 11 | import { CreateDLXMessage, Properties, Fields } from "../types"; 12 | import { Socket } from "dgram"; 13 | 14 | export const runConsume = (URL: string, projectID: number) => { 15 | // Establish client-side socket connection 16 | const io = require("socket.io-client"); 17 | const socket = io.connect("http://localhost:3000/messages", { 18 | reconnection: true, 19 | }); 20 | // Set up event handler for the "connect" event emitted by the socket 21 | socket.on("connect", function (socket: Socket) { 22 | console.log(`Socket connection established in consume file`); 23 | }); 24 | 25 | // Establish connection with user's RabbitMQ instance 26 | amqp.connect(URL, function (error0: Error, connection: Connection) { 27 | if (error0) { 28 | throw error0; 29 | } 30 | // Creates a new channel within the established connection to RabbitMQ 31 | connection.createChannel(function (error1: Error, channel: Channel) { 32 | if (error1) { 33 | throw error1; 34 | } 35 | 36 | // If hooking into pre-existing RabbitMQ Dead Letter Exchange, update DLExchange to match existing exchange name 37 | const DLExchange: string = "RabbitTracks-DLExchange"; 38 | // If multiple users are hooking into the same RabbitMQ instance, but using separate databases, change DLQueue name to be unique for each user 39 | const DLQueue: string = "RabbitTracks-DLQueue"; 40 | // Assert the exchange and queue to establish common understanding and configuration between message producers and consumers 41 | channel.assertExchange(DLExchange, "fanout"); 42 | channel.assertQueue(DLQueue, { durable: true }); 43 | channel.bindQueue(DLQueue, DLExchange, ""); 44 | 45 | console.log( 46 | " [*] Waiting for messages in %s. To exit press CTRL+C", 47 | DLQueue 48 | ); 49 | // Set up message consumer for 'DLQueue' in RabbitMQ channel 50 | channel.consume(DLQueue, async function (msg: Message | null) { 51 | // Unpack and extract relevant information from consumed message 52 | const { 53 | content, 54 | fields, 55 | properties, 56 | }: { 57 | content?: Buffer; 58 | fields?: MessageFields; 59 | properties?: MessageProperties; 60 | } = { ...msg }; 61 | 62 | const { 63 | consumerTag, 64 | deliveryTag, 65 | redelivered, 66 | exchange, 67 | routingKey, 68 | }: Fields = { ...fields }; 69 | 70 | const { 71 | contentType, 72 | contentEncoding, 73 | headers, 74 | deliveryMode, 75 | priority, 76 | correlationId, 77 | replyTo, 78 | expiration, 79 | messageId, 80 | timestamp, 81 | type, 82 | userId, 83 | appId, 84 | clusterId, 85 | }: Properties = { ...properties }; 86 | 87 | // Process the consumed message 88 | if (content) console.log(" [x] Received %s", content.toString()); 89 | // Utilizing projectID passed into function runConsume to send to server in post method 90 | const projectId: number = projectID; 91 | // Add consumed messages to database 92 | await axios 93 | .post( 94 | "http://localhost:8080/messages/add-message", 95 | { 96 | consumerTag, 97 | deliveryTag, 98 | redelivered, 99 | exchange, 100 | routingKey, 101 | contentType, 102 | contentEncoding, 103 | deliveryMode, 104 | priority, 105 | correlationId, 106 | replyTo, 107 | expiration, 108 | messageId, 109 | timestamp, 110 | type, 111 | userId, 112 | appId, 113 | clusterId, 114 | headers, 115 | projectId, 116 | }, 117 | { 118 | headers: { 119 | "Content-Type": "application/json", 120 | }, 121 | } 122 | ) 123 | .then((data) => { 124 | if (msg) channel.ack(msg); 125 | 126 | // Send notification to server-side socket to notify MessageContainer to grab and render newly stored messages 127 | socket.emit("message-added", () => 128 | console.log(`'message-added' event emitted by consumer`) 129 | ); 130 | }) 131 | .catch((err: Error) => { 132 | console.log("Axios error when attempting to add message... ", err); 133 | }); 134 | }); 135 | }); 136 | }); 137 | }; 138 | -------------------------------------------------------------------------------- /server/__tests__/server.test.ts: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | 3 | // this is http://localhost:3000 <<< precisely this 4 | const server = require("../server"); 5 | 6 | describe("POST /auth/login", () => { 7 | describe("when the password is missing", () => { 8 | // respond with status code 400 because user error 9 | test("should return a 400 status code", async () => { 10 | const response = await request(server).post("/auth/login").send({ 11 | email: "test1@test.com", 12 | }); 13 | expect(response.statusCode).toBe(400); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /server/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { QueryTypes } = require("sequelize"); 3 | const db = require("../models/elephantsql"); 4 | const bcrypt = require("bcryptjs"); 5 | const jwt = require("jsonwebtoken"); 6 | const secret = process.env.JWT_SECRET; 7 | const expiresIn = process.env.JWT_EXPIRES_IN; 8 | const jwtSecret = process.env.JWT_SECRET; 9 | const saltFactor = parseInt(process.env.SALT_WORK_FACTOR); 10 | 11 | // Import types from express library 12 | import { Request, Response, NextFunction } from "express"; 13 | 14 | // Import types from types.ts file 15 | import { 16 | AuthController, 17 | AuthParams, 18 | AuthResults, 19 | AuthRequestBody, 20 | } from "./../../types"; 21 | 22 | const authController: AuthController = {}; 23 | 24 | // The encryptPassword method uses the bcrypt library to hash the inputted user password into an encrypted password 25 | authController.encryptPassword = async ( 26 | req: Request, 27 | res: Response, 28 | next: NextFunction 29 | ): Promise => { 30 | const { password, passwordConfirm }: AuthRequestBody = req.body; 31 | if (password && passwordConfirm && password === passwordConfirm) { 32 | try { 33 | const encryptedPassword: string = await bcrypt.hash(password, saltFactor); 34 | res.locals.encryptedPassword = encryptedPassword; 35 | return next(); 36 | } catch (err) { 37 | return next({ 38 | log: `Error in authController.encryptPassword... Password hashing error: ${JSON.stringify( 39 | err 40 | )}`, 41 | status: 500, 42 | message: "Unable to encrypt password.", 43 | }); 44 | } 45 | } else { 46 | return next({ 47 | log: "Error in authController.encryptPassword... Passwords do not match.", 48 | status: 400, 49 | message: "Passwords do not match.", 50 | }); 51 | } 52 | }; 53 | 54 | // The signup method takes the user input and inserts the data into the SQL database, returning the user_id 55 | authController.signup = async ( 56 | req: Request, 57 | res: Response, 58 | next: NextFunction 59 | ): Promise => { 60 | const { firstName, lastName, email }: AuthRequestBody = req.body; 61 | 62 | const params: AuthParams = [ 63 | firstName, 64 | lastName, 65 | email, 66 | res.locals.encryptedPassword, 67 | ]; 68 | 69 | const queryString: string = `INSERT INTO users (first_name, last_name, user_email, user_password) VALUES ($1, $2, $3, $4) RETURNING user_id;`; 70 | 71 | if (email) { 72 | try { 73 | const results: Array = await db.query(queryString, { 74 | bind: [...params], 75 | type: QueryTypes.INSERT, 76 | }); 77 | 78 | res.locals.user_id = results[0][0].user_id; 79 | 80 | return next(); 81 | } catch (err) { 82 | return next({ 83 | log: `Error in authController.signup... Error when attempting signup: ${JSON.stringify( 84 | err 85 | )}`, 86 | status: 500, 87 | message: "Unable to complete signup process.", 88 | }); 89 | } 90 | } else { 91 | return next({ 92 | log: "Error in authController.signup... No email inputted.", 93 | status: 400, 94 | message: "No email inputted.", 95 | }); 96 | } 97 | }; 98 | 99 | // The verifyUser method takes the user input and queries the database. 100 | // If query is successful, user_password and user_id is saved in res.locals as encryptedPassword and user_id, respectively 101 | authController.verifyUser = async ( 102 | req: Request, 103 | res: Response, 104 | next: NextFunction 105 | ): Promise => { 106 | const { email, password }: AuthRequestBody = req.body; 107 | 108 | if (email && password) { 109 | const queryString: string = 110 | "SELECT user_id, user_password FROM users WHERE user_email=$1"; 111 | const params: AuthParams = [email]; 112 | try { 113 | const results: AuthResults = await db.query(queryString, { 114 | bind: [...params], 115 | type: QueryTypes.SELECT, 116 | }); 117 | 118 | if (results.length > 0) { 119 | res.locals.encryptedPassword = results[0].user_password; 120 | res.locals.user_id = results[0].user_id; 121 | return next(); 122 | } else { 123 | return next({ 124 | log: "Error in authController.verifyUser... User does not exist in database.", 125 | status: 400, 126 | message: "User does not exist in database.", 127 | }); 128 | } 129 | } catch (err) { 130 | return next({ 131 | log: `Error in authController.verifyUser: ${JSON.stringify(err)}`, 132 | status: 500, 133 | message: "Error while querying user in database.", 134 | }); 135 | } 136 | } else { 137 | return next({ 138 | log: "Error in authController.verifyUser... Missing email and/or password.", 139 | status: 400, 140 | message: "Missing email and/or password.", 141 | }); 142 | } 143 | }; 144 | 145 | // The verifyPassword method takes the user input and res.locals.encryptedPassword and uses the bcrypt library to compare the user inputted password with the saved 146 | // encryptedPassword 147 | authController.verifyPassword = async ( 148 | req: Request, 149 | res: Response, 150 | next: NextFunction 151 | ): Promise => { 152 | const { password }: AuthRequestBody = req.body; 153 | const encryptedPassword: string = res.locals.encryptedPassword; 154 | 155 | if (password && encryptedPassword) { 156 | try { 157 | const passwordVerified: boolean = await bcrypt.compare( 158 | password, 159 | encryptedPassword 160 | ); 161 | if (passwordVerified) { 162 | return next(); 163 | } else { 164 | return next({ 165 | log: "Error in authController.verifyPassword... Password not verified.", 166 | status: 400, 167 | message: "Password not verified.", 168 | }); 169 | } 170 | } catch (err) { 171 | return next({ 172 | log: `Error in authController.verifyPassword... Error while verifying password: ${JSON.stringify( 173 | err 174 | )}`, 175 | status: 500, 176 | message: "Error while verifying password.", 177 | }); 178 | } 179 | } else { 180 | return next({ 181 | log: "Error in authController.verifyPassword... Missing password and/or encrypted password.", 182 | status: 400, 183 | message: "Missing password and/or encrypted password.", 184 | }); 185 | } 186 | }; 187 | 188 | // The createSession method takes res.locals.user_id and creates a JWT token. The JWT token and user_id are then used to update the database with the new JWT token. 189 | // The cookie session id is then updated with the new JWT token. 190 | authController.createSession = async ( 191 | req: Request, 192 | res: Response, 193 | next: NextFunction 194 | ): Promise => { 195 | const user_id: number = res.locals.user_id; 196 | const queryString: string = 197 | "UPDATE users SET session_key=$1 WHERE user_id=$2"; 198 | 199 | try { 200 | const token: string = await jwt.sign({ user_id: user_id }, secret, { 201 | expiresIn: expiresIn, 202 | }); 203 | 204 | const params: AuthParams = [token, user_id]; 205 | 206 | if (token && user_id) { 207 | await db.query(queryString, { 208 | bind: [...params], 209 | type: QueryTypes.UPDATE, 210 | }); 211 | res.cookie("session_id", token, { httpOnly: true }); 212 | return next(); 213 | } else { 214 | return next({ 215 | log: "Error in authController.createSession... Missing token or user_id.", 216 | status: 500, 217 | message: "Missing token or user_id.", 218 | }); 219 | } 220 | } catch (err) { 221 | return next({ 222 | log: `Error in authController.createSession... Error when attempting to create session_id: ${JSON.stringify( 223 | err 224 | )}`, 225 | status: 500, 226 | message: "Unable to create session_id.", 227 | }); 228 | } 229 | }; 230 | 231 | // The verifySession method takes current session_id from the cookies and decodes it using the JWT library. The decoded user_id is then used to query the database. 232 | // If the query is successful, user_id is saved in res.locals. Any errors along the way result in the current session_id being removed. 233 | authController.verifySession = async ( 234 | req: Request, 235 | res: Response, 236 | next: NextFunction 237 | ): Promise => { 238 | const session_id: string = req.cookies.session_id; 239 | let user_id: number; 240 | 241 | if (session_id) { 242 | try { 243 | const decodedToken: { user_id: number } = await jwt.verify( 244 | session_id, 245 | jwtSecret 246 | ); 247 | if (decodedToken) { 248 | user_id = decodedToken.user_id; 249 | } else { 250 | res.clearCookie("session_id"); 251 | return next({ 252 | log: "Error in authController.verifySession... Invalid session_id. Removed session_id.", 253 | status: 400, 254 | message: "Invalid session_id. Removed session_id.", 255 | }); 256 | } 257 | } catch (err) { 258 | if (err.message === "jwt expired") { 259 | return next({ 260 | log: `Error in authController.verifySession... Session expired: ${JSON.stringify( 261 | err 262 | )}`, 263 | status: 500, 264 | message: "Session expired.", 265 | }); 266 | } else 267 | return next({ 268 | log: `Error in authController.verifySession... Error while decoding session_id: ${JSON.stringify( 269 | err 270 | )}`, 271 | status: 500, 272 | message: "Error while decoding session_id.", 273 | }); 274 | } 275 | } else { 276 | return next({ 277 | log: "Error in authController.verifySession... session_id does not exist.", 278 | status: 400, 279 | message: "session_id does not exist.", 280 | }); 281 | } 282 | 283 | const queryString: string = "SELECT * FROM users WHERE user_id=$1"; 284 | const params: AuthParams = [user_id]; 285 | 286 | try { 287 | const results: AuthResults = await db.query(queryString, { 288 | bind: [...params], 289 | type: QueryTypes.SELECT, 290 | }); 291 | if (results[0].user_id == user_id) { 292 | res.locals.user_id = results[0].user_id; 293 | return next(); 294 | } else { 295 | res.clearCookie("session_id"); 296 | return next({ 297 | log: "Error in authController.verifyUser... Unable to verify that user is authorized. Removed session_id.", 298 | status: 400, 299 | message: 300 | "Unable to verify that user is authorized. Removed session_id.", 301 | }); 302 | } 303 | } catch (err) { 304 | return next({ 305 | log: `Error in authController.verifySession... Error while querying database for authorized user: ${JSON.stringify( 306 | err 307 | )}`, 308 | status: 500, 309 | message: "Error while querying database for authorized user.", 310 | }); 311 | } 312 | }; 313 | 314 | // The logout method clears the current session_id from the cookies. 315 | authController.logout = async ( 316 | req: Request, 317 | res: Response, 318 | next: NextFunction 319 | ): Promise => { 320 | res.clearCookie("session_id"); 321 | return next(); 322 | }; 323 | 324 | module.exports = authController; 325 | -------------------------------------------------------------------------------- /server/controllers/messageController.ts: -------------------------------------------------------------------------------- 1 | const db = require('../models/elephantsql'); 2 | 3 | // IMPORTANT: Import runConsume from /rabbitmq/consume. The consume file is integral to displaying Dead Letter Messages in the UI 4 | import { runConsume } from '../../rabbitmq/consume'; 5 | 6 | // Import types from types.ts file 7 | import { Messages, MessageController } from '../../types'; 8 | 9 | // Import types from express library 10 | import express, { 11 | Request, 12 | Response, 13 | NextFunction, 14 | RequestHandler, 15 | } from 'express'; 16 | 17 | const messageController: MessageController = {}; 18 | 19 | // The getAllMessages method takes the project_id from the request body and selects from the database all the messages that belong to the 20 | // project_id 21 | messageController.getAllMessages = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | const projectId: number = req.body.project_id; 27 | 28 | const queryString: string = `SELECT * FROM messages WHERE project_id = ${projectId}`; 29 | 30 | if (projectId) { 31 | await db 32 | .query(queryString) 33 | .then((data: Array) => { 34 | res.locals.messages = data[0]; 35 | return next(); 36 | }) 37 | .catch((err: Error) => { 38 | return next({ 39 | log: `Error in messageController.getAllMessages... Query from database unsuccessful: ${JSON.stringify( 40 | err 41 | )}`, 42 | status: 500, 43 | message: 'Query from database unsuccessful.', 44 | }); 45 | }); 46 | } else { 47 | return next({ 48 | log: 'Error in messageController.getAllMessages... Did not receive projectId in getAllMessages request.', 49 | status: 500, 50 | message: 'Did not receive projectId in getAllMessages request.', 51 | }); 52 | } 53 | }; 54 | 55 | // The addMessage method is called in the /rabbitmq/consume.ts file. It takes the message properties from the request body and creates 56 | // the SQL query string to only include the properties that have values. The message is then inserted into the database. 57 | messageController.addMessage = async (req, res, next) => { 58 | // Process message content to only include variables that contain values in SQL query 59 | let columnText: string = ''; 60 | let valuesText: string = ''; 61 | let headers: string; 62 | const columns: Array = Object.keys(req.body); 63 | for (let i = 0; i < columns.length; i++) { 64 | // Headers aren't currently being stored in the database--come back to this later 65 | if (columns[i] === 'headers') { 66 | headers = req.body[columns[i]]; 67 | } 68 | // For any properties that aren't undefined, add them to the query text 69 | else if (columns[i] !== undefined) { 70 | if (columnText.length > 0) { 71 | columnText += ', '; 72 | valuesText += ', '; 73 | } 74 | columnText += columns[i] === 'projectId' ? `project_id` : `${columns[i]}`; 75 | valuesText += `'${req.body[columns[i]]}'`; 76 | } 77 | } 78 | 79 | const queryString: string = `INSERT INTO messages (${columnText}) VALUES (${valuesText}) RETURNING *`; 80 | 81 | await db 82 | .query(queryString) 83 | .then((data: Array>) => { 84 | res.locals.message = data[0][0]; 85 | return next(); 86 | }) 87 | .catch((err: Error) => { 88 | return next({ 89 | log: `Error in messageController.addMessage... Unable to add message to database: ${JSON.stringify( 90 | err 91 | )}`, 92 | status: 500, 93 | message: 'Unable to add message to database.', 94 | }); 95 | }); 96 | }; 97 | 98 | // The runConsume method take the projectID from the request body and selects the project_url from the database. The imported runConsume 99 | // file from /rabbitmq/consume.ts takes the project_url to establish a connection with the user's RabbitMQ instance using the amqp library. 100 | // The projectID is added as a property to the request body when the addMessage method is called in runConsume. Called in UserProjects.tsx 101 | // to begin consuming messages when the user navigates to see the Dead Letter Messages of a specific user project 102 | messageController.runConsume = async (req, res, next) => { 103 | const projectID: number = req.body.projectID; 104 | const queryString: string = `SELECT project_url FROM projects WHERE project_id = ${projectID}`; 105 | 106 | // Grab the URL from the database to use in /rabbitmq/consume.ts 107 | if (projectID) { 108 | await db 109 | .query(queryString) 110 | .then((data: Array) => { 111 | const URL = data[0][0]['project_url']; 112 | // Start the channel in /rabbitmq/consume.ts, using the selected project ID and its URL 113 | runConsume(URL, projectID); 114 | return next(); 115 | }) 116 | .catch((err: Error) => { 117 | return next({ 118 | log: `Error in messageController.runConsume... Query from database unsuccessful: ${JSON.stringify( 119 | err 120 | )}`, 121 | status: 500, 122 | message: 'Query from database unsuccessful.', 123 | }); 124 | }); 125 | } else { 126 | return next({ 127 | log: 'Error in messageController.runConsume... Did not receive projectId in runConsume request.', 128 | status: 500, 129 | message: 'Did not receive projectId in runConsume request.', 130 | }); 131 | } 132 | }; 133 | 134 | module.exports = messageController; 135 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | import { UserProjects, UserController } from '../../types'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | import { QueryTypes } from 'sequelize'; 6 | 7 | const db = require('../models/elephantsql'); 8 | 9 | const userController: UserController = {}; 10 | 11 | // The getAllUserProjects method takes the user_id from res.locals and selects all user projects from the database 12 | userController.getAllUserProjects = async ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ) => { 17 | const user_id = res.locals.user_id; 18 | const queryString: string = `SELECT projects.project_url, projects.project_id, users_projects.project_name FROM users_projects 19 | RIGHT JOIN projects ON users_projects.project_id = projects.project_id WHERE user_id = ${user_id} 20 | ORDER BY users_projects.created_at DESC`; 21 | 22 | if (user_id) { 23 | await db 24 | .query(queryString) 25 | .then((data: Array) => { 26 | res.locals.userprojects = data[0]; 27 | return next(); 28 | }) 29 | .catch((err: Error) => { 30 | return next({ 31 | log: `Error in userController.getAllUserProjects... Query from database unsuccessful: ${JSON.stringify( 32 | err 33 | )}`, 34 | status: 500, 35 | message: 'Query from database unsuccessful.', 36 | }); 37 | }); 38 | } else { 39 | return next({ 40 | log: 'Error in userController.getAllUserProjects... Did not receive user_id in getAllUserProjects request.', 41 | status: 500, 42 | message: 'Did not receive user_id in getAllUserProjects request.', 43 | }); 44 | } 45 | }; 46 | 47 | // The addProject method takes the projectName and projectURL from the request body (user input) and inserts into the database 48 | // a new project. 49 | userController.addProject = async ( 50 | req: Request, 51 | res: Response, 52 | next: NextFunction 53 | ): Promise => { 54 | const { 55 | projectName, 56 | projectURL, 57 | }: { projectName: string; projectURL: string } = req.body; 58 | 59 | const queryString: string = `INSERT INTO projects (project_url) 60 | VALUES ('${projectURL}') 61 | ON CONFLICT (project_url) DO NOTHING; 62 | INSERT INTO users_projects (user_id, project_id, project_name) SELECT ${res.locals.user_id}, projects.project_id, '${projectName}' FROM projects WHERE project_url = '${projectURL}' RETURNING user_id`; 63 | 64 | await db 65 | .query(queryString, { type: QueryTypes.INSERT }) 66 | .then((value: Array>): void => { 67 | // Unknown what the purpose of the below line is (Line 68): 68 | res.locals.message = value[0][0]; 69 | return next(); 70 | }) 71 | .catch((err: Error) => { 72 | return next({ 73 | log: `Error in userController.addProject... Unable to add project to database: ${JSON.stringify( 74 | err 75 | )}`, 76 | status: 500, 77 | message: 'Unable to add project to database.', 78 | }); 79 | }); 80 | }; 81 | 82 | module.exports = userController; 83 | -------------------------------------------------------------------------------- /server/models/elephantsql.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize"; 2 | // Declare sequelize variable 3 | let sequelize: Sequelize; 4 | 5 | try { 6 | // Check if SQL_URI environment variable exists 7 | if (process.env.SQL_URI) { 8 | // Create a new Sequelize instance with SQL_URI and logging option 9 | sequelize = new Sequelize(process.env.SQL_URI, { 10 | logging: false, 11 | }); 12 | } else { 13 | // Throw an error if SQL_URI environment variable is missing 14 | throw new Error("Missing SQL_URI environment variable"); 15 | } 16 | 17 | // Authenticate the sequelize connection 18 | sequelize 19 | .authenticate() 20 | .then(() => { 21 | // Connection successful 22 | console.log("Connected to the database"); 23 | }) 24 | .catch((err: any) => { 25 | // Connection error 26 | console.error("Unable to connect to the database:", err); 27 | }); 28 | } catch (err) { 29 | // Catch any error occurred during connection setup 30 | console.error("Unable to connect to the database: ", err); 31 | } 32 | 33 | // Export the sequelize instance 34 | module.exports = sequelize; 35 | -------------------------------------------------------------------------------- /server/routes/authRouter.ts: -------------------------------------------------------------------------------- 1 | import { AuthController } from "../../types"; 2 | import express, { Request, Response } from "express"; 3 | const router = express.Router(); 4 | 5 | const authController: AuthController = require("../controllers/authController"); 6 | 7 | // Signup endpoint 8 | router.post( 9 | "/signup", 10 | authController.encryptPassword, 11 | authController.signup, 12 | authController.createSession, 13 | (req: Request, res: Response) => { 14 | return res.status(200).send("Successful signup!"); 15 | } 16 | ); 17 | 18 | // Login endpoint 19 | router.post( 20 | "/login", 21 | authController.verifyUser, 22 | authController.verifyPassword, 23 | authController.createSession, 24 | (req: Request, res: Response) => { 25 | return res.status(200).send("Successful login!"); 26 | } 27 | ); 28 | 29 | // Logout endpoint 30 | router.post("/logout", authController.logout, (req: Request, res: Response) => { 31 | return res.status(200).send("Successful logout!"); 32 | }); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /server/routes/messageRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { AuthController } from "../../types"; 3 | const router = express.Router(); 4 | 5 | const authController: AuthController = require("../controllers/authController"); 6 | const messageController = require("../controllers/messageController"); 7 | 8 | // Get all messages endpoint 9 | router.post( 10 | "/get-all-messages", 11 | messageController.getAllMessages, 12 | (req: Request, res: Response) => { 13 | return res.status(200).json(res.locals.messages); 14 | } 15 | ); 16 | 17 | // Add message endpoint 18 | router.post( 19 | "/add-message", 20 | messageController.addMessage, 21 | (req: Request, res: Response) => { 22 | return res.status(200).json(res.locals.message); 23 | } 24 | ); 25 | 26 | // Run consume endpoint 27 | router.post( 28 | "/run-consume", 29 | messageController.runConsume, 30 | (req: Request, res: Response) => { 31 | return res.status(200).send("Consume file started"); 32 | } 33 | ); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /server/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import { AuthController, UserController } from "../../types"; 2 | import express, { Request, Response } from "express"; 3 | 4 | const authController: AuthController = require("../controllers/authController"); 5 | const userController: UserController = require("../controllers/userController"); 6 | 7 | const router = express.Router(); 8 | 9 | // Get all user projects endpoint 10 | router.get( 11 | "/get-all-user-projects", 12 | authController.verifySession, 13 | userController.getAllUserProjects, 14 | (req: Request, res: Response) => { 15 | return res.status(200).json(res.locals.userprojects); 16 | } 17 | ); 18 | 19 | // Add project endpoint 20 | router.post( 21 | "/addproject", 22 | authController.verifySession, 23 | userController.addProject, 24 | (req: Request, res: Response) => { 25 | return res.status(200).send("New Project Added!"); 26 | } 27 | ); 28 | 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const cookieParser = require("cookie-parser"); 3 | require("dotenv").config(); 4 | const cors = require("cors"); 5 | 6 | import express, { Application, Request, Response, NextFunction } from "express"; 7 | import { ServerError } from "./../types"; 8 | 9 | const PORT = process.env.PORT; 10 | 11 | const authRouter = require("./routes/authRouter"); 12 | const messageRouter = require("./routes/messageRouter"); 13 | const userRouter = require("./routes/userRouter"); 14 | 15 | const app: Application = express(); 16 | const http = require("http"); 17 | const httpServer = http.createServer(app); 18 | const DIST_DIR = path.join(__dirname, "../build/"); 19 | const HTML_FILE = path.join(DIST_DIR, "index.html"); 20 | 21 | // Middleware 22 | app.use(cookieParser()); // Parse cookies from the request 23 | app.use(cors()); // Enable Cross-Origin Resource Sharing 24 | app.use(express.json()); // Parse JSON in request bodies 25 | app.use(express.urlencoded({ extended: true })); // Parse URL-encoded form data 26 | 27 | // Serve static files: 28 | app.use(express.static(DIST_DIR)); 29 | app.use(express.static("../src/assets")); 30 | 31 | // Routes - mounting routers for handling related-routes 32 | app.use("/auth", authRouter); 33 | app.use("/messages", messageRouter); 34 | app.use("/user", userRouter); 35 | 36 | // Serve index.html 37 | app.get("/*", (req: Request, res: Response) => { 38 | res.status(200).sendFile(path.resolve(__dirname, HTML_FILE)); 39 | }); 40 | 41 | // 404 Catch-All 42 | app.use( 43 | "*", 44 | (req: Request, res: Response) => res.status(404).send("Not Found!!") // Handle undefined routes with a 404 error 45 | ); 46 | 47 | // Universal Error Handler 48 | app.use((err: ServerError, req: Request, res: Response, next: NextFunction) => { 49 | const defaultErr = { 50 | log: "Express error handler caught unknown middleware error.", 51 | status: 500, 52 | message: { err: "An error occurred" }, 53 | }; 54 | const errorObj = Object.assign({}, defaultErr, err); 55 | return res.status(errorObj.status).json(errorObj.message); // Handle errors with a JSON response 56 | }); 57 | 58 | // Establish server-side socket connection 59 | const io = require("socket.io")(httpServer, { 60 | cors: { 61 | origin: ["http://localhost:8080"], // Specify allowed origins for socket connections 62 | methods: ["GET", "POST"], // Specify allowed HTTP methods 63 | }, 64 | }); 65 | const messagesSocket = io.of("/messages"); 66 | messagesSocket.on("connection", (socket: any) => { 67 | socket.join("consume-messages"); // Join the "consume-messages" room 68 | socket.on("join", function (room: any) { 69 | socket.join(room); // Join a specified room 70 | }); 71 | socket.on("message-added", function () { 72 | socket.to("consume-messages").emit("message-added"); 73 | }); 74 | }); 75 | 76 | httpServer.listen(PORT, () => { 77 | console.log(`Listening on port ${PORT}`); 78 | }); 79 | 80 | module.exports = app; 81 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes, Outlet, BrowserRouter } from "react-router-dom"; 3 | import MessageContainer from "./Containers/MessageContainer"; 4 | import UserProjectsContainer from "./Containers/UserProjectsContainer"; 5 | import Login from "./Components/Login"; 6 | import Signup from "./Components/Signup"; 7 | import ErrorPage from "./Containers/ErrorPageContainer"; 8 | // import path from "path"; 9 | 10 | // The App component contains the structure of the application, utilizing React Router to organize which components are displayed 11 | // for which URL path 12 | const App = (): JSX.Element => { 13 | return ( 14 | 15 | } /> 16 | } /> 17 | } /> 18 | {/* } /> */} 19 | } /> 20 | } /> 21 | 22 | ); 23 | }; 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/Components/AddProjectModal.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a modal for adding a project 2 | // It is used to display a form where users can enter the details of a project, such as the project name and project URL 3 | 4 | import * as React from "react"; 5 | import Modal from "react-modal"; 6 | import { ModalProps } from "../../types"; 7 | import FormControl from "@mui/material/FormControl"; 8 | import InputLabel from "@mui/material/InputLabel"; 9 | import Input from "@mui/material/Input"; 10 | import Button from "@mui/material/Button"; 11 | import FormLabel from "@mui/material/FormLabel"; 12 | 13 | export default function AddProjectModal({ 14 | isShown, 15 | handleClose, 16 | handleSave, 17 | headerText, 18 | setNameErr, 19 | setURLErr, 20 | projectNameError, 21 | projectURLError, 22 | }: ModalProps) { 23 | const resetNameErr = () => { 24 | setNameErr(false); 25 | }; 26 | 27 | const resetURLErr = () => { 28 | setURLErr(false); 29 | }; 30 | 31 | return ( 32 |
33 | 65 | 66 | {headerText} 67 | 68 | 69 | Project Name 70 | 75 | 76 | 77 | Project URL 78 | 83 | 84 | 85 | 88 | 91 | 92 | 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/Components/DeadLetterMessage.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a data table for displaying messages 2 | // It uses the DataGrid component from the @mui/x-data-grid package to render the table 3 | // -- includes options for sorting, column visibility, and cell content expansion 4 | 5 | import * as React from "react"; 6 | import { DataGrid, GridToolbar } from "@mui/x-data-grid"; 7 | import PropTypes from "prop-types"; 8 | import Box from "@mui/material/Box"; 9 | import Typography from "@mui/material/Typography"; 10 | import Paper from "@mui/material/Paper"; 11 | import Popper from "@mui/material/Popper"; 12 | import { 13 | DataTableProps, 14 | GridCellExpandProps, 15 | renderCellExpandParams, 16 | GridCellExpand, 17 | Columns, 18 | Rows, 19 | } from "../../types"; 20 | 21 | export default function DataTable(props: DataTableProps) { 22 | const { messages } = props; 23 | 24 | function isOverflown(element: any) { 25 | return ( 26 | element.scrollHeight > element.clientHeight || 27 | element.scrollWidth > element.clientWidth 28 | ); 29 | } 30 | 31 | const GridCellExpand: React.FunctionComponent = React.memo( 32 | function GridCellExpand(props: GridCellExpandProps) { 33 | const { width, value } = props; 34 | 35 | const wrapper = React.useRef(null); 36 | const cellDiv = React.useRef(null); 37 | const cellValue = React.useRef(null); 38 | 39 | const [anchorEl, setAnchorEl] = React.useState(null); 40 | const [showFullCell, setShowFullCell] = React.useState(false); 41 | const [showPopper, setShowPopper] = React.useState(false); 42 | 43 | const handleMouseEnter = () => { 44 | const isCurrentlyOverflown = isOverflown(cellValue.current); 45 | setShowPopper(isCurrentlyOverflown); 46 | setAnchorEl(cellDiv.current); 47 | setShowFullCell(true); 48 | }; 49 | 50 | const handleMouseLeave = () => { 51 | setShowFullCell(false); 52 | }; 53 | 54 | React.useEffect(() => { 55 | if (!showFullCell) { 56 | return undefined; 57 | } 58 | 59 | function handleKeyDown(nativeEvent: KeyboardEvent) { 60 | if (nativeEvent.key === "Escape" || nativeEvent.key === "Esc") { 61 | setShowFullCell(false); 62 | } 63 | } 64 | 65 | document.addEventListener("keydown", handleKeyDown); 66 | 67 | return () => { 68 | document.removeEventListener("keydown", handleKeyDown); 69 | }; 70 | }, [setShowFullCell, showFullCell]); 71 | 72 | return ( 73 | 86 | <> 87 | 97 | 105 | <>{value} 106 | 107 | {showPopper && ( 108 | 113 | 121 | 122 | {value} 123 | 124 | 125 | 126 | )} 127 | 128 | 129 | ); 130 | } 131 | ); 132 | 133 | GridCellExpand.propTypes = { 134 | value: PropTypes.any.isRequired, 135 | width: PropTypes.number.isRequired, 136 | }; 137 | 138 | function renderCellExpand(params: renderCellExpandParams) { 139 | return ( 140 | 144 | ); 145 | } 146 | 147 | renderCellExpand.propTypes = { 148 | /** 149 | * The column of the row that the current cell belongs to. 150 | */ 151 | colDef: PropTypes.object.isRequired, 152 | /** 153 | * The cell value. 154 | * If the column has `valueGetter`, use `params.row` to directly access the fields. 155 | */ 156 | value: PropTypes.string, 157 | }; 158 | 159 | const columns: Columns = [ 160 | { 161 | field: "consumerTag", 162 | headerName: "consumerTag", 163 | renderCell: renderCellExpand, 164 | flex: 1.5, 165 | }, 166 | { 167 | field: "deliveryTag", 168 | headerName: "deliveryTag", 169 | renderCell: renderCellExpand, 170 | flex: 1, 171 | }, 172 | { 173 | field: "redelivered", 174 | headerName: "redelivered", 175 | renderCell: renderCellExpand, 176 | flex: 1, 177 | }, 178 | { 179 | field: "exchange", 180 | headerName: "exchange", 181 | renderCell: renderCellExpand, 182 | flex: 1, 183 | }, 184 | { 185 | field: "routingKey", 186 | headerName: "routingKey", 187 | renderCell: renderCellExpand, 188 | flex: 1, 189 | }, 190 | { 191 | field: "contentType", 192 | headerName: "contentType", 193 | renderCell: renderCellExpand, 194 | flex: 1, 195 | }, 196 | { 197 | field: "contentEncoding", 198 | headerName: "contentEncoding", 199 | renderCell: renderCellExpand, 200 | flex: 1, 201 | }, 202 | { 203 | field: "deliveryMode", 204 | headerName: "deliveryMode", 205 | renderCell: renderCellExpand, 206 | flex: 1, 207 | }, 208 | { 209 | field: "priority", 210 | headerName: "priority", 211 | renderCell: renderCellExpand, 212 | flex: 1, 213 | }, 214 | { 215 | field: "correlationId", 216 | headerName: "correlationId", 217 | renderCell: renderCellExpand, 218 | flex: 1, 219 | }, 220 | { 221 | field: "replyTo", 222 | headerName: "replyTo", 223 | renderCell: renderCellExpand, 224 | flex: 1, 225 | }, 226 | { 227 | field: "expiration", 228 | headerName: "expiration", 229 | renderCell: renderCellExpand, 230 | flex: 1, 231 | }, 232 | { 233 | field: "messageId", 234 | headerName: "messageId", 235 | renderCell: renderCellExpand, 236 | flex: 1, 237 | }, 238 | { 239 | field: "timestamp", 240 | headerName: "timestamp", 241 | renderCell: renderCellExpand, 242 | flex: 1, 243 | }, 244 | { 245 | field: "type", 246 | headerName: "type", 247 | renderCell: renderCellExpand, 248 | flex: 1, 249 | }, 250 | { 251 | field: "userId", 252 | headerName: "userId", 253 | renderCell: renderCellExpand, 254 | flex: 1, 255 | }, 256 | { 257 | field: "appId", 258 | headerName: "appId", 259 | renderCell: renderCellExpand, 260 | flex: 1, 261 | }, 262 | { 263 | field: "clusterId", 264 | headerName: "clusterId", 265 | renderCell: renderCellExpand, 266 | flex: 1, 267 | }, 268 | ]; 269 | 270 | const rows: Rows = messages.map((el) => { 271 | return { 272 | id: el.message_id, 273 | consumerTag: el.consumertag, 274 | deliveryTag: el.deliverytag, 275 | redelivered: el.redelivered, 276 | exchange: el.exchange, 277 | routingKey: el.routingkey, 278 | contentType: el.contenttype, 279 | contentEncoding: el.contentencoding, 280 | deliveryMode: el.deliverymode, 281 | priority: el.priority, 282 | correlationId: el.correlationid, 283 | replyTo: el.replyto, 284 | expiration: el.expiration, 285 | messageId: el.messageid, 286 | timestamp: el.timestamp 287 | ? new Date(Number(el.timestamp)).toISOString() 288 | : "", 289 | type: el.type, 290 | userId: el.userid, 291 | appId: el.appid, 292 | clusterId: el.clusterid, 293 | }; 294 | }); 295 | 296 | return ( 297 |
298 | 328 |
329 | ); 330 | } 331 | -------------------------------------------------------------------------------- /src/Components/ErrorPage/ErrorPageMessage.tsx: -------------------------------------------------------------------------------- 1 | // This componenet represents an error page message 2 | 3 | import React from "react"; 4 | 5 | function ErrorPageMessage(): JSX.Element { 6 | return ( 7 |
8 |

----- Page not found! -----

9 |
10 | ); 11 | } 12 | 13 | export default ErrorPageMessage; 14 | -------------------------------------------------------------------------------- /src/Components/Login.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a login form for a web application 2 | // It includes form fields for email and password, as well as a submit button to sign in 3 | // It uses various components and utilities from Material-UI to style and handle the form 4 | 5 | import * as React from "react"; 6 | import { useState } from "react"; 7 | import { useNavigate } from "react-router-dom"; 8 | import Avatar from "@mui/material/Avatar"; 9 | import Button from "@mui/material/Button"; 10 | import CssBaseline from "@mui/material/CssBaseline"; 11 | import TextField from "@mui/material/TextField"; 12 | import Link from "@mui/material/Link"; 13 | import Grid from "@mui/material/Grid"; 14 | import Box from "@mui/material/Box"; 15 | import Alert from "@mui/material/Alert"; 16 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; 17 | import Typography from "@mui/material/Typography"; 18 | import Container from "@mui/material/Container"; 19 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 20 | import axios from "axios"; 21 | import NavLoginPage from "./NavBar/NavLoginPage"; 22 | 23 | function Copyright(props: any): JSX.Element { 24 | return ( 25 | 31 | {"Copyright © "} 32 | 33 | RabbitTracks 34 | {" "} 35 | {new Date().getFullYear()} 36 | {"."} 37 | 38 | ); 39 | } 40 | 41 | const theme = createTheme(); 42 | 43 | export default function Login(): JSX.Element { 44 | const [loginError, setLoginError] = useState(false); 45 | const [emailValue, setEmailValue] = useState(""); 46 | const [passwordValue, setPasswordValue] = useState(""); 47 | 48 | let navigate = useNavigate(); 49 | 50 | const handleSubmit = async ( 51 | event: React.FormEvent 52 | ): Promise => { 53 | event.preventDefault(); 54 | 55 | const data: FormData = new FormData(event.currentTarget); 56 | 57 | await axios 58 | .post("/auth/login", { 59 | email: data.get("email"), 60 | password: data.get("password"), 61 | }) 62 | 63 | .then((data) => { 64 | setLoginError(false); 65 | navigate("/userprojects"); 66 | }) 67 | .catch((err) => { 68 | setLoginError(true); 69 | }); 70 | }; 71 | 72 | const handleLoginErrorClose = () => { 73 | setLoginError(false); 74 | setEmailValue(""); 75 | setPasswordValue(""); 76 | }; 77 | 78 | return ( 79 | <> 80 | 81 | 82 | 83 | 84 | 92 | 93 | 94 | 95 | 96 | Login 97 | 98 | {loginError && ( 99 | 100 | Incorrect login credentials 101 | 102 | )} 103 | 109 | setEmailValue(e.target.value)} 120 | /> 121 | setPasswordValue(e.target.value)} 132 | /> 133 | 141 | 142 | 143 | 144 | {"Don't have an account? Sign Up"} 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /src/Components/NavBar/NavAfterLoggedIn.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a navigation bar for the user interface after a user has logged in 2 | // It is responsible for rendering the header title, user account icon, and a dropdown menu with logout functionality 3 | 4 | import * as React from "react"; 5 | import { useNavigate } from "react-router-dom"; 6 | import AppBar from "@mui/material/AppBar"; 7 | import Box from "@mui/material/Box"; 8 | import Toolbar from "@mui/material/Toolbar"; 9 | import IconButton from "@mui/material/IconButton"; 10 | import AccountCircle from "@mui/icons-material/AccountCircle"; 11 | import MenuItem from "@mui/material/MenuItem"; 12 | import Menu from "@mui/material/Menu"; 13 | import { Link } from "react-router-dom"; 14 | import axios from "axios"; 15 | 16 | export default function NavAfterLoggedIn(): JSX.Element { 17 | let navigate = useNavigate(); 18 | const [anchorEl, setAnchorEl] = React.useState(null); 19 | 20 | const handleMenu = (event: React.MouseEvent) => { 21 | setAnchorEl(event.currentTarget); 22 | }; 23 | 24 | const handleClose = () => { 25 | setAnchorEl(null); 26 | }; 27 | 28 | const handleLogout = async ( 29 | event: React.MouseEvent 30 | ): Promise => { 31 | event.preventDefault(); 32 | await axios 33 | .post("/auth/logout") 34 | .then((data) => { 35 | navigate("/"); 36 | }) 37 | .catch((err) => {}); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | 44 | { 45 | <> 46 |

47 | RABBIT TRACKS 48 |

49 |
50 | 58 | 59 | 60 | 75 | 76 | Logout 77 | 78 | 79 |
80 | 81 | } 82 |
83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/Components/NavBar/NavLoginPage.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a navigation bar for the login page 2 | // It is responsible for rendering a welcome message and a statement on the page 3 | 4 | import React from "react"; 5 | import AppBar from "@mui/material/AppBar"; 6 | import Box from "@mui/material/Box"; 7 | 8 | export default function NavLoginPage(): JSX.Element { 9 | return ( 10 | 11 | 12 |
13 |

14 | WELCOME TO RABBIT TRACKS 15 |

16 |

17 |
18 |
19 |
20 |

Track. Reprocess. Repeat.

21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/Components/NavBar/NavMessagesPage.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a navigation bar for the application after the user has logged in 2 | // It provides a menu with options for the logged-in user, including a logo, user account icon, and a logout option 3 | 4 | import * as React from "react"; 5 | import { useNavigate } from "react-router-dom"; 6 | import AppBar from "@mui/material/AppBar"; 7 | import Box from "@mui/material/Box"; 8 | import Toolbar from "@mui/material/Toolbar"; 9 | import Typography from "@mui/material/Typography"; 10 | import IconButton from "@mui/material/IconButton"; 11 | import AccountCircle from "@mui/icons-material/AccountCircle"; 12 | import MenuItem from "@mui/material/MenuItem"; 13 | import Menu from "@mui/material/Menu"; 14 | import { Link } from "react-router-dom"; 15 | import RabbitPaw from "../../assets/images/rabbitpaw.jpg"; 16 | import axios from "axios"; 17 | 18 | export default function NavAfterLoggedIn(): JSX.Element { 19 | let navigate = useNavigate(); 20 | const [anchorEl, setAnchorEl] = React.useState(null); 21 | 22 | const handleMenu = (event: React.MouseEvent) => { 23 | setAnchorEl(event.currentTarget); 24 | }; 25 | 26 | const handleClose = () => { 27 | setAnchorEl(null); 28 | }; 29 | 30 | const handleLogout = async ( 31 | event: React.MouseEvent 32 | ): Promise => { 33 | event.preventDefault(); 34 | await axios 35 | .post("/auth/logout") 36 | .then((data) => { 37 | navigate("/"); 38 | }) 39 | .catch((err) => {}); 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | { 55 | <> 56 |

57 | RABBIT TRACKS 58 |

59 |
60 | 68 | 69 | 70 | 85 | 86 | Logout 87 | 88 | 89 |
90 | 91 | } 92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/Components/NavBar/NavSignupPage.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a navigation bar for the signup page 2 | // It provides a title and a welcome statement to greet users 3 | 4 | import React from "react"; 5 | import AppBar from "@mui/material/AppBar"; 6 | import Box from "@mui/material/Box"; 7 | 8 | export default function NavSignupPage(): JSX.Element { 9 | return ( 10 | 11 | 12 |
13 |

14 | RABBIT TRACKS 15 |

16 |

17 |
18 |
19 |
20 |

Start tracking your RabbitMQ message failures today!

21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/Components/Signup.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a signup form 2 | // It uses various components and utilities from Material-UI to style and handle the form 3 | 4 | import * as React from "react"; 5 | import { useNavigate } from "react-router"; 6 | import Avatar from "@mui/material/Avatar"; 7 | import Button from "@mui/material/Button"; 8 | import CssBaseline from "@mui/material/CssBaseline"; 9 | import TextField from "@mui/material/TextField"; 10 | import Link from "@mui/material/Link"; 11 | import Grid from "@mui/material/Grid"; 12 | import Box from "@mui/material/Box"; 13 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; 14 | import Typography from "@mui/material/Typography"; 15 | import Container from "@mui/material/Container"; 16 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 17 | import NavSignupPage from "./NavBar/NavSignupPage"; 18 | import axios from "axios"; 19 | 20 | function Copyright(props: any): JSX.Element { 21 | return ( 22 | 28 | {"Copyright © "} 29 | 30 | RabbitTracks 31 | {" "} 32 | {new Date().getFullYear()} 33 | {"."} 34 | 35 | ); 36 | } 37 | 38 | const theme = createTheme(); 39 | 40 | export default function SignUp() { 41 | let navigate = useNavigate(); 42 | 43 | const handleSubmit = async ( 44 | event: React.FormEvent 45 | ): Promise => { 46 | event.preventDefault(); 47 | const data: FormData = new FormData(event.currentTarget); 48 | 49 | await axios 50 | .post("/auth/signup", { 51 | firstName: data.get("firstName"), 52 | lastName: data.get("lastName"), 53 | email: data.get("email"), 54 | password: data.get("password"), 55 | passwordConfirm: data.get("passwordConfirm"), 56 | }) 57 | .then((data) => { 58 | navigate("/userprojects"); 59 | }) 60 | .catch((err) => {}); 61 | }; 62 | 63 | return ( 64 | <> 65 | 66 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 81 | Sign up 82 | 83 | 89 | 90 | 91 | 100 | 101 | 102 | 110 | 111 | 112 | 120 | 121 | 122 | 131 | 132 | 133 | 142 | 143 | {/* 144 | 147 | } 148 | label="I want to receive inspiration, marketing promotions and updates via email." 149 | /> 150 | */} 151 | 152 | 160 | 161 | 162 | {"Already have an account? Login"} 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /src/Components/UserProjects.tsx: -------------------------------------------------------------------------------- 1 | // This component represents a user's project page 2 | // It displays a list of projects and provides a button to view failed messages for each project 3 | 4 | import * as React from "react"; 5 | import { UserProjectsProps } from "../../types"; 6 | import { useNavigate } from "react-router-dom"; 7 | import axios from "axios"; 8 | 9 | export default function UserProjects(props: UserProjectsProps) { 10 | const { projects } = props; 11 | 12 | let navigate = useNavigate(); 13 | 14 | const handleClickGetMessages = (projectID: Number) => { 15 | navigate("/messages", { state: { projectID: projectID } }); 16 | 17 | axios 18 | .post( 19 | "/messages/run-consume", 20 | { 21 | projectID, 22 | }, 23 | { 24 | headers: { 25 | "Content-Type": "application/json", 26 | }, 27 | } 28 | ) 29 | .catch((err: Error) => {}); 30 | }; 31 | 32 | const rows: JSX.Element[] = projects.map((el, i) => { 33 | return ( 34 |
35 |
36 |

{el.project_name}

37 | 43 |
44 |
45 | ); 46 | }); 47 | 48 | return
{rows}
; 49 | } 50 | -------------------------------------------------------------------------------- /src/Containers/ErrorPageContainer.tsx: -------------------------------------------------------------------------------- 1 | // This container represents an error page 2 | // It displays an error message, an image, and a link to navigate back to the home screen 3 | 4 | import React from "react"; 5 | import { Link } from "react-router-dom"; 6 | import ErrorPageMessage from "../Components/ErrorPage/ErrorPageMessage"; 7 | import RabbitPhoto from "../../src/assets/images/rabbitphoto.jpg"; 8 | 9 | export default function ErrorPage() { 10 | return ( 11 | <> 12 | 13 |
14 |
15 | 16 |  <-- Sad Rabbit photo 22 | 23 |
24 |
25 |

26 | Click on the Rabbit to get back to the home screen! 27 |

28 |
29 |
30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Containers/MessageContainer.tsx: -------------------------------------------------------------------------------- 1 | // This container represents a messages page that retrieves and displays dead letter messages for a specific project 2 | // It also establishes a socket connection to receive real-time updates when new messages are added 3 | 4 | import axios from "axios"; 5 | import * as React from "react"; 6 | import { useEffect, useState, useContext } from "react"; 7 | import { useLocation } from "react-router-dom"; 8 | import DataTable from "../Components/DeadLetterMessage"; 9 | import NavOnMessagesPage from "../Components/NavBar/NavMessagesPage"; 10 | import { io } from "socket.io-client"; 11 | 12 | const MessageContainer = (): JSX.Element => { 13 | const [deadLetterMessages, setDeadLetterMessages] = useState([]); 14 | 15 | const { state } = useLocation(); 16 | 17 | const getData = async (): Promise => { 18 | try { 19 | const { data }: { data: [] } = await axios.post( 20 | "/messages/get-all-messages", 21 | { project_id: state.projectID } 22 | ); 23 | setDeadLetterMessages(data); 24 | } catch (err) {} 25 | }; 26 | 27 | useEffect(() => { 28 | getData(); 29 | 30 | // Establish client-side socket connection on component mount 31 | const messagesSocket = io("http://localhost:3000/messages"); 32 | messagesSocket.on("connect", () => { 33 | messagesSocket.emit("join", "consume-messages"); 34 | }); 35 | messagesSocket.on("message-added", (callback) => { 36 | getData(); 37 | }); 38 | messagesSocket.on("disconnect", () => 39 | console.log("Client side websocket has disconnected") 40 | ); 41 | 42 | return () => { 43 | // close socket connection on component unmount 44 | messagesSocket.disconnect(); 45 | }; 46 | }, []); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default MessageContainer; 57 | -------------------------------------------------------------------------------- /src/Containers/UserProjectsContainer.tsx: -------------------------------------------------------------------------------- 1 | // This container represents a user projects page 2 | // It allows users to view their projects, add new projects, and interact with them 3 | 4 | import axios from "axios"; 5 | import * as React from "react"; 6 | import { useEffect, useState, MouseEvent, MouseEventHandler } from "react"; 7 | import UserProjects from "../Components/UserProjects"; 8 | import AddProjectModal from "../Components/AddProjectModal"; 9 | import NavAfterLoggedIn from "../Components/NavBar/NavAfterLoggedIn"; 10 | 11 | const UserProjectsContainer = (): JSX.Element => { 12 | const [projectsList, setProjectsList] = useState([]); 13 | const [show, setShow] = useState(false); 14 | const [projNameErr, setNameErr] = useState(false); 15 | const [projURLErr, setURLErr] = useState(false); 16 | 17 | const getData = async (): Promise => { 18 | try { 19 | const { data }: { data: [] } = await axios.get( 20 | "/user/get-all-user-projects" 21 | ); 22 | setProjectsList(data); 23 | } catch (err) {} 24 | }; 25 | 26 | useEffect(() => { 27 | getData(); 28 | }, []); 29 | 30 | const onOpen: MouseEventHandler = (e: MouseEvent) => setShow(true); 31 | const onClose = (): void => { 32 | setShow(false); 33 | setNameErr(false); 34 | setURLErr(false); 35 | }; 36 | const onSave = async (): Promise => { 37 | const projectName = ( 38 | document.getElementById("project-name") as HTMLInputElement 39 | ).value; 40 | const projectURL = ( 41 | document.getElementById("project-url") as HTMLInputElement 42 | ).value; 43 | 44 | if (projectName && projectURL) { 45 | await axios 46 | .post( 47 | "/user/addproject", 48 | { 49 | projectName, 50 | projectURL, 51 | }, 52 | { 53 | headers: { 54 | "Content-Type": "application/json", 55 | }, 56 | } 57 | ) 58 | .then((data) => { 59 | setShow(false); 60 | getData(); 61 | }) 62 | .catch((err: Error) => {}); 63 | } else { 64 | if (!projectName) setNameErr(true); 65 | if (!projectURL) setURLErr(true); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 |
72 | 73 |
74 |
75 | 78 | 88 |
89 |
90 | 91 |
92 |
93 | ); 94 | }; 95 | 96 | export default UserProjectsContainer; 97 | -------------------------------------------------------------------------------- /src/assets/images/RT_logo_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/src/assets/images/RT_logo_wide.png -------------------------------------------------------------------------------- /src/assets/images/RabbitTracks-DB-Schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/src/assets/images/RabbitTracks-DB-Schema.png -------------------------------------------------------------------------------- /src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/rabbitpaw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/src/assets/images/rabbitpaw.jpg -------------------------------------------------------------------------------- /src/assets/images/rabbitphoto.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitTracks/f0a5835b8ca019810b79691307660f5a3f1d9bc8/src/assets/images/rabbitphoto.jpg -------------------------------------------------------------------------------- /src/assets/stylesheets/global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | } 4 | .nav-after-logged-in { 5 | display: flex; 6 | justify-content: space-around; 7 | } 8 | #error-image { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | background-color: #ff5a00; 13 | font-family: 'Courier New', Courier, monospace; 14 | } 15 | #error-text { 16 | margin-left: 15px; 17 | } 18 | #error-message { 19 | background-color: white; 20 | text-align: left; 21 | font-family:Arial, Helvetica, sans-serif 22 | } 23 | 24 | #save-button { 25 | margin-top: 10px; 26 | margin-right: 10px; 27 | } 28 | 29 | #nav-bar { 30 | background-color: #ff5a00; 31 | } 32 | 33 | #rabbit-paw-pic { 34 | margin-top: 10px; 35 | height: 50px; 36 | width: 65px; 37 | border-radius: 50% 38 | } 39 | 40 | #nav-title { 41 | font-family: 'Peralta', cursive; 42 | text-align: left; 43 | } 44 | 45 | #header-title { 46 | font-family: 'Peralta', cursive; 47 | text-align: center; 48 | flex-grow: 1; 49 | padding-left: 6.5%; 50 | padding-right: .5%; 51 | } 52 | 53 | #header-title2 { 54 | font-family: 'Peralta', cursive; 55 | text-align: center; 56 | flex-grow: 1; 57 | } 58 | 59 | #icon-button { 60 | height: 60px; 61 | width: 60px; 62 | } 63 | 64 | #first-word { 65 | color: black 66 | } 67 | 68 | #welcome-title { 69 | text-align: center; 70 | font-family: 'Peralta', cursive; 71 | } 72 | 73 | #welcome-statement { 74 | text-align: center; 75 | } 76 | .projects-container { 77 | display: flex; 78 | flex-direction: column; 79 | flex-wrap: nowrap; 80 | align-content: center; 81 | justify-content: center; 82 | align-items: center; 83 | } 84 | 85 | .projects-div { 86 | border: solid #ff5a00; 87 | display: flex; 88 | flex-direction: column; 89 | border-radius: 5%; 90 | border-radius: 25px; 91 | background: rgb(200, 186, 149); 92 | background-position: left top; 93 | background-repeat: repeat; 94 | padding: 20px; 95 | width: 20%; 96 | height: 10%; 97 | color:rgb(58, 42, 3); 98 | text-align: center; 99 | margin-top: 5px; 100 | } 101 | 102 | .messages-btn { 103 | background-color: #ff5a00; 104 | color: rgb(255, 255, 255); 105 | font-size: 15px; 106 | border-radius: 15px; 107 | } 108 | 109 | .hovertext { 110 | position: relative; 111 | } 112 | 113 | .hovertext:before { 114 | content: attr(data-hover); 115 | visibility: hidden; 116 | opacity: 0; 117 | width: 140px; 118 | background-color: black; 119 | color: #fff; 120 | text-align: center; 121 | border-radius: 5px; 122 | padding: 5px 0; 123 | transition: opacity 1s ease-in-out; 124 | 125 | position: absolute; 126 | z-index: 1; 127 | left: 0; 128 | top: 110%; 129 | } 130 | 131 | .hovertext:hover:before { 132 | opacity: 1; 133 | visibility: visible; 134 | } 135 | 136 | .add-project-btn { 137 | background-color: rgb(200, 186, 149); 138 | color: white; 139 | font-size: 15px; 140 | border-radius: 15px; 141 | width: 120px; 142 | border: solid #ff5a00; 143 | font-weight: 600; 144 | margin-top: 15px; 145 | } -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | // need this file to provide Typescript type information about an API that's written in JavaScript. 2 | 3 | declare module "*.jpg"; 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import App from "./App"; 5 | import "./assets/stylesheets/global.scss"; 6 | 7 | // The index.tsx file designates the root from the HTML skeleton and renders the application, utilizing BrowserRouter from 8 | // the React Router library 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | if (!root) throw new Error('Failed to find the root element'); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", // Output directory for compiled files 4 | "target": "es6", // Target ECMAScript version 5 | "allowJs": true, // Allow JavaScript files to be compiled 6 | "moduleResolution": "node", // Specify module resolution strategy 7 | "jsx": "react", // Enable JSX support for React 8 | "module": "CommonJS", // Specify module system (CommonJS in this case) 9 | "esModuleInterop": true, // Enable interoperability between CommonJS and ES modules 10 | "noImplicitAny": true // Disallow implicit 'any' types 11 | }, 12 | "include": [ 13 | "src/**/*", // Include all files in 'src' directory and its subdirectories 14 | "server/**/*", // Include all files in 'server' directory and its subdirectories 15 | "rabbitmq/consume.js", // Include 'consume.js' file in 'rabbitmq' directory 16 | "src/Global.d.ts" // Include 'Global.d.ts' file in 'src' directory 17 | ], 18 | "exclude": [ 19 | "index.html", // Exclude 'index.html' file 20 | "src/assets/stylesheets/*" // Exclude all files in 'src/assets/stylesheets' directory 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestHandler } from "express"; 2 | import * as React from 'react' 3 | 4 | // Types for '/rabbitmq/consume.ts' 5 | export type UserProjects = { 6 | project_id: number; 7 | project_name: string; 8 | project_url: string 9 | } 10 | 11 | export type UserProjectsProps = { 12 | projects: Array; 13 | } 14 | 15 | export type UserMessagesProps = { 16 | projectId?: number; 17 | onClick?: React.MouseEventHandler 18 | } 19 | 20 | export interface CreateDLXMessage extends Fields, Properties { 21 | projectId: number; 22 | } 23 | 24 | export interface Properties { 25 | contentType?: string | null; 26 | contentEncoding?: string | null; 27 | headers?: object | null; 28 | deliveryMode?: string | null; 29 | priority?: number | null; 30 | correlationId?: string | null; 31 | replyTo?: string | null; 32 | expiration?: string | null; 33 | messageId?: string | null; 34 | timestamp?: number | null; 35 | type?: string | null; 36 | userId?: string | null; 37 | appId?: string | null; 38 | clusterId?: string | null; 39 | } 40 | 41 | export interface Fields { 42 | consumerTag?: string; 43 | deliveryTag?: number; 44 | redelivered?: boolean; 45 | exchange?: string; 46 | routingKey?: string; 47 | messageCount?: number; 48 | } 49 | // End types for '/rabbitmq/consume.ts' 50 | 51 | // Lives in server.ts 52 | export type ServerError = { 53 | log?: string; 54 | status?: number; 55 | message?: string; 56 | }; 57 | // End server.ts types 58 | 59 | // All the below Auth Types (used in authController and authRouter) 60 | export type AuthController = { 61 | encryptPassword?: RequestHandler; 62 | signup?: RequestHandler; 63 | verifyUser?: RequestHandler; 64 | verifyPassword?: RequestHandler; 65 | createSession?: RequestHandler; 66 | verifySession?: RequestHandler; 67 | logout?: RequestHandler; 68 | }; 69 | 70 | export type AuthResults = Array<{ 71 | user_id?: number; 72 | user_password?: string; 73 | session_value?: number | string; // should only be one or the other... needs testing/verification 74 | }>; 75 | 76 | export type AuthParams = Array; 77 | 78 | export type AuthRequestBody = { 79 | firstName?: string; 80 | lastName?: string; 81 | email?: string; 82 | password?: string; 83 | passwordConfirm?: string; 84 | }; 85 | // End types for Auth 86 | 87 | // Types for '/server/controllers/messageController' 88 | export type MessageController = { 89 | getAllMessages?: RequestHandler; 90 | addMessage?: RequestHandler; 91 | runConsume?: RequestHandler; 92 | }; 93 | // End types for '/server/controllers/messageController' 94 | 95 | // Types for '/server/controllers/userController' 96 | export type UserController = { 97 | addProject?: RequestHandler; 98 | getAllUserProjects?: RequestHandler; 99 | } 100 | // End types for '/server/controllers/userController' 101 | 102 | // Types for '/src/Components/DeadLetterMessage' 103 | export type DataTableProps = { 104 | messages: Array; 105 | }; 106 | 107 | 108 | export interface GridCellExpand { 109 | propTypes?: any; 110 | value: any; 111 | width: number; 112 | } 113 | 114 | export type Messages = { 115 | id?: number; 116 | message_id: number; 117 | consumertag: string; 118 | deliverytag: number; 119 | redelivered: boolean; 120 | exchange: string; 121 | routingkey: string; 122 | contenttype: string | null; 123 | contentencoding: string | null; 124 | deliverymode: string | null; 125 | priority: number | null; 126 | correlationid: string | null; 127 | replyto: string | null; 128 | expiration: string | null; 129 | messageid: string | null; 130 | timestamp: number | null; 131 | type: string | null; 132 | userid: string | null; 133 | appid: string | null; 134 | clusterid: string | null; 135 | project_id?: number | null; 136 | created_at?: string | null; 137 | first_death_reason?: string | null; 138 | first_death_queue?: string | null; 139 | first_death_exchange?: string | null; 140 | }; 141 | 142 | export interface GridCellExpandProps { 143 | value: any; 144 | width: number; 145 | } 146 | 147 | export type renderCellExpandParams = { 148 | id: number; 149 | field: string; 150 | api?: any; 151 | cellMode: string; 152 | colDef?: any; 153 | formattedValue: string; 154 | value: string; 155 | columns: any; 156 | }; 157 | 158 | export type Columns = Array<{ 159 | field: string; 160 | headerName: string; 161 | renderCell: any; 162 | flex: number; 163 | }>; 164 | 165 | export type Rows = Array<{ 166 | id: number; 167 | consumerTag: string; 168 | deliveryTag: number; 169 | redelivered: boolean; 170 | exchange: string; 171 | routingKey: string; 172 | contentType: string | null; 173 | contentEncoding: string | null; 174 | deliveryMode: string | null; 175 | priority: number | null; 176 | correlationId: string | null; 177 | replyTo: string | null; 178 | expiration: string | null; 179 | messageId: string | null; 180 | timestamp: string; 181 | type: string | null; 182 | userId: string | null; 183 | appId: string | null; 184 | clusterId: string | null; 185 | }>; 186 | // End types for '/src/Components/DeadLetterMessage' 187 | 188 | // Types for AddProjectModal.tsx 189 | export interface ModalProps { 190 | isShown: boolean; 191 | handleClose: () => void; 192 | handleSave: () => void; 193 | headerText: string; 194 | setNameErr: React.Dispatch>; 195 | setURLErr: React.Dispatch>; 196 | projectNameError: boolean; 197 | projectURLError: boolean; 198 | } 199 | // End types for AddProjectModal.tsx -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 4 | require("dotenv").config(); 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | entry: { 9 | bundle: path.resolve(__dirname, "./src/index.tsx"), 10 | }, 11 | output: { 12 | filename: "bundle.js", 13 | publicPath: "/", 14 | path: path.resolve(__dirname, "build"), 15 | }, 16 | externals: { 17 | express: "express", 18 | }, 19 | plugins: [ 20 | // HTML Webpack Plugin to generate an HTML file with injected bundle.js 21 | new HtmlWebpackPlugin({ 22 | title: "RabbitTracks", 23 | template: "index.html", 24 | favicon: "./src/assets/images/favicon.ico", 25 | }), 26 | // Node Polyfill Plugin to provide polyfills for Node.js core modules 27 | new NodePolyfillPlugin(), 28 | ], 29 | devServer: { 30 | // Serve static files from the build directory 31 | static: { 32 | publicPath: "/", 33 | directory: path.resolve(__dirname, "build"), 34 | }, 35 | hot: true, 36 | proxy: { 37 | "*": "http://localhost:3000", 38 | }, 39 | }, 40 | resolve: { 41 | extensions: [".tsx", ".jsx", ".ts", ".js"], 42 | fallback: { 43 | fs: false, 44 | net: false, 45 | async_hooks: false, 46 | }, 47 | }, 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.jsx?/, // Transpile JavaScript and JSX files 52 | exclude: /node_modules/, 53 | use: { 54 | loader: "babel-loader", 55 | options: { 56 | presets: [ 57 | "@babel/preset-env", 58 | "@babel/preset-react", 59 | "@babel/preset-typescript", 60 | ], 61 | }, 62 | }, 63 | }, 64 | { 65 | test: /\.(ts|tsx)$/, // Transpile TypeScript files 66 | exclude: /node_modules/, 67 | use: ["ts-loader"], 68 | }, 69 | { 70 | test: /\.s[ac]ss$/i, // Process SCSS files 71 | exclude: /node_modules/, 72 | use: ["style-loader", "css-loader", "sass-loader"], 73 | }, 74 | { 75 | test: /\.(png|jpe?g|gif)$/i, // Load image files 76 | use: [ 77 | { 78 | loader: "file-loader", 79 | options: { 80 | name: "[path][name].[ext]", // Output file name and path 81 | }, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | }; 88 | --------------------------------------------------------------------------------