├── .gcloudignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── __tests__
├── supertest.js
└── unitTesting.js
├── assets
├── NimbusGIFs
│ ├── apis-LQ.gif
│ ├── apis-light-LQ.gif
│ ├── functions-LQ.gif
│ ├── functions-light-LQ.gif
│ ├── login-to-home-HQ.gif
│ ├── login-to-home-LQ.gif
│ ├── login-to-home-light-LQ.gif
│ ├── logs-LQ.gif
│ ├── logs-light-LQ.gif
│ ├── register-LQ.gif
│ ├── register-light-LQ.gif
│ ├── settings-LQ.gif
│ └── settings-light-LQ.gif
├── apis.gif
├── aws-logo-color.png
├── chartjs-logo-color.svg
├── cloud.png
├── daisyui-logo-color.svg
├── docker-logo-color.png
├── electron-logo-color.png
├── express-logo-color.png
├── functions.gif
├── jest-logo-color.png
├── login.gif
├── logs.gif
├── mongodb-logo-color.png
├── nimbus-logo-color.png
├── nimbus.png
├── nimbus2.png
├── nimbus3.png
├── node-logo-color.png
├── react-logo-color.png
├── settings.gif
├── tailwind-logo-color.png
├── ts-logo-long-blue.png
└── webpack-logo-color.png
├── client
├── components
│ ├── ApiMetrics.tsx
│ ├── ApiRelations.tsx
│ ├── Apis.tsx
│ ├── DonutChart.tsx
│ ├── Function.tsx
│ ├── Functions.tsx
│ ├── HeadBar.tsx
│ ├── Home.tsx
│ ├── LineChart.tsx
│ ├── Login.tsx
│ ├── Logout.tsx
│ ├── Logs.tsx
│ ├── Register.tsx
│ └── Settings.tsx
├── containers
│ ├── App.tsx
│ ├── Layout.tsx
│ ├── UserAuth.tsx
│ └── UserDashboard.tsx
├── index.html
├── index.tsx
├── styles.css
└── types.ts
├── dist
└── styles.scss
├── electron
└── main.js
├── environment.d.ts
├── package-lock.json
├── package.json
├── postcss.config.js
├── server
├── controllers
│ ├── authController.ts
│ ├── aws
│ │ ├── apiController.ts
│ │ ├── apiMetricsController.tsx
│ │ ├── credentialsController.ts
│ │ ├── lambdaController.ts
│ │ ├── logsController.ts
│ │ └── metricsController.ts
│ └── userController.ts
├── routes
│ ├── authRouter.ts
│ ├── dashboardRouter.ts
│ └── settingsRouter.ts
├── server.ts
└── types.ts
├── tailwind.config.js
├── tsconfig.json
└── webpack.config.js
/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 | # If you would like to upload your .git directory, .gitignore file or files
11 | # from your .gitignore file, remove the corresponding line
12 | # below:
13 | .git
14 | .gitignore
15 |
16 | # Node.js dependencies:
17 | node_modules/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
3 | .DS_Store
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13
2 | WORKDIR /usr/src/app
3 | COPY package.json /usr/src/app/
4 | RUN npm install
5 | COPY . /usr/src/app
6 | RUN npm run build
7 | EXPOSE 3000
8 | ENTRYPOINT [ "node", "./server/server.js" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AWS Lambda Performance Tool
6 |
7 |
8 |
9 |
10 | ### About Nimbus
11 | Serverless architecture has become very popular in the field of software engineering, and Amazon's launch of AWS Lambda in 2014 has been a big part of its growth. AWS Lambda is a serverless computing service that lets users run code in response to events like changes to a DynamoDB table, an API call to API Gateway, or the addition of a file to an S3 bucket. As a function-as-a-service (FaaS) offering, AWS Lambda allows developers to focus on the business logic of their applications by abstracting away the underlying server infrastructure, such as maintenance, capacity planning, and scaling.
12 |
13 | AWS Lambda is a well-known service for serverless computing that lets users run code in response to event triggers. It is increasingly difficult to keep track of how well Lambda functions are working and how they are being used, especially as applications grow in size. This is where Nimbus comes in. Nimbus is a cross-platform desktop application that aims to solve this problem by allowing developers to keep an eye on the metrics of Lambda functions. With Nimbus, developers can see information about how their functions are being used and performance metrics in real time. This includes the number of function invocations, execution durations, CPU usage, and any errors or throttles that occurred. Nimbus also provides an estimate of how much function calls will cost, so developers can keep track of how much is being spent. In short, Nimbus is a valuable tool that simplifies the process of monitoring AWS Lambda functions.
14 |
15 | Nimbus is a tool for monitoring, but it also has a number of features that make your AWS development experience better. For example, Nimbus lets developers observe and analyze the logstream that each Lambda function invocation creates. Nimbus' log feature allows developers to filter through different log streams and sort through timelines. Additionally, Nimbus provides the ability to monitor resources within API Gateway. It displays API metrics including the number of calls, 4XX errors, 5XX errors, and latency. Users can view the relationships between specific API resources and Lambda functions. Having a discernible chain of responsibility in software engineering helps to ensure that applications are developed in a consistent and organized manner.
16 |
17 |
18 | ### Installation
19 | Download the desktop application [HERE](https://www.nimbusos.io/)
20 |
21 | ### User Guide
22 | - Visit the landing page and download the app for your operating system. Install it on your computer to get started.
23 |
24 | - Create an account by entering your information and linking it to your AWS account following the instructions provided on the Register page.
25 |
26 |
27 |
28 |
29 |
30 | - If you already have an account, simply log in.
31 |
32 |
33 |
34 | On the home page, you'll find a lot of information about the health of your AWS application, especially as it relates to lambda functions. This includes important metrics like the number of calls, errors, throttles, costs, and runtimes.
35 |
36 | - Head over to the Functions tab to see metrics broken down by individual functions, including invocations, errors, throttles, and durations.
37 |
38 |
39 |
40 | - The Logs tab is where you can find all your lambda function logs and filter them by time period, reports only, errors only, or any keyword.
41 |
42 |
43 |
44 | - The APIs tab lets you view common API metrics, endpoints, and the lambda functions they're connected to.
45 |
46 |
47 |
48 | - In the Settings tab, you can update your personal information (including AWS Cloudformation Stack ARN and region) or change your login details.
49 |
50 |
51 |
52 |
53 | ### Technologies Used
54 |
55 |
56 |
57 |
58 | Chart JS
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | ### How To Contribute
74 | Nimbus is an open-source product supported by the tech accelerator OS Labs. We welcome and appreciate contributions from the community. If you are interested in contributing to the development of our AWS serverless component monitoring and visualization tool, here are a few ways to get started:
75 |
76 | 1. Fork the repository: Go to the main repository on GitHub and click the “Fork” button to create a copy of the code under your own account. This will allow you to make changes to the code without affecting the original repository.
77 | 2. Set up your development environment:
78 | - To install the dependencies configured in the package.json file, run the following command:
79 | ```
80 | npm install
81 | ```
82 | - To set up a MongoDB database, you will need to obtain a connection string. Once you have the connection string, create an ENV file in the root directory and input the connection string as follows:
83 | ```
84 | MONGO_URI='your_connection_string'
85 | ```
86 | - Next, input your desired port number in the ENV file:
87 | ```
88 | PORT=your_port_number
89 | ```
90 | - The Nimbus application requires the use of JWT tokens for implementation, so you will need to create tokens for this purpose. You can input these tokens in the ENV file as follows:
91 | ```
92 | ACCESS_TOKEN_SECRET=your_access_token_secret
93 | REFRESH_TOKEN_SECRET=your_refresh_token_secret
94 | ```
95 | - Finally, you will need to obtain the access key id, secret key, and region from your AWS IAM account. Input these values in the ENV file as follows:
96 | ```
97 | AWS_ACCESS_KEY_ID=your_access_key_id
98 | AWS_SECRET_KEY=your_secret_key
99 | AWS_REGION=your_region
100 | ```
101 | 3. Choose an issue to work on: Browse the open issues in the repository and pick one that interests you. Alternatively, you can also propose your own changes by opening a new issue and describing the feature or improvement you would like to see.
102 | 4. Create a branch: Once you have chosen an issue to work on, create a new branch in your fork of the repository. Name the branch something descriptive, such as “add-feature-x” or “fix-bug-y”. This will allow you to work on your changes without affecting the main branch of the repository.
103 | 5. Make your changes: Make the necessary changes to the code in your branch. Be sure to follow the repository’s style guidelines and best practices, and make sure to test your changes thoroughly before submitting them.
104 | 6. Commit and push your changes: Once you are satisfied with your changes, commit them to your branch and push them to your fork of the repository.
105 | 7. Open a pull request: Go to the main repository on GitHub and click the “Compare & pull request” button. Describe the changes you have made and why they are necessary. Then, submit the pull request for review.
106 |
107 | A member of the repository’s maintainer team will review your pull request and either merge it into the codebase or provide feedback for changes that need to be made. Thank you for considering contributing to our project!
108 |
109 |
110 | ### License
111 | Distributed under the MIT License
112 |
113 | ### Meet The Team
114 | * Madeline Doctor - LinkedIn | GitHub
115 |
116 | * Arturo Kim - LinkedIn |
117 | GitHub
118 |
119 | * Georges Maroun - LinkedIn | GitHub
120 |
121 | * Arthur Su - LinkedIn |
122 | GitHub
123 |
124 | * Zhaowei Sun - LinkedIn |
125 | GitHub
126 |
127 |
128 |
--------------------------------------------------------------------------------
/__tests__/supertest.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const dotenv = require('dotenv');
3 | dotenv.config();
4 |
5 | const app = require('../server/server.js');
6 |
7 |
8 | describe ("POST /", () => {
9 | describe("should respond to a successful login", () => {
10 | test("should respond with a JWT access token", async () => {
11 | const response = await request(app).post("/login").send({
12 | email: process.env.DEMO_USERNAME,
13 | password: process.env.DEMO_PASSWORD,
14 | });
15 | expect(response.body.accessToken).toBeDefined();
16 | });
17 | test("should respond with a 200 status code", async () => {
18 | const response = await request(app).post("/login").send({
19 | email: process.env.DEMO_USERNAME,
20 | password: process.env.DEMO_PASSWORD,
21 | })
22 | expect(response.statusCode).toBe(200);
23 | });
24 | });
25 |
26 | describe("should reject invalid credentials on login", () => {
27 | test("should send error when user does not exist", async () => {
28 | const response = await request(app).post("/login").send({
29 | email: process.env.DEMO_USERNAME,
30 | password: "invalid",
31 | })
32 | expect(response.body.err).toEqual("Wrong password");
33 | });
34 | test("should send error when user does not exist", async () => {
35 | const response = await request(app).post("/login").send({
36 | email: "invalid",
37 | password: "invalid",
38 | })
39 | expect(response.body.err).toEqual("User not in database");
40 | });
41 | });
42 | });
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/__tests__/unitTesting.js:
--------------------------------------------------------------------------------
1 | const { convertToChartJSStructure } = require('../client/types.js');
2 |
3 | // The frontend is receiving data in this format:
4 | const input = {
5 | values: [ 15, 4 ],
6 | timestamp: [ '2023-01-10T02:55:00.000Z', '2023-01-09T02:55:00.000Z' ]
7 | };
8 |
9 | /* In order for ChartJS to graph out data, it has to be an array of objects
10 | with x and y coordinates in date chronological order
11 | const output = [
12 | {y: 4, x: '1/9/23'},
13 | {y: 15, x: '1/10/23'},
14 | */
15 |
16 | describe("Data formatting to be ChartJS compatible", () => {
17 | it("should return the same array size", () => {
18 | expect(convertToChartJSStructure(input).length).toEqual(2);
19 | });
20 | it("should map the cooresponding values to their timestamps", () => {
21 | expect(convertToChartJSStructure(input)[0]['y']).toEqual(4);
22 | expect(convertToChartJSStructure(input)[0]['x']).toEqual('1/9/23');
23 | });
24 | });
--------------------------------------------------------------------------------
/assets/NimbusGIFs/apis-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/apis-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/apis-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/apis-light-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/functions-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/functions-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/functions-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/functions-light-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/login-to-home-HQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-HQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/login-to-home-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/login-to-home-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-light-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/logs-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/logs-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/logs-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/logs-light-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/register-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/register-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/register-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/register-light-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/settings-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/settings-LQ.gif
--------------------------------------------------------------------------------
/assets/NimbusGIFs/settings-light-LQ.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/settings-light-LQ.gif
--------------------------------------------------------------------------------
/assets/apis.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/apis.gif
--------------------------------------------------------------------------------
/assets/aws-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/aws-logo-color.png
--------------------------------------------------------------------------------
/assets/chartjs-logo-color.svg:
--------------------------------------------------------------------------------
1 |
2 |
40 |
45 |
50 |
56 |
61 |
62 |
--------------------------------------------------------------------------------
/assets/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/cloud.png
--------------------------------------------------------------------------------
/assets/daisyui-logo-color.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/assets/docker-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/docker-logo-color.png
--------------------------------------------------------------------------------
/assets/electron-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/electron-logo-color.png
--------------------------------------------------------------------------------
/assets/express-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/express-logo-color.png
--------------------------------------------------------------------------------
/assets/functions.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/functions.gif
--------------------------------------------------------------------------------
/assets/jest-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/jest-logo-color.png
--------------------------------------------------------------------------------
/assets/login.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/login.gif
--------------------------------------------------------------------------------
/assets/logs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/logs.gif
--------------------------------------------------------------------------------
/assets/mongodb-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/mongodb-logo-color.png
--------------------------------------------------------------------------------
/assets/nimbus-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus-logo-color.png
--------------------------------------------------------------------------------
/assets/nimbus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus.png
--------------------------------------------------------------------------------
/assets/nimbus2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus2.png
--------------------------------------------------------------------------------
/assets/nimbus3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus3.png
--------------------------------------------------------------------------------
/assets/node-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/node-logo-color.png
--------------------------------------------------------------------------------
/assets/react-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/react-logo-color.png
--------------------------------------------------------------------------------
/assets/settings.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/settings.gif
--------------------------------------------------------------------------------
/assets/tailwind-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/tailwind-logo-color.png
--------------------------------------------------------------------------------
/assets/ts-logo-long-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/ts-logo-long-blue.png
--------------------------------------------------------------------------------
/assets/webpack-logo-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/webpack-logo-color.png
--------------------------------------------------------------------------------
/client/components/ApiMetrics.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import LineChart from "./LineChart";
3 | import { SelectedApiMetrics, Metric, Message, ApiMetricsProps } from "../types";
4 |
5 | // Display the metrics for the selected API
6 | const ApiMetrics: React.FC = ({ selectedApi, apiMetrics }: ApiMetricsProps) => {
7 | const [message, setMessage] = useState('fetching data...');
8 |
9 | // If data not found, set message
10 | if (apiMetrics === undefined) {
11 | if (message !== 'data not found') {
12 | setMessage('data not found')
13 | }
14 | }
15 |
16 | // Make chart for each metric for the selected API
17 | const makeCharts = (selectedApiMetrics: SelectedApiMetrics) => {
18 | if (!selectedApiMetrics) return;
19 | // Declare array to store the LineChart elements
20 | const lineChartElements = [];
21 | // Loop over each metric
22 | for (let metric in selectedApiMetrics) {
23 | const timeValArr = [];
24 | const currMetricsObj = selectedApiMetrics[metric as Metric];
25 | // Loop over data points: value and timestamp
26 | for (let i = currMetricsObj.values.length - 1; i >= 0; i--) {
27 | const subElement: any = {
28 | y: currMetricsObj.values[i],
29 | x: new Date(currMetricsObj.timestamps[i]).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"}),
30 | };
31 | timeValArr.push(subElement);
32 | // Get the date of the current iteration
33 | let date = new Date(currMetricsObj.timestamps[i])
34 | // If the next day is less than the next date in our iteration push a value of 0 and the next day into our object
35 | if ((date.getTime() + 1) < (new Date (currMetricsObj.timestamps[i - 1])).getTime()) {
36 | date.setDate(date.getDate() + 1)
37 | while (date.getTime() < (new Date (currMetricsObj.timestamps[i - 1])).getTime()) {
38 | const subElement: any = {
39 | y: 0,
40 | x: new Date(date).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"})
41 | }
42 | timeValArr.push(subElement);
43 | date.setDate(date.getDate() + 1)
44 | }
45 | }
46 | }
47 | // Add lineChart element to array
48 | lineChartElements.push(
49 |
54 | )
55 | }
56 | return lineChartElements;
57 | }
58 |
59 | let chartElements;
60 | // Make chart if there is a selected API
61 | if (selectedApi) {
62 | chartElements = makeCharts(apiMetrics[selectedApi]);
63 | }
64 |
65 | return (
66 |
67 |
68 | {chartElements ? chartElements : message}
69 |
70 |
71 | );
72 | };
73 |
74 | export default ApiMetrics;
--------------------------------------------------------------------------------
/client/components/ApiRelations.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Method, Message, ApiRelationsProps } from "../types";
3 |
4 | // Display the relations for the selected API: routes, methods, and functions.
5 | const ApiRelations: React.FC = ({ selectedApi, apiRelations }: ApiRelationsProps) => {
6 | const [message, setMessage] = useState('fetching data...')
7 |
8 | // If data not found, set message
9 | if (apiRelations === undefined || selectedApi === '') {
10 | if (message !== 'data not found') {
11 | setMessage('data not found');
12 | }
13 | }
14 |
15 | // Grab data for the selected API; if not found set to null
16 | const selectedApiRelations = apiRelations
17 | && selectedApi
18 | ?
19 | apiRelations.filter((apiRel:any) => apiRel.apiName === selectedApi)
20 | : null;
21 |
22 |
23 | // Get endpoints data
24 | const endpoints = selectedApiRelations && selectedApiRelations.length > 0 ? selectedApiRelations[0].endpoints : null;
25 |
26 | // If endpoints exist, render api relations, else render message
27 | return (
28 |
29 | {endpoints ?
30 |
31 | {Object.keys(endpoints).map((key) => {
32 | return (
33 |
34 |
35 |
{key}
36 |
37 | {(endpoints)[key].map((method:Method) => {
38 | return (
39 |
40 |
41 |
42 | {method.method}
43 |
44 | λ : {method.func}
45 |
46 |
47 |
48 | );
49 | })}
50 |
51 |
52 |
53 | );
54 | })}
55 |
56 | :
57 | message}
58 |
59 | );
60 | };
61 |
62 | export default ApiRelations;
--------------------------------------------------------------------------------
/client/components/Apis.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import ApiMetrics from './ApiMetrics';
4 | import ApiRelations from './ApiRelations';
5 | import { View } from "../types";
6 |
7 | const Apis = () => {
8 | const [apiRelations, setApiRelations] = useState(null);
9 | const [apiMetrics, setApiMetrics] = useState(null);
10 | const [selectedApi, setSelectedApi] = useState('');
11 | const [showInfo, setShowInfo] = useState('metrics');
12 |
13 | // Switch between metrics and relations
14 | const toggleDisplay = useCallback((e: React.BaseSyntheticEvent) => {
15 | if (e.target.value !== showInfo) {
16 | setShowInfo(e.target.value);
17 | }
18 | }, [showInfo]);
19 |
20 | // Change the selected api
21 | const handleSelectedApi = useCallback((e: React.BaseSyntheticEvent) => {
22 | setSelectedApi(() => e.target.value)
23 | }, [selectedApi]);
24 |
25 | // Fetch Api relations data and set apiRelation state
26 | const getApiRelations = async (signal:AbortSignal) => {
27 | let res;
28 | try {
29 | res = await fetch('/dashboard/apiRelations', {
30 | method: 'POST',
31 | headers: {
32 | 'Content-Type': 'Application/JSON',
33 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
34 | },
35 | signal
36 | });
37 | res = await res.json();
38 | const apiRel = res.apiRelations || undefined;
39 | setApiRelations(apiRel);
40 | }
41 | catch(err){
42 | console.log("Error occurred grabbing API Relations: ", err);
43 | }
44 | }
45 |
46 | // Get api metrics and setApiMetrics
47 | // setSelectedApi to the first api in the metrics object
48 | const getApiMetrics = async (signal:AbortSignal) => {
49 | let res;
50 | try {
51 | res = await fetch('/dashboard/apiMetrics', {
52 | method: 'GET',
53 | headers: {
54 | 'Content-Type': 'Application/JSON',
55 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
56 | },
57 | signal
58 | });
59 | res = await res.json();
60 | let metrics:any;
61 | if (res.allApiMetrics) {
62 | metrics = res.allApiMetrics;
63 | setSelectedApi(Object.keys(metrics)[0])
64 | }
65 | setApiMetrics(metrics);
66 | }
67 | catch(err){
68 | console.log("Error occurred grabbing API Metrics: ", err);
69 | }
70 | }
71 |
72 | // Invoke getApiRelations if apiRelations if falsy
73 | useEffect(() => {
74 | const controller = new AbortController();
75 | const signal = controller.signal;
76 | if (!apiRelations) {
77 | getApiRelations(signal);
78 | }
79 |
80 | return () => {
81 | controller.abort();
82 | }
83 | }, []);
84 |
85 | // Invoke getApiMetrics if apiMetrics if falsy
86 | useEffect(() => {
87 | const controller = new AbortController();
88 | const signal = controller.signal;
89 |
90 | if (!apiMetrics) {
91 | getApiMetrics(signal);
92 | }
93 | return () => {
94 | controller.abort();
95 | }
96 | }, []);
97 |
98 | // Get API names and create and array of button elements
99 | const getApiNames = () => {
100 | return Object.keys(apiMetrics as any).map((el:string) => {
101 | return (
102 |
103 |
108 | {el}
109 |
110 |
111 | )
112 | })
113 | };
114 |
115 | return (
116 |
117 |
118 |
119 |
120 | API list
121 |
122 | {apiMetrics ? getApiNames() : ''}
123 |
124 |
125 |
126 | Metrics
131 | Relations
136 |
137 |
138 | {showInfo === 'metrics' ?
139 | : }
140 |
141 |
142 |
143 |
144 |
145 |
146 | );
147 | };
148 |
149 | export default Apis;
150 |
--------------------------------------------------------------------------------
/client/components/DonutChart.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Chart, ArcElement, Tooltip, Legend } from 'chart.js';
3 | import { Doughnut } from 'react-chartjs-2'
4 | import chroma from "chroma-js"
5 | import { DonutChartProps } from "../types";
6 |
7 | const DonutChart = (props: DonutChartProps) => {
8 |
9 | Chart.register(ArcElement, Tooltip, Legend);
10 |
11 | const colors = chroma.scale(['#623cad','#fb9ce5']).mode('lch').colors(props.rawData.data?.length);
12 |
13 | return (
14 |
40 | );
41 | }
42 |
43 | export default DonutChart;
--------------------------------------------------------------------------------
/client/components/Function.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import LineChart from "./LineChart";
3 | import moment from "moment";
4 | import { FunctionProps, Data, RawData, chartJSData } from "../types";
5 | import { convertToChartJSStructure } from "../types";
6 |
7 | // Component to display a single function's metrics
8 | const Function: React.FC = (props: FunctionProps) => {
9 | const [isClicked, setIsClicked] = useState(false)
10 |
11 | const [totalInvocations, setTotalInvocations] = useState(0)
12 | const [totalErrors, setTotalErrors] = useState(0)
13 | const [totalThrottles, setTotalThrottles] = useState(0)
14 | const [totalDuration, setTotalDuration] = useState(0)
15 |
16 | const [invocations, setInvocations] = useState([])
17 | const [errors, setErrors] = useState([])
18 | const [throttles, setThrottles] = useState([])
19 | const [duration, setDuration] = useState([])
20 |
21 |
22 | useEffect(() => {
23 | // If our metric array has at least one value, accumulate the values
24 | if (props.invocations.values.length > 0) setTotalInvocations(props.invocations.values.reduce((acc: number, curr: number):number => acc + curr))
25 | if (props.errors.values.length > 0) setTotalErrors(props.errors.values.reduce((acc: number, curr: number):number => acc + curr))
26 | if (props.throttles.values.length > 0) setTotalThrottles(props.throttles.values.reduce((acc: number, curr: number):number => acc + curr))
27 | if (props.duration.values.length > 0) setTotalDuration(Math.ceil(props.duration.values.reduce((acc: number, curr: number):number => acc + curr)/props.duration.values.length))
28 | }, [])
29 |
30 | // Generate the chart when the user clicks on the row
31 | const generateChart = () => {
32 | if (!isClicked) {
33 | setInvocations(convertToChartJSStructure(props.invocations))
34 | setErrors(convertToChartJSStructure(props.errors))
35 | setThrottles(convertToChartJSStructure(props.throttles))
36 | setDuration(convertToChartJSStructure(props.duration))
37 | setIsClicked(true);
38 | } else {
39 | setIsClicked(false);
40 | }
41 | }
42 |
43 | return (
44 | <>
45 |
46 | {props.funcName}
47 | {totalInvocations}
48 | {totalErrors}
49 | {totalThrottles}
50 | {totalDuration}
51 |
52 | {isClicked &&
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | }
68 | >
69 | )
70 | }
71 |
72 | export default Function
--------------------------------------------------------------------------------
/client/components/Functions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Function from './Function'
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | // Component to fetch all functions metrics and display them
6 | const Functions = () => {
7 | const [funcMetrics, setFuncMetrics] = useState({});
8 |
9 | // Grab each functions metrics when the component mounts
10 | const grabFuncsMetrics = async() => {
11 | let response;
12 | response = await fetch('/dashboard/funcmetrics', {
13 | method: 'GET',
14 | headers: {
15 | 'Content-Type': 'Application/JSON',
16 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
17 | },
18 | })
19 | response = await response.json()
20 | setFuncMetrics(response.eachFuncMetrics)
21 | };
22 |
23 | // Grab functions metrics when the component mounts
24 | useEffect(() => {
25 | grabFuncsMetrics()
26 | }, []);
27 |
28 | // Display table head and each function metrics component
29 | return (
30 |
31 |
32 |
33 |
34 | Lambda Function
35 | Invocations
36 | Errors
37 | Throttles
38 | Duration (ms)
39 |
40 |
41 |
42 | {/* Update the funcMetric parameter type */}
43 | {Object.entries(funcMetrics).map((funcMetric:any) => (
44 |
50 | ))}
51 |
52 |
53 |
54 | )
55 | };
56 |
57 | export default Functions;
58 |
--------------------------------------------------------------------------------
/client/components/HeadBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react';
2 | import { HeadBarProps } from "../types";
3 |
4 | const HeadBar: React.FC = ({ toggleTheme, theme }: HeadBarProps) => {
5 |
6 | const [checked, setChecked] = useState(false);
7 |
8 | // Toggle theme (light/dark)
9 | const handleToggle = () => {
10 | toggleTheme();
11 | setChecked(prev => !prev);
12 | };
13 |
14 | return (
15 |
16 | {/*
nimbus */}
17 |
18 |
19 | {theme === 'myThemeDark' ?
20 |
:
}
21 |
22 |
23 |
24 |
25 | {theme === 'myThemeDark' ? 'Dark' : 'Light'}
26 |
27 |
28 |
29 |
30 | )
31 |
32 | }
33 |
34 | export default HeadBar;
--------------------------------------------------------------------------------
/client/components/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import LineChart from './LineChart'
3 | import DonutChart from './DonutChart'
4 | import { RawData, chartJSData, costProps, HomeProps } from "../types";
5 | import { convertToChartJSStructure } from "../types";
6 |
7 | const Home: React.FC = (props: HomeProps) => {
8 | const [invocationsData, setInvocations] = useState([]);
9 | const [errorsData, setErrors] = useState([]);
10 | const [throttlesData, setThrottles] = useState([]);
11 | const [durationData, setDurations] = useState([]);
12 | const [cost, setCost] = useState(0)
13 | const [totalInvocations, setTotalInvocations] = useState(0);
14 | const [totalErrors, setTotalErrors] = useState(0);
15 | const [totalThrottles, setTotalThrottles] = useState(0);
16 | const [averageDuration, setAverageDuration] = useState(0);
17 | const [invocationsByFunc, setInvocationsByFunc] = useState({})
18 |
19 | const route = useRef({
20 | allMetrics: '/dashboard/allMetrics',
21 | funcMetrics: '/dashboard/funcmetrics'
22 | });
23 |
24 | // Sends a GET request to the '/dashboard/allMetrics' route
25 | // Uses ReactHooks to change the states based on data received from AWS
26 | const getMetrics = async () => {
27 | let res;
28 | try {
29 | res = await fetch(route.current.allMetrics, {
30 | method: 'GET',
31 | headers: {
32 | 'Content-Type': 'Application/JSON',
33 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
34 | refresh: `BEARER ${localStorage.getItem('refreshToken')}`,
35 | },
36 | });
37 | res = await res.json();
38 | // Convert the data to a format that Chart JS can use and set the states to the new data
39 | setInvocations(convertToChartJSStructure({
40 | values: res.allFuncMetrics.invocations.values,
41 | timestamp: res.allFuncMetrics.invocations.timestamp
42 | }));
43 | setErrors(convertToChartJSStructure({
44 | values: res.allFuncMetrics.errors.values,
45 | timestamp: res.allFuncMetrics.errors.timestamp
46 | }));
47 | setThrottles(convertToChartJSStructure({
48 | values: res.allFuncMetrics.throttles.values,
49 | timestamp: res.allFuncMetrics.throttles.timestamp
50 | }));
51 | setDurations(convertToChartJSStructure({
52 | values: res.allFuncMetrics.duration.values,
53 | timestamp: res.allFuncMetrics.duration.timestamp
54 | }));
55 | setCost(calculateCost(res.cost));
56 | if (res.allFuncMetrics.invocations.values.length > 0) {
57 | setTotalInvocations(res.allFuncMetrics.invocations.values.reduce((a:number, b:number) => a + b, 0));
58 | }
59 | if (res.allFuncMetrics.errors.values.length > 0) {
60 | setTotalErrors(res.allFuncMetrics.errors.values.reduce((a:number, b:number) => a + b, 0));
61 | }
62 | if (res.allFuncMetrics.throttles.values.length > 0) {
63 | setTotalThrottles(res.allFuncMetrics.throttles.values.reduce((a:number, b:number) => a + b, 0));
64 | }
65 | if (res.allFuncMetrics.duration.values.length > 0) {
66 | setAverageDuration(res.allFuncMetrics.duration.values.reduce((a:number, b:number) => a + b, 0) / res.allFuncMetrics.duration.values.length);
67 | }
68 |
69 | } catch(error) {
70 | console.log(error);
71 | }
72 | }
73 |
74 | const getFuncMetrics = async () => {
75 | let res;
76 | try {
77 | res = await fetch(route.current.funcMetrics, {
78 | method: 'GET',
79 | headers: {
80 | 'Content-Type': 'Application/JSON',
81 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
82 | },
83 | });
84 | res = await res.json();
85 | const labelArr = [];
86 | const invocationArr = [];
87 | for (const func in res.eachFuncMetrics) {
88 | labelArr.push(func);
89 | const invocations = res.eachFuncMetrics[func].invocations.values;
90 | if (invocations.length > 0) {
91 | invocationArr.push(invocations.reduce((a: number, b: number) => a + b, 0));
92 | } else {
93 | invocationArr.push(0);
94 | }
95 | }
96 | setInvocationsByFunc({labels: labelArr, data: invocationArr})
97 | } catch (error) {
98 | console.log(error);
99 | }
100 | }
101 |
102 | // Calculates the running cost of all functions
103 | const calculateCost = (costObj: costProps) => {
104 | let totalCost = 0;
105 | for (let i = 0; i < costObj.memory.length; i++) {
106 | totalCost += costObj.memory[i] * 0.0009765625 * costObj.duration[i] * 0.001
107 | }
108 | return Math.round(totalCost * 100) / 100
109 | }
110 |
111 | // Invokes the getMetrics function
112 | useEffect(() => {
113 | getMetrics();
114 | getFuncMetrics();
115 | }, []);
116 |
117 | return (
118 | <>
119 |
120 |
121 |
122 |
Welcome to Your Dashboard, {props.firstName}
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
Total Invocations
131 |
{totalInvocations.toLocaleString(undefined, {maximumFractionDigits:2})}
132 |
133 |
134 |
135 |
136 |
Total Errors
137 |
{totalErrors.toLocaleString(undefined, {maximumFractionDigits:2})}
138 |
139 |
140 |
141 |
142 |
Total Throttles
143 |
{totalThrottles.toLocaleString(undefined, {maximumFractionDigits:2})}
144 |
145 |
146 |
147 |
148 |
Average Duration
149 |
{averageDuration.toLocaleString(undefined, {maximumFractionDigits:2})}ms
150 |
151 |
152 |
153 |
154 |
Cost
155 |
${cost.toLocaleString(undefined, {maximumFractionDigits:2})}
156 |
157 |
158 |
159 |
160 |
161 |
162 |
Invocations by Functions
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
175 |
180 |
185 |
190 |
191 | >
192 | );
193 | };
194 |
195 | export default Home;
196 |
197 |
198 |
199 |
--------------------------------------------------------------------------------
/client/components/LineChart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Chart, CategoryScale, TimeScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from "chart.js";
3 | import { Line } from "react-chartjs-2";
4 | import { RawData, LineChartProps } from "../types";
5 |
6 | const LineChart = (props: LineChartProps) => {
7 |
8 | // Registers plugins to be applied on all charts
9 | Chart.register(
10 | TimeScale,
11 | CategoryScale,
12 | LinearScale,
13 | PointElement,
14 | LineElement,
15 | Title,
16 | Tooltip,
17 | Legend
18 | );
19 |
20 | // Set chart data
21 | const data = {
22 | datasets: [
23 | {
24 | label: props.label,
25 | data: props.rawData,
26 | fill: false,
27 | borderColor: [
28 | "#fb9ce5",
29 | ],
30 | tension: 0.3,
31 | },
32 | ]
33 | };
34 |
35 | return (
36 |
40 | );
41 | };
42 |
43 | export default LineChart;
--------------------------------------------------------------------------------
/client/components/Login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { AuthProps } from "../types";
3 |
4 |
5 | const Login: React.FC = ({ swapAuthView, handleUserLogin }: AuthProps) => {
6 | const [email, setEmail] = useState("");
7 | const [password, setPassword] = useState("");
8 | const [errorMessage, setErrorMessage] = useState("");
9 |
10 | // Update state when user types email or password
11 | const updateEmail = (e: React.ChangeEvent) => {
12 | setEmail(e.target.value)
13 | }
14 | const updatePassword = (e: React.ChangeEvent) => {
15 | setPassword(e.target.value)
16 | }
17 |
18 | // Hnadle wrong user input
19 | const handleError = (err: string) => {
20 | setErrorMessage(err)
21 | }
22 |
23 | // Send user credentials to server and receive access and refresh tokens
24 | const submitForm = (e: React.FormEvent) => {
25 | e.preventDefault();
26 | const credentials = {
27 | email,
28 | password
29 | }
30 |
31 | fetch('/login', {
32 | method: 'POST',
33 | headers: { 'Content-Type': 'Application/JSON' },
34 | body: JSON.stringify(credentials),
35 | })
36 | .then(res => res.json())
37 | .then((result) => {
38 | if (result.err) {
39 | handleError('Wrong username or password');
40 | }
41 | else {
42 | handleUserLogin();
43 | // Save access token to local storage
44 | localStorage.setItem("accessToken", result.accessToken)
45 | }
46 | });
47 | }
48 |
49 | return (
50 |
51 |
52 |
Login Now
53 |
54 | Go Serverless with Confidence. Monitor and Visualize your AWS Lamda with Ease.
55 |
56 |
57 |
89 | { (errorMessage !== '')
90 | &&
91 |
92 |
93 |
94 |
{errorMessage}
95 |
96 |
97 | }
98 |
99 |
100 | )
101 | }
102 |
103 | export default Login;
--------------------------------------------------------------------------------
/client/components/Logout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Logout: React.FC = () => {
4 | const logOut = () => {
5 | fetch('/logout')
6 | .then(res => {
7 | window.location.reload();
8 | })
9 | }
10 |
11 | return (
12 | <>
13 | Log Out
14 | >
15 | )
16 | }
17 |
18 | export default Logout;
--------------------------------------------------------------------------------
/client/components/Logs.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | // import { userContext } from 'react'
3 | // import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
4 |
5 | // To Do:
6 | // highlight selected buttons
7 |
8 | // Types
9 | type Period = '30d' | '14d' | '7d' | '1d' | '1hr';
10 | type Search = String;
11 |
12 | const Logs = () => {
13 | const [functions, setFunctions] = useState([]);
14 | const [selectedFunc, setSelectedFunc] = useState('');
15 | const [logs, setLogs] = useState(['Fetching logs...']);
16 | const [period, setPeriod] = useState('30d');
17 | const [search, setSearch] = useState('');
18 | // const [buttonIsActive, setButtonIsActive] = useState(false);
19 | // const [buttonState, setButtonState] = useState({
20 | // activeObject: null,
21 | // objects: functionsList,
22 | // });
23 | const [selectedTimeButton, setSelectedTimeButton] = useState('30d');
24 | const [selectedLogsButton, setSelectedLogsButton] = useState('All logs');
25 | const [selectedFunctionButton, setSelectedFunctionButton] = useState('');
26 |
27 | const routes = {
28 | functions: '/dashboard/functions',
29 | logs: '/dashboard/filteredLogs',
30 | };
31 |
32 | // Change period
33 | const changePeriod = (e: any) => {
34 | if (e.target.value !== period) {
35 | setPeriod(e.target.value);
36 | }
37 | };
38 |
39 | // Change search keyword
40 | const changeSearch = (e: any) => {
41 | if (e.target.value === 'allLogs') {
42 | setSearch('');
43 | } else if (e.target.value === 'reports') {
44 | setSearch('REPORT');
45 | } else if (e.target.value === 'errors') {
46 | setSearch('error');
47 | } else {
48 | setSearch(e.target.value);
49 | }
50 | };
51 |
52 | const changeSelectedFunc = (e: any) => {
53 | if (e.target.value !== selectedFunc) {
54 | setSelectedFunc(e.target.value);
55 | }
56 | };
57 |
58 | // Get the names of Lambda functions in a string[], setFunctions to result and setSelectedFunc to first function
59 | const getFunctions = async () => {
60 | let res;
61 | try {
62 | res = await fetch(`${routes.functions}`, {
63 | method: 'GET',
64 | headers: {
65 | 'Content-Type': 'Application/JSON',
66 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
67 | },
68 | });
69 | // convert response to JS object
70 | res = await res.json();
71 |
72 | // func arr is an array of strings (function names)
73 | const funcArr = res.functions || ['unable to fetch lambda functions'];
74 | // change functions to be array with all function names
75 | setFunctions(funcArr);
76 | //
77 | setSelectedFunc(funcArr[0]);
78 | setSelectedFunctionButton(funcArr[0]);
79 | } catch (err) {
80 | console.log('ERROR FROM GET FUNCTIONS', err);
81 | }
82 | };
83 |
84 | // Fetch logs for the selectedFunc in a string[] and setLogs
85 | const getLogs = async () => {
86 | let res;
87 | const reqBody = {
88 | functionName: selectedFunc,
89 | filterPattern: search,
90 | period: period,
91 | };
92 | try {
93 | res = await fetch(`${routes.logs}`, {
94 | method: 'POST',
95 | headers: {
96 | 'Content-Type': 'Application/JSON',
97 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
98 | },
99 | body: JSON.stringify(reqBody),
100 | });
101 | // convert response to JS object
102 | res = await res.json();
103 | let logsArr = res.filteredLogs || ['Logs not found'];
104 | setLogs(logsArr);
105 | } catch (err) {
106 | console.log('ERROR FROM GET LOGS', err);
107 | }
108 | };
109 |
110 | // On component mount: get all lambda functions
111 | useEffect(() => {
112 | getFunctions();
113 | }, []);
114 |
115 | // On state change selectedFunc, period, search: get logs based on selected lambda func and options
116 | useEffect(() => {
117 | if (selectedFunc !== '') {
118 | getLogs();
119 | }
120 | }, [selectedFunc, period, search]);
121 |
122 | const logsList = logs.map((log, i) => (
123 |
124 | {/*
132 |
137 | */}
138 | {i + 1}
139 | {/* {log} */}
140 | {log}
141 |
142 | // overflow-hidden
143 | ));
144 |
145 | /*
146 |
147 |
148 |
149 |
150 | Logs
151 |
152 |
153 |
154 |
155 |
156 |
157 |
*/
158 | // ['func1', 'func2, 'func3']
159 | const functionsList = functions.map((funcStr, i) => (
160 | {
164 | // changeSelectedFunc(e);
165 | // setSelectedFunctionButton(funcStr);
166 | // }}
167 | // value={funcStr}
168 | >
169 | {funcStr}
170 |
171 | ));
172 |
173 | return (
174 | <>
175 |
176 |
177 |
181 | {functionsList}
182 |
183 |
184 | {
191 | changePeriod(e);
192 | setSelectedTimeButton('30d');
193 | }}
194 | >
195 | 30D
196 |
197 | {
204 | changePeriod(e);
205 | setSelectedTimeButton('14d');
206 | }}
207 | >
208 | 14D
209 |
210 | {
215 | changePeriod(e);
216 | setSelectedTimeButton('7d');
217 | }}
218 | >
219 | 7D
220 |
221 | {
226 | changePeriod(e);
227 | setSelectedTimeButton('1d');
228 | }}
229 | >
230 | 1D
231 |
232 | {
239 | changePeriod(e);
240 | setSelectedTimeButton('1hr');
241 | }}
242 | >
243 | 1H
244 |
245 |
246 |
247 |
248 | {
254 | changeSearch(e);
255 | setSelectedLogsButton('All logs');
256 | }}
257 | >
258 | All Logs
259 |
260 | {
266 | changeSearch(e);
267 | setSelectedLogsButton('Reports');
268 | }}
269 | >
270 | Reports
271 |
272 | {
278 | changeSearch(e);
279 | setSelectedLogsButton('Errors');
280 | }}
281 | >
282 | Errors
283 |
284 |
285 |
286 |
287 |
288 |
294 |
295 |
302 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 | Logs
320 |
321 |
322 | {logsList}
323 |
324 |
325 |
326 |
327 | >
328 | );
329 | };
330 |
331 | export default Logs;
--------------------------------------------------------------------------------
/client/components/Register.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react';
2 | import { UserData, AuthProps } from "../types";
3 |
4 | type FormErrors = {email:boolean; firstName:boolean; lastName:boolean; password:boolean; confirmation:boolean; arn:boolean};
5 |
6 | const Register: React.FC = ({swapAuthView, handleUserLogin }: AuthProps) => {
7 | const [email, setEmail] = useState('');
8 | const [firstName, setFirstName] = useState('');
9 | const [lastName, setLastName] = useState('');
10 | const [password, setPassword] = useState('');
11 | const [confirmation, setConfirmation] = useState('');
12 | const [arn, setArn] = useState('');
13 | const [region, setRegion] = useState('');
14 | const [errorMessage, setErrorMessage] = useState('');
15 | const [errors, setErrors] = useState({email: false, firstName: false, lastName: false, password: false, confirmation: false, arn: false});
16 |
17 | // Update state when user types email, password etc.
18 | const updateEmail = useCallback((e: React.ChangeEvent) => {
19 | setEmail(e.target.value);
20 | }, [email]);
21 |
22 | const updateFirstName = useCallback((e: React.ChangeEvent) => {
23 | setFirstName(e.target.value);
24 | }, [firstName]);
25 |
26 | const updateLastName = useCallback((e: React.ChangeEvent) => {
27 | setLastName(e.target.value);
28 | }, [lastName]);
29 |
30 | const updatePassword = useCallback((e: React.ChangeEvent) => {
31 | setPassword(e.target.value);
32 | }, [password]);
33 |
34 | const updateConfirmation = useCallback((e: React.ChangeEvent) => {
35 | setConfirmation(e.target.value);
36 | }, [confirmation]);
37 |
38 | const updateArn = useCallback((e: React.ChangeEvent) => {
39 | setArn(e.target.value);
40 | }, [arn]);
41 |
42 | const updateRegion = useCallback((e: any) => {
43 | setRegion(e.target.value);
44 | }, [region]);
45 |
46 | // Hnadle wrong user input
47 | const handleError = useCallback(() => {
48 | setErrorMessage('Some information is missing or incorrect');
49 | }, [errorMessage]);
50 |
51 | // Update errors object
52 | const updateErrors = (errors: Array): void => {
53 | const errorObj:any = {email:false, firstName:false, lastName:false, password:false, confirmation:false, arn:false}
54 |
55 | errors.forEach((el) => {
56 | errorObj[el] = true;
57 | });
58 | setErrors(errorObj);
59 | };
60 |
61 | // Send user credentials to server and receive access and refresh tokens
62 | const submitForm = (e: any) => {
63 | e.preventDefault();
64 |
65 | const userData: UserData = {
66 | email,
67 | firstName,
68 | lastName,
69 | password,
70 | confirmation,
71 | arn,
72 | region,
73 | };
74 |
75 | fetch('/register', {
76 | method: 'POST',
77 | headers: { 'Content-Type': 'Application/JSON' },
78 | body: JSON.stringify(userData),
79 | })
80 | .then((res) => res.json())
81 | .then((result) => {
82 | if (result.errMessage) {
83 | handleError();
84 | updateErrors(result.errors);
85 | } else {
86 | handleUserLogin();
87 | // Save access token to local storage
88 | localStorage.setItem('accessToken', result.accessToken);
89 | }
90 | });
91 | };
92 |
93 | // List of AWS regions
94 | const regionsOptions = [
95 | 'us-east-2',
96 | 'us-east-1',
97 | 'us-west-1',
98 | 'us-west-2',
99 | 'af-south-1',
100 | 'ap-east-1',
101 | 'ap-south-2',
102 | 'ap-southeast-3',
103 | 'ap-south-1',
104 | 'ap-northeast-3',
105 | 'ap-northeast-2',
106 | 'ap-southeast-1',
107 | 'ap-southeast-2',
108 | 'ap-northeast-1',
109 | 'ca-central-1',
110 | 'eu-central-1',
111 | 'eu-west-1',
112 | 'eu-west-2',
113 | 'eu-south-1',
114 | 'eu-west-3',
115 | 'eu-south-2',
116 | 'eu-north-1',
117 | 'eu-central-2',
118 | 'me-south-1',
119 | 'me-central-1',
120 | 'sa-east-1',
121 | 'us-gov-east-1',
122 | 'us-gov-west-1',
123 | ];
124 |
125 | return (
126 |
253 |
254 |
255 | );
256 | };
257 |
258 | export default Register;
259 |
--------------------------------------------------------------------------------
/client/components/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { ProfileData, PasswordData, SettingsProps } from "../types";
3 |
4 | const Settings: React.FC = (props: SettingsProps) => {
5 | const [errorMessage, setErrorMessage] = useState('');
6 | const [successMessage, setSuccessMessage] = useState('');
7 |
8 | // Create refs for password and confirmation
9 | const passwordRef = useRef() as React.MutableRefObject;
10 | const confirmationRef = useRef() as React.MutableRefObject;
11 |
12 | // Store routes in object
13 | const routes = {
14 | updateProfile: '/dashboard/settings/updateProfile',
15 | updatePassword: '/dashboard/settings/updatePassword'
16 | }
17 |
18 | // Update state on change
19 | const updateFirstName = (e: React.ChangeEvent) => {
20 | props.setFirstName(e.target.value);
21 | };
22 |
23 | const updateLastName = (e: React.ChangeEvent) => {
24 | props.setLastName(e.target.value);
25 | };
26 |
27 | const updatePassword = (e: React.ChangeEvent) => {
28 | props.setPassword(e.target.value);
29 | };
30 |
31 | const updateConfirmation = (e: React.ChangeEvent) => {
32 | props.setConfirmation(e.target.value);
33 | };
34 |
35 | const updateArn = (e: React.ChangeEvent) => {
36 | props.setArn(e.target.value);
37 | };
38 |
39 | const updateRegion = (e: any) => {
40 | props.setRegion(e.target.value);
41 | };
42 |
43 | // Reset password fields
44 | const resetPasswords = () => {
45 | passwordRef.current.value = "";
46 | confirmationRef.current.value = "";
47 | }
48 |
49 | const regionsOptions = [
50 | 'us-east-2',
51 | 'us-east-1',
52 | 'us-west-1',
53 | 'us-west-2',
54 | 'af-south-1',
55 | 'ap-east-1',
56 | 'ap-south-2',
57 | 'ap-southeast-3',
58 | 'ap-south-1',
59 | 'ap-northeast-3',
60 | 'ap-northeast-2',
61 | 'ap-southeast-1',
62 | 'ap-southeast-2',
63 | 'ap-northeast-1',
64 | 'ca-central-1',
65 | 'eu-central-1',
66 | 'eu-west-1',
67 | 'eu-west-2',
68 | 'eu-south-1',
69 | 'eu-west-3',
70 | 'eu-south-2',
71 | 'eu-north-1',
72 | 'eu-central-2',
73 | 'me-south-1',
74 | 'me-central-1',
75 | 'sa-east-1',
76 | 'us-gov-east-1',
77 | 'us-gov-west-1',
78 | ];
79 |
80 | const filteredRegionsOptions = regionsOptions.filter(r => r !== props.region);
81 |
82 | // Set error and success messages
83 | const handleError = () => {
84 | setErrorMessage('Some information is missing or incorrect!');
85 | };
86 |
87 | const handleSuccess= () => {
88 | setSuccessMessage('Your profile details are updated successfully!');
89 | };
90 |
91 | const handlePasswordSuccess= () => {
92 | setSuccessMessage('Password updated successfully');
93 | };
94 |
95 | // Highlight erroneusly filled fields in red
96 | const highlightInput = (errors: Array): void => {
97 | errors.forEach((el) => {
98 | const input = document.querySelector(`#${el}`);
99 | if (input) {
100 | input.style.borderColor = 'red';
101 | }
102 | });
103 | };
104 |
105 | // Update profile
106 | const submitProfileForm = (e: any) => {
107 | e.preventDefault();
108 | const updatedProfileData: ProfileData = {
109 | firstName: props.firstName,
110 | lastName: props.lastName,
111 | arn: props.arn,
112 | region: props.region,
113 | };
114 | fetch(routes.updateProfile, {
115 | method: 'POST',
116 | headers: {
117 | 'Content-Type': 'Application/JSON' ,
118 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
119 | },
120 | body: JSON.stringify(updatedProfileData),
121 | }).then(res => res.json())
122 | .then((result) => {
123 | if (result.errMessage) {
124 | handleError();
125 | highlightInput(result.errors);
126 | } else {
127 | handleSuccess();
128 | props.setFirstName(result.firstName);
129 | props.setLastName(result.lastName);
130 | props.setArn(result.arn);
131 | props.setRegion(result.region);
132 | }
133 | })
134 | }
135 |
136 | // Update password
137 | const submitPasswordForm = (e: any) => {
138 | e.preventDefault();
139 | const updatedPasswordData: PasswordData = {
140 | password: props.password,
141 | confirmation: props.confirmation
142 | };
143 | fetch(routes.updatePassword, {
144 | method: 'POST',
145 | headers: {
146 | 'Content-Type': 'Application/JSON' ,
147 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
148 | },
149 | body: JSON.stringify(updatedPasswordData),
150 | }).then(res => res.json())
151 | .then((result) => {
152 | if (result.errMessage) {
153 | handleError();
154 | highlightInput(result.errors);
155 | } else if (result.successMessage) {
156 | handlePasswordSuccess();
157 | props.setPassword('');
158 | props.setConfirmation('');
159 | resetPasswords();
160 | }
161 | })
162 | }
163 |
164 | return (
165 | <>
166 |
167 |
168 |
Profile
169 |
211 |
212 |
213 |
214 |
Login Details
215 |
253 |
254 |
255 | { successMessage !== ''
256 | &&
257 |
258 |
259 |
260 |
{successMessage}
261 |
262 |
263 | }
264 | { (errorMessage !== '' && successMessage === '')
265 | &&
266 |
267 |
268 |
269 |
{errorMessage}
270 |
271 |
272 | }
273 | >)
274 |
275 | };
276 |
277 | export default Settings;
278 |
--------------------------------------------------------------------------------
/client/containers/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState , useCallback} from 'react';
2 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
3 | import { Theme } from 'react-daisyui'
4 | import UserAuth from './UserAuth';
5 | import UserDashboard from './UserDashboard';
6 | import HeadBar from '../components/HeadBar'
7 |
8 | const App = () => {
9 | const [userLoggedIn, setUserLoggedIn] = useState(false);
10 | const [theme, setTheme] = React.useState('myThemeDark');
11 |
12 | const toggleTheme = useCallback(() => {
13 | setTheme(theme === 'myThemeDark' ? 'myThemeLight' : 'myThemeDark');
14 | }, [theme]);
15 |
16 | const handleUserLogin = useCallback(() => {
17 | setUserLoggedIn((userLoggedIn) => !userLoggedIn);
18 | }, [userLoggedIn]);
19 |
20 |
21 | // If user is logged in, render UserDashboard component, otherwise render UserAuth component
22 | return (
23 |
24 |
25 |
26 | {userLoggedIn ? (
27 |
28 | ) : (
29 |
30 | )}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default App;
38 |
--------------------------------------------------------------------------------
/client/containers/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState} from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Logout from '../components/Logout';
4 |
5 | // Sidebar component
6 | const Layout = () => {
7 | const [selectedTab, setSelectedTab] = useState('Home');
8 |
9 | return (
10 |
11 | setSelectedTab('Home')}>
12 | Home
13 |
14 | setSelectedTab('Functions')}>
15 | Functions
16 |
17 | setSelectedTab('Logs')}>
18 | Logs
19 |
20 | setSelectedTab('APIs')}>
21 | APIs
22 |
23 | setSelectedTab('Settings')}>
24 | Settings
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Layout;
35 |
--------------------------------------------------------------------------------
/client/containers/UserAuth.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import Login from "../components/Login.js";
3 | import Register from "../components/Register.js";
4 | import { UserAuthProps } from "../types";
5 |
6 | // UserAuth component, displays login or register component depending on state
7 | const UserAuth: React.FC = ({ handleUserLogin, toggleTheme }: UserAuthProps) => {
8 | const [showLogin, setShowLogin] = useState(true);
9 |
10 | // Swap between login and register views
11 | const swapAuthView = useCallback(() => {
12 | setShowLogin((showLogin) => !showLogin);
13 | }, [showLogin]);
14 |
15 | return (
16 |
17 | {showLogin === true ? : }
18 |
19 | )
20 | }
21 |
22 | export default UserAuth;
23 |
--------------------------------------------------------------------------------
/client/containers/UserDashboard.tsx:
--------------------------------------------------------------------------------
1 | import { request } from 'http';
2 | import React, { useState, useEffect, useCallback } from 'react';
3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
4 | import Layout from './Layout';
5 | import Home from '../components/Home';
6 | import Functions from '../components/Functions';
7 | import Logs from '../components/Logs';
8 | import Apis from '../components/Apis';
9 | import Settings from '../components/Settings';
10 | import Logout from '../components/Logout'
11 | import { UserAuthProps, FetchHeader} from "../types";
12 |
13 | const UserDashboard: React.FC = ({ handleUserLogin, toggleTheme }: UserAuthProps) => {
14 |
15 | const routes = {
16 | userDetails: '/dashboard/settings/userDetails',
17 | }
18 |
19 | const [data, setData] = useState([]);
20 | const [email, setEmail] = useState('');
21 | const [firstName, setFirstName] = useState('');
22 | const [lastName, setLastName] = useState('');
23 | const [password, setPassword] = useState('');
24 | const [confirmation, setConfirmation] = useState('');
25 | const [arn, setArn] = useState('');
26 | const [region, setRegion] = useState('');
27 |
28 | const getUserDetails = useCallback(async () => {
29 | let res;
30 | try {
31 | res = await fetch(`${routes.userDetails}`, {
32 | method: 'GET',
33 | headers: {
34 | 'Content-Type': 'Application/JSON',
35 | authorization: `BEARER ${localStorage.getItem('accessToken')}`,
36 | refresh: `BEARER ${localStorage.getItem('refreshToken')}`,
37 | },
38 | });
39 | // convert response to JS object
40 | res = await res.json();
41 | setEmail(res.email);
42 | setFirstName(res.firstName);
43 | setLastName(res.lastName);
44 | setArn(res.arn);
45 | setRegion(res.region);
46 | } catch (err) {
47 | console.log(err);
48 | }
49 | }, [email, firstName, lastName, arn, region]);
50 |
51 | useEffect(() => {
52 | getUserDetails();
53 | }, []);
54 |
55 | return (
56 | <>
57 |
58 |
59 |
60 |
61 |
62 | }>
63 | }>
64 | }>
65 | }>
66 | } >
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | >
98 | );
99 | };
100 |
101 | export default UserDashboard;
102 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | nimbus
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from './containers/App';
4 | import './styles.css';
5 |
6 | const container = document.getElementById('root')!;
7 | const root = createRoot(container);
8 | root.render(
9 |
10 |
11 |
12 | );
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/client/types.ts:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction } from 'react';
2 |
3 | export interface SelectedApiMetrics {
4 | Latency: { timestamps: Date[], values: number[] },
5 | Count: { timestamps: Date[], values: number[] },
6 | '5XXError': { timestamps: Date[], values: number[] },
7 | '4XXError': { timestamps: Date[], values: number[] }
8 | };
9 |
10 | export type ApiMetricsProps = {
11 | selectedApi: string
12 | apiMetrics: any;
13 | };
14 |
15 | export type Metric = 'Latency' | 'Count' | '5XXError' | '4XXError';
16 |
17 | export type Message = 'fetching data...' | 'data not found';
18 |
19 | export type ApiRelationsProps = {
20 | selectedApi: string
21 | apiRelations: Array<{apiName: string, endpoints: {[key: string]: Method[]}}> | null | undefined;
22 | };
23 |
24 | export type Method = {
25 | func: string,
26 | method: string,
27 | };
28 |
29 | export type View = 'metrics' | 'relations';
30 |
31 | export type DonutChartProps = {
32 | rawData: {labels?: string[], data?: number[]}
33 | };
34 |
35 | export type FunctionProps = {
36 | funcName: string
37 | invocations: Data
38 | errors: Data
39 | throttles: Data
40 | duration: Data
41 | }
42 |
43 | export type Data = {
44 | values: Array,
45 | timestamp: Array
46 | }
47 |
48 | export type RawData = {
49 | y: number,
50 | x: string,
51 | };
52 |
53 | export type chartJSData = Array;
54 |
55 | export type costProps = {
56 | memory: number[],
57 | invocations: number[],
58 | duration: number[]
59 | };
60 |
61 | export type HomeProps = {
62 | firstName: string;
63 | };
64 |
65 | export interface HeadBarProps {
66 | toggleTheme: () => void;
67 | theme: String;
68 | };
69 |
70 | export type LineChartProps = {
71 | rawData : Array,
72 | label : string,
73 | };
74 |
75 | export interface AuthProps {
76 | swapAuthView: () => void
77 | handleUserLogin: () => void
78 | };
79 |
80 | export interface UserAuthProps {
81 | handleUserLogin: () => void;
82 | toggleTheme: () => void;
83 | };
84 |
85 | export interface FetchHeader {
86 | headers: {
87 | 'Content-Type': string;
88 | authorization: {
89 | accessToken: string;
90 | refreshToken: string;
91 | };
92 | };
93 | };
94 |
95 | export interface UserData {
96 | email: String;
97 | firstName: String;
98 | lastName: String;
99 | password: String;
100 | confirmation: String;
101 | arn: String;
102 | region: String;
103 | };
104 |
105 | export interface ProfileData {
106 | firstName: String;
107 | lastName: String;
108 | arn: String;
109 | region: String;
110 | };
111 |
112 | export interface PasswordData {
113 | password: String;
114 | confirmation: String;
115 | };
116 |
117 | export type SettingsProps = {
118 | email: string;
119 | firstName: string;
120 | lastName: string;
121 | password: string;
122 | confirmation: string;
123 | arn: string;
124 | region: string;
125 | setEmail: Dispatch>;
126 | setFirstName: Dispatch>;
127 | setLastName: Dispatch>;
128 | setPassword: Dispatch>;
129 | setConfirmation: Dispatch>;
130 | setArn: Dispatch>;
131 | setRegion: Dispatch>;
132 | };
133 |
134 | // Function is used in Home&Function components
135 | // Converted RawData into a structure that is compatible with ChartJS
136 | export const convertToChartJSStructure = (rawData: Data) => {
137 | const output = [];
138 |
139 | for (let i = rawData.values.length - 1; i >= 0; i--) {
140 | const subElement: RawData = {
141 | y: rawData.values[i],
142 | x: new Date(rawData.timestamp[i]).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"})
143 | }
144 | output.push(subElement);
145 | // Get the date of the current iteration
146 | let date = new Date(rawData.timestamp[i])
147 | // If the next day is less than the next date in our iteration push a value of 0 and the next day into our object
148 | if ((date.getTime() + 1) < (new Date (rawData.timestamp[i - 1])).getTime()) {
149 | date.setDate(date.getDate() + 1)
150 | while (date.getTime() < (new Date (rawData.timestamp[i - 1])).getTime()) {
151 | const subElement: RawData = {
152 | y: 0,
153 | x: new Date(date).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"})
154 | }
155 | output.push(subElement);
156 | date.setDate(date.getDate() + 1)
157 | }
158 | }
159 | }
160 | return output;
161 | };
162 |
163 |
--------------------------------------------------------------------------------
/dist/styles.scss:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | 5. Use the user's configured `sans` font-feature-settings by default.
34 | */
35 |
36 | html {
37 | line-height: 1.5;
38 | /* 1 */
39 | -webkit-text-size-adjust: 100%;
40 | /* 2 */
41 | -moz-tab-size: 4;
42 | /* 3 */
43 | -o-tab-size: 4;
44 | tab-size: 4;
45 | /* 3 */
46 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
47 | /* 4 */
48 | font-feature-settings: normal;
49 | /* 5 */
50 | }
51 |
52 | /*
53 | 1. Remove the margin in all browsers.
54 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
55 | */
56 |
57 | body {
58 | margin: 0;
59 | /* 1 */
60 | line-height: inherit;
61 | /* 2 */
62 | }
63 |
64 | /*
65 | 1. Add the correct height in Firefox.
66 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
67 | 3. Ensure horizontal rules are visible by default.
68 | */
69 |
70 | hr {
71 | height: 0;
72 | /* 1 */
73 | color: inherit;
74 | /* 2 */
75 | border-top-width: 1px;
76 | /* 3 */
77 | }
78 |
79 | /*
80 | Add the correct text decoration in Chrome, Edge, and Safari.
81 | */
82 |
83 | abbr:where([title]) {
84 | -webkit-text-decoration: underline dotted;
85 | text-decoration: underline dotted;
86 | }
87 |
88 | /*
89 | Remove the default font size and weight for headings.
90 | */
91 |
92 | h1,
93 | h2,
94 | h3,
95 | h4,
96 | h5,
97 | h6 {
98 | font-size: inherit;
99 | font-weight: inherit;
100 | }
101 |
102 | /*
103 | Reset links to optimize for opt-in styling instead of opt-out.
104 | */
105 |
106 | a {
107 | color: inherit;
108 | text-decoration: inherit;
109 | }
110 |
111 | /*
112 | Add the correct font weight in Edge and Safari.
113 | */
114 |
115 | b,
116 | strong {
117 | font-weight: bolder;
118 | }
119 |
120 | /*
121 | 1. Use the user's configured `mono` font family by default.
122 | 2. Correct the odd `em` font sizing in all browsers.
123 | */
124 |
125 | code,
126 | kbd,
127 | samp,
128 | pre {
129 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
130 | /* 1 */
131 | font-size: 1em;
132 | /* 2 */
133 | }
134 |
135 | /*
136 | Add the correct font size in all browsers.
137 | */
138 |
139 | small {
140 | font-size: 80%;
141 | }
142 |
143 | /*
144 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
145 | */
146 |
147 | sub,
148 | sup {
149 | font-size: 75%;
150 | line-height: 0;
151 | position: relative;
152 | vertical-align: baseline;
153 | }
154 |
155 | sub {
156 | bottom: -0.25em;
157 | }
158 |
159 | sup {
160 | top: -0.5em;
161 | }
162 |
163 | /*
164 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
165 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
166 | 3. Remove gaps between table borders by default.
167 | */
168 |
169 | table {
170 | text-indent: 0;
171 | /* 1 */
172 | border-color: inherit;
173 | /* 2 */
174 | border-collapse: collapse;
175 | /* 3 */
176 | }
177 |
178 | /*
179 | 1. Change the font styles in all browsers.
180 | 2. Remove the margin in Firefox and Safari.
181 | 3. Remove default padding in all browsers.
182 | */
183 |
184 | button,
185 | input,
186 | optgroup,
187 | select,
188 | textarea {
189 | font-family: inherit;
190 | /* 1 */
191 | font-size: 100%;
192 | /* 1 */
193 | font-weight: inherit;
194 | /* 1 */
195 | line-height: inherit;
196 | /* 1 */
197 | color: inherit;
198 | /* 1 */
199 | margin: 0;
200 | /* 2 */
201 | padding: 0;
202 | /* 3 */
203 | }
204 |
205 | /*
206 | Remove the inheritance of text transform in Edge and Firefox.
207 | */
208 |
209 | button,
210 | select {
211 | text-transform: none;
212 | }
213 |
214 | /*
215 | 1. Correct the inability to style clickable types in iOS and Safari.
216 | 2. Remove default button styles.
217 | */
218 |
219 | button,
220 | [type='button'],
221 | [type='reset'],
222 | [type='submit'] {
223 | -webkit-appearance: button;
224 | /* 1 */
225 | background-color: transparent;
226 | /* 2 */
227 | background-image: none;
228 | /* 2 */
229 | }
230 |
231 | /*
232 | Use the modern Firefox focus style for all focusable elements.
233 | */
234 |
235 | :-moz-focusring {
236 | outline: auto;
237 | }
238 |
239 | /*
240 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
241 | */
242 |
243 | :-moz-ui-invalid {
244 | box-shadow: none;
245 | }
246 |
247 | /*
248 | Add the correct vertical alignment in Chrome and Firefox.
249 | */
250 |
251 | progress {
252 | vertical-align: baseline;
253 | }
254 |
255 | /*
256 | Correct the cursor style of increment and decrement buttons in Safari.
257 | */
258 |
259 | ::-webkit-inner-spin-button,
260 | ::-webkit-outer-spin-button {
261 | height: auto;
262 | }
263 |
264 | /*
265 | 1. Correct the odd appearance in Chrome and Safari.
266 | 2. Correct the outline style in Safari.
267 | */
268 |
269 | [type='search'] {
270 | -webkit-appearance: textfield;
271 | /* 1 */
272 | outline-offset: -2px;
273 | /* 2 */
274 | }
275 |
276 | /*
277 | Remove the inner padding in Chrome and Safari on macOS.
278 | */
279 |
280 | ::-webkit-search-decoration {
281 | -webkit-appearance: none;
282 | }
283 |
284 | /*
285 | 1. Correct the inability to style clickable types in iOS and Safari.
286 | 2. Change font properties to `inherit` in Safari.
287 | */
288 |
289 | ::-webkit-file-upload-button {
290 | -webkit-appearance: button;
291 | /* 1 */
292 | font: inherit;
293 | /* 2 */
294 | }
295 |
296 | /*
297 | Add the correct display in Chrome and Safari.
298 | */
299 |
300 | summary {
301 | display: list-item;
302 | }
303 |
304 | /*
305 | Removes the default spacing and border for appropriate elements.
306 | */
307 |
308 | blockquote,
309 | dl,
310 | dd,
311 | h1,
312 | h2,
313 | h3,
314 | h4,
315 | h5,
316 | h6,
317 | hr,
318 | figure,
319 | p,
320 | pre {
321 | margin: 0;
322 | }
323 |
324 | fieldset {
325 | margin: 0;
326 | padding: 0;
327 | }
328 |
329 | legend {
330 | padding: 0;
331 | }
332 |
333 | ol,
334 | ul,
335 | menu {
336 | list-style: none;
337 | margin: 0;
338 | padding: 0;
339 | }
340 |
341 | /*
342 | Prevent resizing textareas horizontally by default.
343 | */
344 |
345 | textarea {
346 | resize: vertical;
347 | }
348 |
349 | /*
350 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
351 | 2. Set the default placeholder color to the user's configured gray 400 color.
352 | */
353 |
354 | input::-moz-placeholder, textarea::-moz-placeholder {
355 | opacity: 1;
356 | /* 1 */
357 | color: #9ca3af;
358 | /* 2 */
359 | }
360 |
361 | input::placeholder,
362 | textarea::placeholder {
363 | opacity: 1;
364 | /* 1 */
365 | color: #9ca3af;
366 | /* 2 */
367 | }
368 |
369 | /*
370 | Set the default cursor for buttons.
371 | */
372 |
373 | button,
374 | [role="button"] {
375 | cursor: pointer;
376 | }
377 |
378 | /*
379 | Make sure disabled buttons don't get the pointer cursor.
380 | */
381 |
382 | :disabled {
383 | cursor: default;
384 | }
385 |
386 | /*
387 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
388 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
389 | This can trigger a poorly considered lint error in some tools but is included by design.
390 | */
391 |
392 | img,
393 | svg,
394 | video,
395 | canvas,
396 | audio,
397 | iframe,
398 | embed,
399 | object {
400 | display: block;
401 | /* 1 */
402 | vertical-align: middle;
403 | /* 2 */
404 | }
405 |
406 | /*
407 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
408 | */
409 |
410 | img,
411 | video {
412 | max-width: 100%;
413 | height: auto;
414 | }
415 |
416 | /* Make elements with the HTML hidden attribute stay hidden by default */
417 |
418 | [hidden] {
419 | display: none;
420 | }
421 |
422 | *, ::before, ::after {
423 | --tw-border-spacing-x: 0;
424 | --tw-border-spacing-y: 0;
425 | --tw-translate-x: 0;
426 | --tw-translate-y: 0;
427 | --tw-rotate: 0;
428 | --tw-skew-x: 0;
429 | --tw-skew-y: 0;
430 | --tw-scale-x: 1;
431 | --tw-scale-y: 1;
432 | --tw-pan-x: ;
433 | --tw-pan-y: ;
434 | --tw-pinch-zoom: ;
435 | --tw-scroll-snap-strictness: proximity;
436 | --tw-ordinal: ;
437 | --tw-slashed-zero: ;
438 | --tw-numeric-figure: ;
439 | --tw-numeric-spacing: ;
440 | --tw-numeric-fraction: ;
441 | --tw-ring-inset: ;
442 | --tw-ring-offset-width: 0px;
443 | --tw-ring-offset-color: #fff;
444 | --tw-ring-color: rgb(59 130 246 / 0.5);
445 | --tw-ring-offset-shadow: 0 0 #0000;
446 | --tw-ring-shadow: 0 0 #0000;
447 | --tw-shadow: 0 0 #0000;
448 | --tw-shadow-colored: 0 0 #0000;
449 | --tw-blur: ;
450 | --tw-brightness: ;
451 | --tw-contrast: ;
452 | --tw-grayscale: ;
453 | --tw-hue-rotate: ;
454 | --tw-invert: ;
455 | --tw-saturate: ;
456 | --tw-sepia: ;
457 | --tw-drop-shadow: ;
458 | --tw-backdrop-blur: ;
459 | --tw-backdrop-brightness: ;
460 | --tw-backdrop-contrast: ;
461 | --tw-backdrop-grayscale: ;
462 | --tw-backdrop-hue-rotate: ;
463 | --tw-backdrop-invert: ;
464 | --tw-backdrop-opacity: ;
465 | --tw-backdrop-saturate: ;
466 | --tw-backdrop-sepia: ;
467 | }
468 |
469 | ::backdrop {
470 | --tw-border-spacing-x: 0;
471 | --tw-border-spacing-y: 0;
472 | --tw-translate-x: 0;
473 | --tw-translate-y: 0;
474 | --tw-rotate: 0;
475 | --tw-skew-x: 0;
476 | --tw-skew-y: 0;
477 | --tw-scale-x: 1;
478 | --tw-scale-y: 1;
479 | --tw-pan-x: ;
480 | --tw-pan-y: ;
481 | --tw-pinch-zoom: ;
482 | --tw-scroll-snap-strictness: proximity;
483 | --tw-ordinal: ;
484 | --tw-slashed-zero: ;
485 | --tw-numeric-figure: ;
486 | --tw-numeric-spacing: ;
487 | --tw-numeric-fraction: ;
488 | --tw-ring-inset: ;
489 | --tw-ring-offset-width: 0px;
490 | --tw-ring-offset-color: #fff;
491 | --tw-ring-color: rgb(59 130 246 / 0.5);
492 | --tw-ring-offset-shadow: 0 0 #0000;
493 | --tw-ring-shadow: 0 0 #0000;
494 | --tw-shadow: 0 0 #0000;
495 | --tw-shadow-colored: 0 0 #0000;
496 | --tw-blur: ;
497 | --tw-brightness: ;
498 | --tw-contrast: ;
499 | --tw-grayscale: ;
500 | --tw-hue-rotate: ;
501 | --tw-invert: ;
502 | --tw-saturate: ;
503 | --tw-sepia: ;
504 | --tw-drop-shadow: ;
505 | --tw-backdrop-blur: ;
506 | --tw-backdrop-brightness: ;
507 | --tw-backdrop-contrast: ;
508 | --tw-backdrop-grayscale: ;
509 | --tw-backdrop-hue-rotate: ;
510 | --tw-backdrop-invert: ;
511 | --tw-backdrop-opacity: ;
512 | --tw-backdrop-saturate: ;
513 | --tw-backdrop-sepia: ;
514 | }
515 |
516 | .container {
517 | width: 100%;
518 | }
519 |
520 | @media (min-width: 640px) {
521 | .container {
522 | max-width: 640px;
523 | }
524 | }
525 |
526 | @media (min-width: 768px) {
527 | .container {
528 | max-width: 768px;
529 | }
530 | }
531 |
532 | @media (min-width: 1024px) {
533 | .container {
534 | max-width: 1024px;
535 | }
536 | }
537 |
538 | @media (min-width: 1280px) {
539 | .container {
540 | max-width: 1280px;
541 | }
542 | }
543 |
544 | @media (min-width: 1536px) {
545 | .container {
546 | max-width: 1536px;
547 | }
548 | }
549 |
550 | .flex {
551 | display: flex;
552 | }
553 |
554 | .gap-8 {
555 | gap: 2rem;
556 | }
557 |
--------------------------------------------------------------------------------
/electron/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require("electron");
2 |
3 | // const server = require("../server/server.js");
4 |
5 | let mainWindow;
6 |
7 | function createWindow() {
8 | mainWindow = new BrowserWindow({
9 | width: 1200,
10 | height: 700,
11 | webPreferences: {
12 | nodeIntegration: true,
13 | },
14 | });
15 |
16 | mainWindow.loadURL("http://localhost:8080");
17 | mainWindow.on("closed", function () {
18 | mainWindow = null;
19 | });
20 | }
21 |
22 | app.on("ready", createWindow);
23 |
24 | app.on("resize", function (e, x, y) {
25 | mainWindow.setSize(x, y);
26 | });
27 |
28 | app.on("window-all-closed", function () {
29 | if (process.platform !== "darwin") {
30 | app.quit();
31 | }
32 | });
33 |
34 | app.on("activate", function () {
35 | if (mainWindow === null) {
36 | createWindow();
37 | }
38 | });
--------------------------------------------------------------------------------
/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | AWS_ACCESS_KEY_ID: string;
5 | AWS_SECRET_KEY: string;
6 | }
7 | }
8 | }
9 |
10 | declare module "*.png" {
11 | export default "" as string;
12 | }
13 | declare module "*.svg" {
14 | export default "" as string;
15 | }
16 | declare module "*.jpeg" {
17 | export default "" as string;
18 | }
19 | declare module "*.jpg" {
20 | export default "" as string;
21 | }
22 |
23 |
24 | export {}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nimbus",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "electron/main.js",
6 | "scripts": {
7 | "test": "jest",
8 | "start": "node server/server.js",
9 | "dev": "webpack serve --mode development & npm start",
10 | "build": "webpack --config ./webpack.config.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/oslabs-beta/nimbus.git"
15 | },
16 | "keywords": [],
17 | "author": "Arturo Kim https://github.com/arturokim, Zhaowei Sun https://github.com/zhaowei-sun, Arthur Su https://github.com/suster22, Georges Maroun https://github.com/george-maroun, Madeline Doctor https://github.com/madelinedoctor1",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/oslabs-beta/nimbus/issues"
21 | },
22 | "homepage": "https://github.com/oslabs-beta/nimbus#readme",
23 | "dependencies": {
24 | "@aws-sdk/client-api-gateway": "^3.231.0",
25 | "@aws-sdk/client-cloudwatch": "^3.231.0",
26 | "@aws-sdk/client-cloudwatch-logs": "^3.231.0",
27 | "@aws-sdk/client-lambda": "^3.231.0",
28 | "@aws-sdk/client-sts": "^3.231.0",
29 | "@aws-sdk/types": "^3.226.0",
30 | "@headlessui/react": "^1.7.7",
31 | "@heroicons/react": "^2.0.13",
32 | "@types/jsonwebtoken": "^8.5.9",
33 | "aws-sdk": "^2.1339.0",
34 | "bcrypt": "^5.1.0",
35 | "chart.js": "^4.1.2",
36 | "chartjs-adapter-moment": "^1.0.1",
37 | "chroma-js": "^2.4.2",
38 | "cookie-parser": "^1.4.6",
39 | "cors": "^2.8.5",
40 | "date-fns": "^2.29.3",
41 | "express": "^4.18.2",
42 | "jsonwebtoken": "^8.5.1",
43 | "moment": "^2.29.4",
44 | "mongoose": "^6.8.0",
45 | "react": "^18.2.0",
46 | "react-chartjs-2": "^5.1.0",
47 | "react-daisyui": "^2.5.0",
48 | "react-dom": "^18.2.0",
49 | "react-router": "^6.4.3",
50 | "react-router-dom": "^6.4.3",
51 | "reactflow": "^11.4.0",
52 | "sass": "^1.56.1",
53 | "typescript": "^4.9.4",
54 | "uuid": "^9.0.0"
55 | },
56 | "devDependencies": {
57 | "@babel/core": "^7.20.2",
58 | "@babel/preset-env": "^7.20.2",
59 | "@babel/preset-react": "^7.18.6",
60 | "@testing-library/react": "^13.4.0",
61 | "@types/bcryptjs": "^2.4.2",
62 | "@types/chroma-js": "^2.1.4",
63 | "@types/express": "^4.17.14",
64 | "@types/node": "^18.11.18",
65 | "@types/react": "^18.0.26",
66 | "@types/react-dom": "^18.0.9",
67 | "@types/uuid": "^9.0.0",
68 | "autoprefixer": "^10.4.13",
69 | "babel-loader": "^9.1.0",
70 | "css-loader": "^6.7.2",
71 | "daisyui": "^2.46.0",
72 | "docker": "^1.0.0",
73 | "dotenv": "^16.0.3",
74 | "dotenv-webpack": "^8.0.1",
75 | "electron": "^22.0.0",
76 | "eslint": "^8.29.0",
77 | "file-loader": "^6.2.0",
78 | "html-webpack-plugin": "^5.5.0",
79 | "jest": "^29.3.1",
80 | "nodemon": "^2.0.20",
81 | "postcss": "^8.4.20",
82 | "postcss-loader": "^7.0.2",
83 | "sass-loader": "^13.2.0",
84 | "style-loader": "^3.3.1",
85 | "supertest": "^6.3.3",
86 | "tailwindcss": "^3.2.4",
87 | "ts-jest": "^29.0.3",
88 | "ts-loader": "^9.4.2",
89 | "webpack": "^5.75.0",
90 | "webpack-cli": "^5.0.0",
91 | "webpack-dev-server": "^4.11.1"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/server/controllers/authController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getUserToken, authController } from '../types';
3 | const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env;
4 | const jwt = require('jsonwebtoken')
5 | require('dotenv').config();
6 |
7 |
8 | const authController: authController = {
9 | async generateJWT (req, res, next) {
10 | try {
11 | const { email } = req.body;
12 | // Grab user from database
13 | res.locals.user = {
14 | email
15 | }
16 | // Generate an access and refresh token for user
17 | const accessToken = jwt.sign(res.locals.user, ACCESS_TOKEN_SECRET, { expiresIn: '24h'});
18 | const refreshToken = jwt.sign(res.locals.user, REFRESH_TOKEN_SECRET);
19 | // Store refresh token in cookies
20 | res.cookie('refreshToken', refreshToken, { secure: true, httpOnly: true, sameSite: 'strict' });
21 | res.locals.accessToken = accessToken;
22 | return next();
23 | } catch (err) {
24 | return next({
25 | log: "Error caught in userController.generateJWT middleware function",
26 | status: 500,
27 | message: {err: `Error generating JWT for user`}
28 | })
29 | }
30 | },
31 |
32 | async verifyToken (req, res, next) {
33 | const { authorization } = req.headers;
34 | // Token should be 'BEARER TOKEN NUMBER' so slice the string to grab only the token number
35 | const token = authorization && authorization.slice(7);
36 | // Check if user has an access token
37 | if (token) {
38 | // Verify if token is valid
39 | let user;
40 | try {
41 | user = jwt.verify(token, ACCESS_TOKEN_SECRET);
42 | } catch (err) {
43 | console.log(err);
44 | }
45 | if (user) {
46 | res.locals.accessToken = token;
47 | res.locals.email = user.email;
48 | }
49 | else {
50 | // If the user's token was not verified, check for refresh token
51 | const refreshToken = req.cookies.refreshToken
52 | try {
53 | // Verify if user's refresh token is valid
54 | const user = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
55 | if (user) {
56 | // Generate new access token
57 | user.iat = Date.now()
58 | const newAccessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '24h'})
59 | res.locals.accessToken = newAccessToken;
60 | res.locals.email = user.email
61 | }
62 | } catch (err) {
63 | console.log(err);
64 | }
65 | }
66 | }
67 | return next();
68 | } ,
69 |
70 | removeToken (req, res, next) {
71 | res.clearCookie("refreshToken");
72 | return next();
73 | }
74 | };
75 |
76 | export default authController;
--------------------------------------------------------------------------------
/server/controllers/aws/apiController.ts:
--------------------------------------------------------------------------------
1 | import { APIGatewayClient, GetRestApisCommand, GetResourcesCommand, GetResourcesCommandInput, GetRestApisCommandOutput } from "@aws-sdk/client-api-gateway";
2 | import { Request, Response, NextFunction } from "express";
3 | import { LambdaClient, GetPolicyCommand, GetPolicyCommandOutput } from "@aws-sdk/client-lambda";
4 | import { Endpoint, LambdaAPIs, API, Relation } from "../../types";
5 |
6 | // Controller for the API Gateway endpoints
7 | const apiController = {
8 | // Get relations between API Gateway endpoints and Lambda functions
9 | async getAPIRelations(req: Request, res: Response, next: NextFunction) {
10 | // Create new APIGatewayClient
11 | const apiClient = new APIGatewayClient({
12 | region: res.locals.region,
13 | credentials: res.locals.credentials,
14 | });
15 |
16 | // Create new LambdaClient
17 | const lambdaClient = new LambdaClient({
18 | region: res.locals.region,
19 | credentials: res.locals.credentials
20 | });
21 |
22 | // Declare array to store APIs
23 | const apiList: API[] = [];
24 | const lambdaAPIsList: LambdaAPIs[] = [];
25 |
26 | try {
27 | // Get list of APIs and store in apiList
28 | const restAPIs: GetRestApisCommandOutput = await apiClient.send(new GetRestApisCommand({}));
29 | const restAPIsItems = restAPIs?.items;
30 |
31 | if (restAPIsItems !== undefined) {
32 | for (const item of restAPIsItems) {
33 | const getResourcesInput: GetResourcesCommandInput = { restApiId: item.id };
34 | const resources = await apiClient.send(new GetResourcesCommand(getResourcesInput));
35 | const paths = resources?.items?.map(item => item.path);
36 | const apiDetails: API = {apiName: item.name, apiId: item.id, paths: paths};
37 | apiList.push(apiDetails);
38 | }
39 | }
40 |
41 | // Get list of Lambda functions from res.locals
42 | const functions: string[] = res.locals.functions;
43 |
44 | // For each function, get the policy, create apiInfo and store apiInfo in lambdaAPIsList
45 | for (const func of functions) {
46 | try {
47 | const functionName = func;
48 |
49 | const getPolicyCommand = new GetPolicyCommand({
50 | FunctionName: functionName
51 | });
52 |
53 | const policyResults: GetPolicyCommandOutput = await lambdaClient.send(getPolicyCommand);
54 |
55 | if (policyResults) {
56 | const policy: (string | undefined) = policyResults.Policy;
57 | if (policy) {
58 |
59 | const statements = JSON.parse(policy).Statement;
60 | const lambdaAPIs: LambdaAPIs = {
61 | functionName,
62 | endpoints: []
63 | }
64 | for (const statement of statements) {
65 | const apiEndpoint = statement.Condition.ArnLike['AWS:SourceArn'];
66 | const apiEndpointStrArr = apiEndpoint.split('/');
67 | const apiIdStrArr = apiEndpointStrArr[apiEndpointStrArr.length - 4].split(':');
68 | const apiId = apiIdStrArr[apiIdStrArr.length - 1];
69 | const apiMethod = apiEndpointStrArr[apiEndpointStrArr.length - 2];
70 | const apiPath = apiEndpointStrArr[apiEndpointStrArr.length - 1];
71 | const apiInfo = { apiMethod, apiPath: '/' + apiPath, apiId };
72 | lambdaAPIs.endpoints.push(apiInfo);
73 | }
74 | lambdaAPIsList.push(lambdaAPIs);
75 | }
76 | }
77 |
78 | } catch (err) {
79 | console.log(err);
80 | }
81 | }
82 | // Declare array to store relations
83 | const relations = [];
84 |
85 | // For each API, create relationObj and store in relations
86 | for (const api of apiList) {
87 | const relationObj:Relation = { apiName: api.apiName, endpoints: {} };
88 | if (relationObj.endpoints) {
89 | for (const lambdaAPI of lambdaAPIsList) {
90 | for (const ep of lambdaAPI.endpoints) {
91 | if (ep && ep?.apiId === api.apiId) {
92 | const path: string = ep.apiPath
93 | if (relationObj.endpoints[path] === undefined) {
94 | relationObj.endpoints[path] = [];
95 | }
96 | relationObj.endpoints[ep.apiPath].push({
97 | method: ep.apiMethod,
98 | func: lambdaAPI.functionName
99 | });
100 | }
101 | }
102 | }
103 | }
104 | relations.push(relationObj);
105 | }
106 |
107 | res.locals.apiRelations = relations;
108 | return next();
109 | }
110 | // If error, pass to error handler
111 | catch (err) {
112 | next({
113 | log: "Error caught in apiController.getAPIRelations middleware function",
114 | status: 500,
115 | message: {errMessage: `Error getting API relations for the account`, err: err}
116 | });
117 | }
118 | },
119 |
120 | // Get list of API endpoints
121 | async getAPIList(req: Request, res: Response, next: NextFunction) {
122 |
123 | // Create new APIGatewayClient
124 | const apiClient = new APIGatewayClient({
125 | region: res.locals.region,
126 | credentials: res.locals.credentials,
127 | });
128 |
129 | // Declare array to store APIs
130 | const apiList: API[] = [];
131 |
132 | try {
133 | // Get list of APIs and store in apiList
134 | const restAPIs: GetRestApisCommandOutput = await apiClient.send(new GetRestApisCommand({}));
135 | const restAPIsItems = restAPIs?.items;
136 |
137 | if (restAPIsItems !== undefined) {
138 | for (const item of restAPIsItems) {
139 | const getResourcesInput: GetResourcesCommandInput = { restApiId: item.id };
140 | const resources = await apiClient.send(new GetResourcesCommand(getResourcesInput));
141 | const paths = resources?.items?.map(item => item.path);
142 | const apiDetails: API = {apiName: item.name, apiId: item.id, paths: paths};
143 | apiList.push(apiDetails);
144 | }
145 | }
146 | res.locals.apiList = apiList;
147 | return next();
148 |
149 | } catch (err) {
150 | // If error, pass to error handler
151 | next({
152 | log: "Error caught in apiController.getAPIList middleware function",
153 | status: 500,
154 | message: {errMessage: `Error getting API relations for the account`, err: err}
155 | });
156 | }
157 | }
158 | }
159 |
160 | export default apiController;
161 |
--------------------------------------------------------------------------------
/server/controllers/aws/apiMetricsController.tsx:
--------------------------------------------------------------------------------
1 | import { CloudWatchClient, GetMetricDataCommand, ListDashboardsCommand, GetMetricDataCommandInput, GetMetricDataCommandOutput, MetricDataQuery, MetricDataResult } from "@aws-sdk/client-cloudwatch";
2 | import { Request, Response, NextFunction } from "express";
3 | require('dotenv').config();
4 |
5 | // Controller for getting metrics for all APIs
6 | const apiMetricsController = {
7 | // Gets metrics for each API in res.locals.apiList (from apiController.getAPIList)
8 | getAPIMetrics: async (req: Request, res: Response, next: NextFunction) => {
9 |
10 | // Create a new CloudWatch client with provided credentials and region
11 | const cwClient = new CloudWatchClient({
12 | region: res.locals.region,
13 | credentials: res.locals.credentials,
14 | });
15 |
16 | // Declare output object
17 | const allApiMetrics:any = {};
18 |
19 | const metrics = ['Latency', 'Count', '5XXError', '4XXError']
20 |
21 | // For each API in res.locals.apiList, get metrics and store in allApiMetrics
22 | for (let apiObj of res.locals.apiList) {
23 | const { apiName } = apiObj;
24 | // Declare obj to store all the metrics of the current API
25 | const currApiMetrics = {};
26 |
27 | for (let metric of metrics) {
28 | // Obtain input for GetMetricDataCommand using helper function: getCommandInput
29 | const metricParams: GetMetricDataCommandInput = getCommandInput(
30 | apiName,
31 | metric
32 | );
33 |
34 | try {
35 | // Obtain the data for curr API and curr metric
36 | const currMetricData: GetMetricDataCommandOutput = await cwClient.send(
37 | new GetMetricDataCommand(metricParams)
38 | );
39 |
40 | interface TimeValObj {
41 | timestamps?: any;
42 | values?: any;
43 | }
44 |
45 | // Declare an object to store the timestamps and values
46 | const timeValObj: TimeValObj = {}
47 | const results = currMetricData?.MetricDataResults;
48 | if (results) {
49 | timeValObj.timestamps = results[0].Timestamps;
50 | timeValObj.values = results[0].Values;
51 | }
52 | (currApiMetrics as any)[metric] = timeValObj;
53 | }
54 | // If error, invoke next middleware function
55 | catch (err) {
56 | next({
57 | log: "Error caught in apiMetricsController.getAPIMetrics middleware function",
58 | status: 500,
59 | message: {errMessage: `Error getting all API metrics`, err: err}
60 | });
61 | }
62 | }
63 | allApiMetrics[apiName] = currApiMetrics;
64 | }
65 | res.locals.allApiMetrics = allApiMetrics
66 | return next();
67 | }
68 | }
69 |
70 | // Helper function to obtain input for GetMetricDataCommand
71 | const getCommandInput = (apiName:string, metricName:string, stat='Sum') => {
72 |
73 | if (metricName === 'Count') {
74 | stat = 'SampleCount';
75 | }
76 |
77 | const metricParamsBaseAllFunc = {
78 | StartTime: new Date(new Date().setDate(new Date().getDate() - 7)),
79 | EndTime: new Date(),
80 | LabelOptions: {
81 | Timezone: '-0400',
82 | },
83 | };
84 |
85 | const metricDataQueryAllfunc = [
86 | {
87 | Id: `m_API_Gateway_${metricName}`,
88 | Label: `${apiName} API ${metricName}`,
89 | MetricStat: {
90 | Metric: {
91 | Namespace: 'AWS/ApiGateway',
92 | MetricName: metricName,
93 | Dimensions: [{ Name: 'ApiName', Value: apiName }],
94 | },
95 | Period: 60 * 60 * 24,
96 | Stat: stat,
97 | },
98 | },
99 | ];
100 |
101 | const metricParamsAllfunc = {
102 | ...metricParamsBaseAllFunc,
103 | MetricDataQueries: metricDataQueryAllfunc,
104 | };
105 |
106 | return metricParamsAllfunc;
107 |
108 | }
109 |
110 |
111 | export default apiMetricsController;
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/server/controllers/aws/credentialsController.ts:
--------------------------------------------------------------------------------
1 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
2 | import { AwsCredentialIdentity } from "@aws-sdk/types";
3 | import { Request, Response, NextFunction } from "express";
4 | import dotenv from 'dotenv';
5 | dotenv.config();
6 | const AWS = require('aws-sdk');
7 |
8 | AWS.config.update({region: process.env.AWS_REGION});
9 | const dynamodb = new AWS.DynamoDB.DocumentClient();
10 |
11 | const credentials: AwsCredentialIdentity = {
12 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
13 | secretAccessKey: process.env.AWS_SECRET_KEY,
14 | };
15 |
16 | const region = process.env.AWS_REGION;
17 |
18 | // Create Amazon Cloudwatch Logs service client object
19 | const client = new STSClient({ region, credentials });
20 |
21 | // Establish relationship between Nimbus AWS account and client's account
22 | const credentialsController = {
23 | // Get credentials for client's account from AWS and store in res.locals
24 | async getCredentials(req: Request, res: Response, next: NextFunction) {
25 | const roleDetails = {
26 | RoleArn: req.body.arn, //example: 'arn:aws:iam::588640996282:role/NimbusDelegationRole',
27 | RoleSessionName: 'NimbusSession'
28 | };
29 |
30 | try {
31 | // Granting ourselves permission to client's account
32 | const assumedRole = await client.send(new AssumeRoleCommand(roleDetails));
33 | const accessKeyId = assumedRole?.Credentials?.AccessKeyId;
34 | const secretAccessKey = assumedRole?.Credentials?.SecretAccessKey;
35 | const sessionToken = assumedRole?.Credentials?.SessionToken;
36 | const expiration = assumedRole?.Credentials?.Expiration;
37 | res.locals.credentials = { accessKeyId, secretAccessKey, sessionToken, expiration };
38 | res.locals.arnValidation = {validated: true};
39 | return next();
40 | }
41 | // If the ARN user input is invalid, send info to front end so that field will be highlighted red
42 | catch (err) {
43 | console.log(err);
44 | res.locals.arnValidation = {validated: false};
45 | return next();
46 | }
47 | },
48 |
49 | // Get credentials for client's account from database and store in res.locals
50 | // This function is used when grabbing information from Lambda, Gateway, etc
51 | async getCredentialsFromDB(req: Request, res: Response, next: NextFunction) {
52 | const { email } = res.locals;
53 |
54 | const params = {
55 | TableName: process.env.TABLE_NAME,
56 | Key: {
57 | 'email' : email,
58 | },
59 | };
60 |
61 | const user: any = await dynamodb.get(params).promise();
62 |
63 | if (user) {
64 | res.locals.arn = user.Item.arn;
65 | res.locals.region = user.Item.region;
66 | }
67 |
68 | const roleDetails = {
69 | RoleArn: res.locals.arn, //example: 'arn:aws:iam::588640996282:role/NimbusDelegationRole',
70 | RoleSessionName: 'NimbusSession'
71 | };
72 |
73 | try {
74 | // Granting ourselves permission to client's account
75 | const assumedRole = await client.send(new AssumeRoleCommand(roleDetails));
76 | const accessKeyId = assumedRole?.Credentials?.AccessKeyId;
77 | const secretAccessKey = assumedRole?.Credentials?.SecretAccessKey;
78 | const sessionToken = assumedRole?.Credentials?.SessionToken;
79 | res.locals.credentials = { accessKeyId, secretAccessKey, sessionToken };
80 | return next();
81 | }
82 | catch (err) {
83 | console.log(err);
84 | return next({
85 | log: "Error caught in credentialsController.getCredentialsFromDB middleware function",
86 | status: 500,
87 | message: {errMessage: `Error assigning assumed role to the provided ARN`, errors: err}
88 | });
89 | }
90 | }
91 | };
92 |
93 | export default credentialsController;
94 |
--------------------------------------------------------------------------------
/server/controllers/aws/lambdaController.ts:
--------------------------------------------------------------------------------
1 | import { LambdaClient, ListFunctionsCommand } from "@aws-sdk/client-lambda";
2 | import { Request, Response, NextFunction } from "express";
3 | import dotenv from 'dotenv';
4 | dotenv.config();
5 |
6 | const lambdaController = {
7 | // Get list of Lambda functions and store in res.locals.functions
8 | async getFunctions(req: Request, res: Response, next: NextFunction) {
9 | // Create new LambdaClient
10 | const lambdaClient = new LambdaClient({
11 | region: res.locals.region,
12 | credentials: res.locals.credentials
13 | });
14 |
15 | // Create new ListFunctionsCommand
16 | const getFunctionsCommand = new ListFunctionsCommand({});
17 |
18 | try {
19 | const commandResults = await lambdaClient.send(getFunctionsCommand);
20 | const lambdaFunctions = commandResults?.Functions;
21 | const lambdaFunctionDetails = lambdaFunctions?.map(f => f.FunctionName);
22 | res.locals.functions = lambdaFunctionDetails;
23 | return next();
24 | } catch (err) {
25 | console.log(err);
26 | return next({
27 | log: "Error caught in lambdaController.getFunctions middleware function",
28 | status: 500,
29 | message: {errMessage: `Error getting functions for the account`, errors: err}
30 | });
31 | }
32 | }
33 | }
34 |
35 | export default lambdaController;
36 |
--------------------------------------------------------------------------------
/server/controllers/aws/logsController.ts:
--------------------------------------------------------------------------------
1 | import { CloudWatchLogsClient, DescribeLogStreamsCommand, GetLogEventsCommand, OutputLogEvent, FilterLogEventsCommand, FilterLogEventsCommandInput, GetLogEventsCommandInput, DescribeLogStreamsCommandInput } from "@aws-sdk/client-cloudwatch-logs";
2 | import { Request, Response, NextFunction } from "express";
3 |
4 |
5 | const logsController = {
6 | async getAllLogs(req: Request, res: Response, next: NextFunction) {
7 | try {
8 | // Start a new CloudWatchLogsClient connection with provided region and credentials
9 | const cwLogsClient = new CloudWatchLogsClient({
10 | region: res.locals.region, //'us-east-1'
11 | credentials: res.locals.credentials,
12 | });
13 |
14 | const functionName = req.body.functionName;
15 |
16 | // Create inputs for DescribeLogStreamsCommand
17 | const streamsCommandInputs: DescribeLogStreamsCommandInput = {
18 | logGroupName: "/aws/lambda/" + functionName,
19 | }
20 |
21 | const getStreamsCommand = new DescribeLogStreamsCommand(streamsCommandInputs);
22 | const getStreamsCommandResults = await cwLogsClient.send(getStreamsCommand);
23 | const streams = getStreamsCommandResults.logStreams;
24 | const logEvents: OutputLogEvent[] = [];
25 |
26 | if (streams !== undefined) {
27 | // Get logs for each stream
28 | for (const stream of streams) {
29 | const logsCommandInputs: GetLogEventsCommandInput = {
30 | logGroupName: "/aws/lambda/" + functionName,
31 | logStreamName: stream.logStreamName
32 | }
33 |
34 | const getLogsCommand = new GetLogEventsCommand(logsCommandInputs);
35 | const getLogsCommandResults = await cwLogsClient.send(getLogsCommand);
36 | getLogsCommandResults.events?.forEach(e => logEvents.push(e));
37 | }
38 | }
39 |
40 | res.locals.logs = logEvents.map(e => e.message);
41 | return next();
42 | }
43 | catch (err) {
44 | return next({
45 | log: "Error caught in logsController.getAllLogs middleware function",
46 | status: 500,
47 | message: {errMessage: `Error getting logs for this function`, errors: err}
48 | })
49 | }
50 |
51 | },
52 |
53 | // Get filtered logs for a given func and store in res.locals.filteredLogs
54 | async getFilteredLogs(req: Request, res: Response, next: NextFunction) {
55 |
56 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond
57 | try {
58 | let StartTime;
59 | if (req.body.period === '1hr') {
60 | StartTime = new Date(
61 | new Date().setMinutes(new Date().getMinutes() - 60)
62 | ).valueOf();
63 | } else if (req.body.period === '1d') {
64 | StartTime = new Date(
65 | new Date().setDate(new Date().getDate() - 1)
66 | ).valueOf();
67 | } else if (req.body.period === '7d' || req.body.period === '' ) {
68 | StartTime = new Date(
69 | new Date().setDate(new Date().getDate() - 7)
70 | ).valueOf();
71 | } else if (req.body.period === '14d') {
72 | StartTime = new Date(
73 | new Date().setDate(new Date().getDate() - 14)
74 | ).valueOf();
75 | } else if (req.body.period === '30d') {
76 | StartTime = new Date(
77 | new Date().setDate(new Date().getDate() - 30)
78 | ).valueOf();
79 | }
80 |
81 | const filterPattern: string = req.body.filterPattern;
82 |
83 | // Start a new CloudWatchLogsClient connection with provided region and credentials
84 | const cwLogsClient = new CloudWatchLogsClient({
85 | region: res.locals.region, //'us-east-1'
86 | credentials: res.locals.credentials, //credentials
87 | });
88 |
89 | const functionName = req.body.functionName
90 |
91 | // Create input for FilterLogEventsCommand
92 | const filterCommandInputs: FilterLogEventsCommandInput = {
93 | logGroupName: "/aws/lambda/" + functionName,
94 | startTime: StartTime,
95 | endTime: new Date().valueOf(),
96 | filterPattern: filterPattern
97 | }
98 |
99 | // Get filtered logs and store in res.locals.filteredLogs
100 | const filterLogsCommand = new FilterLogEventsCommand(filterCommandInputs);
101 | const filterLogsCommandResults = await cwLogsClient.send(filterLogsCommand);
102 | res.locals.filteredLogs = filterLogsCommandResults.events?.map(e => e.message);
103 | return next();
104 | }
105 | catch(err) {
106 | return next({
107 | log: "Error caught in logsController.getFilteredLogs middleware function",
108 | status: 500,
109 | message: {errMessage: `Error getting filtered logs for this function`, err: err}
110 | })
111 | }
112 | }
113 | }
114 |
115 | export default logsController;
--------------------------------------------------------------------------------
/server/controllers/aws/metricsController.ts:
--------------------------------------------------------------------------------
1 | import { CloudWatchClient, GetMetricDataCommand, GetMetricDataCommandInput, MetricDataQuery, MetricDataResult} from "@aws-sdk/client-cloudwatch";
2 | import { LambdaClient, GetFunctionConfigurationCommand} from "@aws-sdk/client-lambda"
3 | import { Request, Response, NextFunction } from "express";
4 | import { subMetrics, Metrics } from "../../types";
5 | require('dotenv').config();
6 |
7 | const metricsController = {
8 | // Grab the Invocation, Error, Duration, and Throttle metrics for all functions
9 | async getAllMetrics(req: Request, res: Response, next: NextFunction) {
10 | try {
11 | // Initiate client with credentials
12 | const client = new CloudWatchClient({
13 | region: res.locals.region,
14 | credentials: res.locals.credentials
15 | })
16 |
17 | // Specify parameters for each metric
18 | const metricMemoryData = {
19 | Id: "m1",
20 | MetricStat: {
21 | Metric: {
22 | MetricName: "MemoryUsage",
23 | Namespace: "AWS/Lambda",
24 | },
25 | Period: 60 * 60 * 24,
26 | Stat: "Average",
27 | },
28 | Label: "Average memory usage of Lambda Functions"
29 | }
30 | const metricInvocationData = {
31 | Id: "i1",
32 | MetricStat: {
33 | Metric: {
34 | MetricName: "Invocations",
35 | Namespace: "AWS/Lambda",
36 | },
37 | Period: 60 * 60 * 24,
38 | Stat: "Sum",
39 | },
40 | Label: "Total Invocations of Lambda Functions"
41 | }
42 | const metricErrorData = {
43 | Id: "e1",
44 | MetricStat: {
45 | Metric: {
46 | MetricName: "Errors",
47 | Namespace: "AWS/Lambda"
48 | },
49 | Period: 60 * 60 * 24,
50 | Stat: "Sum",
51 | },
52 | Label: "Total Errors of Lambda Functions"
53 | }
54 | const metricThrottlesData = {
55 | Id: "t1",
56 | MetricStat: {
57 | Metric: {
58 | MetricName: "Throttles",
59 | Namespace: "AWS/Lambda"
60 | },
61 | Period: 60 * 60 * 24,
62 | Stat: "Sum",
63 | },
64 | Label: "Total Throttles of Lambda Functions"
65 | }
66 | const metricDurationData = {
67 | Id: "d1",
68 | MetricStat: {
69 | Metric: {
70 | MetricName: "Duration",
71 | Namespace: "AWS/Lambda"
72 | },
73 | Period: 60 * 60 * 24,
74 | Stat: "Sum",
75 | },
76 | Label: "Total Duration of Lambda Functions"
77 | }
78 |
79 | // Create input for GetMetricDataCommand
80 | const input: GetMetricDataCommandInput = {
81 | "StartTime": new Date(new Date().setDate(new Date().getDate() - 30)),
82 | "EndTime": new Date(),
83 | "MetricDataQueries": [metricInvocationData, metricErrorData, metricThrottlesData, metricDurationData, metricMemoryData],
84 | }
85 | const command = new GetMetricDataCommand(input)
86 | const response = await client.send(command);
87 | // Create a metrics object to send the values and timestamps of each metric to front end
88 | if (response.MetricDataResults) {
89 | const metrics: Metrics = {
90 | invocations: {
91 | values: response.MetricDataResults[0].Values,
92 | timestamp: response.MetricDataResults[0].Timestamps
93 | },
94 | errors: {
95 | values: response.MetricDataResults[1].Values,
96 | timestamp: response.MetricDataResults[1].Timestamps
97 | },
98 | throttles: {
99 | values: response.MetricDataResults[2].Values,
100 | timestamp: response.MetricDataResults[2].Timestamps
101 | },
102 | duration: {
103 | values: response.MetricDataResults[3].Values,
104 | timestamp: response.MetricDataResults[3].Timestamps
105 | },
106 | };
107 | res.locals.allFuncMetrics = metrics;
108 | }
109 | return next();
110 | } catch (err) {
111 | return next({
112 | log: "Error caught in metricsController.getAllMetrics middleware function",
113 | status: 500,
114 | message: { err: "Error grabbing metrics for all Lambda Functions" }
115 | })
116 | }
117 | },
118 | // Grab metrics from cloudwatch depending on user input (seleted func)
119 | async getMetricsByFunc (req: Request, res: Response, next: NextFunction) {
120 | try {
121 | // Start a new CloudWatchClient instance
122 | const client = new CloudWatchClient({
123 | region: res.locals.region,
124 | credentials: res.locals.credentials
125 | })
126 | const metricData: MetricDataQuery[] = []
127 | // functions from lamda controller
128 | res.locals.functions.forEach((functionName:string, i:number) => {
129 | const metricInvocationData = {
130 | Id: `i${i}`,
131 | MetricStat: {
132 | Metric: {
133 | MetricName: "Invocations",
134 | Namespace: "AWS/Lambda",
135 | Dimensions: [
136 | {
137 | Name: 'FunctionName',
138 | Value: `${functionName}`
139 | },
140 | ],
141 | },
142 | Period: 60 * 60 * 24,
143 | Stat: "Sum",
144 | },
145 | Label: `${functionName} Total invocations of Lambda Function`
146 | }
147 | metricData.push(metricInvocationData)
148 |
149 | const metricErrorData = {
150 | Id: `e${i}`,
151 | MetricStat: {
152 | Metric: {
153 | MetricName: "Errors",
154 | Namespace: "AWS/Lambda",
155 | Dimensions: [
156 | {
157 | Name: 'FunctionName',
158 | Value: `${functionName}`
159 | },
160 | ],
161 | },
162 | Period: 60 * 60 * 24,
163 | Stat: "Sum",
164 | },
165 | Label: `${functionName} Total errors of Lambda Function`
166 | }
167 | metricData.push(metricErrorData)
168 |
169 | const metricThrottlesData = {
170 | Id: `t${i}`,
171 | MetricStat: {
172 | Metric: {
173 | MetricName: "Throttles",
174 | Namespace: "AWS/Lambda",
175 | Dimensions: [
176 | {
177 | Name: 'FunctionName',
178 | Value: `${functionName}`
179 | },
180 | ],
181 | },
182 | Period: 60 * 60 * 24,
183 | Stat: "Sum",
184 | },
185 | Label: `${functionName} Total throttles of Lambda Function`
186 | }
187 | metricData.push(metricThrottlesData)
188 |
189 | const metricDurationData = {
190 | Id: `d${i}`,
191 | MetricStat: {
192 | Metric: {
193 | MetricName: "Duration",
194 | Namespace: "AWS/Lambda",
195 | Dimensions: [
196 | {
197 | Name: 'FunctionName',
198 | Value: `${functionName}`
199 | },
200 | ],
201 | },
202 | Period: 60 * 60 * 24,
203 | Stat: "Sum",
204 | },
205 | Label: `${functionName} Total duration of Lambda Function`
206 | }
207 | metricData.push(metricDurationData)
208 | })
209 | // Input to get metric data command
210 | const input: GetMetricDataCommandInput = {
211 | "StartTime": new Date(new Date().setDate(new Date().getDate() - 30)),
212 | "EndTime": new Date(),
213 | "MetricDataQueries": metricData
214 | }
215 | const command = new GetMetricDataCommand(input)
216 |
217 | const response = await client.send(command);
218 | // Create a metrics object to store the values and timestamps of specific metric
219 | if (response.MetricDataResults) {
220 |
221 | // Parse data into an object where keys are function names and values are the metrics for each function
222 | const parseData = (arr: MetricDataResult[]) => {
223 | const allFuncMetrics = {};
224 | // :oop over elements in arr in chunks of 4
225 | for (let i = 0; i < arr.length; i+=4) {
226 | // Get function name
227 | const funcName = arr[i].Label!.split(' ')[0];
228 | const metricsByFunc = {};
229 | // Populate allMetricsObj
230 | // Loop over number of metrics
231 | for (let j = 0; j < 4; j++) {
232 | const metricName = arr[i+j].Label!.split(' ')[2];
233 | // Declare func object
234 | const singleMetric: subMetrics = {
235 | values: arr[i+j].Values,
236 | timestamp: arr[i+j].Timestamps
237 | };
238 | (metricsByFunc as any)[metricName] = singleMetric;
239 | }
240 | (allFuncMetrics as any)[funcName] = metricsByFunc;
241 | }
242 | return allFuncMetrics;
243 | }
244 | // Metrics data for the functions page
245 | res.locals.eachFuncMetrics = parseData(response.MetricDataResults)
246 | }
247 | return next();
248 | } catch (err) {
249 | return next({
250 | log: "Error caught in metricsController.getMetricsByFunc middleware function",
251 | status: 500,
252 | message: { err: "Error grabbing metrics for Lambda Function" }
253 | })
254 | }
255 | },
256 | // Grab required properties to calculate cost of application
257 | async getCostProps(req: Request, res: Response, next: NextFunction) {
258 | try {
259 | // Start a new LambdaClient instance
260 | const client = new LambdaClient({
261 | region: res.locals.region,
262 | credentials: res.locals.credentials
263 | });
264 |
265 | const memory: Array = [];
266 | const invocations: Array = [];
267 | const duration: Array = [];
268 | // For each function grab the memory allocated, total invocations, and total duration
269 | for (const funcName of res.locals.functions) {
270 | const command = new GetFunctionConfigurationCommand({FunctionName: funcName});
271 | const response = await client.send(command)
272 | if (response.MemorySize) {
273 | memory.push(response.MemorySize)
274 | if (res.locals.eachFuncMetrics[funcName].invocations.values.length > 0) {
275 | invocations.push(res.locals.eachFuncMetrics[funcName].invocations.values.reduce((acc: number, curr: number) => acc + curr))
276 | } else {
277 | invocations.push(res.locals.eachFuncMetrics[funcName].invocations.values[0])
278 | }
279 | if (res.locals.eachFuncMetrics[funcName].duration.values.length > 0) {
280 | duration.push(res.locals.eachFuncMetrics[funcName].duration.values.reduce((acc: number, curr: number) => acc + curr))
281 | } else {
282 | duration.push(res.locals.eachFuncMetrics[funcName].duration.values[0])
283 | }
284 | }
285 | }
286 |
287 | // Store cost related arrays in res.locals
288 | res.locals.cost = {
289 | memory,
290 | invocations,
291 | duration
292 | }
293 |
294 | return next();
295 | } catch (err) {
296 | return next({
297 | log: "Error caught in metricsController.getCostProps middleware function",
298 | status: 500,
299 | message: { err: "Error grabbing cost for all Lambda Function" }
300 | })
301 | }
302 | }
303 | }
304 |
305 | // Change to export default syntaix
306 | export default metricsController;
--------------------------------------------------------------------------------
/server/controllers/userController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { userController , KeyType, KeyTypeSettings, KeyTypePassword } from '../types';
3 | require('dotenv').config();
4 | const AWS = require('aws-sdk');
5 | const bcrypt = require('bcrypt');
6 | const SALT_WORK_FACTOR = 10;
7 | const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
8 | const jwt = require('jsonwebtoken');
9 |
10 | AWS.config.update({region: process.env.AWS_REGION});
11 | const dynamodb = new AWS.DynamoDB.DocumentClient();
12 |
13 | const userController: userController = {
14 |
15 | // Middleware for user login
16 | async verifyUser(req, res, next) {
17 | const { email, password } = req.body;
18 |
19 | const params = {
20 | TableName: process.env.TABLE_NAME,
21 | Key: {
22 | 'email' : email,
23 | },
24 | };
25 |
26 | try {
27 | const user: any = await dynamodb.get(params).promise();
28 |
29 | // If the user does not exist in the database, invoke global error handler
30 | if (!user) {
31 | return next({
32 | log: "Error caught in userController.verifyUser middleware function",
33 | status: 500,
34 | message: {err: 'User not in database'}
35 | });
36 | };
37 |
38 | // If user exists with cooresponding email, compare password from client with password in database
39 | const isValid: boolean = await bcrypt.compare(password, user.Item.password);
40 |
41 | if (!isValid) {
42 | return next({
43 | log: "Error caught in userController.verifyUser middleware function",
44 | status: 500,
45 | message: {err: 'Wrong password'}
46 | });
47 | };
48 |
49 | res.locals.email = email;
50 | return next();
51 | // All other errors, invoke global error handler
52 | } catch (err) {
53 | return next({
54 | log: "Error caught in userController.verifyUser middleware function",
55 | status: 500,
56 | message: {err: `Error logging in`}
57 | })
58 | }
59 | },
60 |
61 | // Middleware for user registration
62 | async createUser (req, res, next) {
63 | const { email, firstName, lastName, password, confirmation, arn, region } = req.body;
64 | const { arnValidation } = res.locals;
65 |
66 | // Declare an array to store errors
67 | const errors: Array<"email" | "firstName" | "lastName" | "password" | "confirmation" | "arn" | "region"> = [];
68 |
69 | // Validate email:
70 | if (/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(email) === false) {
71 | errors.push("email");
72 | }
73 |
74 | // Check if user left any input fields empty
75 | // Front end will highlight input fields if errors occur
76 | for (const key in req.body) {
77 | if (req.body[key as KeyType].length === 0) errors.push(key as KeyType);
78 | };
79 |
80 | // Check if password matches confirmation
81 | if (password !== confirmation) errors.push("password", "confirmation");
82 |
83 | // Check if arn is validated
84 | if (!arnValidation.validated) errors.push("arn");
85 |
86 | // Send errors array to the front end if they exist
87 | if (errors.length > 0) {
88 | return next({
89 | log: "Error caught in userController.createUser middleware function",
90 | status: 500,
91 | message: {errMessage: `Error found in user input`, errors: errors}
92 | })
93 | };
94 |
95 | // If the registration form was successfully filled out, create a new user in the database
96 | try {
97 | const hashedPass = await bcrypt.hash(password, SALT_WORK_FACTOR);
98 |
99 | const params = {
100 | TableName: process.env.TABLE_NAME,
101 | Item: {
102 | 'email' : email ,
103 | 'firstName' : firstName ,
104 | 'lastName' : lastName ,
105 | 'password' : hashedPass ,
106 | 'arn' : arn ,
107 | 'region' : region ,
108 | },
109 | };
110 |
111 | const user = await dynamodb.put(params).promise();
112 |
113 | res.locals.user = user;
114 | return next();
115 | // Invoke global error handler if a DB error occurs
116 | } catch (err) {
117 | return next({
118 | log: "Error caught in userController.signupUser middleware function",
119 | status: 500,
120 | message: {errMessage: `Error inserting user to database`, errors: errors}
121 | })
122 | }
123 | },
124 |
125 | // Middleware for grabbing user info on Settings tab
126 | async getUser(req, res, next) {
127 | const { email } = res.locals;
128 |
129 | const params = {
130 | TableName: process.env.TABLE_NAME,
131 | Key: {
132 | 'email' : email
133 | },
134 | };
135 |
136 | const user: any = await dynamodb.get(params).promise();
137 |
138 | res.locals.user = {
139 | email: user.Item.email,
140 | firstName: user.Item.firstName,
141 | lastName: user.Item.lastName,
142 | arn: user.Item.arn,
143 | region: user.Item.region
144 | }
145 | return next();
146 | },
147 |
148 | // Middleware for updating user settings
149 | async updateUserProfile(req, res, next) {
150 | const originalEmail = res.locals.email;
151 | const { firstName, lastName, arn, region } = req.body;
152 | const { arnValidation } = res.locals;
153 | // Declare an array to store errors
154 | const errors: Array<"firstName" | "lastName" | "arn" | "region"> = [];
155 |
156 | // Check if input fields are empty
157 | for (const key in req.body) {
158 | if (req.body[key as KeyTypeSettings].length === 0) errors.push(key as KeyTypeSettings);
159 | }
160 |
161 | //Check if arn is validated
162 | if (!arnValidation.validated) errors.push("arn");
163 |
164 | // Send errors array to the front end
165 | if (errors.length > 0) {
166 | return next({
167 | log: "Error caught in userController.updateUserProfile middleware function",
168 | status: 500,
169 | message: {errMessage: `Error found in user input`, errors: errors}
170 | })
171 | };
172 |
173 | const originalUserParams = {
174 | TableName: process.env.TABLE_NAME,
175 | Key: {
176 | 'email' : originalEmail,
177 | },
178 | }
179 |
180 | try {
181 | const params = {
182 | TableName: process.env.TABLE_NAME,
183 | Key: {
184 | 'email' : originalEmail
185 | },
186 | UpdateExpression: 'set firstName = :value1, lastName = :value2',
187 | ExpressionAttributeValues: {
188 | ':value1' : firstName,
189 | ':value2' : lastName,
190 | },
191 | ReturnValues: 'ALL_NEW'
192 | };
193 |
194 | // Update user in database with hashedPass as password
195 | const updatedUser = await dynamodb.update(params).promise();
196 |
197 | res.locals.user = {
198 | 'email' : originalEmail ,
199 | 'firstName' : firstName ,
200 | 'lastName' : lastName ,
201 | 'arn' : arn ,
202 | 'region' : region ,
203 | };
204 |
205 | return next();
206 | } catch (err) {
207 | return next({
208 | log: "Error caught in userController.updateUserProfile middleware function" + err,
209 | status: 500,
210 | message: {errMessage: `Error updating user's profile`, errors: errors}
211 | })
212 | }
213 | },
214 |
215 | async updateUserPassword(req, res, next) {
216 | const originalEmail = res.locals.email;
217 | const { password, confirmation } = req.body;
218 | // Declare an array to store errors
219 | const errors: Array< "password" | "confirmation" > = [];
220 |
221 | // Check if input fields are empty
222 | for (const key in req.body) {
223 | if (req.body[key as KeyTypePassword].length === 0) errors.push(key as KeyTypePassword);
224 |
225 | }
226 | // Check if password matches confirmation
227 | if (password !== confirmation) errors.push("password", "confirmation");
228 |
229 | // Send errors array back to front end
230 | if (errors.length > 0) {
231 | return next({
232 | log: "Error caught in userController.updateUserPassword middleware function",
233 | status: 500,
234 | message: {errMessage: `Error found in user input`, errors: errors}
235 | })
236 | };
237 |
238 | try {
239 | const hashedPass = await bcrypt.hash(password, SALT_WORK_FACTOR);
240 |
241 | const params = {
242 | TableName: process.env.TABLE_NAME,
243 | Key: {
244 | 'email' : originalEmail
245 | },
246 | UpdateExpression: `set password = :value1`,
247 | ExpressionAttributeValues: {
248 | ':value1' : hashedPass
249 | },
250 | };
251 |
252 | // create a new user in database with hashedPass as password
253 | const updatedUser = await dynamodb.update(params).promise();
254 |
255 | res.locals.success = {successMessage: 'Password updated!'};
256 | return next();
257 | } catch (err) {
258 | return next({
259 | log: "Error caught in userController.updateUserPassword middleware function",
260 | status: 500,
261 | message: {errMessage: `Error updating the password`, errors: errors}
262 | })
263 | }
264 | },
265 | };
266 |
267 | export default userController;
268 |
--------------------------------------------------------------------------------
/server/routes/authRouter.ts:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | import { Request, Response } from 'express'
4 | import userController from '../controllers/userController'
5 | import credentialsController from '../controllers/aws/credentialsController'
6 | import authController from '../controllers/authController'
7 | import metricsController from '../controllers/aws/metricsController'
8 | const router = express.Router();
9 |
10 | // Handle post requests sent to /login endpoint from the client
11 | router.post('/login', userController.verifyUser, authController.generateJWT, (req: Request, res: Response) => {
12 | return res.status(200).send({
13 | email: res.locals.email,
14 | success: res.locals.success,
15 | accessToken: res.locals.accessToken,
16 | });
17 | });
18 |
19 | // Handle post requests sent to /signup endpoint
20 | router.post('/register', credentialsController.getCredentials, userController.createUser, authController.generateJWT, (req: Request, res: Response) => {
21 | return res.status(200).json({
22 | accessToken: res.locals.accessToken,
23 | })
24 | });
25 |
26 | router.get('/verifyToken', authController.verifyToken, (req: Request, res: Response) => {
27 | return res.status(200).json({
28 | message: res.locals.accessToken ? 'YOU ARE AUTHENTICATED' : 'NOT AUTHENTICATED',
29 | accessToken: res.locals.accessToken,
30 | });
31 | });
32 |
33 | router.get('/logout', authController.removeToken, (req: Request, res: Response) => {
34 | return res.status(200).json({
35 | message: 'YOU ARE LOGGED OUT'
36 | });
37 | });
38 |
39 | module.exports = router;
40 |
--------------------------------------------------------------------------------
/server/routes/dashboardRouter.ts:
--------------------------------------------------------------------------------
1 | const express = require('express') ;
2 | import { Request, Response } from 'express';
3 | import apiController from '../controllers/aws/apiController';
4 | const router = express.Router();
5 | const settingsRouter = require('./settingsRouter');
6 |
7 | import authController from '../controllers/authController';
8 | import credentialsController from '../controllers/aws/credentialsController';
9 | import lambdaController from '../controllers/aws/lambdaController';
10 | import logsController from '../controllers/aws/logsController';
11 | import metricsController from '../controllers/aws/metricsController';
12 | import userController from '../controllers/userController';
13 | import apiMetricsController from '../controllers/aws/apiMetricsController'
14 |
15 | // All routes verify JWT Token to get email
16 | // Email is used to query the database for ARN
17 | // ARN is used to get credentials from client's AWS account
18 | // Credentials used to grab metrics
19 |
20 | router.use(authController.verifyToken, credentialsController.getCredentialsFromDB);
21 |
22 | router.get('/allMetrics', metricsController.getAllMetrics, lambdaController.getFunctions, metricsController.getMetricsByFunc, metricsController.getCostProps, (req: Request, res: Response) => {
23 | return res.status(200).json({
24 | allFuncMetrics: res.locals.allFuncMetrics,
25 | cost: res.locals.cost
26 | });
27 | });
28 |
29 | router.get('/funcmetrics', lambdaController.getFunctions, metricsController.getMetricsByFunc, (req: Request, res: Response) => {
30 | return res.status(200).json({
31 | eachFuncMetrics: res.locals.eachFuncMetrics,
32 | });
33 | });
34 |
35 | router.get('/functions', lambdaController.getFunctions, (req: Request, res: Response) => {
36 | return res.status(200).json({
37 | functions: res.locals.functions
38 | });
39 | });
40 |
41 | // Handles POST Requests to get Logs for all functions and the ability to filter
42 | router.post('/allLogs', logsController.getAllLogs, (req: Request, res: Response) => {
43 | return res.status(200).json({
44 | logs: res.locals.logs
45 | });
46 | });
47 |
48 | router.post('/filteredLogs', logsController.getFilteredLogs, (req: Request, res: Response) => {
49 | return res.status(200).json({
50 | filteredLogs: res.locals.filteredLogs
51 | });
52 | });
53 |
54 | // Handles GET/POST Requests to grab API Metrics + Relationships
55 | router.post('/apiRelations', lambdaController.getFunctions, apiController.getAPIRelations, (req: Request, res: Response) => {
56 | return res.status(200).json({
57 | apiRelations: res.locals.apiRelations
58 | });
59 | });
60 |
61 |
62 | router.get('/apiList', apiController.getAPIList, (req: Request, res: Response) => {
63 | return res.status(200).json({
64 | apiList: res.locals.apiList
65 | });
66 | });
67 |
68 | router.get('/apiMetrics', apiController.getAPIList, apiMetricsController.getAPIMetrics, (req: Request, res: Response) => {
69 | return res.status(200).json({
70 | allApiMetrics: res.locals.allApiMetrics
71 | });
72 | });
73 |
74 | router.get('/apiList', apiController.getAPIList, (req: Request, res: Response) => {
75 | return res.status(200).json({
76 | apiList: res.locals.apiList
77 | });
78 | });
79 |
80 | router.use('/settings', settingsRouter);
81 |
82 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/settingsRouter.ts:
--------------------------------------------------------------------------------
1 | const express = require('express') ;
2 | const router = express.Router();
3 | import { Request, Response } from 'express';
4 | import credentialsController from '../controllers/aws/credentialsController';
5 | import userController from '../controllers/userController';
6 |
7 |
8 | //Handles GET/POST requests to the Settings Tab
9 | router.get('/userDetails', userController.getUser, (req: Request, res: Response) => {
10 | return res.status(200).json(res.locals.user);
11 | });
12 |
13 | router.post('/updateProfile', credentialsController.getCredentials, userController.updateUserProfile, (req: Request, res: Response) => {
14 | return res.status(200).json(res.locals.user);
15 | });
16 |
17 | router.post('/updatePassword', userController.updateUserPassword, (req: Request, res: Response) => {
18 | return res.status(200).json(res.locals.success);
19 | });
20 |
21 |
22 | module.exports = router;
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Express, Request, Response, NextFunction } from 'express';
2 | import { ErrorObj } from './types';
3 | import authController from './controllers/authController';
4 | const dotenv = require('dotenv');
5 | const cookieParser = require('cookie-parser');
6 | const path = require("path");
7 | dotenv.config();
8 | const app = express();
9 | const { PORT } = process.env;
10 |
11 | app.use(express.json());
12 | const authRouter = require('./routes/authRouter');
13 | const dashboardRouter = require('./routes/dashboardRouter');
14 |
15 | app.use(cookieParser());
16 | app.use(express.static(path.resolve(__dirname, '../build')));
17 |
18 |
19 | app.get('/', function(req, res) {
20 | return res.sendFile(path.resolve(__dirname, '../build/index.html'));
21 | });
22 |
23 |
24 | app.use('/', authRouter);
25 | app.use('/dashboard', dashboardRouter);
26 |
27 | // Handle all remaining endpoints that are not defined in the server/routers
28 | app.use('*', (req: Request, res: Response) => {
29 | return res.status(404).json('Not Found');
30 | });
31 |
32 | // Global Error Handler
33 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
34 | const defaultErr: ErrorObj = {
35 | log: "Express error handler caught unknown middleware error",
36 | status: 500,
37 | message: {err: "Global error handler invoked"},
38 | }
39 | const error = Object.assign({}, defaultErr, err);
40 | console.log(`${error.log}: ${error.message.err}`);
41 | return res.status(error.status).json(error.message);
42 | })
43 |
44 | app.listen(PORT, () => {
45 | console.log(`Server is running at https://localhost:${PORT}`);
46 | });
47 |
48 | module.exports = app;
--------------------------------------------------------------------------------
/server/types.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 |
3 | export type ErrorObj = {
4 | log: string,
5 | status: number,
6 | message: {err: string}
7 | };
8 |
9 | export type userController = {
10 | verifyUser: (req: Request, res: Response, next: NextFunction) => Promise>>;
11 | createUser: (req: Request, res: Response, next: NextFunction) => Promise;
12 | getUser: (req: Request, res: Response, next: NextFunction) => Promise;
13 | updateUserProfile: (req: Request, res: Response, next: NextFunction) => Promise;
14 | updateUserPassword: (req: Request, res: Response, next: NextFunction) => Promise;
15 | };
16 |
17 | export type KeyType = "email" | "firstName" | "lastName" | "password" | "confirmation" | "arn" | "region";
18 |
19 | export type KeyTypeSettings = "firstName" | "lastName" | "arn" | "region";
20 |
21 | export type KeyTypePassword = "password" | "confirmation";
22 |
23 | // Created an interface for verifyToken to store the token into the request
24 | export interface getUserToken extends Request {
25 | token: string;
26 | };
27 |
28 | export type authController = {
29 | verifyToken: (req: getUserToken, res: Response, next: NextFunction) => Promise;
30 | generateJWT: (req: Request, res: Response, next: NextFunction) => Promise;
31 | removeToken: (req: Request, res: Response, next: NextFunction) => void;
32 | };
33 |
34 |
35 | export type Endpoint = {
36 | apiId: string;
37 | apiMethod: string;
38 | apiPath: string;
39 | };
40 |
41 | export type LambdaAPIs = {
42 | functionName: string;
43 | endpoints: (Endpoint | undefined)[];
44 | };
45 |
46 | export type API = {
47 | apiName: (string | undefined);
48 | apiId: (string | undefined);
49 | paths: (string | undefined)[] | undefined;
50 | };
51 |
52 | export type Relation = {
53 | apiName: string | undefined;
54 | endpoints: { [key: string]: { method: string, func: string }[] } | undefined;
55 | };
56 |
57 | export interface subMetrics {
58 | values: number[] | undefined
59 | timestamp: Date[] | undefined
60 | };
61 |
62 | export interface Metrics {
63 | invocations: subMetrics,
64 | errors: subMetrics,
65 | throttles: subMetrics,
66 | duration: subMetrics
67 | };
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './client/components/**/*.{js,jsx,ts,tsx}',
5 | './client/containers/**/*.{js,jsx,ts,tsx}',
6 | './client/**/*.{js,jsx,ts,tsx}'
7 | ],
8 | theme: {
9 | extend: {},
10 | },
11 | plugins: [require("daisyui")],
12 | daisyui: {
13 | themes: [
14 | {
15 | myThemeDark: {
16 | "primary": "#623cad",
17 | "secondary": "#3a3279",
18 | "accent": "#ae6db0",
19 | "neutral": "#232343",
20 | "base-100": "#0f0f31",
21 | "base-300": "#e4d5ff",
22 | "info": "#0CA6E9",
23 | "success": "#2BD4BD",
24 | "warning": "#F4C152",
25 | "error": "#FB6F84",
26 | },
27 | myThemeLight: {
28 | "primary": "#DECCFF",
29 | "secondary": "#ece2ff",
30 | "accent": "#f6d5ff",
31 | "neutral": "#f6eefd",
32 | "base-100": "#FFFFFF",
33 | "base-200": "#f6f6f7",
34 | "base-300": '#5d597a',
35 | "info": "#3ABFF8",
36 | "success": "#36D399",
37 | "warning": "#FBBD23",
38 | "error": "#F87272",
39 | },
40 | },
41 | ],
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
4 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
5 | // "jsx": "preserve", /* Specify what JSX code is generated. */
6 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
7 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
8 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
9 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
10 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
11 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
12 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
13 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
14 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
15 |
16 | /* Modules */
17 | "module": "commonjs", /* Specify what module code is generated. */
18 | // "rootDir": "./", /* Specify the root folder within your source files. */
19 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
20 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
21 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
22 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
23 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
24 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
25 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
26 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
27 | // "resolveJsonModule": true, /* Enable importing .json files. */
28 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
29 |
30 | /* JavaScript Support */
31 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
32 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
33 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
34 |
35 | /* Emit */
36 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
37 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
38 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
39 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
40 | // "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. */
41 | // "outDir": "./", /* Specify an output folder for all emitted files. */
42 | // "removeComments": true, /* Disable emitting comments. */
43 | // "noEmit": true, /* Disable emitting files from a compilation. */
44 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
45 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
46 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
47 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
48 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
49 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
50 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
51 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
52 | // "newLine": "crlf", /* Set the newline character for emitting files. */
53 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
54 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
55 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
56 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
57 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
58 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
59 |
60 | /* Interop Constraints */
61 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
62 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
63 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
64 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
65 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
66 |
67 | /* Type Checking */
68 | "strict": true, /* Enable all strict type-checking options. */
69 | "jsx": "react",
70 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
71 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
72 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
73 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
74 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
75 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
76 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
77 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
78 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
79 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
80 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
81 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
82 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
83 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
84 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
85 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
86 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
87 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
88 |
89 | /* Completeness */
90 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
91 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
92 | },
93 | }
94 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | const config = {
6 | entry: ['./client/index.tsx'],
7 | output: {
8 | path: path.resolve(__dirname, 'build'),
9 | filename: 'bundle.js',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.(js|jsx)$/,
15 | use: 'babel-loader',
16 | exclude: /node_modules/,
17 | },
18 | {
19 | test: /\.(scss|css)$/,
20 | use: [
21 | 'style-loader',
22 | {
23 | loader: 'css-loader',
24 | options: {
25 | importLoaders: 1,
26 | },
27 | },
28 | 'sass-loader',
29 | 'postcss-loader',
30 | ],
31 | },
32 | {
33 | test: /\.ts(x)?$/,
34 | loader: 'ts-loader',
35 | exclude: /node_modules/,
36 | },
37 | {
38 | test: /\.(png|jpe?g|gif)$/i,
39 | use: [
40 | {
41 | loader: 'file-loader',
42 | },
43 | ],
44 | },
45 | ],
46 | },
47 | devServer: {
48 | port: 8080,
49 | hot: true,
50 | static: {
51 | directory: './dist',
52 | },
53 | proxy: {
54 | '/': {
55 | target: 'http://localhost:3000',
56 | // secure: true,
57 | },
58 | },
59 | },
60 | resolve: {
61 | extensions: ['.tsx', '.ts', '.js'],
62 | },
63 | plugins: [
64 | new HtmlWebpackPlugin({
65 | template: path.join(__dirname, './client/index.html'),
66 | }),
67 | ],
68 | };
69 |
70 | module.exports = config;
71 |
--------------------------------------------------------------------------------