├── .gitignore
├── Dockerfile
├── Dockerfile-dev
├── Dockerfile-postgres
├── Dockerfile-server
├── LICENSE
├── README.md
├── backend
├── .aws
│ └── config
├── assume-role.js
├── cloudwatch.js
├── controllers
│ └── authMiddleware.js
├── data.js
├── externalIDGenerator.js
├── fetch.js
├── metrics.js
├── modal.js
├── routes
│ └── ApiRoutes.js
├── server.js
└── serverDelete.js
├── docker-compose-prod.yaml
├── eslint.config.js
├── index.html
├── nginx
└── default.conf
├── package-lock.json
├── package.json
├── src
├── icon.png
├── index.css
├── main.jsx
├── pages
│ ├── Dashboard.jsx
│ ├── Error.jsx
│ ├── Home.jsx
│ ├── Layout.jsx
│ ├── components
│ │ ├── Barchart.jsx
│ │ ├── Linechart.jsx
│ │ ├── ProtectedRoutes.jsx
│ │ └── menu.jsx
│ ├── context
│ │ └── AuthContext.jsx
│ ├── login
│ │ ├── Forgot.tsx
│ │ ├── Login.tsx
│ │ ├── Signup.tsx
│ │ └── Verification.tsx
│ └── newUserProfile.jsx
└── state
│ ├── graph-reducer.js
│ └── store.js
├── tailwind.config.js
├── tsconfig.json
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | #userPool
2 |
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | pnpm-debug.log*
11 | lerna-debug.log*
12 |
13 | node_modules
14 | .env
15 | dist
16 | dist-ssr
17 | *.local
18 |
19 | # Editor directories and fil-es
20 | .vscode/*
21 | !.vscode/extensions.json
22 | .idea
23 | .DS_Store
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | # Aws
31 | backend/.aws/*
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use an official Node.js image.
2 | FROM node:23.6.0 AS builder
3 |
4 | WORKDIR /app
5 | COPY package*.json ./
6 | RUN npm install
7 | COPY ./ /app
8 | RUN npm run build
9 |
10 | #Running Production
11 | FROM nginx:stable-alpine AS production
12 | COPY --from=builder /app/nginx /etc/nginx/conf.d
13 | COPY --from=builder /app/dist /usr/share/nginx/html
14 | EXPOSE 80
15 | # Run the dev server.
16 | ENTRYPOINT ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/Dockerfile-dev:
--------------------------------------------------------------------------------
1 | FROM node:16.13
2 | WORKDIR /app
3 | COPY . /app
4 | RUN npm install
5 | EXPOSE 80
6 | ENTRYPOINT ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/Dockerfile-postgres:
--------------------------------------------------------------------------------
1 | FROM node:23.6.0
2 |
3 |
--------------------------------------------------------------------------------
/Dockerfile-server:
--------------------------------------------------------------------------------
1 | # Use an official Node.js image.
2 | FROM node:23.6.0
3 |
4 | # Set the working directory.
5 | WORKDIR /server
6 | COPY ./package.json /server
7 | RUN npm install
8 |
9 | # Copy package files and install dependencies.
10 | COPY ./backend /server
11 |
12 | # Copy the rest of your application code.
13 | # Expose the port Vite is running on.
14 | EXPOSE 81
15 |
16 | # Run the dev server.
17 | CMD ["npm", "run", "server-docker"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Open Source Labs 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 |
4 |
5 |
6 |
7 |
8 |
AWSome
9 |
Real-Time AWS EC2 Instance Monitoring Tool
10 |
11 |
12 |
13 |
14 | ## Table of Contents
15 |
16 | About The Project
17 | Technologies Used
18 | Getting Started
19 | Usage
20 | Roadmap
21 | Contributing
22 | License
23 | Contact
24 | Acknowledgments
25 |
26 |
27 |
28 |
29 | ## About The Project
30 |
31 | AWSome is a real-time AWS EC2 instance monitoring tool designed to provide comprehensive insights into your EC2 instances using AWS CloudWatch. The project integrates with various AWS services such as Cognito for user authentication, IAM roles for permissions, and EC2 for instance management. The tool offers an interactive user interface built with React, allowing users to view EC2 instance metrics, generate heat maps, and analyze trends.
32 |
33 | ### Key Features:
34 | - Real-time EC2 instance monitoring
35 | - Interactive visualizations using graphs
36 | - Customizable thresholds and alarms for memory usage and CPU load
37 | - Secure user authentication with AWS Cognito
38 | - User-friendly interface built with React and Redux
39 |
40 | ### Built With
41 | This project uses several technologies to provide a full-stack solution for AWS EC2 monitoring:
42 |
43 | - [](https://reactjs.org/) - Frontend framework for building interactive UIs
44 | - [](https://aws.amazon.com/cognito/) - User authentication and authorization
45 | - [](https://aws.amazon.com/ec2/) - Cloud computing platform for running instances
46 | - [](https://aws.amazon.com/cloudwatch/) - Monitoring and logging service
47 | - [](https://aws.amazon.com/iam/) - Identity and access management
48 | - [](https://redux.js.org/) - State management for React
49 |
50 | (back to top )
51 |
52 | ## Getting Started
53 | If you'd like to learn more visit our website here
54 | Website
55 |
56 | If you'd like to use our app you can visit the following link below.
57 | AWSome Tool
58 |
59 |
60 | Otherwise, if you'd like to contribute to our product you can follow the steps below to get the development environment started!
61 |
62 | ### Installation
63 | //steps on accessing our deployed app, and our landing page
64 |
65 | ### Prerequisites
66 | - Node.js
67 | - AWS access
68 | - NPM
69 | - Docker
70 |
71 | (back to top )
72 |
73 | Usage
74 | AWSome provides an intuitive interface for managing and monitoring your EC2 instances. You can:
75 |
76 | View EC2 instance metrics such as CPU usage, memory load, and disk I/O in real-time.
77 | Set custom alarms to get notified when instance thresholds are exceeded.
78 | Use trend analysis to visualize your AWS EC2 instance performance.
79 |
80 |
81 | For more examples, please refer to the Documentation
82 |
83 | (back to top )
84 |
85 | ## Roadmap
86 | - [x] Add support for viewing EC2 instance performance metrics in real-time
87 | - [x] Integrate AWS CloudWatch for detailed metrics visualization
88 | - [x] Add authorization for a user to view their own metrics
89 | - [ ] Add Dark Mode
90 | - [ ] Improve UI responsiveness and accessibility
91 |
92 | See the open issues for a full list of proposed features and known issues.
93 |
94 | (back to top )
95 | Contributing
96 | ## Contributing
97 |
98 | We welcome contributions from the open-source community! Here's how you can contribute to AWSome:
99 |
100 | ## 🚀 Contributing to Our AWS Monitoring Tool
101 |
102 | We ❤️ contributions from the community! Follow these steps to get involved:
103 |
104 | ### 1️⃣ Fork the Repo
105 | Click the **"Fork"** button at the top-right corner of this page to create your own copy.
106 |
107 | ### 2️⃣ Create a Feature Branch
108 | ```sh
109 | git checkout -b feature/yourNewFeatureName
110 | ```
111 |
112 | ### 3️⃣ Build Something Awesome 🛠️
113 | - **Implement Your Feature**: Add your code, tests, or documentation.
114 | - **Ensure Your Changes Align with the Project's Goals**: Make sure your work fits the overall vision.
115 | - **Keep Your Implementation Clean, Modular, and Well-Documented**: Strive for readable, reusable, and well-commented code.
116 |
117 | ### 4️⃣ Commit Your Changes ✅
118 | ```sh
119 | git commit -m "✨ Added [your-new-feature-description]"
120 | ```
121 |
122 | ### 5️⃣ Push to Your Branch 🚀
123 | ```sh
124 | git push origin feature/yourNewFeatureName
125 | ```
126 |
127 | ### 6️⃣ Open a Pull Request (PR) 🔄
128 | - Navigate to your fork on GitHub.
129 | - Click "New Pull Request" and select the dev branch as the base (unless otherwise specified).
130 | - Provide a detailed description of your changes.
131 | - Submit the PR and wait for our team to review it.
132 |
133 | ### 7️⃣ Celebrate! 🎉
134 | Once approved, your contribution will be merged into the project. Thank you for helping us improve this tool for developers!
135 |
136 | 💡 Need help? Feel free to open an issue or reach out to us in discussions! 🚀
137 |
138 | (back to top )
139 |
140 | ## License
141 |
142 | Distributed under the Unlicense License. See `LICENSE.txt` for more information.
143 |
144 | (back to top )
145 |
146 | ### Top contributors:
147 |
148 |
149 |
150 |
151 |
152 | Jayson Sanon
153 |
154 | 💼
155 | 💻
156 |
157 |
158 |
159 |
160 | Jose Andrew
161 |
162 | 💼
163 | 💻
164 |
165 |
166 |
167 |
168 | Christina Abraham
169 |
170 | 💼
171 | 💻
172 |
173 |
174 |
175 |
176 | Elijah Egede
177 |
178 | 💼
179 | 💻
180 |
181 |
182 |
183 |
184 | Nathalie Owusu
185 |
186 | 💼
187 | 💻
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | ## Contact
197 |
198 | Feel free to contact us with any questions or suggestions:
199 |
200 | - Jayson: 💼 Jaysonsanon@gmail.com
201 | - Jose: 💼 Joseandrew@live.com
202 | - Christina: 💼 Christinajabraham@yahoo.com
203 | - Elijah: 💼 Eegede22@gmail.com
204 | - Salem: 💼 Nathalieowusudev@gmail.com
205 |
206 | (back to top )
207 |
208 | ## Acknowledgments
209 |
210 | Special thanks to the following resources:
211 |
212 | - [AWS Documentation](https://aws.amazon.com/documentation/)
213 | - [React Documentation](https://reactjs.org/docs/getting-started.html)
214 | - [Redux Documentation](https://redux.js.org/introduction/getting-started)
215 | - [AWS CloudWatch Documentation](https://docs.aws.amazon.com/cloudwatch/)
216 | - [Choose an Open Source License](https://choosealicense.com)
217 |
218 |
219 | ### Highlights:
220 | - **Technologies**: React, AWS (Cognito, CloudWatch, EC2), Redux, IAM Roles
221 | - **Project Features**: EC2 instance monitoring, CloudWatch visualizations, real-time analytics
222 | - **Usage**: Setting up AWS credentials and visualizing EC2 instance metrics
223 | - **Roadmap**: List of future features like automated scaling recommendations and email/SMS notifications
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
--------------------------------------------------------------------------------
/backend/.aws/config:
--------------------------------------------------------------------------------
1 | [default]
2 | region=us-east-1
3 | output=json
--------------------------------------------------------------------------------
/backend/assume-role.js:
--------------------------------------------------------------------------------
1 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; // ES Modules import
2 |
3 | const REGION = "us-east-1";
4 |
5 | const client = new STSClient({ region: REGION });
6 | const input = { // AssumeRoleRequest
7 | RoleArn: "arn:aws:iam::536697262297:role/AWSome-help", // required
8 | RoleSessionName: "test-session-1", // required
9 | DurationSeconds: 3600,
10 | ExternalId: "UserJayson",
11 | };
12 |
13 | const command = new AssumeRoleCommand(input);
14 |
15 | try {
16 | const response = await client.send(command);
17 | console.log(response);
18 | } catch (error) {
19 | console.error(error);
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/backend/cloudwatch.js:
--------------------------------------------------------------------------------
1 | import {
2 | CloudWatchClient, // Used to connect to CloudWatch
3 | CloudWatchServiceException, // Handles specific errors from CloudWatch
4 | GetMetricDataCommand, // Sends a request to fetch metric data
5 | } from '@aws-sdk/client-cloudwatch';
6 | import { fromIni } from '@aws-sdk/credential-providers';
7 | const region = 'us-east-1';
8 |
9 | // let log = {
10 | // metric: ['NetworkIn'],
11 | // data: [],
12 | // graph: ['bar'],
13 | // };
14 |
15 | async function MixedMetrix(metricMap) {
16 | const client = new CloudWatchClient({
17 | region,
18 | credentials: fromIni({
19 | profile: 'default',
20 | }),
21 | });
22 | let response;
23 | const InstanceId = 'i-0610f2356e0d72fcd';
24 |
25 | const { metric, data, graph } = metricMap;
26 | const queries = [];
27 |
28 | let currentTime = new Date();
29 | currentTime.setSeconds(0);
30 | while (currentTime.getMinutes() % 5 !== 0) {
31 | currentTime.setMinutes(currentTime.getMinutes() - 1);
32 | }
33 |
34 | let pastHour = new Date(currentTime);
35 | pastHour.setHours(pastHour.getHours() - 1);
36 |
37 | let num = 0;
38 |
39 | for (let i = 0; i < metric.length; i++) {
40 | let id = metric[i].toLowerCase() + graph[i] + num.toString();
41 | num++;
42 | queries.push(
43 | {
44 | // MetricDataQuery
45 | Id: id, // required
46 | MetricStat: {
47 | // MetricStat
48 | Metric: {
49 | // Metric
50 | Namespace: 'AWS/EC2',
51 | MetricName: metric[i],
52 | Dimensions: [
53 | // Dimensions
54 | {
55 | // Dimension
56 | Name: 'InstanceId', // required
57 | Value: InstanceId, // required
58 | },
59 | ],
60 | },
61 | Period: 300, // required
62 | Stat: 'Average', // required
63 | }, // Lines 17-38 fetches the CPU usage for the EC2 instance
64 | } // Data is averaged over 5 min (period:300 seconds) resulting in a percentage format (Unit:percent)
65 | );
66 | }
67 |
68 | const input = {
69 | // Input object describes the data we're requesting from CloudWatch
70 | // GetMetricDataInput
71 | MetricDataQueries: queries,
72 | StartTime: pastHour, // required
73 | EndTime: currentTime, // required
74 | ScanBy: 'TimestampAscending', // Gets the newest data first
75 | MaxDatapoints: 10000, // Max Datapoints 100,000
76 | };
77 |
78 | const command = new GetMetricDataCommand(input); //Creates the request to send to CloudWatch using the input
79 | try {
80 | // Sends the request and waits for the response
81 | response = await client.send(command);
82 | //console.log('response meta: ', response);
83 |
84 | return response.MetricDataResults; // logs the metric data and entire response if successful
85 | } catch (caught) {
86 | if (caught instanceof CloudWatchServiceException) {
87 | // if theres a CloudWatch error, it logs the error name and message
88 | console.error(`Error from CloudWatch. ${caught.name}: ${caught.message}`);
89 | } else {
90 | throw caught; // if its a different error, it throws it so it can be handled elsewhere
91 | }
92 | }
93 | }
94 |
95 | //MixedMetrix(log);
96 |
97 | export default MixedMetrix;
98 |
--------------------------------------------------------------------------------
/backend/controllers/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import jwksClient from 'jwks-rsa';
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | //cognito issuer url
7 | const COGNITO_ISSUER = `https://cognito-idp.us-east-1.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`;
8 |
9 | //this client is configured to fetch public keys (JWT)
10 | const client = jwksClient({
11 | jwksUri: `${COGNITO_ISSUER}/.well-known/jwks.json`,
12 | });
13 |
14 | //function to get signing key
15 | const getKey = async (header) => {
16 | try {
17 | const key = await client.getSigningKey(header.kid);
18 | return key.getPublicKey();
19 | } catch (err) {
20 | console.error('Error retrieving signing key:', err);
21 | }
22 | };
23 |
24 | const authenticateToken = async (req, res, next) => {
25 | try {
26 | //grabs the first token (access token) sent in the header otherwise just returns null or undefined
27 | const token = req.headers.authorization?.split(' ')[1];
28 |
29 | //if token is nonexisten, return error
30 | if (!token) {
31 | return res
32 | .status(401)
33 | .json({ error: 'Access denied. No token provided' });
34 | }
35 | //if token is not properly fetched and sent (incorrect token)
36 | if (token.split('.').length !== 3) {
37 | return res.status(400).json({ error: 'Malformed JWT token' });
38 | }
39 |
40 | // Decode the JWT to get the kid (key id)
41 | const decodedHeader = jwt.decode(token, { complete: true })?.header;
42 | //if decodedheader exists, grab the key id
43 | const kid = decodedHeader?.kid;
44 |
45 | //if no key id, return error
46 | if (!kid) {
47 | return res
48 | .status(400)
49 | .json({ error: 'Invalid token: Missing kid in token header' });
50 | }
51 | // Fetch the public key using the kid
52 | const publicKey = await getKey({ kid });
53 |
54 | // Verify the JWT using the public key
55 | const decoded = jwt.verify(token, publicKey, { issuer: COGNITO_ISSUER });
56 |
57 | req.user = decoded; // Attach the decoded user to the request object
58 | next();
59 | } catch (err) {
60 | console.error('Error authenticating token:', err);
61 | return res.status(403).json({ error: 'Access denied' });
62 | }
63 | };
64 |
65 | export default authenticateToken;
66 |
--------------------------------------------------------------------------------
/backend/data.js:
--------------------------------------------------------------------------------
1 | import {
2 | CloudWatchClient, // Used to connect to CloudWatch
3 | CloudWatchServiceException, // Handles specific errors from CloudWatch
4 | GetMetricDataCommand, // Sends a request to fetch metric data
5 | } from '@aws-sdk/client-cloudwatch';
6 | import awsCredentialProviders from '@aws-sdk/credential-providers';
7 |
8 | const { fromSSO } = awsCredentialProviders;
9 |
10 | const credentials = await fromSSO({
11 | profile: 'AdministratorAccess-913524940612',
12 | });
13 | const client = new CloudWatchClient({
14 | region: 'us-east-1',
15 | credentials,
16 | });
17 |
18 |
19 | import pkg from 'pg';
20 | const { Pool } = pkg;
21 |
22 | let response;
23 |
24 | export { client };
25 |
26 | export const awsData = async () => {
27 | // Starts the awsData function, marked as async because it handles promises (waiting for AWS data)
28 | const input = {
29 | // Input object describes the data we're requesting from CloudWatch
30 | // GetMetricDataInput
31 | MetricDataQueries: [
32 | // A list of all the metrics we want
33 | // MetricDataQueries // required
34 | {
35 | // MetricDataQuery
36 | Id: 'cpuutilization', // required
37 | MetricStat: {
38 | // MetricStat
39 | Metric: {
40 | // Metric
41 | Namespace: 'AWS/EC2',
42 | MetricName: 'CPUUtilization',
43 | Dimensions: [
44 | // Dimensions
45 | {
46 | // Dimension
47 | Name: 'InstanceId', // required
48 | Value: 'i-03dc51c409e900f26', // required
49 | },
50 | ],
51 | },
52 | Period: 300, // required
53 | Stat: 'Average', // required
54 | Unit: 'Percent',
55 | }, // Lines 17-38 fetches the CPU usage for the EC2 instance
56 | }, // Data is averaged over 5 min (period:300 seconds) resulting in a percentage format (Unit:percent)
57 | {
58 | // MetricDataQuery
59 | Id: 'networkin', // required
60 | MetricStat: {
61 | // MetricStat
62 | Metric: {
63 | // Metric
64 | Namespace: 'AWS/EC2',
65 | MetricName: 'NetworkIn',
66 | Dimensions: [
67 | // Dimensions
68 | {
69 | // Dimension
70 | Name: 'InstanceId', // required
71 | Value: 'i-03dc51c409e900f26', // required
72 | },
73 | ],
74 | },
75 | Period: 300, // required
76 | Stat: 'Average', // required
77 | Unit: 'Bytes',
78 | }, // 41-60 fetching the incoming network traffic (NetworkIn) in Bytes
79 | },
80 | {
81 | // MetricDataQuery
82 | Id: 'networkout', // required
83 | MetricStat: {
84 | // MetricStat
85 | Metric: {
86 | // Metric
87 | Namespace: 'AWS/EC2',
88 | MetricName: 'NetworkOut',
89 | Dimensions: [
90 | // Dimensions
91 | {
92 | // Dimension
93 | Name: 'InstanceId', // required
94 | Value: 'i-03dc51c409e900f26', // required
95 | },
96 | ],
97 | },
98 | Period: 300, // required
99 | Stat: 'Average', // required
100 | Unit: 'Bytes',
101 | }, // lines 64-83 fetching the outgoing network traffic (NetworkOut) in Bytes
102 | },
103 | {
104 | // MetricDataQuery
105 | Id: 'write', // required
106 | MetricStat: {
107 | // MetricStat
108 | Metric: {
109 | // Metric
110 | Namespace: 'AWS/EC2',
111 | MetricName: 'EBSWriteOps',
112 | Dimensions: [
113 | // Dimensions
114 | {
115 | // Dimension
116 | Name: 'InstanceId', // required
117 | Value: 'i-03dc51c409e900f26', // required
118 | },
119 | ],
120 | },
121 | Period: 300, // required
122 | Stat: 'Average', // required
123 | Unit: 'Count',
124 | }, //lines 87-106 fetches write operations on the instance's disk (EBSWriteOps)
125 | },
126 | {
127 | // MetricDataQuery
128 | Id: 'read', // required
129 | MetricStat: {
130 | // MetricStat
131 | Metric: {
132 | // Metric
133 | Namespace: 'AWS/EC2',
134 | MetricName: 'EBSReadOps',
135 | Dimensions: [
136 | // Dimensions
137 | {
138 | // Dimension
139 | Name: 'InstanceId', // required
140 | Value: 'i-03dc51c409e900f26', // required
141 | },
142 | ],
143 | },
144 | Period: 300, // required
145 | Stat: 'Average', // required
146 | Unit: 'Count',
147 | }, //lines 110-129 fetches read operations on the instance's disk (EBSReadOps)
148 | },
149 | ],
150 | StartTime: new Date('2025-01-18T23:05:00.000Z'), // required
151 | EndTime: new Date('2025-01-18T23:30:00.000Z'), // required
152 | ScanBy: 'TimestampDescending', // Gets the newest data first
153 | MaxDatapoints: 1000, // Max Datapoints 100,000
154 | };
155 |
156 | const command = new GetMetricDataCommand(input); //Creates the request to send to CloudWatch using the input
157 | try {
158 | // Sends the request and waits for the response
159 | response = await client.send(command);
160 | return response; // logs the metric data and entire response if successful
161 | } catch (caught) {
162 | if (caught instanceof CloudWatchServiceException) {
163 | // if theres a CloudWatch error, it logs the error name and message
164 | console.error(`Error from CloudWatch. ${caught.name}: ${caught.message}`);
165 | } else {
166 | throw caught; // if its a different error, it throws it so it can be handled elsewhere
167 | }
168 | }
169 |
170 | console.log('Inserting metrics into database...');
171 | for (const metric of response.MetricDataResults) {
172 | for (let i = 0; i < metric.Timestamps.length; i++) {
173 | // console.log('Metric:', metric.Id, 'Value:', metric.Values[i]); // Debugging output
174 | const query = `
175 | INSERT INTO aws_metrics
176 | (aws_account_id, metric_name, metric_value, timestamp, service_name, region)
177 | VALUES ($1, $2, $3, $4, $5, $6)
178 | `;
179 | const values = [
180 | 1, // Replace with the actual AWS account ID
181 | metric.Id,
182 | metric.Values[i],
183 | metric.Timestamps[i],
184 | 'EC2',
185 | 'us-east-1',
186 | ];
187 |
188 | await Pool.query(query, values);
189 | }
190 | }
191 | // console.log('Metrics inserted successfully!');
192 | };
193 |
194 | // ✅ Correctly formatted IIFE to execute `awsData()`
195 | // (async () => {
196 | // const data = await awsData();
197 | // console.log("Final response:", data);
198 | // })();
199 |
200 | export const awsHourData = async () => {
201 | const input = {
202 | // GetMetricDataInput
203 | MetricDataQueries: [
204 | // MetricDataQueries // required
205 | {
206 | // MetricDataQuery
207 | Id: 'cpuutilization', // required
208 | MetricStat: {
209 | // MetricStat
210 | Metric: {
211 | // Metric
212 | Namespace: 'AWS/EC2',
213 | MetricName: 'CPUUtilization',
214 | Dimensions: [
215 | // Dimensions
216 | {
217 | // Dimension
218 | Name: 'InstanceId', // required
219 | Value: 'i-02b1d8cc57898903d', // required
220 | },
221 | ],
222 | },
223 | Period: 300, // required
224 | Stat: 'Average', // required
225 | Unit: 'Percent',
226 | }, // Lines 17-38 fetches the CPU usage for the EC2 instance
227 | }, // Data is averaged over 5 min (period:300 seconds) resulting in a percentage format (Unit:percent)
228 | ],
229 | StartTime: new Date('2025-01-27T18:50:00.000Z'), // required
230 | EndTime: new Date('2025-01-27T19:55:00.000Z'), // required
231 | ScanBy: 'TimestampAscending', // Gets the oldest data first
232 | MaxDatapoints: 1000, // Max Datapoints 100,000
233 | };
234 | const command = new GetMetricDataCommand(input); //Creates the request to send to CloudWatch using the input
235 | try {
236 | // Sends the request and waits for the response
237 | const response = await client.send(command);
238 | //console.log('response results', response.MetricDataResults);
239 | return response.MetricDataResults; // logs the metric data and entire response if successful
240 | } catch (caught) {
241 | if (caught instanceof CloudWatchServiceException) {
242 | // if theres a CloudWatch error, it logs the error name and message
243 | // console.error(`Error from CloudWatch. ${caught.name}: ${caught.message}`);
244 | } else {
245 | console.log('why is it spicy');
246 | throw caught; // if its a different error, it throws it so it can be handled elsewhere
247 | }
248 | }
249 | };
250 |
--------------------------------------------------------------------------------
/backend/externalIDGenerator.js:
--------------------------------------------------------------------------------
1 | const externalIdGenerator = () => {
2 | const letters = '1234567890qwertyuiopasdfghjklzxcvbnm';
3 | let result = '';
4 | for (let i = 0; i < 10; i++) {
5 | result += letters[Math.floor(Math.random() * 35)];
6 | }
7 | return result;
8 | };
9 |
10 | export default externalIdGenerator;
11 |
--------------------------------------------------------------------------------
/backend/fetch.js:
--------------------------------------------------------------------------------
1 | export async function AWSdata(graphs) {
2 | try {
3 | let res = await fetch('http://localhost:81/data', {
4 | headers: {
5 | 'Content-Type': 'application/json',
6 | },
7 | method: 'POST',
8 | body: JSON.stringify(graphs),
9 | });
10 |
11 | if (!res.ok) {
12 | throw new Error('Network response was not ok');
13 | }
14 |
15 | let data = await res.json();
16 |
17 | return data;
18 | } catch (error) {
19 | console.error(error);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/metrics.js:
--------------------------------------------------------------------------------
1 | import client from './modal.js'; // Import PostgreSQL client from modal.js
2 | import { awsData } from './data.js'; // Import the awsData function from data.js
3 |
4 | // Function to transform fetched metric data
5 | const transformMetrics = (metricResults) => {
6 | return metricResults.map((result) => ({
7 | metricName: result.Id,
8 | timestamps: result.Timestamps || [],
9 | values: result.Values || [],
10 | }));
11 | };
12 | // Function to save the transformed metrics data into PostgreSQL (using UPSERT)
13 | const saveMetricsToDatabase = async (awsAccountId, transformedMetrics) => {
14 | for (const metric of transformedMetrics) {
15 | for (let i = 0; i < metric.timestamps.length; i++) {
16 | // Call updateMetric to handle insert/update logic
17 | await updateMetric(
18 | awsAccountId,
19 | metric.metricName,
20 | null, // instanceId (if available, otherwise null)
21 | metric.values[i],
22 | metric.timestamps[i],
23 | 'Average', // Stat (you can modify based on data)
24 | '%', // Unit (you can modify based on data)
25 | 60 // Period (you can modify based on data)
26 | );
27 | }
28 | }
29 | };
30 | // Function to update or insert the metric using UPSERT logic
31 | async function updateMetric(
32 | awsAccountId,
33 | metricName,
34 | instanceId,
35 | metricValue,
36 | timestamp,
37 | stat,
38 | unit,
39 | period
40 | ) {
41 | const query = `
42 | INSERT INTO aws_metrics (aws_account_id, metric_name, instance_id, metric_value, timestamp, stat, unit, period)
43 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
44 | ON CONFLICT (aws_account_id, metric_name, timestamp)
45 | DO UPDATE
46 | SET
47 | metric_value = $4,
48 | stat = $6,
49 | unit = $7,
50 | period = $8;
51 | `;
52 |
53 | try {
54 | // Execute the UPSERT query with the provided parameters
55 | await client.query(query, [
56 | awsAccountId,
57 | metricName,
58 | instanceId,
59 | metricValue,
60 | timestamp,
61 | stat,
62 | unit,
63 | period,
64 | ]);
65 | console.log('Metric updated successfully');
66 | } catch (err) {
67 | console.error('Error updating metric:', err);
68 | }
69 | }
70 |
71 | // Function to get AWS account ID by user email
72 | const getAwsAccountIdByEmail = async (email) => {
73 | const query = `SELECT aws_account_id FROM aws_accounts WHERE user_id = (SELECT id FROM users WHERE email = $1)`;
74 | const result = await client.query(query, [email]);
75 | if (result.rows.length === 0) {
76 | throw new Error('AWS account not found for the provided email.');
77 | }
78 | return result.rows[0].aws_account_id;
79 | };
80 |
81 | // Orchestrating the full workflow
82 | const Workflow = async () => {
83 | try {
84 | // Get the AWS account ID from the email
85 | const awsAccountId = await getAwsAccountIdByEmail('salem.moon@icloud.com');
86 | if (!awsAccountId) {
87 | throw new Error('No AWS account found for the provided email.');
88 | }
89 | //CAN BE DELETED --> testing Account ID
90 | console.log('AWS Account ID:', awsAccountId);
91 | // Fetch the data from AWS CloudWatch
92 | const data = await awsData();
93 | console.log('Fetched AWS Data:', data); // Log the raw AWS data to inspect it
94 |
95 | // Check if data.MetricDataResults exists
96 | if (!data.MetricDataResults) {
97 | throw new Error('MetricDataResults not found in AWS data.');
98 | }
99 |
100 | // Transform the data
101 | const transformedMetrics = transformMetrics(data.MetricDataResults);
102 | console.log('Transformed Metrics:', transformedMetrics); // Log the transformed metrics
103 |
104 | // Save the transformed metrics into the database
105 | await saveMetricsToDatabase(awsAccountId, transformedMetrics);
106 | } catch (err) {
107 | console.error('Error in the workflow:', err);
108 | } finally {
109 | await client.end(); // Close connection after all queries
110 | }
111 | };
112 |
113 | // Call Workflow to initiate the process
114 | Workflow();
115 |
--------------------------------------------------------------------------------
/backend/modal.js:
--------------------------------------------------------------------------------
1 | import pg from 'pg';
2 |
3 | // Create a PostgreSQL client instance
4 | const client = new pg.Client({
5 | user: 'awsome_members', // Your PostgreSQL username
6 | host: 'localhost', // If PostgreSQL is running locally
7 | database: 'aws_monitoring', // The database you created
8 | password: 'AWSomepassword', // Your PostgreSQL user's password
9 | port: 5432, // Default PostgreSQL port
10 | });
11 |
12 | // Connect to the PostgreSQL database
13 | client
14 | .connect()
15 | .then(() => console.log('Connected to PostgreSQL!'))
16 | .catch((err) => console.log('uncomment below in modal:', err));
17 | //console.error("Error connecting to PostgreSQL:", err)
18 |
19 | export default client; // Export the client instance for use in other modules
20 |
--------------------------------------------------------------------------------
/backend/routes/ApiRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import client from "../modal.js";
3 |
4 | const Awsrouter = express.Router();
5 |
6 | // get all monitored AWS metrics
7 | Awsrouter.get("/", async (req, res) => {
8 | try {
9 | const results = await client.query("SELECT * FROM aws_metrics");
10 | res.json(results.rows);
11 | } catch (err) {
12 | console.error("Error fetching AWS metrics:", err);
13 | res.status(500).send("Server error");
14 | }
15 | });
16 |
17 | //Add a new AWS metrics
18 | Awsrouter.post("/", async (req, res) => {
19 | try {
20 | const { metrics_name, status } = req.body;
21 | const result = await client.query(
22 | "INSERT INTO aws_metrics (metrics_name, status) VALUES ($1, $2) RETURNING *",
23 | [metrics_name, status]
24 | );
25 | res.json(result.rows[0]);
26 | } catch (err) {
27 | console.error("Error inserting AWS metrics:", err);
28 | res.status(500).send("Server error");
29 | }
30 | });
31 |
32 | export default Awsrouter;
33 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import externalIdGenerator from './externalIDGenerator.js';
4 | import MixedMetrix from './cloudwatch.js';
5 | import authenticateToken from './controllers/authMiddleware.js';
6 |
7 | const port = 81;
8 |
9 | const app = express();
10 |
11 | app.use(
12 | cors({
13 | origin: true,
14 | methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
15 | allowedHeaders: ['Content-Type'],
16 | credentials: true,
17 | preflightContinue: false,
18 | })
19 | );
20 | app.use(express.json());
21 | app.use(express.urlencoded({ extended: true }));
22 |
23 | app.get('/random', async (req, res) => {
24 | let id = await externalIdGenerator();
25 | return res.status(200).json({ id });
26 | });
27 |
28 | app.post('/data', async (req, res) => {
29 | const { graph, metric, data } = req.body;
30 | let result = await MixedMetrix({ graph, metric, data });
31 | return res.status(200).json({ result });
32 | });
33 |
34 | app.options('/data', async (req, res) => {
35 | const { graph, metric, data } = req.body;
36 | let result = await MixedMetrix({ graph, metric, data });
37 | return res.status(200).json({ result });
38 | });
39 |
40 | // WILL BE USED TO PROTECT ANY REQUESTS FOR DATA (ENSURES USER IS AUTHENTICATED)
41 | // app.get('/protected', authenticateToken, (req, res) => {
42 | // res.status(200).json('Success, accessed a protected route');
43 | // });
44 |
45 | app.use((req, res) => res.status(404).send('No Data'));
46 |
47 | app.use((err, req, res, next) => {
48 | const defaultErr = {
49 | log: 'Something Went Wrong in server 2',
50 | status: 500,
51 | message: { err: 'An error occurred' },
52 | };
53 | const errorObj = Object.assign({}, defaultErr, err);
54 | console.log(errorObj.log);
55 | return res.status(errorObj.status).json(errorObj.message);
56 | });
57 |
58 | app.listen(port, () => {
59 | console.log(`Server started at http://localhost:${port}`);
60 | });
61 |
--------------------------------------------------------------------------------
/backend/serverDelete.js:
--------------------------------------------------------------------------------
1 | //CAN BE DELETED
2 | // import express from 'express';
3 | // import { fileURLToPath } from 'node:url';
4 | // import path from 'node:path';
5 | // import { awsData, awsHourData } from './data.js';
6 | // import Awsrouter from './routes/ApiRoutes.js';
7 | // import externalIdGenerator from './externalIDGenerator.js';
8 |
9 | // const port = 3000;
10 | // const app = express();
11 |
12 | // const __dirname =
13 | // path.dirname(fileURLToPath(import.meta.url)) || path.resolve();
14 |
15 | // app.use(express.json());
16 | // app.use(express.urlencoded({ extended: true }));
17 | // app.use(express.static(path.join(__dirname, 'dist')));
18 |
19 | // //CAN BE DELETED, WAS ONCE A TEST ROUTER
20 | // app.use('/aws_services', Awsrouter);
21 |
22 | // //TEST ROUTE, can be deleted
23 | // app.get('/protected', authenticateToken, (req, res) => {
24 | // res.json({ message: 'You have accessed a protected route!', user: req.user });
25 | // });
26 |
27 | // //VITE CONFIG file allows for this to be just /data instead of /Home/data
28 | // app.get('/data', async (req, res) => {
29 | // let data = await awsHourData();
30 | // res.status(200).json(data);
31 | // });
32 |
33 |
34 |
35 | // app.use((req, res) =>
36 | // res.status(404).send("This is not the page you're looking for...")
37 | // );
38 |
39 | // //default global error handler
40 | // app.use((err, req, res, next) => {
41 | // const defaultErr = {
42 | // log: 'Express error handler caught unknown middleware error',
43 | // status: 500,
44 | // message: { err: 'An error occurred' },
45 | // };
46 | // const errorObj = Object.assign({}, defaultErr, err);
47 | // console.log(errorObj.log);
48 | // return res.status(errorObj.status).json(errorObj.message);
49 | // });
50 |
51 | // app.listen(port, () => {
52 | // console.log(`Server started at http://localhost:${port}`);
53 | // });
54 |
--------------------------------------------------------------------------------
/docker-compose-prod.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | dev:
3 | image: prod
4 | container_name: prod-c
5 | ports:
6 | - "80:80"
7 | networks:
8 | - mynetwork
9 | depends_on:
10 | - "server"
11 | server:
12 | image: server
13 | container_name: server-c
14 | networks:
15 | - mynetwork
16 | ports:
17 | - "81:81"
18 |
19 | networks:
20 | mynetwork:
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | AWSome: AWS Monitoring Tool
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | # Nginx listens on port 80 by default. You can change this if needed.
3 | listen 80;
4 |
5 | # Specifies your domain. Use "localhost" for local development or your domain name for production.
6 | server_name Awsome-prod;
7 |
8 | # The root directory that contains the `dist` folder generated after building your app.
9 | root /usr/share/nginx/html;
10 | index index.html;
11 |
12 | # Serve all routes and pages
13 | # Use the base name to serve all pages. In this case, the base name is "/".
14 | location / {
15 | try_files $uri /index.html =404;
16 |
17 |
18 |
19 |
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AWSome",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite & nodemon ./backend/server.js",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview & node ./backend/server.js",
11 | "server-docker": "node ./serve.js"
12 | },
13 | "dependencies": {
14 | "@aws-sdk/client-cloudwatch": "^3.731.1",
15 | "@aws-sdk/client-cognito-identity-provider": "^3.744.0",
16 | "@aws-sdk/client-s3": "^3.730.0",
17 | "@aws-sdk/client-sts": "^3.744.0",
18 | "@aws-sdk/credential-providers": "^3.744.0",
19 | "@headlessui/react": "^2.2.0",
20 | "@material-tailwind/react": "^2.1.10",
21 | "@reduxjs/toolkit": "^2.5.1",
22 | "@tailwindcss/vite": "^4.0.0",
23 | "amazon-cognito-identity-js": "^6.3.12",
24 | "aws-sdk": "^2.1692.0",
25 | "chart.js": "^4.4.7",
26 | "cors": "^2.8.5",
27 | "d3": "^7.9.0",
28 | "express": "^4.21.2",
29 | "jsonwebtoken": "^9.0.2",
30 | "jwks-rsa": "^3.1.0",
31 | "pg": "^8.13.1",
32 | "postgres": "^3.4.5",
33 | "react": "^18.3.1",
34 | "react-chartjs-2": "^5.3.0",
35 | "react-dom": "^18.3.1",
36 | "react-redux": "^9.2.0",
37 | "react-router": "^7.1.2",
38 | "react-router-dom": "^7.2.0",
39 | "uuid": "^11.0.5",
40 | "vite-plugin-node-polyfills": "^0.23.0"
41 | },
42 | "devDependencies": {
43 | "@eslint/js": "^9.17.0",
44 | "@types/react": "^18.3.18",
45 | "@types/react-dom": "^18.3.5",
46 | "@vitejs/plugin-react": "^4.3.4",
47 | "autoprefixer": "^10.4.20",
48 | "babel-plugin-transform-import-meta": "^2.3.2",
49 | "dotenv": "^16.4.7",
50 | "eslint": "^9.17.0",
51 | "eslint-plugin-react": "^7.37.2",
52 | "eslint-plugin-react-hooks": "^5.0.0",
53 | "eslint-plugin-react-refresh": "^0.4.16",
54 | "globals": "^15.14.0",
55 | "nodemon": "^3.1.9",
56 | "postcss": "^8.5.2",
57 | "tailwindcss": "^3.4.17",
58 | "typescript": "^5.7.3",
59 | "vite": "^6.0.11"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/AWSome/534d7ddad81a1cf7e02f59cd2ab70c698b3de3ed/src/icon.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html {
6 | background-color: rgb(255, 255, 255);
7 | }
8 | h1 {
9 | color: rgb(0, 0, 0);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import './index.css';
3 | import { createRoot } from 'react-dom/client';
4 | import Error from './pages/Error.jsx';
5 | import Login from './pages/login/Login.tsx';
6 | import Signup from './pages/login/Signup.tsx';
7 | import Layout from './pages/Layout.jsx';
8 | import Forgot from './pages/login/Forgot.tsx';
9 | import BarChart from './pages/components/Barchart.jsx';
10 | import { BrowserRouter, Routes, Route } from 'react-router';
11 | import LineChart from './pages/components/Linechart.jsx';
12 | import { AuthProvider } from './pages/context/AuthContext.jsx';
13 | import ProtectedRoute from './pages/components/ProtectedRoutes.jsx';
14 | import NewUser from './pages/newUserProfile.jsx';
15 | import Dashboard from './pages/Dashboard.jsx';
16 |
17 | //polyfill for global
18 | window.global = window;
19 |
20 | createRoot(document.getElementById('root')).render(
21 |
22 |
23 |
24 | {/* }> */}
25 | } />
26 | } />
27 | {/* */}
28 | }>
29 | } />
30 | } />
31 |
32 | } />
33 | } />
34 |
35 |
36 |
37 | );
38 | // removed strict mode
39 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import store from '../state/store.js';
2 | import Home from './Home.jsx';
3 | import { Provider } from 'react-redux';
4 |
5 | const Dashboard = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default Dashboard;
14 |
--------------------------------------------------------------------------------
/src/pages/Error.jsx:
--------------------------------------------------------------------------------
1 | function Error() {
2 | return (
3 | <>
4 | This is not the page you are looking for, sorry about that.
5 | >
6 | );
7 | }
8 |
9 | export default Error;
10 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router';
2 | import BarChart from './components/Barchart.jsx';
3 | import LineChart from './components/Linechart.jsx';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import DropDownMenu from './components/menu.jsx';
6 | import { useAuth } from './context/AuthContext.jsx';
7 | import { useState, useRef, useEffect } from 'react';
8 | import { getData } from '../state/graph-reducer.js';
9 | import { AWSdata } from '../../backend/fetch.js';
10 |
11 | const Home = () => {
12 | const { signOut, userSession } = useAuth();
13 | const navigate = useNavigate();
14 |
15 | useEffect(() => {
16 | if (!userSession) navigate('/');
17 | }, [userSession, navigate]);
18 |
19 | const logOut = () => {
20 | signOut();
21 | };
22 |
23 | const graphs = useSelector((state) => state.graphs);
24 | const dispatch = useDispatch();
25 | const newgraph = [];
26 |
27 | for (let i = 0; i < graphs.graph.length; i++) {
28 | if (graphs.graph[i] === 'bar') {
29 | let bar = (
30 |
34 |
{graphs.metric[i]}
35 |
36 |
37 | );
38 | newgraph.push(bar);
39 | } else if (graphs.graph[i] === 'areaLine') {
40 | let line = (
41 |
45 |
{graphs.metric[i]}
46 |
47 |
48 | );
49 |
50 | newgraph.push(line);
51 | }
52 | }
53 |
54 | const [isOpen, setIsOpen] = useState(false);
55 | const dropdownRef = useRef(null);
56 |
57 | const handleMouseEnter = () => {
58 | setIsOpen(true);
59 | };
60 |
61 | useEffect(() => {
62 | const handleClickOutside = (event) => {
63 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
64 | setIsOpen(false);
65 | }
66 | };
67 |
68 | document.addEventListener('mousedown', handleClickOutside);
69 | return () => {
70 | document.removeEventListener('mousedown', handleClickOutside);
71 | };
72 | }, []);
73 |
74 | return (
75 |
76 | {/* Navbar */}
77 |
113 |
114 | {/*
*/}
115 |
116 | {/* */}
117 |
118 | {/*Connect to User's AWS Account Button */}
119 |
120 | {/* {
123 | let data = await AWSdata(graphs);
124 | dispatch(getData({ data }));
125 | }}
126 | >
127 | Get Metrics
128 | */}
129 |
130 |
131 |
132 |
136 | {newgraph}
137 |
138 |
139 |
140 | {/* Footer */}
141 |
146 |
147 | );
148 | };
149 |
150 | export default Home;
151 |
--------------------------------------------------------------------------------
/src/pages/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router';
2 |
3 | //layout for navigation between pages
4 | function Layout() {
5 | //REFACTOR TO ADD NAVBAR COMPONENTS HERE, to reduce repetition of code
6 | return (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | export default Layout;
16 |
--------------------------------------------------------------------------------
/src/pages/components/Barchart.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS,
3 | CategoryScale,
4 | LinearScale,
5 | BarElement,
6 | Title,
7 | Tooltip,
8 | Legend,
9 | } from 'chart.js';
10 | import { Bar } from 'react-chartjs-2';
11 | import { useEffect, useState } from 'react';
12 |
13 | export default function BarChart({ table }) {
14 | let Timestamps = [];
15 | let Values = [];
16 | let type = 'No Data';
17 |
18 | if (table !== 1) {
19 | const graphed = [];
20 |
21 | Timestamps = table.Timestamps;
22 | Values = table.Values;
23 | let Label = table.Label;
24 | let labels = [];
25 |
26 | if (Label === 'NetworkIn' || Label === 'NetworkOut') {
27 | type = 'Bytes';
28 | } else if (Label === 'CPUUtilization') {
29 | type = 'Percent';
30 | } else {
31 | type = 'Count';
32 | }
33 |
34 | // why does Timestamps.length give error
35 | for (let i = 0; i < 12; i++) {
36 | let string = '';
37 | const newdate = new Date(Timestamps[i]);
38 | string = `${newdate.getHours()}:${newdate.getMinutes()}`;
39 |
40 | labels.push();
41 | graphed.push({ x: string, y: Values[i] });
42 | }
43 |
44 | ChartJS.register(
45 | CategoryScale,
46 | LinearScale,
47 | BarElement,
48 | Title,
49 | Tooltip,
50 | Legend
51 | );
52 | const options = {
53 | responsive: true,
54 | plugins: {
55 | legend: {
56 | position: 'top',
57 | labels: {
58 | color: 'white',
59 | },
60 | },
61 | title: {
62 | display: true,
63 | text: 'Instance: i-0610f2356e0d72fcd',
64 | color: 'white',
65 | },
66 | },
67 | scales: {
68 | x: {
69 | ticks: {
70 | color: 'white',
71 | },
72 | },
73 | y: {
74 | ticks: {
75 | color: 'white',
76 | },
77 | },
78 | },
79 | };
80 |
81 | const data = {
82 | labels,
83 | datasets: [
84 | {
85 | barPercentage: 1,
86 | categoryPercentage: 1,
87 | label: type,
88 | data: graphed,
89 | backgroundColor: [
90 | '#3e95cd',
91 | '#8e5ea2',
92 | '#3cba9f',
93 | '#e8c3b9',
94 | '#c45850',
95 | ],
96 | },
97 | ],
98 | };
99 |
100 | return (
101 |
102 |
103 |
104 | );
105 | } else {
106 |
107 | const [barX, setData] = useState([
108 | { x: 0, y: null },
109 | { x: 5, y: null },
110 | { x: 10, y: null },
111 | { X: 15, y: null },
112 | { x: 20, y: null },
113 | { x: 25, y: null },
114 | { x: 30, y: null },
115 | { x: 35, y: null },
116 | { x: 40, y: null },
117 | { x: 45, y: null },
118 | { x: 50, y: null },
119 | { x: 55, y: null },
120 | { x: 60, y: null },
121 | { x: null, y: 100 },
122 | ]);
123 |
124 | const labels = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
125 |
126 | ChartJS.register(
127 | CategoryScale,
128 | LinearScale,
129 | BarElement,
130 | Title,
131 | Tooltip,
132 | Legend
133 | );
134 | const options = {
135 | responsive: true,
136 | plugins: {
137 | legend: {
138 | position: 'top',
139 | labels: {
140 | color: 'white',
141 | },
142 | },
143 | title: {
144 | display: true,
145 | color: 'white',
146 | },
147 | },
148 | scales: {
149 | x: {
150 | ticks: {
151 | color: 'white',
152 | },
153 | },
154 | y: {
155 | ticks: {
156 | color: 'white',
157 | },
158 | },
159 | },
160 | };
161 |
162 | const data = {
163 | labels,
164 | datasets: [
165 | {
166 | barPercentage: 1,
167 | categoryPercentage: 1,
168 | data: barX,
169 | label: 'No Data',
170 | backgroundColor: [
171 | '#3e95cd',
172 | '#8e5ea2',
173 | '#3cba9f',
174 | '#e8c3b9',
175 | '#c45850',
176 | ],
177 | },
178 | ],
179 | };
180 |
181 | return (
182 |
183 |
184 |
185 | );
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/pages/components/Linechart.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS,
3 | CategoryScale,
4 | LinearScale,
5 | PointElement,
6 | Filler,
7 | LineElement,
8 | Title,
9 | Tooltip,
10 | Legend,
11 | } from 'chart.js';
12 | import { Line } from 'react-chartjs-2';
13 | import { useEffect, useState } from 'react';
14 |
15 | export default function LineChart({ table }) {
16 | let Timestamps = [];
17 | let Values = [];
18 | let type = 'No Data';
19 |
20 |
21 | if (table !== 1) {
22 | const graphed = [];
23 |
24 | Timestamps = table.Timestamps;
25 | Values = table.Values;
26 | let Label = table.Label;
27 | let labels = [];
28 |
29 | if (Label === 'NetworkIn' || Label === 'NetworkOut') {
30 | type = 'Bytes';
31 | } else if (Label === 'CPUUtilization') {
32 | type = "Percent";
33 | } else {
34 | type = 'Count';
35 | }
36 |
37 | for (let i = 0; i < 12; i++) {
38 | let string = ''
39 | const newdate = new Date(Timestamps[i]);
40 | string = `${newdate.getHours()}:${newdate.getMinutes()}`;
41 |
42 | labels.push()
43 | graphed.push({ x: string, y: Values[i] });
44 | }
45 |
46 | ChartJS.register(
47 | CategoryScale,
48 | LinearScale,
49 | PointElement,
50 | LineElement,
51 | Title,
52 | Tooltip,
53 | Filler,
54 | Legend
55 | );
56 |
57 | const options = {
58 | responsive: true,
59 | plugins: {
60 | legend: {
61 | position: 'top',
62 | labels: {
63 | color: 'white',
64 | },
65 | },
66 | title: {
67 | display: true,
68 | text: 'Instance: i-0610f2356e0d72fcd',
69 | color: 'white',
70 | },
71 | },
72 | scales: {
73 | x: {
74 | ticks: {
75 | color: 'white',
76 | },
77 | },
78 | y: {
79 | ticks: {
80 | color: 'white',
81 | },
82 | },
83 | },
84 | };
85 |
86 | const data = {
87 | labels,
88 | datasets: [
89 | {
90 | fill: true,
91 | label: type,
92 | data: graphed,
93 | borderColor: 'rgb(53, 162, 235)',
94 | backgroundColor: 'rgba(53, 162, 235, 0.5)',
95 | },
96 | ],
97 | };
98 |
99 | return (
100 |
101 |
102 |
103 | );
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | } else {
117 | ChartJS.register(
118 | CategoryScale,
119 | LinearScale,
120 | PointElement,
121 | LineElement,
122 | Title,
123 | Tooltip,
124 | Filler,
125 | Legend
126 | );
127 |
128 | const options = {
129 | responsive: true,
130 | plugins: {
131 | legend: {
132 | position: 'top',
133 | labels: {
134 | color: 'white',
135 | },
136 | },
137 | title: {
138 | display: true,
139 | text: 'Instance: i-0610f2356e0d72fcd',
140 | color: 'white',
141 | },
142 | },
143 | scales: {
144 | x: {
145 | ticks: {
146 | color: 'white',
147 | },
148 | },
149 | y: {
150 | ticks: {
151 | color: 'white',
152 | },
153 | },
154 | },
155 | };
156 |
157 | const labels = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
158 |
159 | const fakedata = [
160 | { x: null , y: 0 },
161 | // { x: 5, y: null },
162 | // { x: 10, y: null },
163 | // { X: 15, y: null },
164 | // { x: 20, y: null },
165 | // { x: 25, y: null },
166 | // { x: 30, y: null },
167 | // { x: 35, y: null },
168 | // { x: 40, y: null },
169 | // { x: 45, y: null },
170 | // { x: 50, y: null },
171 | // { x: 55, y: null },
172 | // { x: 60, y: null },
173 | { x: null, y: 100 },
174 | ];
175 |
176 | const data = {
177 | labels,
178 | datasets: [
179 | {
180 | fill: true,
181 | label: 'No Data',
182 | data: fakedata,
183 | borderColor: 'rgb(53, 162, 235)',
184 | backgroundColor: 'rgba(53, 162, 235, 0.5)',
185 | },
186 | ],
187 | };
188 |
189 | return (
190 |
191 |
192 |
193 | );
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/pages/components/ProtectedRoutes.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Outlet } from 'react-router';
2 | import { useAuth } from '../context/AuthContext';
3 |
4 | const ProtectedRoute = () => {
5 | const { userSession } = useAuth();
6 |
7 | //checks user session, if not active, redirect to Login; replace here esssentially replaces the current route/page with new route,
8 | // therefore user cannot simply hit back button to go back to this page.
9 | if (!userSession) return ;
10 |
11 | //placeholder for any child routes that will load if user session exists
12 | return ;
13 | };
14 |
15 | export default ProtectedRoute;
16 |
--------------------------------------------------------------------------------
/src/pages/components/menu.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { addGraph, getData } from '../../state/graph-reducer.js';
4 | import { AWSdata } from '../../../backend/fetch.js';
5 | import { useRef, useEffect } from 'react';
6 |
7 | export default function DropDownMenu() {
8 | const graphs = useSelector((state) => state.graphs);
9 | const dispatch = useDispatch();
10 | const first = useRef(true);
11 |
12 | useEffect(() => {
13 | if (first.current === true) {
14 | first.current = false;
15 | return;
16 | }
17 |
18 | async function auto() {
19 | let data = await AWSdata(graphs);
20 | dispatch(getData({ data }));
21 | }
22 |
23 | auto();
24 | }, [graphs.graph]);
25 |
26 | return (
27 |
28 |
29 |
30 | Add Metrics
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Bar Graph
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 | {
58 | dispatch(
59 | addGraph({
60 | type: 'bar',
61 | metric: 'NetworkOut',
62 | })
63 | );
64 | }}
65 | >
66 | NetworkOut
67 |
68 |
69 |
70 |
73 | dispatch(
74 | addGraph({
75 | type: 'bar',
76 | metric: 'NetworkIn',
77 | })
78 | )
79 | }
80 | >
81 | NetworkIn
82 |
83 |
84 |
85 |
88 | dispatch(
89 | addGraph({
90 | type: 'bar',
91 | metric: 'EBSWriteOps',
92 | })
93 | )
94 | }
95 | >
96 | EBSWriteOps
97 |
98 |
99 |
100 |
103 | dispatch(
104 | addGraph({
105 | type: 'bar',
106 | metric: 'EBSReadOps',
107 | })
108 | )
109 | }
110 | >
111 | EBSReadOps
112 |
113 |
114 |
115 |
118 | dispatch(
119 | addGraph({
120 | type: 'bar',
121 | metric: 'CPUUtilization',
122 | })
123 | )
124 | }
125 | >
126 | CPUUtilization
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | Line Graph
139 |
140 |
141 |
142 |
143 |
147 |
148 |
149 |
152 | dispatch(
153 | addGraph({
154 | type: 'areaLine',
155 | metric: 'NetworkOut',
156 | })
157 | )
158 | }
159 | >
160 | NetworkOut
161 |
162 |
163 |
164 |
167 | dispatch(
168 | addGraph({
169 | type: 'areaLine',
170 | metric: 'NetworkIn',
171 | })
172 | )
173 | }
174 | >
175 | NetworkIn
176 |
177 |
178 |
179 |
182 | dispatch(
183 | addGraph({
184 | type: 'areaLine',
185 | metric: 'EBSWriteOps',
186 | })
187 | )
188 | }
189 | >
190 | EBSWriteOps
191 |
192 |
193 |
194 |
197 | dispatch(
198 | addGraph({
199 | type: 'areaLine',
200 | metric: 'EBSReadOps',
201 | })
202 | )
203 | }
204 | >
205 | EBSReadOps
206 |
207 |
208 |
209 |
212 | dispatch(
213 | addGraph({
214 | type: 'areaLine',
215 | metric: 'CPUUtilization',
216 | })
217 | )
218 | }
219 | >
220 | CPUUtilization
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 | );
231 | }
232 |
--------------------------------------------------------------------------------
/src/pages/context/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext, createContext } from 'react';
2 | // import userPool from '../../pools/userPool';
3 | import { CognitoUserPool } from 'amazon-cognito-identity-js';
4 |
5 | //allows for any children component to use this CONTEXT, small scale state management
6 | const AuthContext = createContext();
7 |
8 | //this gets exported to be used in main
9 | export const AuthProvider = ({ children }) => {
10 | const [userSession, setUserSession] = useState(null);
11 | const [loading, setLoading] = useState(true);
12 |
13 | //grabbing the pool data
14 | const poolData = {
15 | UserPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,
16 | ClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
17 | };
18 | //makes a userPool instance using pool data
19 | const userPool = new CognitoUserPool(poolData);
20 |
21 | useEffect(() => {
22 | //grabs current logged in user from the browser's local storage
23 | const currentUser = userPool.getCurrentUser();
24 |
25 | //if current user is active, do the following
26 | if (currentUser) {
27 | //grab userSession info
28 | currentUser.getSession((err, session) => {
29 | if (err || !session.isValid()) {
30 | //if user session is not valid, set session to null
31 | setUserSession(null);
32 | } else {
33 | //save the current user and their session
34 | setUserSession({ user: currentUser, session });
35 | }
36 | setLoading(false);
37 | });
38 | } else {
39 | setLoading(false);
40 | }
41 | }, []);
42 |
43 | //to be used to change User's status back to signout, should redirect user
44 | //to Login page
45 | const signOut = () => {
46 | const currentUser = userPool.getCurrentUser();
47 | if (currentUser) {
48 | currentUser.signOut();
49 | }
50 | setUserSession(null);
51 | localStorage.clear(); //clear any stored tokens
52 | sessionStorage.clear(); //clear out session data
53 | };
54 |
55 | //AuthContext.Provider component provided by React's Context API, allows for all child components to receive
56 | //context from parent
57 | return (
58 |
59 | {loading ? Loading...
: children}
60 |
61 | );
62 | };
63 |
64 | //alows us to use our auth context by creating a custom hook
65 | //reduces need to repetitively write useContext(AuthContext) all the time
66 | export const useAuth = () => useContext(AuthContext);
67 |
--------------------------------------------------------------------------------
/src/pages/login/Forgot.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import {
3 | CognitoIdentityProviderClient,
4 | ConfirmForgotPasswordCommand,
5 | ForgotPasswordCommand,
6 | } from '@aws-sdk/client-cognito-identity-provider';
7 |
8 | const client = new CognitoIdentityProviderClient({
9 | region: 'us-east-1',
10 | });
11 |
12 | const Forgot: React.FC = (): JSX.Element => {
13 | const [email, setEmail] = useState('');
14 | const [code, setCode] = useState('');
15 | const [delivery, setDelivery] = useState('');
16 | const [passwordOne, setPasswordOne] = useState('');
17 | const [passwordTwo, setPasswordTwo] = useState('');
18 | const [verificationComponent, setVerificationComponent] =
19 | useState(false);
20 | const [resetSucess, setResetSuccess] = useState(false);
21 | const [errorMessage, setErrorMessage] = useState('');
22 |
23 | //form is submitted, code is sent to email
24 | const retrieveCode = async (
25 | event: React.FormEvent
26 | ): Promise => {
27 | event.preventDefault();
28 |
29 | //sets email to lowercase first before sending it to Cognito
30 | const lowerCaseEmail = email.toLowerCase();
31 |
32 | //This is what will be sent to Cognito, so that user can get a verification code
33 | const input = {
34 | ClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
35 | Username: lowerCaseEmail,
36 | };
37 | const command = new ForgotPasswordCommand(input);
38 | //sends the email
39 | try {
40 | //sends the client the command in order to get code
41 | const response = await client.send(command);
42 | //the response we get back tells us how we received the code
43 | setDelivery(response?.CodeDeliveryDetails?.DeliveryMedium || 'unknown');
44 |
45 | console.log(response);
46 | } catch (error) {
47 | console.error('error:', error);
48 | }
49 | //after we receive code, set this to true to conditionally render the next step
50 | setVerificationComponent(true);
51 | };
52 |
53 | //code is submitted to Cognito and verified for password reset
54 | const codeSubmission = async (event: React.FormEvent) => {
55 | //prevents full page refresh
56 | event.preventDefault();
57 |
58 | //If user types in two passwords that are not the same, this error message will display
59 | if (passwordOne !== passwordTwo) {
60 | setErrorMessage('Passwords do not match.');
61 | return;
62 | }
63 | //
64 | const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID;
65 | if (!clientId) {
66 | setErrorMessage('Client Id is missing');
67 | return;
68 | }
69 |
70 | setErrorMessage('');
71 | interface Input {
72 | ClientId: string;
73 | Username: string;
74 | ConfirmationCode: string;
75 | Password: string;
76 | }
77 |
78 | const input: Input = {
79 | ClientId: clientId,
80 | Username: email,
81 | ConfirmationCode: code,
82 | Password: passwordOne,
83 | };
84 |
85 | const command = new ConfirmForgotPasswordCommand(input);
86 | try {
87 | await client.send(command);
88 | setResetSuccess(true);
89 | } catch (error) {
90 | if (error instanceof Error) {
91 | console.error('Error:', error.message);
92 | setErrorMessage(error.message);
93 | } else {
94 | console.error('Error: Unexpected Error');
95 | setErrorMessage('An unexpected error occurred');
96 | }
97 | }
98 | };
99 |
100 | return (
101 |
102 | {!verificationComponent ? (
103 |
104 |
105 |
106 |
107 |
108 | {' '}
109 | Forgot password?
110 |
111 |
112 | Enter the email you used to signup below:
113 |
114 |
115 |
138 |
139 |
140 |
141 |
142 | ) : !resetSucess ? (
143 |
144 |
145 |
146 |
147 |
148 | Your is code sent via {delivery}
149 |
150 |
151 | Enter the verification code you received below
152 |
153 |
200 |
201 |
202 |
203 |
204 | ) : (
205 |
206 |
207 | Password Reset Successful
208 |
209 |
210 | Log in with your new password.
211 |
212 |
213 | )}
214 |
218 |
219 | );
220 | };
221 |
222 | export default Forgot;
223 |
--------------------------------------------------------------------------------
/src/pages/login/Login.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useNavigate, useSearchParams } from 'react-router-dom';
3 | import {
4 | CognitoUser,
5 | AuthenticationDetails,
6 | CognitoUserPool,
7 | ICognitoUserSessionData,
8 | CognitoUserSession,
9 | } from 'amazon-cognito-identity-js';
10 | import { useAuth } from '../context/AuthContext.jsx';
11 |
12 | //types for user below
13 | interface User {
14 | id: string;
15 | email: string;
16 | }
17 |
18 | interface TokenResponse {
19 | access_token: string;
20 | id_token: string;
21 | email: string;
22 | }
23 |
24 | //grabs the pool data from local .env file
25 | //types for the data required for configuring the user pool in AWS Cognito
26 | interface PoolData {
27 | UserPoolId: string;
28 | ClientId: string;
29 | }
30 |
31 | const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID;
32 | const poolID = import.meta.env.VITE_COGNITO_USER_POOL_ID;
33 | const urlPoolID = poolID.toLowerCase().replace('_', ''); //poolid transformed for url
34 |
35 | const authUrl = `https://${urlPoolID}.auth.us-east-1.amazoncognito.com/login?client_id=${clientId}&redirect_uri=http://localhost:80&response_type=code`;
36 |
37 | const Login: React.FC = () => {
38 | const [email, setEmail] = useState('');
39 | const [password, setPassword] = useState('');
40 | const [error, setError] = useState('');
41 | const { setUserSession } = useAuth();
42 | const navigate = useNavigate();
43 |
44 | //hook allows you to grab URL query params and manipulate query params as well
45 | const [searchParams] = useSearchParams();
46 |
47 | //runs everytime there are new parameters in the url after 'sign in with google'
48 | useEffect(() => {
49 | // Get auth code from URL
50 | const code: string | null = searchParams.get('code');
51 | //if code exists, run the function to exchange code for tokens
52 | if (code) {
53 | exchangeCodeForToken(code);
54 | }
55 | }, [searchParams]);
56 |
57 | //handles the exchange of tokens that are received in the url
58 | const exchangeCodeForToken = async (code: string): Promise => {
59 | try {
60 | //this is an endpoint that is used to fetch access,id, and refresh tokens
61 | const response = await fetch(
62 | `https://${urlPoolID}.auth.us-east-1.amazoncognito.com/oauth2/token`,
63 | {
64 | method: 'POST',
65 | headers: {
66 | 'Content-Type': 'application/x-www-form-urlencoded',
67 | },
68 | //builds a url to send to this endpoint to retrieve the tokens
69 | body: new URLSearchParams({
70 | grant_type: 'authorization_code',
71 | client_id: import.meta.env.VITE_COGNITO_CLIENT_ID,
72 | code: code,
73 | redirect_uri: 'http://localhost:80',
74 | }),
75 | }
76 | );
77 |
78 | //waits to receive a reponse with the proper tokens
79 | const data: TokenResponse = await response.json();
80 |
81 | if (data.access_token) {
82 | // Store the tokens with Cognito-like format
83 | const userId: string = data.id_token.split('.')[0]; // Using the ID token's first part as a user ID
84 |
85 | localStorage.setItem(
86 | `CognitoIdentityServiceProvider.${clientId}.${userId}.accessToken`,
87 | data.access_token
88 | );
89 | localStorage.setItem(
90 | `CognitoIdentityServiceProvider.${clientId}.${userId}.idToken`,
91 | data.id_token
92 | );
93 |
94 | // After tokens are saved, create the session object and call setUserSession
95 | const user: User = { id: userId, email: data.email }; // Customize as per the user data you get
96 |
97 | //creates session from the data collected
98 | const session = {
99 | accessToken: data.access_token,
100 | idToken: data.id_token,
101 | };
102 | setUserSession({ user, session }); // Set user session after successful login
103 | navigate('/newUserProfile');
104 | }
105 | } catch (error) {
106 | console.error('Error exchanging auth code for token:', error);
107 | }
108 | };
109 |
110 | const poolData: PoolData = {
111 | UserPoolId: poolID,
112 | ClientId: clientId,
113 | };
114 |
115 | //ensures our poolID stays safe, along with ClientId
116 | const userPool = new CognitoUserPool(poolData);
117 |
118 | //handles the login process for users, using AWS Cognito
119 | const handlesLogin = (event: React.FormEvent): void => {
120 | //prevents default action of form from taking place when submitting
121 | event.preventDefault();
122 |
123 | //sets email to be lowercase (case insensitive)
124 | let lowerCaseEmail: string = email;
125 | lowerCaseEmail = lowerCaseEmail.toLocaleLowerCase();
126 |
127 | //creates a new CognitoUser object, containing the username and the pool it will access
128 | const user = new CognitoUser({
129 | Username: lowerCaseEmail,
130 | Pool: userPool,
131 | });
132 |
133 | //Creates a new AuthenticationDetails object containing the username and password information
134 | const authenticationDetails = new AuthenticationDetails({
135 | Username: lowerCaseEmail,
136 | Password: password,
137 | });
138 |
139 | //using the user object, we pass in the authentication to see if this user's password matches
140 | user.authenticateUser(authenticationDetails, {
141 | //on success, we want to print the success and print it to console
142 | onSuccess: (data): void => {
143 | console.log('Login Successful:', data);
144 | setUserSession({ user, session: data });
145 | navigate('/newUserProfile'); //immediately navigates to Home page,
146 | },
147 | //upon failure, we instead console the error message, reason why
148 | onFailure: (err): void => {
149 | console.error('Login not successful', err);
150 | setError(err.message || 'Something did not go right');
151 | },
152 | });
153 | };
154 |
155 | //this function allows user to go to signup page
156 | const signUp = () => {
157 | //this function will call the signup endpoint
158 | navigate('/signup');
159 | };
160 |
161 | return (
162 | <>
163 |
164 |
165 |
166 |
167 |
168 |
169 | Welcome back!
170 |
171 |
172 | Welcome back! Please enter your details.
173 |
174 |
175 |
176 |
211 |
212 | (window.location.href = authUrl)}
215 | >
216 | Sign in with Google
217 |
218 |
219 |
220 |
221 |
222 | Do not have an account?
223 |
224 |
{
227 | signUp();
228 | }}
229 | >
230 | Sign up
231 |
232 |
233 | Forgot Password?{' '}
234 |
238 | Click here
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
251 |
252 | >
253 | );
254 | };
255 |
256 | export default Login;
257 |
258 | /*
259 | WILL EVENTUALLY MAKE A PARENT COMPONENT HOLDING THE STYLING THAT IS COMMON
260 | BETWEEN BOTH LOGIN AND SIGNUP PAGES TO AVOID THE UNNECESARY REPETITION OF STYLES
261 |
262 |
263 | */
264 |
--------------------------------------------------------------------------------
/src/pages/login/Signup.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import Verify from './Verification.tsx';
3 | import { useNavigate } from 'react-router';
4 | import {
5 | CognitoUserPool,
6 | CognitoUserAttribute,
7 | } from 'amazon-cognito-identity-js';
8 |
9 | interface PoolData {
10 | UserPoolId: string;
11 | ClientId: string;
12 | }
13 |
14 | const Signup: React.FC = (): JSX.Element => {
15 | const navigate = useNavigate();
16 | const [email, setEmail] = useState('');
17 | const [password, setPassword] = useState('');
18 | const [success, setSuccess] = useState(false);
19 | const [isVerified, setIsVerified] = useState(false);
20 | const [errorMessage, setErrorMessage] = useState('');
21 |
22 | //url data to redirect to when user wishes to sign up with Google
23 | const authUrl: string = `https://${import.meta.env.VITE_COGNITO_USER_POOL_ID.toLowerCase().replace(
24 | '_',
25 | ''
26 | )}.auth.us-east-1.amazoncognito.com/login?client_id=${
27 | import.meta.env.VITE_COGNITO_CLIENT_ID
28 | }&redirect_uri=http://localhost:80&response_type=code`;
29 |
30 | //function to handle the signup process for our users
31 | const handleSignups = (event: React.FormEvent): void => {
32 | //prevents default form loading upon submission
33 | event.preventDefault();
34 |
35 | //grabs pool data
36 | const poolData: PoolData = {
37 | UserPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,
38 | ClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
39 | };
40 | //makes a userPool instance using the data from above
41 | const userPool = new CognitoUserPool(poolData);
42 |
43 | //Becomes 'false' to begin
44 | setSuccess(false);
45 |
46 | //ensures email will be saved case insensitive
47 | let lowerCaseEmail: string = email;
48 | lowerCaseEmail = lowerCaseEmail.toLowerCase();
49 |
50 | //attributes to be included when we send the signup
51 | const attributeList: CognitoUserAttribute[] = [
52 | new CognitoUserAttribute({
53 | Name: 'email',
54 | Value: lowerCaseEmail,
55 | }),
56 | ];
57 |
58 | userPool.signUp(
59 | lowerCaseEmail,
60 | password,
61 | attributeList,
62 | [],
63 | (err, data) => {
64 | if (err) {
65 | //if the email, password or anything is off throw error
66 | console.error('Sign up failed:', err.message);
67 | setErrorMessage(`${err.message}`);
68 | return;
69 | }
70 | //otherwise console log success and show next steps
71 | console.log('Sign up with this was good:', data);
72 | setSuccess(true);
73 | setIsVerified(true);
74 | }
75 | );
76 | };
77 |
78 | //This allows user to go to login page
79 | const login = (): void => {
80 | console.log('testing');
81 | navigate('/');
82 | };
83 |
84 | return (
85 | <>
86 | {!isVerified ? (
87 |
88 |
89 |
90 |
91 |
92 |
93 | Get Started with AWSome!
94 |
95 |
96 | See all your metrics in one place with an AWSome monitoring
97 | tool for your EC2 instances!
98 |
99 |
100 |
101 |
144 |
145 |
(window.location.href = authUrl)}
148 | >
149 | Sign up with Google
150 |
151 |
152 |
153 | Have an account?{' '}
154 | {
156 | login();
157 | }}
158 | className='flex justify-center ml-5 text-violet-500 font-medium ml-2'
159 | >
160 | Login
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
173 |
174 | ) : (
175 |
176 | )}
177 | >
178 | );
179 | };
180 |
181 | export default Signup;
182 |
--------------------------------------------------------------------------------
/src/pages/login/Verification.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js';
3 | import { useNavigate } from 'react-router';
4 |
5 | //types for the props
6 | interface VerifyProps {
7 | email: string;
8 | }
9 |
10 | interface PoolData {
11 | UserPoolId: string;
12 | ClientId: string;
13 | }
14 |
15 | const Verify: React.FC = ({ email }): JSX.Element => {
16 | const [verificationCode, setVerificationCode] = useState('');
17 | const [message, setMessage] = useState('');
18 | const navigate = useNavigate();
19 |
20 | //handles checking if user enters appropriate code after signup
21 | const handleVerification = (): void => {
22 | //grabs pool data, ensures our poolID stays safe, along with ClientId
23 | const poolData: PoolData = {
24 | UserPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,
25 | ClientId: import.meta.env.VITE_COGNITO_CLIENT_ID,
26 | };
27 |
28 | //creates a userPool out of the data provided above
29 | const userPool = new CognitoUserPool(poolData);
30 |
31 | //creates a CognitoUser instance, which we can run operations on
32 | const cognitoUser = new CognitoUser({
33 | Username: email,
34 | Pool: userPool,
35 | });
36 |
37 | //uses confirmRegistration method to ensure the verification code is true
38 | //DEPRECATED CAN BE UPDATED TO new AWS SDK
39 | cognitoUser.confirmRegistration(verificationCode, true, (err, result) => {
40 | if (err) {
41 | setMessage(`Verification failed: ${err.message}`);
42 | } else {
43 | setMessage(`Account verified successfully!`);
44 | navigate('/');
45 | }
46 | });
47 | };
48 |
49 | return (
50 |
51 |
Verify your account
52 |
Enter the verification code sent to your email
53 |
setVerificationCode(e.target.value)}
58 | >
59 |
Verify
60 |
{message}
61 |
62 | );
63 | };
64 |
65 | export default Verify;
66 |
--------------------------------------------------------------------------------
/src/pages/newUserProfile.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { useAuth } from './context/AuthContext';
3 | import { useState, useRef, useEffect } from 'react';
4 |
5 | function NewUser() {
6 | const navigate = useNavigate();
7 |
8 | const { signOut } = useAuth();
9 |
10 | const logOut = async () => {
11 | signOut();
12 | navigate('/');
13 | };
14 |
15 | async function RandomID() {
16 | let res = await fetch('http://localhost:81/random');
17 | let data = await res.json();
18 | return data;
19 | }
20 |
21 | const [isOpen, setIsOpen] = useState(false);
22 | const dropdownRef = useRef(null);
23 |
24 | // Open dropdown on hover or click
25 | const handleMouseEnter = () => {
26 | setIsOpen(true);
27 | };
28 |
29 | // Close dropdown when clicking outside
30 | useEffect(() => {
31 | const handleClickOutside = (event) => {
32 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
33 | setIsOpen(false);
34 | }
35 | };
36 |
37 | document.addEventListener('mousedown', handleClickOutside);
38 | document.addEventListener('mousedown', handleClickOutside);
39 | return () => {
40 | document.removeEventListener('mousedown', handleClickOutside);
41 | document.removeEventListener('mousedown', handleClickOutside);
42 | };
43 | }, []);
44 |
45 | return (
46 |
47 | {/* Navbar */}
48 |
AWSome
49 |
50 | AWSome
51 |
52 | {/* Dropdown Button */}
53 |
57 | Account
58 |
59 | navigate('/Home')}
62 | >
63 | Dashboard
64 |
65 |
66 |
70 | Logout
71 |
72 |
73 | {/* Dropdown Menu */}
74 | {/* {isOpen && ( */}
75 | {/* */}
79 | {/*
83 | Settings
84 | */}
85 |
86 | {/*
87 | )} */}
88 |
89 |
90 | {/* Page Content */}
91 |
92 |
93 | Welcome, New User!
94 |
95 |
96 | Start setting up your AWS monitoring dashboard.
97 |
98 | {
101 | let pass = await RandomID();
102 | document.querySelector('#password').innerText = pass.id;
103 | }}
104 | >
105 | Generate External Id
106 |
107 |
111 | Unique Id Here!
112 |
113 |
114 |
119 | {}}
121 | className='bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-pink-700 transition duration-200 ease-in-out'
122 | >
123 | Submit!
124 |
125 |
126 |
127 | {/* Footer */}
128 |
133 |
134 | );
135 | }
136 |
137 | export default NewUser;
138 |
--------------------------------------------------------------------------------
/src/state/graph-reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { data } from 'react-router';
3 |
4 | export const graphReducer = createSlice({
5 | name: 'graphReducer',
6 | initialState: {
7 | graph: [],
8 | metric: [],
9 | data: [],
10 | },
11 | reducers: {
12 | // Redux Toolkit allows us to write "mutating" logic in reducers. It
13 | // doesn't actually mutate the state because it uses the Immer library,
14 | // which detects changes to a "draft state" and produces a brand new
15 | // immutable state based off those changes
16 | addGraph: (state, action) => {
17 | if (action.payload.type === 'bar') {
18 | state.graph.push('bar');
19 | state.metric.push(action.payload.metric);
20 | } else if (action.payload.type === 'areaLine') {
21 | state.graph.push('areaLine');
22 | state.metric.push(action.payload.metric);
23 | } else if (action.payload === 'line') {
24 | state.graph.push('line');
25 | state.metric.push(action.payload.metric);
26 | }
27 | },
28 | getData: (state, action) => {
29 | if (action.payload.data) {
30 | // console.log('let: ', action.payload.data.result.length);
31 |
32 | state.data = [];
33 |
34 | for (let i = 0; i < action.payload.data.result.length; i++) {
35 | state.data.push(action.payload.data.result[i]);
36 | }
37 | }
38 | },
39 | },
40 | });
41 |
42 | // Action creators are generated for each case reducer function
43 | export const { addGraph, getData } = graphReducer.actions;
44 |
45 | export default graphReducer.reducer;
46 |
--------------------------------------------------------------------------------
/src/state/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import graphReducer from './graph-reducer.js'
3 |
4 | export default configureStore({
5 | reducer: {
6 | graphs: graphReducer
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | export default {
4 | content: [
5 | './index.html',
6 | './src/**/*.{js,ts,jsx,tsx}', // Include all React files
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [],
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | "jsx": "react-jsx" /* Specify what JSX code is generated. */,
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "NodeNext" /* Specify what module code is generated. */,
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | "types": [
36 | "vite/client"
37 | ] /* Specify type package names to be included without being referenced in a source file. */,
38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
40 | "noEmit": true,
41 | "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */,
42 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
43 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
44 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
45 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
46 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
47 | // "resolveJsonModule": true, /* Enable importing .json files. */
48 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
49 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
50 |
51 | /* JavaScript Support */
52 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
53 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
54 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
55 |
56 | /* Emit */
57 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
58 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
59 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
60 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
62 | // "noEmit": true, /* Disable emitting files from a compilation. */
63 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
64 | // "outDir": "./", /* Specify an output folder for all emitted files. */
65 | // "removeComments": true, /* Disable emitting comments. */
66 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
67 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
68 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
70 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
71 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
72 | // "newLine": "crlf", /* Set the newline character for emitting files. */
73 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
74 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
75 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
76 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
77 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
78 |
79 | /* Interop Constraints */
80 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
81 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
82 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
83 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
84 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
85 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
86 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
87 |
88 | /* Type Checking */
89 | "strict": true /* Enable all strict type-checking options. */,
90 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
91 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
92 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
93 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
94 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
95 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
96 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
97 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
98 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
99 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
100 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
101 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
102 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
103 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
104 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
105 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
106 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
107 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
108 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
109 |
110 | /* Completeness */
111 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
112 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
113 | },
114 | "include": ["src/**/*"],
115 | "exclude": ["node_modules", "dist"]
116 | }
117 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import tailwindcss from 'tailwindcss';
3 | import react from '@vitejs/plugin-react';
4 | import { nodePolyfills } from 'vite-plugin-node-polyfills';
5 |
6 | export default defineConfig({
7 | plugins: [
8 | react(),
9 | nodePolyfills({
10 | globals: true, // Enable polyfilling for `global`
11 | }),
12 | ],
13 | build: {
14 | outDir: 'dist',
15 | },
16 | css: {
17 | postcss: {
18 | plugins: [tailwindcss()],
19 | },
20 | },
21 | server: {
22 | port: 3000, //runs frontend on 3000
23 | open: true, //will automatically open up the page
24 | proxy: {
25 | //can be deleted
26 | // '/login': 'http://localhost:3000',
27 | // '/signup': 'http://localhost:3000',
28 | // '/protected': 'http://localhost:3000',
29 | '/random': 'http://localhost:81',
30 | '/data': 'http://localhost:81',
31 | },
32 | historyApiFallback: true, //ensures client-side routing works
33 | },
34 |
35 | // preview: {
36 | // allowedHosts: true,
37 | // },
38 | });
39 |
--------------------------------------------------------------------------------