├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ └── supertest.js ├── dist ├── assets │ ├── Lambda_Potato-removebg-preview.png │ ├── connecting.gif │ ├── failing.gif │ ├── ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp │ ├── greg.jpeg │ ├── index-5548e6c0.css │ ├── index-d2746986.js │ ├── michael.png │ ├── nhat.jpeg │ ├── removing.gif │ └── zach.png └── index.html ├── index.html ├── jest.config.js ├── nodemon.json ├── package.json ├── public └── assets │ ├── LambdaPeelerIcon.jpeg │ ├── Lambda_Potato-removebg-preview.png │ ├── connecting.gif │ ├── failing.gif │ ├── ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp │ ├── greg.jpeg │ ├── lambdaicon.png │ ├── michael.png │ ├── nhat.jpeg │ ├── removing.gif │ └── zach.png ├── server ├── controllers │ ├── functionController.ts │ ├── js │ │ ├── functionControllers.js │ │ ├── layerControllers.js │ │ ├── testControllers.js │ │ └── userControllers.js │ ├── layerController.ts │ ├── testController.ts │ └── userController.ts ├── db.js ├── db.ts ├── js │ └── servers.js ├── models │ ├── historyLogModel.ts │ ├── js │ │ ├── notificationModel.js │ │ └── userModels.js │ ├── notificationModel.ts │ └── userModel.ts ├── routes │ ├── functionRouter.ts │ ├── js │ │ ├── functionRouters.js │ │ ├── layerRouters.js │ │ └── userRouters.js │ ├── layerRouter.ts │ └── userRouter.ts └── server.ts ├── src ├── App.jsx ├── Main.jsx ├── components │ ├── Display.jsx │ ├── Function.jsx │ ├── FunctionModal.jsx │ ├── HistoryLog.jsx │ ├── Layer.jsx │ ├── LayersModal.jsx │ ├── LinkedFunctions.jsx │ ├── LinkedLayers.jsx │ ├── Login.jsx │ ├── MainDisplay.jsx │ ├── Navbar.jsx │ ├── Notification.jsx │ ├── Settings.jsx │ └── Splash.jsx ├── containers │ ├── FunctionsContainer.jsx │ ├── HistoryContainer.jsx │ ├── LayersContainer.jsx │ └── NotificationContainer.jsx └── styles.css ├── tsconfig.json ├── vercel.json └── vite.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env", "@babel/preset-typescript"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | db.ts 4 | dbs.js 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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 | # LambdaPeeler 2 | ![Lambda Peeler V2](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/d2dcfdf0-d4a3-4549-b2a0-57f69f7c3f81) 3 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/4a40068b-3000-4bc2-b401-f9a89fd4c130) 4 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/e8363ab1-9650-43ae-b569-1e22ac594acb) 5 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/0ede3d9f-fa64-4d84-ad92-c85566e90fc4) 6 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/d3bf398d-34b7-480d-b217-d1095b7fb401) 7 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/21bd9abf-f5ef-4504-bbc3-4d41e4b3bdb4) 8 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/a856aca2-b048-4100-8c68-5232cac0544d) 9 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/9a2a204b-c276-4ada-94fa-a9ec35062b10) 10 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/dfd23f70-cca2-4ae1-9df6-01a30c93daea) 11 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/5e25d4e4-dbc5-4667-8721-c23ce76fe600) 12 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/b270f1ae-79f4-4c6d-a2c7-1dbf7342008e) 13 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/ccd77e55-0399-4d4a-b41f-41f369774f7a) 14 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/8c57c3c0-ed7d-495c-8c8e-1f229c817a7b) 15 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/4963c2d6-fa49-43a1-9d83-fdea0822eb9d) 16 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/0a86a0f5-18bc-403c-9bf1-ba281edbe033) 17 | ![image](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/fc30a09f-10b5-4dad-9d6f-03f0baa0cee0) 18 | 19 | 20 |

Web dashboard for managing AWS Lambda functions and layers

21 | Lambda Peeler is a web-based dashboard tailored for AWS Lambda developers. It is meticulously designed to bridge the gap between managing Lambda functions and layers, simplifying AWS cloud operations. 22 | 23 | # Table of Contents 24 | - Link to Site 25 | - [Features](#features) 26 | - [Demo](#demo) 27 | - [Connecting to our App](#connecting-to-our-app) 28 | - [Contributing](#contributing) 29 | - [License](#license) 30 | - [Authors](#authors) 31 | ## Features 32 | **Bulk Operations**: Time is of the essence. And Lambda Peeler understands that. Perform bulk connections and disconnections without a hitch, and with the assurance of compatibility. 33 | 34 | **Risk Mitigation**: Gone are the days of the dreaded trial-and-error. Our built-in compatibility testing feature cross-examines functions with layers, ensuring they're in harmony. This not only guarantees smoother integrations but significantly curtails the risk of runtime failures. 35 | 36 | **Error Handling**: Our robust error messages notify users of failed connection attempts, allowing them to easily troubleshoot problems. 37 | 38 | **Direct AWS Integration**: Leveraging the AWS SDK, Lambda Peeler is deeply integrated with AWS services. This ensures real-time operations and a seamless user experience. 39 | 40 | ## Demo 41 | **Connecting a function to a layer** 42 | ![Connect2](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/8159a173-0024-4f7b-b315-5e710203128f) 43 | 44 | **Removing a function from a layer** 45 | ![RemoveGif](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/532bfef1-75f4-4843-ab00-d3eb4f60edf0) 46 | 47 | **Failed Compatibility** 48 | ![Failed](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/ab1706a9-82e4-4660-89ef-5e51093543ba) 49 | 50 | **Please note that Lambda functions must have at least one shareable test event in order to connect via our dashboard!** 51 | 52 | **How to Make a Shareable Test** 53 | ![Sep-27-2023 11-55-21](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/7ba6bd04-fc74-4bc0-8899-741aca452af0) 54 | 55 | ## Connecting to our App 56 | 1. Navigate to your IAM dashboard on your AWS account and create a new role. 57 | ![img1](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/fcc97b75-2fd7-41a8-a328-6c9d5ff75ba9) 58 | 59 | 2. Select AWS Account as the trusted entity and enter our tool's ARN number: 524403604286. 60 | ![img2](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/200c84c7-7767-4d9c-ba68-aa495edf907c) 61 | 62 | 3. When adding permissions, make sure to add AmazonEventBridgeSchemasFullAccess and AWSLambda_FullAccess. Your permissions should end up looking like this when you are finalizing the role. 63 | ![img3](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/d2d2cc54-47db-4d38-8eaa-774fa66e001f) 64 | 65 | 4. The role name has to be OSPTool. 66 | ![img4](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/370fd6ee-ca1b-4178-8d33-dda603c6beee) 67 | 68 | 5. Click on OSPTool under roles and copy the ARN number. 69 | ![img6](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/eadb46d5-ee24-497b-b42b-e85b0b2d78ed) 70 | 71 | 6. Your trust relationships under your OSPTool role should also look like this. 72 | ![img5](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/15767a90-c054-4502-8cba-d4ca0e421859) 73 | 74 | 7. Head over to our website lambdapeeler.com and sign up by entering your ARN number as well as the region of your AWS account. 75 | ![Screenshot 2023-09-27 at 11 44 49 AM](https://github.com/oslabs-beta/LambdaPeeler/assets/135868272/23a6580a-aff6-4917-a61a-b9467e7dcb16) 76 | 77 | ## Contributing 78 | Contributions are the foundation of the Open Source Community, fostering an environment where developers can openly share, collaborate, and ignite inspiration! Your contributions, whatever you decide to offer, are deeply valued and welcomed. Please create a fork of the dev branch and create a feature branch on your own repo. Make all pull requests from your feature branch into LambdaPeeler's dev branch. Also, feel free to open an issue! 79 | 80 | **Features to Add** 81 | - Users can currently connect functions to layers on the layers tab but not the other way around on the functions tab 82 | - We would like to move any unused layers to a separate log in order to reduce clutter on the dashboard 83 | - The ability for users to see information about their layers on our dashboard such as dependencies and runtime enviroment 84 | 85 | If you have any questions or need help troubleshooting, please feel free to reach out on Linkedin! 86 | 87 | ## License 88 | LambdaPeeler is licensed under the MIT License. See LICENSE for more details. 89 | 90 | ## Authors 91 |
92 | 93 | 94 | 107 | 120 | 133 | 146 | 147 |
95 | 96 |
97 | Michael Shand 98 |
99 | 100 | GitHub 101 | 102 |
103 | 104 | LinkedIn 105 | 106 |
108 | 109 |
110 | Greg Osborn 111 |
112 | 113 | GitHub 114 | 115 |
116 | 117 | LinkedIn 118 | 119 |
121 | 122 |
123 | Zach Hamilton 124 |
125 | 126 | GitHub 127 | 128 |
129 | 130 | LinkedIn 131 | 132 |
134 | 135 |
136 | Nhat Trinh 137 |
138 | 139 | GitHub 140 | 141 |
142 | 143 | LinkedIn 144 | 145 |
148 |
149 | -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | const Cookies = require("js-cookie"); 4 | const { mockClient } = require('aws-sdk-client-mock'); 5 | const functionController = require('../server/controllers/js/functionControllers.js') 6 | const layerController = require('../server/controllers/js/layerControllers.js') 7 | const removeFunction = layerController.removeFunction; 8 | //import functionController from '../server/controllers/functionController'; 9 | 10 | const { LambdaClient, UpdateFunctionConfigurationCommand, GetFunctionCommand} = require('@aws-sdk/client-lambda'); 11 | 12 | 13 | // import { LambdaClient, UpdateFunctionConfigurationCommand, GetFunctionConfigurationCommand} from '@aws-sdk/client-lambda'; 14 | //const lambdaDB = new LambdaClient; 15 | const lambdaMock = mockClient(LambdaClient); 16 | jest.mock('../server/models/js/userModels', () => { 17 | return { 18 | create: jest.fn(), 19 | findOne: jest.fn(), 20 | } 21 | }) 22 | 23 | /* 24 | Functionality to cover 25 | Layers 26 | Get a list of all layers (success) 27 | Delete all layers from createAccount, LodashFunction, and TestFunctionWithNoLayer. 28 | Adding a function to a layer (success). Add createAccount to ValidationLayer version 7. 29 | Adding a function to a layer (runtime fail). Add createAccount to PythonLayer. 30 | Adding a function to a layer (versions of same layer fail). Add createAccount to ValidationLayer version 2. 31 | Adding a function to a layer (dependency fail). Add LodashFunction to LodashLayer version 1. 32 | Adding a function to a layer (no shareable test event fail). Add TestFunctionWithNoLayer to LodashLayer version 1. 33 | List all functions associated with a specific layer (success). Try for ValidationLayer version 7, should return createAccount. 34 | Removing a function from a layer (success). Remove createAccount from ValidationLayer version 7. 35 | 36 | Functions 37 | Get a list of all functions (success) 38 | Get a list of all layers associated with a specific function (success). Try for createAccount, should return ValidationLayer version 7. 39 | 40 | Users 41 | Create a user (success). Use test account 1. 42 | Create a user (user already exists fail). Use test account 1. 43 | User log in (success). Use test account 1. <--- Is it possible to test the JWT being set? 44 | User log in (fail). Use test account 2, not in the DB. 45 | 46 | 47 | Testing process 48 | 49 | Have test account set up with three functions and three layers 50 | Function 1: createAccount 51 | Function 2: LodashFunction 52 | Function 3: TestFunctionWithNoLayer 53 | 54 | Layer 1: ValidationLayer. 55 | Layer 2: LodashLayer version 1. 56 | Layer 3: PythonLayer. 57 | 58 | 59 | At the beginning of the test, send an empty Layers array for createAccount, LodashFunction, and TestFunctionWithNoLayer. 60 | Ensure test accounts 1 and 2 are not in the DB and delete them if they are. 61 | Layers tests using hardcoded test account (not test account 1 or 2). 62 | Functions tests using hardcoded test account (not test account 1 or 2). 63 | 64 | */ 65 | 66 | describe('Route integration', () => { 67 | beforeEach(() => { 68 | jest.clearAllMocks(); 69 | }) 70 | xdescribe('/layers routes', () => { 71 | // GET to /layers/list 72 | describe('GET to /list', () => { 73 | it('response with 200 status and application json', () => { 74 | return request(server) 75 | .get('/layers/list') 76 | .expect('Content-Type', /application\/json/) 77 | .expect(200); 78 | }) 79 | }) 80 | 81 | describe('POST to /functions/removeAll for each of the three test functions', () => { 82 | // do these all need to be under their own describes? 83 | it('createAccount', async () => { 84 | return await request(server) 85 | .post('/functions/removeAll') 86 | .send({FunctionName: 'createAccount'}) 87 | .expect(200); 88 | }) 89 | 90 | it('LodashFunction', async () => { 91 | return await request(server) 92 | .post('/functions/removeAll') 93 | .send({FunctionName: 'LodashFunction'}) 94 | .expect(200); 95 | }) 96 | 97 | it('TestFunctionWithNoLayer', async () => { 98 | return await request(server) 99 | .post('/functions/removeAll') 100 | .send({FunctionName: 'TestFunctionWithNoLayer'}) 101 | .expect(200); 102 | }) 103 | }) 104 | // POST to /layers/remove 105 | //takes object {ARN:string, functionName:string, LayerName:string} 106 | describe('POST to /remove', () => { 107 | it('response with 200 status and application json', () => { 108 | 109 | return request(server) 110 | .post('/layers/remove') 111 | .send() 112 | .expect('Content-Type', /application\/json/) 113 | .expect(200); 114 | }) 115 | }) 116 | // POST to /layers/add 117 | //takes {ARN, arrayOfCheckedFunctions, layerName} 118 | describe('POST to /add', () => { 119 | it('response with 200 status and application json', () => { 120 | return request(server) 121 | .post('/layers/add') 122 | .send() 123 | .expect('Content-Type', /application\/json/) 124 | .expect(200); 125 | }) 126 | }) 127 | // POST to / layers/function 128 | //takes {ARN} 129 | describe('POST to /functions', () => { 130 | it('response with 200 status ad application json', () => { 131 | return request(server) 132 | .post('/layers/functions') 133 | .send() 134 | .expect('Content-Type', /application\/json/) 135 | .expect(200); 136 | }) 137 | }) 138 | }) 139 | describe('/functions routes', () => { 140 | // GET to /functions/list 141 | xdescribe('GET to /list', () => { 142 | it('response with 200 status and application/json content type', async () => { 143 | const response = await request(server) 144 | .get('/functions/list') 145 | .set('Cookie', 'ARN=arn:aws:iam::082338669350:role/OSPTool') 146 | .expect('Content-Type', /application\/json/) 147 | .expect(200) 148 | expect(response.body.Functions).toBeTruthy(); 149 | }) 150 | }) 151 | // POST to /functions/layers 152 | //takes {function ARN: layers:array} 153 | xdescribe('POST to /layers', () => { 154 | it('response with 200 status and application/json content type', async () => { 155 | const response = await request(server) 156 | .post('/functions/layers') 157 | .send({ARN: 'arn:aws:lambda:us-east-1:082338669350:function:Nhats1stFunction', 158 | layers: [{ 159 | name: 'ZachLayer', 160 | versions: [ 1 ], 161 | ARN: [ 'arn:aws:lambda:us-east-1:082338669350:layer:ZachLayer:1' ]}, 162 | { 163 | name: 'GregLayer', 164 | versions: [ 1 ], 165 | ARN: [ 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1' ] 166 | }]}) 167 | .expect('Content-Type', /application\/json/) 168 | .expect(200) 169 | expect(response.body).toEqual([{ 170 | LayerArn: "arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1", 171 | LayerName: "GregLayer", 172 | LayerVersion: 1 173 | }]); 174 | }) 175 | }) 176 | // POST to /functions/remove 177 | // {ARN, funcationName, layerName} 178 | describe('POST to /remove', () => { 179 | beforeEach(() => { 180 | lambdaMock.reset(); 181 | }) 182 | it('responds with 200 status ', async () => { 183 | // const response = await request(server) 184 | // .post('/functions/remove') 185 | // .send({ARN: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1', 186 | // functionName: 'Nhats1stFunction'}) 187 | // .expect('Content-Type, /application\/json/') 188 | // .expect(200) 189 | // expect(response.body).toBe(true); 190 | 191 | // const data = {FunctionName: 'Nhats1stFunction'} 192 | // const input = {FunctionName: 'Nhats1stFunction', Layers: ['arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1']}; 193 | 194 | // lambdaMock 195 | // .on(GetFunctionCommand, data).resolves( 196 | // { 197 | // Configuration: { 198 | // Layers: [{Arn: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1'}] 199 | // } 200 | // }); 201 | // lambdaMock.on(UpdateFunctionConfigurationCommand, input).resolves({ 202 | // FunctionName: 'Nhats1stFunction', 203 | // }); 204 | 205 | // const mockreq = {body: {ARN: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1', functionName: 'Nhats1stFunction'}}; 206 | // const mockres = { 207 | // status: jest.fn().mockReturnThis(), 208 | // json: jest.fn(), 209 | // locals: {} 210 | // }; 211 | // const mocknext = jest.fn() 212 | 213 | // const response = await functionController.removeLayer(mockreq, mockres, mocknext) 214 | // console.log('mockres:', mockres); 215 | // console.log('response: ', response) 216 | // expect(mockres.status).toHaveBeenCalledWith(200) 217 | // expect(mockres.locals.successful).toBe(true) 218 | // expect(mocknext).toHaveBeenCalled(); 219 | 220 | lambdaMock.on(GetFunctionCommand({FunctionName: 'Nhats1stFunction'})).resolves({ 221 | Configuration:{ 222 | Layers:[] 223 | } 224 | }); 225 | lambdaMock.on(UpdateFunctionConfigurationCommand, {FunctionName: 'Nhats1stFunction'}).resolves({ 226 | Configuration: { 227 | Layers: [] 228 | } 229 | }); 230 | 231 | const mockreq = {body: {ARN: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1', functionName: 'Nhats1stFunction'}}; 232 | const mockres = { 233 | status: jest.fn().mockReturnThis(), 234 | json: jest.fn(), 235 | locals: {} 236 | }; 237 | const mocknext = jest.fn() 238 | 239 | 240 | const response = await removeFunction(mockreq, mockres, mocknext) 241 | // console.log('mockres:', mockres); 242 | // console.log('response: ', response) 243 | expect(mockres.status).toHaveBeenCalledWith(200) 244 | expect(mockres.locals.successful).toBe(true) 245 | expect(mocknext).toHaveBeenCalled(); 246 | }) 247 | 248 | 249 | // import { mockClient } from "aws-sdk-client-mock"; 250 | // import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; 251 | 252 | // const ddbMock = mockClient(DynamoDBDocumentClient); 253 | 254 | // beforeEach(() => { 255 | // ddbMock.reset(); 256 | // }); 257 | 258 | // getUserNames.spec.ts 259 | // import { getUserNames } from "./getUserNames"; 260 | // import { GetCommand } from "@aws-sdk/lib-dynamodb"; 261 | 262 | // it("should get user names from the DynamoDB", async () => { 263 | // ddbMock.on(GetCommand).resolves({ 264 | // Item: { id: "user1", name: "John" }, 265 | // }); 266 | // const names = await getUserNames(["user1"]); 267 | // expect(names).toStrictEqual(["John"]); 268 | // }); 269 | 270 | 271 | }) 272 | // POST to /functions/add <----- not currently used 273 | 274 | 275 | }) 276 | xdescribe('/users routes', () => { 277 | // POST to /users/signup 278 | describe('POST to /signup', () => { 279 | it('should create a user successfully', async () => { 280 | const response = await request(server) 281 | .post('/user/signup') 282 | .send({ 283 | username: 'test', 284 | password: 'password', 285 | ARN: '1234' 286 | }) 287 | .expect('Content-Type', /json/) 288 | .expect(200) 289 | expect(response.body.message).toBe('Successfully signed up') 290 | expect(User.create).toHaveBeenCalled(); 291 | }) 292 | }) 293 | // POST to /users/login 294 | describe('POST to /login', () => { 295 | it('should log user in successfully', async () => { 296 | const response = await request(server) 297 | .post('/user/login') 298 | .send({ 299 | username: 'test', 300 | password: 'password' 301 | }) 302 | .expect(200) 303 | 304 | expect(response.body.message).toBe('Successfully signed up') 305 | }) 306 | it('should handle incorrect username/password', async () => { 307 | try { 308 | const response = await request(server) 309 | .post('user/login') 310 | .send({ 311 | username: 'WrongUsername', 312 | password: 'WrongPassword' 313 | }) 314 | .expect(400) 315 | } catch(err) { 316 | console.log(err) 317 | } 318 | }) 319 | }) 320 | // DELETE to /users/logout <---- frontend button not implemented yet 321 | describe('DELETE to /logout', () => { 322 | // beforeEach(() => { 323 | // Object.defineProperty(window.document, 'cookie', { 324 | // writable: true, 325 | // value: 'ARN=1234', 326 | // }); 327 | // }) 328 | it('should handle logout successfully', async () => { 329 | const login = await request(server) 330 | .post('/user/login') 331 | .send({ 332 | username: 'test', 333 | password: 'password' 334 | }) 335 | .expect(200) 336 | expect(login.body.message).toBe('Successfully signed up') 337 | const logout = await request(server) 338 | .post('users/logout') 339 | .expect(200) 340 | expect(logout.header['set-cookie']).toBeUndefined(); 341 | }) 342 | }) 343 | }) 344 | }) -------------------------------------------------------------------------------- /dist/assets/Lambda_Potato-removebg-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/Lambda_Potato-removebg-preview.png -------------------------------------------------------------------------------- /dist/assets/connecting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/connecting.gif -------------------------------------------------------------------------------- /dist/assets/failing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/failing.gif -------------------------------------------------------------------------------- /dist/assets/ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp -------------------------------------------------------------------------------- /dist/assets/greg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/greg.jpeg -------------------------------------------------------------------------------- /dist/assets/index-5548e6c0.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;1,100;1,200;1,300;1,400";body{margin:0;width:100vw;height:100vh;font-family:Poppins,sans-serif;background-color:#fad0a0}#main{display:flex;flex-direction:column;gap:25px;min-height:100vh;background-color:#fff}#LayersContainer{display:flex;flex-direction:column;padding:5px;gap:5px;width:50vw}.layer{padding:5px}#FunctionsContainer{display:flex;flex-direction:column;padding:5px;gap:5px;width:50vw}.function{padding:5px}#display{display:flex;flex-direction:column;justify-content:center;align-items:center}#navbar{display:flex;justify-content:space-between;margin:0;background-color:#fad0a0;color:#444;box-shadow:#00000059 0 5px 15px}a{text-decoration:none;color:#000}#navbar>ul{display:flex;justify-content:space-between;list-style:none;padding:0 15px;margin:10;width:100%}#dropdown{background-color:#fff;cursor:pointer;border-radius:5px;text-align:left;outline:none;font-size:15px;transition:background-color 1s ease-in-out}.collapsible{background-color:#eee;color:#444;cursor:pointer;padding:18px;width:100%;border:none;border-radius:5px;text-align:left;outline:none;font-size:15px;transition:background-color .5s ease-in-out}.active,.collapsible:hover{background-color:#b0b0b0}.content{padding:0 18px;display:none;overflow:hidden}.switch{position:relative;display:inline-block;width:60px;height:34px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s}.slider:before{position:absolute;content:"";height:26px;width:26px;left:4px;bottom:4px;background-color:#fff;-webkit-transition:.4s;transition:.4s}input:checked+.slider{background-color:#2196f3}input:focus+.slider{box-shadow:0 0 1px #2196f3}input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translate(26px)}.functionDropDown,.layerDropDown{display:flex;flex-direction:row;justify-content:space-between}#loginButtons{display:flex;flex-direction:column;gap:10px;justify-content:center}#login{height:100vh;width:100vw;margin:0;background-color:#fad0a0}.listItem{display:flex;align-items:center}#imgid{display:flex;position:absolute;height:150px;width:auto;left:50%;top:20%;transform:translate(-50%,-50%)}#update{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:3em;margin:1em}#notificationDisplay,#historyDisplay{display:flex;width:50%}#link:hover{text-decoration:underline}#splash{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:5em;width:100%;background-color:#fad0a0}#hero{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:60%;height:100%;padding-top:10em}#features{display:flex;flex-direction:column;justify-content:center;align-items:center;padding-top:10em}.feature{display:flex;justify-content:center;align-items:center;padding-top:5em;padding-bottom:10em;gap:15em}.featureDiscription{width:30em}#gif{width:40em;border-radius:5px}#team{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;padding:5em;gap:3em}#people{display:flex;justify-content:space-evenly;align-items:center;width:100%}.person{display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:5px;height:15em;width:15em;margin:3em}#profileLinks{display:flex} 2 | -------------------------------------------------------------------------------- /dist/assets/michael.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/michael.png -------------------------------------------------------------------------------- /dist/assets/nhat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/nhat.jpeg -------------------------------------------------------------------------------- /dist/assets/removing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/removing.gif -------------------------------------------------------------------------------- /dist/assets/zach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/dist/assets/zach.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lambda Peeler 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lambda Peeler 6 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/__tests__'], 4 | transform: { 5 | '^.+\\.js$': 'babel-jest' 6 | }, 7 | testPathIgnorePatterns: ['/node_modules/'] 8 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server/**/*"], 3 | "ext": ".ts,.js", 4 | "exec": "ts-node server/server.ts" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambdapeeler", 3 | "version": "1.0.0", 4 | "description": "ECRI42 OSP3", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "vite build", 8 | "dev": "concurrently 'vite' 'nodemon server/server.ts'", 9 | "test": "jest --verbose", 10 | "start": "concurrently \"npm run server\" \"npm run dev\"", 11 | "server": "nodemon", 12 | "client": "webpack serve --config webpack.config.js" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@aws-sdk/lib-dynamodb": "^3.418.0", 19 | "@babel/core": "^7.22.11", 20 | "@babel/plugin-proposal-class-properties": "^7.18.6", 21 | "@babel/preset-env": "^7.22.15", 22 | "@babel/preset-react": "^7.22.5", 23 | "@babel/preset-typescript": "^7.22.15", 24 | "@types/bcrypt": "^5.0.0", 25 | "@types/cookie-parser": "^1.4.4", 26 | "@types/express": "^4.17.17", 27 | "@types/jsonwebtoken": "^9.0.3", 28 | "@types/mongoose": "^5.11.97", 29 | "@types/node": "^20.6.3", 30 | "@types/react": "^18.2.22", 31 | "@types/react-dom": "^18.2.7", 32 | "aws-sdk-client-mock": "^3.0.0", 33 | "babel-jest": "^29.7.0", 34 | "babel-loader": "^9.1.3", 35 | "babel-preset-react": "^6.24.1", 36 | "css-loader": "^6.8.1", 37 | "file-loader": "^6.2.0", 38 | "html-webpack-plugin": "^5.5.3", 39 | "jest": "^29.7.0", 40 | "style-loader": "^3.3.3", 41 | "supertest": "^6.3.3", 42 | "ts-loader": "^9.4.4", 43 | "typescript": "^5.2.2", 44 | "url-loader": "^4.1.1", 45 | "vite": "^4.4.9", 46 | "webpack": "^5.88.2", 47 | "webpack-cli": "^5.1.4", 48 | "webpack-dev-server": "^4.15.1" 49 | }, 50 | "dependencies": { 51 | "@aws-sdk/client-eventbridge": "^3.409.0", 52 | "@aws-sdk/client-lambda": "^3.405.0", 53 | "@aws-sdk/client-schemas": "^3.409.0", 54 | "@aws-sdk/client-sts": "^3.410.0", 55 | "@aws-sdk/credential-provider-node": "^3.409.0", 56 | "@babel/runtime": "^7.22.15", 57 | "@emotion/react": "^11.11.1", 58 | "@emotion/styled": "^11.11.0", 59 | "@mui/icons-material": "^5.14.8", 60 | "@mui/material": "^5.14.8", 61 | "@vitejs/plugin-react": "^4.1.0", 62 | "aws-sdk": "^2.1449.0", 63 | "axios": "^1.5.0", 64 | "bcrypt": "^5.1.1", 65 | "concurrently": "^8.2.1", 66 | "cookie-parser": "^1.4.6", 67 | "cors": "^2.8.5", 68 | "cross-env": "^7.0.3", 69 | "dotenv": "^16.3.1", 70 | "express": "^4.18.2", 71 | "js-cookie": "^3.0.5", 72 | "jsonwebtoken": "^9.0.2", 73 | "mongodb": "^6.1.0", 74 | "mongoose": "^7.5.2", 75 | "nodemon": "^3.0.1", 76 | "react": "^18.2.0", 77 | "react-dom": "^18.2.0", 78 | "react-router-dom": "^6.15.0", 79 | "ts-node": "^10.9.1", 80 | "vite-plugin-html": "^3.2.0", 81 | "vite": "^4.4.9" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/assets/LambdaPeelerIcon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/LambdaPeelerIcon.jpeg -------------------------------------------------------------------------------- /public/assets/Lambda_Potato-removebg-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/Lambda_Potato-removebg-preview.png -------------------------------------------------------------------------------- /public/assets/connecting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/connecting.gif -------------------------------------------------------------------------------- /public/assets/failing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/failing.gif -------------------------------------------------------------------------------- /public/assets/ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/ghows-DA-e4d56fd0-0b53-432b-94f5-0038a178bea7-bcfea8ee.webp -------------------------------------------------------------------------------- /public/assets/greg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/greg.jpeg -------------------------------------------------------------------------------- /public/assets/lambdaicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/lambdaicon.png -------------------------------------------------------------------------------- /public/assets/michael.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/michael.png -------------------------------------------------------------------------------- /public/assets/nhat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/nhat.jpeg -------------------------------------------------------------------------------- /public/assets/removing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/removing.gif -------------------------------------------------------------------------------- /public/assets/zach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LambdaPeeler/8c5a42480aac3d495485fcfc087ca50373c0d011/public/assets/zach.png -------------------------------------------------------------------------------- /server/controllers/functionController.ts: -------------------------------------------------------------------------------- 1 | // AWS SDK V3 syntax 2 | import { LambdaClient, 3 | ListFunctionsCommand, ListFunctionsCommandOutput, 4 | GetFunctionConfigurationCommand, GetFunctionConfigurationCommandOutput, 5 | ListLayersCommand, 6 | UpdateFunctionConfigurationCommand, UpdateFunctionConfigurationCommandInput, 7 | GetFunctionCommand, Layer} from '@aws-sdk/client-lambda'; 8 | // const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 9 | import { defaultProvider } from '@aws-sdk/credential-provider-node'; 10 | // const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 11 | import { STSClient, AssumeRoleCommand, AssumeRoleCommandOutput } from '@aws-sdk/client-sts'; 12 | import { Request, Response, NextFunction } from 'express'; 13 | // OSP Account connection 14 | // const lambdaClient = new LambdaClient({ 15 | // region: 'us-east-1', 16 | // credentials: defaultProvider(), 17 | // }); 18 | 19 | let lambdaClient: LambdaClient; 20 | const functionController: any = { 21 | // Begin: To connect to users' AWS accounts 22 | assumeRole: async (req: Request, res: Response, next: NextFunction): Promise => { 23 | try { 24 | const stsClient: STSClient = new STSClient({ 25 | region: req.cookies.region, 26 | }); 27 | const roleToAssume: {RoleArn: string, RoleSessionName: string} = { 28 | //RoleArn has to end in /OSPTool 29 | //'arn:aws:iam::082338669350:role/OSPTool' 30 | RoleArn: req.cookies.ARN, 31 | //RoleArn: ARN, 32 | RoleSessionName: 'FunctionControllerSession', 33 | }; 34 | 35 | const command: AssumeRoleCommand = new AssumeRoleCommand(roleToAssume); 36 | const { Credentials } = await stsClient.send(command) as AssumeRoleCommandOutput 37 | 38 | const tempCredentials: {accessKeyId: string, secretAccessKey: string, sessionToken: string} = { 39 | accessKeyId: Credentials.AccessKeyId, 40 | secretAccessKey: Credentials.SecretAccessKey, 41 | sessionToken: Credentials.SessionToken, 42 | }; 43 | 44 | lambdaClient = new LambdaClient({ 45 | region: req.cookies.region, 46 | credentials: tempCredentials, 47 | }); 48 | return next(); 49 | } catch (err) { 50 | return next({ 51 | log: 52 | `Failed to assume role. Error: ${err}`, 53 | status: 500, 54 | //basic message to user 55 | message: {err: 'Failed to assume role'}, 56 | }) 57 | } 58 | 59 | // End: To connect to users' AWS accounts 60 | }, 61 | 62 | // Gets a list of all the user's functions 63 | getFunction: async (req: Request, res: Response, next: NextFunction): Promise => { 64 | try { 65 | const input = {}; 66 | // Gets a list of functions on AWS account 67 | const command: ListFunctionsCommand = new ListFunctionsCommand(input); 68 | const response: ListFunctionsCommandOutput= await lambdaClient.send(command); 69 | res.locals.functions = response; 70 | return next(); 71 | } catch (error) { 72 | console.log(error); 73 | res.status(400).json({ error: 'Failed to fetch AWS functions' }); 74 | } 75 | }, 76 | 77 | //gets a list of layers attached to specified functions 78 | getLayers: async (req: Request, res: Response, next: NextFunction): Promise => { 79 | // holds the Arn of the layers currently attached to the function 80 | const layerArray: string[] = []; 81 | // res.locals.layers will hold layer information about the layers attached to this function. it will be returned to the frontend. 82 | res.locals.layers = []; 83 | // ARN: ARN of specified function passed in by front end 84 | // layers: layers state array of layer objects, which comes down from Display.jsx. Return value from GET to /layers/list. 85 | /*layers Example: 86 | [ 87 | { 88 | "name": "LodashLayer", 89 | "versions": [ 90 | 5, 91 | 4, 92 | 2, 93 | 1 94 | ], 95 | "ARN": [ 96 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:5", 97 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:4", 98 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:2", 99 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:1" 100 | ] 101 | }, 102 | ] 103 | */ 104 | 105 | const ARN: string = req.body.ARN 106 | const layers: {name: string, versions: number[], ARN: string[]}[] = req.body.layers; 107 | try { 108 | // Gets info about functions 109 | const command: GetFunctionConfigurationCommand = new GetFunctionConfigurationCommand({ FunctionName: ARN }); 110 | const Configuration: GetFunctionConfigurationCommandOutput = await lambdaClient.send(command); 111 | //check if the function currently has layers 112 | if (Configuration.Layers) { 113 | //if it does, map out that array, pushing each layerArn to layerArray 114 | Configuration.Layers.map((el) => { 115 | layerArray.push(el.Arn); 116 | }); 117 | } 118 | //iterate through the layers input, layers is array of objects see lines 71-90 119 | layers.map((layer: {name: string, versions: number[], ARN: string[]}) => { 120 | //layer.Arn is array of arn values for every version of a given layer - iterate through list 121 | layer['ARN'].map((layerARN, index) => { 122 | // if this function has this specific layer version, add an object to res.locals.layers with the layer information we need on the frontend 123 | if (layerArray.includes(layerARN)) { 124 | res.locals.layers.push({ 125 | LayerName: layer.name, 126 | LayerVersion: layer.versions[index], 127 | LayerArn: layerARN, 128 | }); 129 | } 130 | }); 131 | }); 132 | 133 | return next(); 134 | } catch (error) { 135 | console.log(error); 136 | res 137 | .status(400) 138 | .json({ error: `Failed to fetch layers for function ${ARN}` }); 139 | } 140 | }, 141 | 142 | // removes a layer from a specific function 143 | removeLayer: async (req: Request, res: Response, next: NextFunction): Promise => { 144 | try { 145 | // layer ARN 146 | const ARN: string = req.body.ARN; 147 | const functionName: string = req.body.functionName 148 | const input: {FunctionName: string} = { FunctionName: functionName }; 149 | const getFunctionConfigurationCommand: GetFunctionConfigurationCommand = new GetFunctionConfigurationCommand(input); 150 | const Configuration: GetFunctionConfigurationCommandOutput = await lambdaClient.send(getFunctionConfigurationCommand); 151 | // remove the layer from the Layers array by ARN and store it into const newArray 152 | const newArray: Layer[] = Configuration.Layers.filter((layer) => { 153 | return layer.Arn !== ARN; 154 | }); 155 | // update the configuration of functionName using the new Layers array 156 | const updateInput: UpdateFunctionConfigurationCommandInput = { 157 | FunctionName: functionName, 158 | Layers: newArray.map((element) => element.Arn), 159 | }; 160 | const updateFunctionConfigurationCommand: UpdateFunctionConfigurationCommand = 161 | new UpdateFunctionConfigurationCommand(updateInput); 162 | const response = await lambdaClient.send(updateFunctionConfigurationCommand); 163 | 164 | res.locals.successful = true; 165 | return next(); 166 | } catch (err) { 167 | res.status(500).json({ error: 'Failed to remove layer from function' }); 168 | } 169 | }, 170 | 171 | // removes all layers from a specific function 172 | removeAllLayers: async (req: Request, res: Response, next: NextFunction): Promise => { 173 | 174 | const functionName: string = req.body.functionName; 175 | try { 176 | const updateInput: UpdateFunctionConfigurationCommandInput = { 177 | FunctionName: functionName, 178 | Layers: [], 179 | }; 180 | const updateFunctionConfigurationCommand: UpdateFunctionConfigurationCommand = 181 | new UpdateFunctionConfigurationCommand(updateInput); 182 | const response = await lambdaClient.send(updateFunctionConfigurationCommand); 183 | return next(); 184 | } 185 | catch(err) { 186 | res.status(500).json({ error: `Failed to remove all layers from function ${functionName}` }); 187 | } 188 | } 189 | 190 | 191 | }; 192 | 193 | export default functionController; 194 | -------------------------------------------------------------------------------- /server/controllers/js/functionControllers.js: -------------------------------------------------------------------------------- 1 | // AWS SDK V3 syntax 2 | const { 3 | LambdaClient, 4 | ListFunctionsCommand, 5 | GetFunctionConfigurationCommand, 6 | ListLayersCommand, 7 | UpdateFunctionConfigurationCommand, 8 | GetFunctionCommand, 9 | } = require('@aws-sdk/client-lambda'); 10 | const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 11 | const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 12 | 13 | // OSP Account connection 14 | // const lambdaClient = new LambdaClient({ 15 | // region: 'us-east-1', 16 | // credentials: defaultProvider(), 17 | // }); 18 | let lambdaClient; 19 | const functionController = {}; 20 | 21 | // Begin: To connect to users' AWS accounts 22 | functionController.assumeRole = async (req, res, next) => { 23 | try { 24 | const stsClient = new STSClient({ 25 | region: 'us-east-1', 26 | }); 27 | const roleToAssume = { 28 | //RoleArn has to end in /OSPTool 29 | //'arn:aws:iam::082338669350:role/OSPTool' 30 | RoleArn: req.cookies.ARN, 31 | //RoleArn: ARN, 32 | RoleSessionName: 'LayerControllerSession', 33 | }; 34 | 35 | const command = new AssumeRoleCommand(roleToAssume); 36 | const { Credentials } = await stsClient.send(command); 37 | 38 | const tempCredentials = { 39 | accessKeyId: Credentials.AccessKeyId, 40 | secretAccessKey: Credentials.SecretAccessKey, 41 | sessionToken: Credentials.SessionToken, 42 | }; 43 | 44 | lambdaClient = new LambdaClient({ 45 | region: 'us-east-1', 46 | credentials: tempCredentials, 47 | }); 48 | next(); 49 | } catch (err) { 50 | return next(res.status(500).json({ error: 'Failed to assume role' })); 51 | } 52 | }; 53 | 54 | // (async () => { 55 | // const tempCredentials = await assumeRole(); 56 | 57 | // lambdaClient = new LambdaClient({ 58 | // region: "us-east-1", 59 | // credentials: tempCredentials 60 | // }); 61 | // })(); 62 | 63 | // End: To connect to users' AWS accounts 64 | 65 | // Gets a list of all the user's functions 66 | functionController.getFunction = async (req, res, next) => { 67 | try { 68 | const input = {}; 69 | // Gets a list of functions on AWS account 70 | const command = new ListFunctionsCommand(input); 71 | const response = await lambdaClient.send(command); 72 | res.locals.functions = response; 73 | return next(); 74 | } catch (error) { 75 | console.log(error); 76 | res.status(400).json({ error: 'Failed to fetch AWS functions' }); 77 | } 78 | }; 79 | 80 | //gets a list of layers attached to specified functions 81 | functionController.getLayers = async (req, res, next) => { 82 | // holds the Arn of the layers currently attached to the function 83 | const layerArray = []; 84 | // res.locals.layers will hold layer information about the layers attached to this function. it will be returned to the frontend. 85 | res.locals.layers = []; 86 | // ARN: ARN of specified function passed in by front end 87 | // layers: layers state array of layer objects, which comes down from Display.jsx. Return value from GET to /layers/list. 88 | /*layers Example: 89 | [ 90 | { 91 | "name": "LodashLayer", 92 | "versions": [ 93 | 5, 94 | 4, 95 | 2, 96 | 1 97 | ], 98 | "ARN": [ 99 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:5", 100 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:4", 101 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:2", 102 | "arn:aws:lambda:us-east-1:524403604286:layer:LodashLayer:1" 103 | ] 104 | }, 105 | ] 106 | */ 107 | const { ARN, layers } = req.body; 108 | try { 109 | // Gets info about functions 110 | const command = new GetFunctionConfigurationCommand({ FunctionName: ARN }); 111 | const Configuration = await lambdaClient.send(command); 112 | //check if the function currently has layers 113 | if (Configuration.Layers) { 114 | //if it does, map out that array, pushing each layerArn to layerArray 115 | Configuration.Layers.map((el) => { 116 | layerArray.push(el.Arn); 117 | }); 118 | } 119 | //iterate through the layers input, layers is array of objects see lines 71-90 120 | layers.map((layer) => { 121 | //layer.Arn is array of arn values for every version of a given layer - iterate through list 122 | layer['ARN'].map((layerARN, index) => { 123 | // if this function has this specific layer version, add an object to res.locals.layers with the layer information we need on the frontend 124 | if (layerArray.includes(layerARN)) { 125 | res.locals.layers.push({ 126 | LayerName: layer.name, 127 | LayerVersion: layer.versions[index], 128 | LayerArn: layerARN, 129 | }); 130 | } 131 | }); 132 | }); 133 | 134 | return next(); 135 | } catch (error) { 136 | console.log(error); 137 | res 138 | .status(400) 139 | .json({ error: `Failed to fetch layers for function ${ARN}` }); 140 | } 141 | }; 142 | 143 | functionController.removeLayer = async (req, res, next) => { 144 | try { 145 | const { ARN, LayerName, layerVersion, functionName } = req.body; 146 | 147 | const input = { FunctionName: functionName }; 148 | console.log('input: ', input); 149 | const getFunctionCommand = new GetFunctionCommand(input); 150 | console.log('getFunctionCOmmand: ', getFunctionCommand); 151 | console.log('lambdaClient: ', lambdaClient); 152 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 153 | // remove the layer from the Layers array by ARN and store it into const newArray 154 | console.log('before new Array:'); 155 | // removes specified ARN from existing layers array 156 | const newArray = Configuration.Layers.filter((layer) => { 157 | return layer.Arn !== ARN; 158 | }); 159 | console.log('newArray: ', newArray) 160 | // update the configuration of functionName using the new Layers array 161 | const updateInput = { 162 | FunctionName: functionName, 163 | Layers: newArray.map((element) => element.Arn), 164 | }; 165 | console.log('updateInput: ', updateInput); 166 | const updateFunctionConfigurationCommand = 167 | new UpdateFunctionConfigurationCommand(updateInput); 168 | await lambdaClient.send(updateFunctionConfigurationCommand); 169 | console.log('sent update command'); 170 | res.locals.successful = true; 171 | return next(); 172 | } catch (err) { 173 | console.log('500 Error: ',err) 174 | return res.status(500).json({ error: 'Failed to remove layer from function' }); 175 | } 176 | }; 177 | module.exports = functionController; 178 | -------------------------------------------------------------------------------- /server/controllers/js/layerControllers.js: -------------------------------------------------------------------------------- 1 | const { 2 | LambdaClient, 3 | ListLayersCommand, 4 | ListLayerVersionsCommand, 5 | ListFunctionsCommand, 6 | GetFunctionCommand, 7 | UpdateFunctionConfigurationCommand, 8 | } = require('@aws-sdk/client-lambda'); 9 | const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 10 | const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 11 | 12 | // OSP Account connection 13 | // const lambdaClient = new LambdaClient({ 14 | // region: 'us-east-1', 15 | // credentials: defaultProvider(), 16 | // }); 17 | let lambdaClient 18 | const layerController = {}; 19 | // Begin: To connect to users' AWS accounts 20 | // Pull ARN from cookie after login 21 | 22 | layerController.assumeRole = async (req, res, next) => { 23 | try { 24 | const stsClient = new STSClient({ 25 | region: 'us-east-1', 26 | }); 27 | const roleToAssume = { 28 | //RoleArn has to end in /OSPTool 29 | //'arn:aws:iam::082338669350:role/OSPTool' 30 | RoleArn: req.cookies.ARN, 31 | //RoleArn: ARN, 32 | RoleSessionName: 'LayerControllerSession', 33 | }; 34 | 35 | const command = new AssumeRoleCommand(roleToAssume); 36 | const { Credentials } = await stsClient.send(command); 37 | 38 | const tempCredentials = { 39 | accessKeyId: Credentials.AccessKeyId, 40 | secretAccessKey: Credentials.SecretAccessKey, 41 | sessionToken: Credentials.SessionToken, 42 | }; 43 | 44 | lambdaClient = new LambdaClient({ 45 | region: 'us-east-1', 46 | credentials: tempCredentials, 47 | }) 48 | next(); 49 | } 50 | catch (err) { 51 | return next( 52 | res.status(500).json({ error: 'Failed to assume role' }) 53 | ); 54 | } 55 | }; 56 | 57 | // let lambdaClient; 58 | 59 | // (async () => { 60 | // const tempCredentials = await assumeRole(); 61 | 62 | // lambdaClient = new LambdaClient({ 63 | // region: 'us-east-1', 64 | // credentials: tempCredentials, 65 | // }); 66 | // }); 67 | 68 | // End: To connect to users' AWS accounts 69 | 70 | 71 | // Middleware function to get information about all layers from this account 72 | layerController.getLayer = async (req, res, next) => { 73 | try { 74 | // call the listLayers command to get all layers 75 | const input = {}; 76 | const listLayersCommand = new ListLayersCommand(input); 77 | const layersData = await lambdaClient.send(listLayersCommand); 78 | 79 | // extract the Layers array from the response and assign it to res.locals.layer 80 | res.locals.layer = layersData.Layers; 81 | // proceed to next middleware 82 | return next(); 83 | } catch (err) { 84 | res.status(500).json({ error: 'Failed to fetch layers' }); 85 | } 86 | }; 87 | 88 | // Gets the versions of all the layers to display on our front end 89 | layerController.getVersions = async (req, res, next) => { 90 | try { 91 | // retrieve layer data stored in the res.locals from getLayer middleware 92 | const layers = res.locals.layer; 93 | // loop over each layer and its versions 94 | const layerPromises = layers.map(async (layer) => { 95 | // call the listLayerVersions method on each layer and save it to a const 96 | const input = { LayerName: layer.LayerName }; 97 | const listLayerVersionsCommand = new ListLayerVersionsCommand(input); 98 | const versionsData = await lambdaClient.send(listLayerVersionsCommand); 99 | /*VersionData Example: 100 | { 101 | MetaData: {...}, 102 | LayerVersions: [{ 103 | CompatibleRuntimes: [Array], 104 | LicenseInfo: null, 105 | Description: 'We need 6 different total layers for edgecase', 106 | LayerVersionArn: 'arn:aws:lambda:us-east-1:082338669350:layer:MichaelLayer:1', 107 | Version: 1, 108 | CreatedDate: '2023-09-13T18:00:05.842+0000', 109 | CompatibleArchitectures: null 110 | }] 111 | } 112 | */ 113 | // construct and return an object that contains the layer name, its versions, and the ARN of each version 114 | // versions will be an array 115 | return { 116 | name: layer.LayerName, 117 | versions: versionsData.LayerVersions.map((element) => element.Version), 118 | ARN: versionsData.LayerVersions.map((v) => v.LayerVersionArn), 119 | }; 120 | }); 121 | // Wait for all promises to resolve 122 | const layersWithVersions = await Promise.all(layerPromises); 123 | // store an array that contains layer info and their version onto res.locals 124 | res.locals.layersWithVersions = layersWithVersions; 125 | return next(); 126 | } catch (err) { 127 | return next( 128 | res.status(500).json({ error: 'Failed to fetch layer versions' }) 129 | ); 130 | } 131 | }; 132 | 133 | // Middleware to get all functions associated with a layer component 134 | layerController.getFunctions = async (req, res, next) => { 135 | try { 136 | // pull ARN from req body 137 | //Layer ARN 138 | const { ARN } = req.body; 139 | // array that will contain func names that have the layer we're looking for 140 | const functionArray = []; 141 | const input = {}; 142 | // lists all functions 143 | const listFunctionsCommand = new ListFunctionsCommand(input); 144 | const { Functions } = await lambdaClient.send(listFunctionsCommand); 145 | 146 | // iterate through the Functions array, checking each function to find if it has the layer that we're looking for 147 | // if so, push it to functionArray 148 | Functions.forEach((element) => { 149 | // if it currently has layers 150 | if (element.Layers) { 151 | // iterate thru each 152 | for (const item of element.Layers) { 153 | // if layer ARN matches, push func to func array 154 | //compare input layer ARN and ARN stored on Function.Layers 155 | if (item.Arn === ARN) { 156 | functionArray.push(element.FunctionName); 157 | break; 158 | } 159 | } 160 | } 161 | }); 162 | // store functionArray in res.locals 163 | res.locals.functionArray = functionArray; 164 | return next(); 165 | } catch (err) { 166 | res.status(500).json({ error: 'Failed to fetch associated functions' }); 167 | } 168 | }; 169 | 170 | // Middleware to remove function from a layer component 171 | layerController.removeFunction = async (req, res, next) => { 172 | try { 173 | // req.body includes the layer ARN and functionName 174 | // Layer ARN 175 | const { ARN, functionName } = req.body; 176 | // get the list of layers connected to functionName 177 | const input = { FunctionName: functionName }; 178 | // gets info about a specific function 179 | console.log('getFunctionCommand'); 180 | const getFunctionCommand = new GetFunctionCommand(input); 181 | console.log('before config'); 182 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 183 | console.log('after config'); 184 | // remove the layer from the Layers array by ARN and store it into const newArray 185 | const newArray = Configuration.Layers.filter((layer) => { 186 | return layer.Arn !== ARN; 187 | }); 188 | // update the configuration of functionName using the new Layers array 189 | const updateInput = { 190 | FunctionName: functionName, 191 | Layers: newArray.map((element) => element.Arn), 192 | }; 193 | const updateFunctionConfigurationCommand = 194 | new UpdateFunctionConfigurationCommand(updateInput); 195 | await lambdaClient.send(updateFunctionConfigurationCommand); 196 | 197 | return next(); 198 | } catch (err) { 199 | res.status(500).json({ error: 'Failed to remove function from layer' }); 200 | } 201 | }; 202 | 203 | // Middleware to add function to a layer component 204 | layerController.addFunction = async (req, res, next) => { 205 | // req.body is an object with keys ARN (string layer ARN) and functionArray (array of string function names) 206 | // functionArray is not used here. instead we use passFuncs below 207 | const { ARN } = req.body; 208 | // passFuncs contains all funcs that pass initial runtime compatability test 209 | const passFuncs = res.locals.passedRuntime; 210 | 211 | const updateFunctions = async (functionName) => { 212 | try { 213 | const getFunctionInput = { 214 | FunctionName: functionName, 215 | } 216 | const getFunctionCommand = new GetFunctionCommand(getFunctionInput); 217 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 218 | let newArray; 219 | // edge case: if the function has no layers yet, Configuration.Layers will be undefined 220 | if (Configuration.Layers === undefined) { 221 | newArray = []; 222 | } else { 223 | // else, set the array to be the current layers array 224 | newArray = Configuration.Layers; 225 | } 226 | // add this layer ARN to the current Layers array 227 | if (!newArray.includes(ARN)) { 228 | newArray.push({ Arn: ARN }); 229 | } 230 | 231 | // send the updated Layers array to AWS 232 | const updateFunctionConfigurationInput = { 233 | FunctionName: functionName, 234 | Layers: newArray.map((element) => element.Arn), 235 | } 236 | const updateFunctionConfigurationCommand = new UpdateFunctionConfigurationCommand(updateFunctionConfigurationInput); 237 | await lambdaClient.send(updateFunctionConfigurationCommand); 238 | } catch (error) { 239 | // add error message to error object to be sent to frontend 240 | res.locals.addError.push( 241 | `Failed to update function ${functionName}. Error: ${error.message}` 242 | ); 243 | } 244 | }; 245 | 246 | try { 247 | // resolves all promises before heading to next middleware 248 | await Promise.all(passFuncs.map((func) => updateFunctions(func))); 249 | return next(); 250 | } catch (error) { 251 | console.log(error); 252 | return res.status(403).send(error.message); 253 | } 254 | }; 255 | 256 | module.exports = layerController; 257 | -------------------------------------------------------------------------------- /server/controllers/js/testControllers.js: -------------------------------------------------------------------------------- 1 | const { 2 | SchemasClient, 3 | DescribeSchemaCommand, 4 | } = require('@aws-sdk/client-schemas'); 5 | const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 6 | const { 7 | LambdaClient, 8 | InvokeCommand, 9 | GetLayerVersionByArnCommand, 10 | GetFunctionCommand, 11 | UpdateFunctionConfigurationCommand, 12 | } = require('@aws-sdk/client-lambda'); 13 | const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 14 | 15 | const ErrorMessage = require('../models/notificationModel'); 16 | const User = require('../models/userModel'); 17 | // OSP Account connection 18 | // const lambdaClient = new LambdaClient({ 19 | // region: 'us-east-1', 20 | // credentials: defaultProvider(), 21 | // }); 22 | 23 | // const schemasClient = new SchemasClient({ 24 | // region: 'us-east-1', 25 | // credentials: defaultProvider(), 26 | // }); 27 | let lambdaClient; 28 | let schemasClient; 29 | const testController = {}; 30 | // Begin: To connect to users' AWS accounts 31 | testController.assumeRole = async (req, res, next) => { 32 | try { 33 | console.log('arn: ', req.cookies.ARN); 34 | const stsClient = new STSClient({ 35 | region: 'us-east-1', 36 | }); 37 | const roleToAssume = { 38 | //RoleArn has to end in /OSPTool 39 | //'arn:aws:iam::082338669350:role/OSPTool' 40 | RoleArn: req.cookies.ARN, 41 | //RoleArn: ARN, 42 | RoleSessionName: 'LayerControllerSession', 43 | }; 44 | 45 | const command = new AssumeRoleCommand(roleToAssume); 46 | const { Credentials } = await stsClient.send(command); 47 | 48 | const tempCredentials = { 49 | accessKeyId: Credentials.AccessKeyId, 50 | secretAccessKey: Credentials.SecretAccessKey, 51 | sessionToken: Credentials.SessionToken, 52 | }; 53 | 54 | lambdaClient = new LambdaClient({ 55 | region: 'us-east-1', 56 | credentials: tempCredentials, 57 | }); 58 | schemasClient = new SchemasClient({ 59 | region: 'us-east-1', 60 | credentials: tempCredentials, 61 | }); 62 | next(); 63 | } catch (err) { 64 | return next(res.status(500).json({ error: 'Failed to assume role' })); 65 | } 66 | }; 67 | 68 | // (async () => { 69 | // const tempCredentials = await assumeRole(); 70 | 71 | // lambdaClient = new LambdaClient({ 72 | // region: 'us-east-1', 73 | // credentials: tempCredentials, 74 | // }); 75 | 76 | // schemasClient = new SchemasClient({ 77 | // region: 'us-east-1', 78 | // credentials: tempCredentials, 79 | // }); 80 | // })(); 81 | // End: To connect to users' AWS accounts 82 | 83 | // Middleware that tests runtime compatibility between layers and functions 84 | testController.testRuntime = async (req, res, next) => { 85 | // initialize an array of funcs that have compatible runtimes, will be passed to next middleware 86 | const passFuncs = []; 87 | // initialize an array of funcs that don't have comptable runtimes, will be saved on res.locals 88 | // to display on the front end 89 | const failFuncs = []; 90 | // deconstructs the Layer ARN and the selected functions sent in the req.body 91 | const { ARN, functionArray } = req.body; 92 | // gets info about a specfic layer version 93 | const getLayerVersionCommand = new GetLayerVersionByArnCommand({ Arn: ARN }); 94 | const getLayerResponse = await lambdaClient.send(getLayerVersionCommand); 95 | /* //getLayerReponse Example: 96 | { 97 | MetaData: {...}, 98 | CompatibleRuntimes: [ 'nodejs18.x' ], 99 | Content: {...}, 100 | CreatedDate: '2023-09-13T17:58:15.777+0000', 101 | Description: 'We need 6 different total layers for edgecase', 102 | LayerArn: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer', 103 | LayerVersionArn: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1', 104 | Version: 1 105 | } 106 | */ 107 | const layerRuntime = getLayerResponse.CompatibleRuntimes; 108 | // a property on res.locals that will store all of the errors we catch along our middlewares 109 | res.locals.addError = []; 110 | 111 | //helper function, using map line 130, iterate over Function name array checking runtime compatibility 112 | const runTimeFunction = async (element) => { 113 | try { 114 | // gets info about the function configuration, including compatible runtimes 115 | const getFunctionCommand = new GetFunctionCommand({ 116 | FunctionName: element, 117 | }); 118 | const getFunctionResponse = await lambdaClient.send(getFunctionCommand); 119 | const functionRuntime = getFunctionResponse.Configuration.Runtime; 120 | 121 | // if layer runtime and function runtime match 122 | if (layerRuntime.includes(functionRuntime)) { 123 | // push func to passed 124 | passFuncs.push(element); 125 | } else { 126 | await ErrorMessage.create({ 127 | message: `${element} does not have the correct runtime`, 128 | }); 129 | // add error to locals and push func to failed 130 | res.locals.addError.push( 131 | `${element} does not have the correct runtime` 132 | ); 133 | 134 | failFuncs.push(element); 135 | } 136 | } catch (error) { 137 | return next({ 138 | log: 139 | ('there was a problem in testController.testRuntime. Error: ', error), 140 | status: 400, 141 | message: { err: 'Problem testing runtime' }, 142 | }); 143 | } 144 | }; 145 | // stored funcs that pass and fail onto locals 146 | res.locals.passedRuntime = passFuncs; 147 | res.locals.failRuntime = failFuncs; 148 | try { 149 | // resolve all promises before going to next 150 | await Promise.all(functionArray.map(async (func) => runTimeFunction(func))); 151 | return next(); 152 | } catch (error) { 153 | return res.status(403).send(error.message); 154 | } 155 | }; 156 | 157 | // Middleware to get all shareable tests asssociated with a function 158 | testController.getTest = async (req, res, next) => { 159 | // pull func names that pass initial runtime compatibility tests 160 | const funcNames = res.locals.passedRuntime; 161 | // initialize locals array to store funcs that have no shareable tests or fail tests 162 | res.locals.failedFunctions = []; 163 | try { 164 | // schemaData will be an array that holds the tests of each function in funcNames 165 | // if a function has no tests, null will be returned in its place in the array 166 | const schemaData = await Promise.all( 167 | funcNames.map(async (funcName) => { 168 | try { 169 | const input = { 170 | RegistryName: 'lambda-testevent-schemas', 171 | SchemaName: `_${funcName}-schema`, 172 | }; 173 | const command = new DescribeSchemaCommand(input); 174 | const response = await schemasClient.send(command); 175 | const data = JSON.parse(response.Content); 176 | const dataComp = data.components.examples; 177 | console.log('datajs: ', data); 178 | console.log('dataCompjs: ', dataComp); 179 | // dataComp is the shareable tests associated with a function, will be an array 180 | return dataComp; 181 | } catch { 182 | await ErrorMessage.create({ 183 | message: `No shareable tests available for ${funcName}`, 184 | }); 185 | // if no shareable tests, push to errors and failed funcs 186 | // also return null to the schemaData array 187 | res.locals.addError.push( 188 | `No shareable tests available for ${funcName}` 189 | ); 190 | res.locals.failedFunctions.push(funcName); 191 | return null; 192 | } 193 | }) 194 | ); 195 | console.log('schemaDatajs: ', schemaData); 196 | // filter out schemaData that is null 197 | // only sends schemaData for funcs that have shareable tests 198 | res.locals.schemaData = schemaData.filter((item) => item !== null); 199 | next(); 200 | } catch (error) { 201 | return next({ 202 | log: ('there was a problem in testController.getTest. Error: ', error), 203 | status: 400, 204 | message: { err: 'Problem getting test' }, 205 | }); 206 | } 207 | }; 208 | 209 | // Middleware to test of the dependecies in a layer are comptabile with the function 210 | testController.testDependencies = async (req, res, next) => { 211 | const funcNames = res.locals.passedRuntime; 212 | const listOfTests = res.locals.schemaData; 213 | /* 214 | res.locals.passedRuntime (funcNames) stores the array of function names, in order. eg [ 'createAccount', 'getAccountBalance' ] 215 | res.locals.schemaData (listOfTests) stores the array of function test payloads, in order. each function gets an object like {firstTestName: {value: test payload}, secondTestName: {value: test payload}} 216 | eg [{"1stShareableTest":{"value":{"AcctNo":"12346"}},"2ndShareableEvent":{"value":{"AcctNo":"12347"}}},{"3rdSharebableTest":{"value":{"AcctNo":"12345"}}}] 217 | console.log(listOfTests) 218 | */ 219 | // initialize empty array to store funcs that pass all shareable tests 220 | const passedFuncs = []; 221 | 222 | const dependenciesFunction = async (element, index) => { 223 | try { 224 | //Deconstruct the Layer ARN(string) and functionArray from the request body 225 | const { ARN, functionArray } = req.body; 226 | // iterate over tests and extract the payload "value" which will be the tests 227 | for (const key in listOfTests[index]) { 228 | const payload = listOfTests[index][key].value; 229 | const lambdaInput = { 230 | FunctionName: element, 231 | Payload: JSON.stringify(payload), 232 | }; 233 | // will invoke the function with the test 234 | const command = new InvokeCommand(lambdaInput); 235 | const response = await lambdaClient.send(command); 236 | 237 | // if the function fails the test 238 | if (response.FunctionError) { 239 | // push the function name to failedFunctions array, initialized on line 142 240 | res.locals.failedFunctions.push(lambdaInput.FunctionName); 241 | const failedFuncName = lambdaInput.FunctionName; 242 | const errorType = response.Payload.transformToString(); 243 | const errorParse = JSON.parse(errorType); 244 | const messageToUser = `Error linking ${failedFuncName} to layer ${ARN}. Please fix the following: ${errorParse.errorMessage}.`; 245 | await ErrorMessage.create({ message: messageToUser }); 246 | // push the constructed error message to addError array, initialized on line 92 247 | res.locals.addError.push(messageToUser); 248 | } else { 249 | // push passing funcs to arr 250 | if (!passedFuncs.includes(element)) { 251 | passedFuncs.push(element); 252 | } 253 | } 254 | } 255 | //pass down array of passing functions 256 | res.locals.passFuncs = passedFuncs; 257 | } catch (error) { 258 | return next({ 259 | log: 260 | ('there was a problem in testController.testDependencies. Error: ', 261 | error), 262 | status: 400, 263 | message: { err: 'Your test failed' }, 264 | }); 265 | } 266 | }; 267 | 268 | try { 269 | // setTimeout is necessary to avoid moving to the next middleware function before all of the tests have been completed 270 | // the exact timeout time is currently 5000ms, but lower values could be tested 271 | setTimeout(async () => { 272 | await Promise.all( 273 | funcNames.map((func, index) => dependenciesFunction(func, index)) 274 | ); 275 | next(); 276 | }, 5000); 277 | } catch (error) { 278 | return res.status(403).send(error.message); 279 | } 280 | }; 281 | 282 | // Middleware to disconnect all the functions that failed our runtime and dependecies tests 283 | testController.removeFailedFunc = async (req, res, next) => { 284 | // req.body includes the layer ARN and res.locals includes array of failed funcs 285 | const { ARN } = req.body; 286 | const failedFunctions = res.locals.failedFunctions; 287 | //helper function to remove layer from function on failing 288 | const disconnect = async (functionName) => { 289 | try { 290 | // removes incompatible layer from function 291 | const input = { FunctionName: functionName }; 292 | const getFunctionCommand = new GetFunctionCommand(input); 293 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 294 | // filter out incompatible layer 295 | // newArray contains all layers that are compatible 296 | const newArray = Configuration.Layers.filter((layer) => { 297 | return layer.Arn !== ARN; 298 | }); 299 | 300 | // update layers property of function with compatible layers only 301 | const updateInput = { 302 | FunctionName: functionName, 303 | Layers: newArray.map((element) => element.Arn), 304 | }; 305 | const updateFunctionConfigurationCommand = 306 | new UpdateFunctionConfigurationCommand(updateInput); 307 | const updateResponse = await lambdaClient.send( 308 | updateFunctionConfigurationCommand 309 | ); 310 | } catch (err) { 311 | return next(err); 312 | } 313 | }; 314 | 315 | try { 316 | // resolve all promises before moving to next 317 | await Promise.all(failedFunctions.map((func) => disconnect(func))); 318 | return next(); 319 | } catch (err) { 320 | return next(err); 321 | } 322 | }; 323 | 324 | // Nhat's attempt to test compatibility on functions tab of app 325 | testController.testRuntimeFunctions = async (req, res, next) => { 326 | // initialize an array of layers that have compatible runtimes, will be passed to next middleware 327 | const passLayers = []; 328 | // initialize an array of layers that don't have compatible runtimes, will be saved on res.locals 329 | // to display on the front end 330 | const failLayers = []; 331 | // deconstruct req.body. ARN in this case is a specific function ARN 332 | const { ARN, layerArray, FunctionName } = req.body; 333 | // get info about a specfic function 334 | const getFunctionCommand = new GetFunctionCommand({ 335 | FunctionName: FunctionName, 336 | }); 337 | const getFunctionResponse = await lambdaClient.send(getFunctionCommand); 338 | // gets the function's compatible runtimes 339 | const functionRuntime = getFunctionResponse.Configuration.Runtime; 340 | // a property on res.locals that will store all of the errors we catch along our middlewares 341 | res.locals.addError = []; 342 | 343 | //helper function, iterate through layerArray checking runtime compatibilty 344 | const runTimeLayer = async (element) => { 345 | try { 346 | // gets info about layer 347 | const getLayerVersionCommand = new GetLayerVersionByArnCommand({ Arn }); 348 | } catch (error) { 349 | return next({ 350 | log: 351 | ('there was a problem in testController.testRuntimeFunctions. Error: ', 352 | error), 353 | status: 400, 354 | message: { err: 'Problem testing function runtime' }, 355 | }); 356 | } 357 | }; 358 | }; 359 | module.exports = testController; 360 | -------------------------------------------------------------------------------- /server/controllers/js/userControllers.js: -------------------------------------------------------------------------------- 1 | // // import bcrypt 2 | // const bcrypt = require('bcrypt'); 3 | // const saltRounds = 10; 4 | // // import jwt 5 | // const jwt = require('jsonwebtoken'); 6 | // // import db 7 | // const db = require('../models/userModel') 8 | // MUST CREATE .env FILE WITH SECRET KEY FOR JWT 9 | // Ex: ACCESS_TOKEN_SECRET= 10 | // // import env config 11 | // require('dotenv').config(); 12 | 13 | // const userController = {}; 14 | 15 | // userController.createUser = (req, res, next) => { 16 | // console.log('inside create user') 17 | // // pull user/pass/ARN off req.body 18 | // const { username, password, ARN } = req.body; 19 | // try { 20 | // // check if username already exists in DB 21 | // db.findOne({username: username}) 22 | // .then(obj => { 23 | // // if so, pause and then notify the user 24 | // if(obj) { 25 | // setTimeout(() => { 26 | // // TODO: notify user that username is taken 27 | // return next({ 28 | // error: 'An account with this username already exists.' 29 | // }); 30 | // }, 500); 31 | // } 32 | // }) 33 | // // use bcrypt.hash to hash password 34 | // bcrypt.hash(password, saltRounds, (err, hashedPassword) => { 35 | // if (err) { 36 | // console.log(err); 37 | // return next(err); 38 | // } 39 | // // insert into db using user, hash and arn 40 | // db.create({username: username, password: hashedPassword, ARN: ARN}) 41 | // // store user or arn on cookies or locals to pull and populate role arn on controllers? 42 | // }); 43 | // res.locals.username = username; 44 | // console.log(username); 45 | // res.locals.ARN = ARN; 46 | // return next(); 47 | // } catch (err) { 48 | // console.log(err); 49 | // return next(err); 50 | // } 51 | // }; 52 | 53 | // userController.verifyUser = async (req, res, next) => { 54 | // // pull user/pass off req.body 55 | // try { 56 | // const { username, password } = req.body; 57 | // // find user in db 58 | // const user = await db.findOne({username: username}) 59 | // let hashedPassword; 60 | // // if user doesn't exist, set an empty hashedPassword 61 | // if(!user) { 62 | // hashedPassword = ''; 63 | // } 64 | // // otherwise grab hashed pass 65 | // else { 66 | // hashedPassword = user.password; 67 | // } 68 | // try { 69 | // // use bcrypt.compare to check password 70 | // const match = await bcrypt.compare(password, hashedPassword); 71 | // // if it doesnt match 72 | // if (!match) { 73 | // // return next with err message 74 | // return next({ error: 'Incorrect username or password' }); 75 | // } 76 | // res.locals.username = username; 77 | // res.locals.ARN = user.ARN; 78 | // // return next 79 | // return next(); 80 | // } catch (err) { 81 | // console.log(err); 82 | // return next(err); 83 | // } 84 | // } catch (err) { 85 | // console.log(err); 86 | // return next(err); 87 | // } 88 | // }; 89 | 90 | // userController.createToken = async (req, res, next) => { 91 | // try { 92 | // // pull user off res.locals 93 | // const {username, ARN} = res.locals; 94 | // // find user in db 95 | // const user = await db.findOne({username: username}); 96 | // // use jwt.sign on user obj with secret env key 97 | // const token = await jwt.sign({username: user.username}, process.env.ACCESS_TOKEN_SECRET, { 98 | // expiresIn: 60 * 60// Expires in one hour 99 | // }) 100 | // // create cookie with token 101 | // await res.cookie('token', token, { 102 | // maxAge: (60 * 60 * 1000), // Expires in one hour 103 | // httpOnly: true 104 | // }) 105 | // // create cookie with arn 106 | // await res.cookie('ARN', ARN); 107 | // // give this an expiration to persist session? 108 | // // ex. delete when they logout 109 | // // and delete after an hour 110 | // return next(); 111 | // } catch (err) { 112 | // console.log(err); 113 | // return next(err); 114 | // } 115 | // }; 116 | 117 | // userController.verifyToken = async (req, res, next) => { 118 | // // pull token from cookies 119 | // const {token} = req.cookies; 120 | // try { 121 | // // use jwt.verify to check if token is valid with secret env key 122 | // await jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, success) => { 123 | // if (err) { 124 | // console.log(err); 125 | // return next(err) 126 | // } 127 | // return next(); 128 | // }) 129 | // } catch (err) { 130 | // console.log(err); 131 | // return next(err); 132 | // } 133 | // }; 134 | 135 | // userController.deleteToken = (req, res, next) => { 136 | // try { 137 | // // use res.clearCookie to delete both cookies 138 | // res.clearCookie('token'); 139 | // return next(); 140 | // } catch (err) { 141 | // console.log(err); 142 | // return next(err); 143 | // } 144 | // }; 145 | 146 | // module.exports = userController; 147 | -------------------------------------------------------------------------------- /server/controllers/layerController.ts: -------------------------------------------------------------------------------- 1 | import { LambdaClient, 2 | ListLayersCommand, 3 | ListLayerVersionsCommand, 4 | ListFunctionsCommand, 5 | GetFunctionCommand, 6 | UpdateFunctionConfigurationCommand, 7 | LayersListItem, Layer} from '@aws-sdk/client-lambda'; 8 | import { Request, Response, NextFunction } from 'express'; 9 | 10 | import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'; 11 | 12 | import { defaultProvider } from '@aws-sdk/credential-provider-node'; 13 | 14 | import ErrorMessage from '../models/notificationModel'; 15 | import { IError } from '../models/notificationModel'; 16 | import HistoryLog from '../models/historyLogModel'; 17 | import { IHistory } from '../models/historyLogModel'; 18 | 19 | // OSP Account connection 20 | // const lambdaClient = new LambdaClient({ 21 | // region: 'us-east-1', 22 | // credentials: defaultProvider(), 23 | // }); 24 | let lambdaClient: LambdaClient; 25 | const layerController = { 26 | // Begin: To connect to users' AWS accounts 27 | // Pull ARN from cookie after login 28 | 29 | assumeRole: async (req: Request, res: Response, next: NextFunction): Promise => { 30 | try { 31 | const stsClient = new STSClient({ 32 | region: req.cookies.region, 33 | }); 34 | const roleToAssume: {RoleArn: string, RoleSessionName: string} = { 35 | //RoleArn has to end in /OSPTool 36 | //'arn:aws:iam::082338669350:role/OSPTool' 37 | RoleArn: req.cookies.ARN, 38 | //RoleArn: ARN, 39 | RoleSessionName: 'LayerControllerSession', 40 | }; 41 | const command = new AssumeRoleCommand(roleToAssume); 42 | const { Credentials } = await stsClient.send(command); 43 | 44 | const tempCredentials = { 45 | accessKeyId: Credentials.AccessKeyId, 46 | secretAccessKey: Credentials.SecretAccessKey, 47 | sessionToken: Credentials.SessionToken, 48 | }; 49 | 50 | lambdaClient = new LambdaClient({ 51 | region: req.cookies.region, 52 | credentials: tempCredentials, 53 | }); 54 | next(); 55 | } 56 | catch (err) { 57 | return next({ 58 | log: 59 | `Failed to assume role. Error: ${err}`, 60 | status: 500, 61 | //basic message to user 62 | message: {err: 'Failed to assume role'}, 63 | }) 64 | } 65 | }, 66 | 67 | // End: To connect to users' AWS accounts 68 | 69 | 70 | // Middleware function to get information about all layers from this account 71 | getLayer: async (req: Request, res: Response, next: NextFunction): Promise => { 72 | try { 73 | // call the listLayers command to get all layers 74 | const input: {} = {}; 75 | const listLayersCommand = new ListLayersCommand(input); 76 | const layersData = await lambdaClient.send(listLayersCommand); 77 | 78 | // extract the Layers array from the response and assign it to res.locals.layer 79 | res.locals.layer = layersData.Layers; 80 | // proceed to next middleware 81 | return next(); 82 | } catch (err) { 83 | res.status(500).json({ error: 'Failed to fetch layers' }); 84 | } 85 | }, 86 | 87 | // Gets the versions of all the layers to display on our front end 88 | getVersions: async (req: Request, res: Response, next: NextFunction): Promise => { 89 | try { 90 | // retrieve layer data stored in the res.locals from getLayer middleware 91 | const layers: LayersListItem[] = res.locals.layer; 92 | // loop over each layer and its versions 93 | const layerPromises = layers.map(async (layer) => { 94 | // call the listLayerVersions method on each layer and save it to a const 95 | const input = { LayerName: layer.LayerName }; 96 | const listLayerVersionsCommand = new ListLayerVersionsCommand(input); 97 | const versionsData = await lambdaClient.send(listLayerVersionsCommand); 98 | /*VersionData Example: 99 | { 100 | MetaData: {...}, 101 | LayerVersions: [{ 102 | CompatibleRuntimes: [Array], 103 | LicenseInfo: null, 104 | Description: 'We need 6 different total layers for edgecase', 105 | LayerVersionArn: 'arn:aws:lambda:us-east-1:082338669350:layer:MichaelLayer:1', 106 | Version: 1, 107 | CreatedDate: '2023-09-13T18:00:05.842+0000', 108 | CompatibleArchitectures: null 109 | }] 110 | } 111 | */ 112 | // construct and return an object that contains the layer name, its versions, and the ARN of each version 113 | // versions will be an array 114 | return { 115 | name: layer.LayerName, 116 | versions: versionsData.LayerVersions.map((element) => element.Version), 117 | ARN: versionsData.LayerVersions.map((v) => v.LayerVersionArn), 118 | }; 119 | }); 120 | // Wait for all promises to resolve 121 | const layersWithVersions = await Promise.all(layerPromises); 122 | // store an array that contains layer info and their version onto res.locals 123 | res.locals.layersWithVersions = layersWithVersions; 124 | return next(); 125 | } catch (err) { 126 | res.status(500).json({ err: 'Failed to fetch layer versions' }); 127 | } 128 | }, 129 | 130 | // Middleware to get all functions associated with a layer component 131 | getFunctions: async (req: Request, res: Response, next: NextFunction)=> { 132 | try { 133 | // pull ARN from req body 134 | //Layer ARN 135 | const ARN: string = req.body.ARN; 136 | // array that will contain func names that have the layer we're looking for 137 | const functionArray: string[] = []; 138 | const input: {} = {}; 139 | // lists all functions 140 | const listFunctionsCommand = new ListFunctionsCommand(input); 141 | const { Functions } = await lambdaClient.send(listFunctionsCommand); 142 | 143 | // iterate through the Functions array, checking each function to find if it has the layer that we're looking for 144 | // if so, push it to functionArray 145 | Functions.forEach((element) => { 146 | // if it currently has layers 147 | if (element.Layers) { 148 | // iterate thru each 149 | for (const item of element.Layers) { 150 | // if layer ARN matches, push func to func array 151 | //compare input layer ARN and ARN stored on Function.Layers 152 | if (item.Arn === ARN) { 153 | functionArray.push(element.FunctionName); 154 | break; 155 | } 156 | } 157 | } 158 | }); 159 | // store functionArray in res.locals 160 | res.locals.functionArray = functionArray; 161 | return next(); 162 | } catch (err) { 163 | res.status(500).json({ error: 'Failed to fetch associated functions' }); 164 | } 165 | }, 166 | 167 | // Middleware to remove function from a layer component 168 | removeFunction: async (req: Request, res: Response, next: NextFunction): Promise => { 169 | try { 170 | // req.body includes the layer ARN and functionName 171 | // Layer ARN 172 | const ARN: string = req.body.ARN; 173 | const functionName: string = req.body.functionName; 174 | const layerName: string = req.body.layerName; 175 | // get the list of layers connected to functionName 176 | const input = { FunctionName: functionName }; 177 | // gets info about a specific function 178 | const getFunctionCommand = new GetFunctionCommand(input); 179 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 180 | 181 | // remove the layer from the Layers array by ARN and store it into const newArray 182 | const newArray = Configuration.Layers.filter((layer) => { 183 | return layer.Arn !== ARN; 184 | }); 185 | // update the configuration of functionName using the new Layers array 186 | const updateInput = { 187 | FunctionName: functionName, 188 | Layers: newArray.map((element) => element.Arn), 189 | }; 190 | const updateFunctionConfigurationCommand = 191 | new UpdateFunctionConfigurationCommand(updateInput); 192 | await lambdaClient.send(updateFunctionConfigurationCommand); 193 | 194 | await HistoryLog.create({message: `${functionName} was removed from ${layerName}`, ARN: req.cookies['ARN']}) as IHistory; 195 | 196 | return next(); 197 | } catch (err) { 198 | res.status(500).json({ error: 'Failed to remove function from layer' }); 199 | } 200 | }, 201 | 202 | // Middleware to add function to a layer component 203 | addFunction: async (req: Request, res: Response, next: NextFunction) => { 204 | // req.body is an object with keys ARN (string layer ARN) and functionArray (array of string function names) 205 | // functionArray is not used here. instead we use passFuncs below 206 | const ARN: string = req.body.ARN; 207 | // passFuncs contains all funcs that pass initial runtime compatability test 208 | const passFuncs = res.locals.passedRuntime; 209 | 210 | const updateFunctions = async (functionName: string) => { 211 | try { 212 | const getFunctionInput = { 213 | FunctionName: functionName, 214 | } 215 | const getFunctionCommand = new GetFunctionCommand(getFunctionInput); 216 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 217 | let newArray: Layer[] = []; 218 | // edge case: if the function has no layers yet, Configuration.Layers will be undefined 219 | if (Configuration.Layers === undefined) { 220 | newArray = []; 221 | } else { 222 | // else, set the array to be the current layers array 223 | newArray = Configuration.Layers; 224 | } 225 | // add this layer ARN to the current Layers array 226 | newArray.push({ Arn: ARN }); 227 | 228 | // send the updated Layers array to AWS 229 | const updateFunctionConfigurationInput = { 230 | FunctionName: functionName, 231 | Layers: newArray.map((element) => element.Arn), 232 | } 233 | const updateFunctionConfigurationCommand = new UpdateFunctionConfigurationCommand(updateFunctionConfigurationInput); 234 | await lambdaClient.send(updateFunctionConfigurationCommand); 235 | } catch (error) { 236 | res.locals.allFailingFuncs.push(functionName); 237 | await ErrorMessage.create({message: `Failed to update function ${functionName}. Error: ${error.message}`, ARN: req.cookies['ARN']}) as IError; 238 | // add error message to error object to be sent to frontend 239 | res.locals.addError.push( 240 | `Failed to update function ${functionName}. Error: ${error.message}` 241 | ); 242 | } 243 | }; 244 | 245 | try { 246 | // resolves all promises before heading to next middleware 247 | await Promise.all(passFuncs.map((func: string) => updateFunctions(func))); 248 | return next(); 249 | } catch (error) { 250 | console.log(error); 251 | return res.status(403).send(error.message); 252 | } 253 | } 254 | }; 255 | 256 | export default layerController; 257 | -------------------------------------------------------------------------------- /server/controllers/testController.ts: -------------------------------------------------------------------------------- 1 | // const { 2 | // SchemasClient, 3 | // DescribeSchemaCommand, 4 | // } = require('@aws-sdk/client-schemas'); 5 | import { SchemasClient, DescribeSchemaCommand, DescribeSchemaCommandOutput } from '@aws-sdk/client-schemas'; 6 | //const { defaultProvider } = require('@aws-sdk/credential-provider-node'); 7 | import { defaultProvider } from '@aws-sdk/credential-provider-node' 8 | import { Request, Response, NextFunction } from 'express'; 9 | 10 | import { 11 | LambdaClient, 12 | InvokeCommand, 13 | GetLayerVersionByArnCommand, GetLayerVersionByArnCommandOutput, 14 | GetFunctionCommand, GetFunctionCommandOutput, 15 | UpdateFunctionConfigurationCommand, 16 | } from '@aws-sdk/client-lambda' 17 | 18 | //const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 19 | import { STSClient, AssumeRoleCommand, AssumeRoleCommandOutput } from '@aws-sdk/client-sts' 20 | 21 | //const ErrorMessage = require('../models/notificationModel'); 22 | import ErrorMessage from '../models/notificationModel'; 23 | import { IError } from '../models/notificationModel'; 24 | import HistoryLog from '../models/historyLogModel'; 25 | import { IHistory } from '../models/historyLogModel'; 26 | 27 | 28 | //const User = require('../models/userModel'); 29 | import User from '../models/userModel'; 30 | 31 | // OSP Account connection 32 | // const lambdaClient = new LambdaClient({ 33 | // region: 'us-east-1', 34 | // credentials: defaultProvider(), 35 | // }); 36 | 37 | // const schemasClient = new SchemasClient({ 38 | // region: 'us-east-1', 39 | // credentials: defaultProvider(), 40 | // }); 41 | let lambdaClient: LambdaClient; 42 | let schemasClient: SchemasClient; 43 | const testController = { 44 | 45 | // Begin: To connect to users' AWS accounts 46 | assumeRole: async (req: Request, res: Response, next: NextFunction): Promise => { 47 | res.locals.allFailingFuncs = []; 48 | try { 49 | const stsClient: STSClient = new STSClient({ 50 | region: req.cookies.region, 51 | }); 52 | const roleToAssume: {RoleArn: string, RoleSessionName: string} = { 53 | //RoleArn has to end in /OSPTool 54 | //'arn:aws:iam::082338669350:role/OSPTool' 55 | RoleArn: req.cookies.ARN, 56 | //RoleArn: ARN, 57 | RoleSessionName: 'TestControllerSession', 58 | }; 59 | const layerName: string = req.body.layerName; 60 | res.locals.layerName = layerName; 61 | 62 | const command: AssumeRoleCommand = new AssumeRoleCommand(roleToAssume); 63 | const { Credentials } = await stsClient.send(command) as AssumeRoleCommandOutput; 64 | 65 | 66 | const tempCredentials: {accessKeyId: string, secretAccessKey: string, sessionToken: string} = { 67 | accessKeyId: Credentials.AccessKeyId, 68 | secretAccessKey: Credentials.SecretAccessKey, 69 | sessionToken: Credentials.SessionToken, 70 | } 71 | 72 | lambdaClient = new LambdaClient({ 73 | region: req.cookies.region, 74 | credentials: tempCredentials, 75 | }); 76 | 77 | schemasClient = new SchemasClient({ 78 | region: req.cookies.region, 79 | credentials: tempCredentials, 80 | }); 81 | return next(); 82 | 83 | } catch (err) { 84 | // return next(res.status(500).json({ error: 'Failed to assume role' })); 85 | return next({ 86 | log: 87 | `Failed to assume role. Error: ${err}`, 88 | status: 500, 89 | //basic message to user 90 | message: {err: 'Failed to assume role'}, 91 | }) 92 | } 93 | // End: To connect to users' AWS accounts 94 | }, 95 | 96 | 97 | // Middleware that tests runtime compatibility between layers and functions 98 | testRuntime: async (req: Request, res: Response, next: NextFunction): Promise => { 99 | // initialize an array of funcs that have compatible runtimes, will be passed to next middleware 100 | const passFuncs: string[] = []; 101 | // initialize an array of funcs that don't have comptable runtimes, will be saved on res.locals 102 | // to display on the front end 103 | const failFuncs: string[] = []; 104 | // deconstructs the Layer ARN and the selected functions sent in the req.body 105 | const ARN: string = req.body.ARN; 106 | const functionArray: string[] = req.body.functionArray 107 | // gets info about a specfic layer version 108 | const getLayerVersionCommand: GetLayerVersionByArnCommand = new GetLayerVersionByArnCommand({ Arn: ARN }); 109 | const getLayerResponse: GetLayerVersionByArnCommandOutput = await lambdaClient.send(getLayerVersionCommand); 110 | /* //getLayerReponse Example: 111 | { 112 | MetaData: {...}, 113 | CompatibleRuntimes: [ 'nodejs18.x' ], 114 | Content: {...}, 115 | CreatedDate: '2023-09-13T17:58:15.777+0000', 116 | Description: 'We need 6 different total layers for edgecase', 117 | LayerArn: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer', 118 | LayerVersionArn: 'arn:aws:lambda:us-east-1:082338669350:layer:GregLayer:1', 119 | Version: 1 120 | } 121 | */ 122 | const layerRuntime: string[] | undefined = getLayerResponse.CompatibleRuntimes; 123 | // a property on res.locals that will store all of the errors we catch along our middlewares 124 | res.locals.addError = []; 125 | 126 | //helper function, using map line 130, iterate over Function name array checking runtime compatibility 127 | const runTimeFunction = async (element: string): Promise => { 128 | try { 129 | // gets info about the function configuration, including compatible runtimes 130 | const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({ 131 | FunctionName: element, 132 | }); 133 | const getFunctionResponse: GetFunctionCommandOutput = await lambdaClient.send(getFunctionCommand); 134 | const functionRuntime: string = getFunctionResponse.Configuration.Runtime; 135 | 136 | // if layer runtime and function runtime match 137 | if (layerRuntime.includes(functionRuntime)) { 138 | // push func to passed 139 | passFuncs.push(element); 140 | 141 | } else { 142 | 143 | res.locals.allFailingFuncs.push(element); 144 | 145 | await ErrorMessage.create({message: `${element} does not have the correct runtime`, ARN: req.cookies['ARN']}) as IError; 146 | // add error to locals and push func to failed 147 | res.locals.addError.push( 148 | `${element} does not have the correct runtime` 149 | ); 150 | 151 | failFuncs.push(element); 152 | } 153 | } catch (error) { 154 | return next({ 155 | log: 156 | `there was a problem in testController.testRuntime. Error: ${error}`, 157 | status: 400, 158 | message: { err: 'Problem testing runtime' }, 159 | }); 160 | } 161 | }; 162 | // stored funcs that pass and fail onto locals 163 | res.locals.passedRuntime = passFuncs; 164 | res.locals.failRuntime = failFuncs; 165 | try { 166 | // resolve all promises before going to next 167 | await Promise.all(functionArray.map(async (func) => runTimeFunction(func))); 168 | return next(); 169 | } catch (error) { 170 | return res.status(403).send(error.message); 171 | } 172 | }, 173 | 174 | // Middleware to get all shareable tests asssociated with a function 175 | getTest: async (req: Request, res: Response, next: NextFunction): Promise => { 176 | // pull func names that pass initial runtime compatibility tests 177 | const funcNames: string[] = res.locals.passedRuntime; 178 | 179 | // initialize locals array to store funcs that have no shareable tests or fail tests 180 | res.locals.failedFunctions = []; 181 | try { 182 | // schemaData will be an array that holds the tests of each function in funcNames 183 | // if a function has no tests, null will be returned in its place in the array 184 | const schemaData: any = await Promise.all( 185 | funcNames.map(async (funcName) => { 186 | try { 187 | const input = { 188 | RegistryName: 'lambda-testevent-schemas', 189 | SchemaName: `_${funcName}-schema`, 190 | }; 191 | const command: DescribeSchemaCommand = new DescribeSchemaCommand(input); 192 | const response: DescribeSchemaCommandOutput = await schemasClient.send(command); 193 | const data: any = JSON.parse(response.Content); 194 | const dataComp: any = data.components.examples; 195 | // dataComp is the shareable tests associated with a function, will be an array 196 | return dataComp; 197 | } catch { 198 | res.locals.allFailingFuncs.push(funcName); 199 | await ErrorMessage.create({message: `No shareable tests available for ${funcName}`, ARN: req.cookies['ARN']}) as IError; 200 | // if no shareable tests, push to errors and failed funcs 201 | // also return null to the schemaData array 202 | res.locals.addError.push( 203 | `No shareable tests available for ${funcName}` 204 | ); 205 | res.locals.failedFunctions.push(funcName); 206 | return null; 207 | } 208 | }) 209 | ); 210 | 211 | // filter out schemaData that is null 212 | // only sends schemaData for funcs that have shareable tests 213 | res.locals.schemaData = schemaData.filter((item: any) => item !== null); 214 | next(); 215 | } catch (error) { 216 | return next({ 217 | log: (`there was a problem in testController.getTest. Error: ${error}`), 218 | status: 400, 219 | message: { err: 'Problem getting test' }, 220 | }); 221 | 222 | } 223 | }, 224 | 225 | // Middleware to test of the dependecies in a layer are comptabile with the function 226 | testDependencies: async (req: Request, res: Response, next: NextFunction) => { 227 | const funcNames: string[] = res.locals.passedRuntime; 228 | const listOfTests: any = res.locals.schemaData; 229 | /* 230 | res.locals.passedRuntime (funcNames) stores the array of function names, in order. eg [ 'createAccount', 'getAccountBalance' ] 231 | res.locals.schemaData (listOfTests) stores the array of function test payloads, in order. each function gets an object like {firstTestName: {value: test payload}, secondTestName: {value: test payload}} 232 | eg [{"1stShareableTest":{"value":{"AcctNo":"12346"}},"2ndShareableEvent":{"value":{"AcctNo":"12347"}}},{"3rdSharebableTest":{"value":{"AcctNo":"12345"}}}] 233 | console.log(listOfTests) 234 | */ 235 | // initialize empty array to store funcs that pass all shareable tests 236 | const passedFuncs: string[] = []; 237 | 238 | const dependenciesFunction = async (element: string, index: number) => { 239 | try { 240 | //Deconstruct the Layer ARN(string) and functionArray from the request body 241 | const ARN: string = req.body.ARN 242 | // iterate over tests and extract the payload "value" which will be the tests 243 | for (const key in listOfTests[index]) { 244 | const payload: any = listOfTests[index][key].value; 245 | const lambdaInput = { 246 | FunctionName: element, 247 | Payload: JSON.stringify(payload), 248 | }; 249 | // will invoke the function with the test 250 | const command = new InvokeCommand(lambdaInput); 251 | const response = await lambdaClient.send(command); 252 | 253 | // if the function fails the test 254 | if (response.FunctionError) { 255 | // push the function name to failedFunctions array, initialized on line 142 256 | res.locals.failedFunctions.push(lambdaInput.FunctionName); 257 | res.locals.allFailingFuncs.push(lambdaInput.FunctionName); 258 | const failedFuncName: string = lambdaInput.FunctionName; 259 | const errorType: string = response.Payload.transformToString(); 260 | const errorParse: any = JSON.parse(errorType); 261 | const messageToUser: string = `Error linking ${failedFuncName} to layer ${res.locals.layerName}. Please fix the following: ${errorParse.errorMessage}.`; 262 | await ErrorMessage.create({message: messageToUser, ARN: req.cookies['ARN']}) as IError; 263 | // push the constructed error message to addError array, initialized on line 92 264 | res.locals.addError.push(messageToUser); 265 | 266 | } else { 267 | // push passing funcs to arr 268 | if (!res.locals.allFailingFuncs.includes(element)) { 269 | await HistoryLog.create({message: `${lambdaInput.FunctionName} function was successfully added ${res.locals.layerName}`, ARN: req.cookies['ARN']}) as IHistory; 270 | 271 | passedFuncs.push(element); 272 | } 273 | } 274 | } 275 | //pass down array of passing functions 276 | res.locals.passFuncs = passedFuncs; 277 | } catch (error) { 278 | return next({ 279 | log: 280 | (`there was a problem in testController.testDependencies. Error: 281 | ${error}`), 282 | status: 400, 283 | message: { err: 'Your test failed' }, 284 | }); 285 | } 286 | }; 287 | 288 | try { 289 | // setTimeout is necessary to avoid moving to the next middleware function before all of the tests have been completed 290 | // the exact timeout time is currently 5000ms, but lower values could be tested 291 | setTimeout(async () => { 292 | await Promise.all( 293 | funcNames.map((func: string, index: number) => dependenciesFunction(func, index)) 294 | ); 295 | return next(); 296 | }, 5000); 297 | } catch (error) { 298 | return res.status(403).send(error.message); 299 | } 300 | }, 301 | 302 | // Middleware to disconnect all the functions that failed our runtime and dependencies tests 303 | removeFailedFunc: async (req: Request, res: Response, next: NextFunction): Promise => { 304 | // req.body includes the layer ARN and res.locals includes array of failed funcs 305 | const ARN: string = req.body.ARN; 306 | const failedFunctions: string[] = res.locals.failedFunctions; 307 | //helper function to remove layer from function on failing 308 | const disconnect = async (functionName: string): Promise => { 309 | try { 310 | // removes incompatible layer from function 311 | const input = { FunctionName: functionName }; 312 | const getFunctionCommand = new GetFunctionCommand(input); 313 | const { Configuration } = await lambdaClient.send(getFunctionCommand); 314 | // filter out incompatible layer 315 | // newArray contains all layers that are compatible 316 | const newArray = Configuration.Layers.filter((layer) => { 317 | return layer.Arn !== ARN; 318 | }); 319 | 320 | // update layers property of function with compatible layers only 321 | const updateInput = { 322 | FunctionName: functionName, 323 | Layers: newArray.map((element) => element.Arn), 324 | }; 325 | const updateFunctionConfigurationCommand = 326 | new UpdateFunctionConfigurationCommand(updateInput); 327 | const updateResponse = await lambdaClient.send( 328 | updateFunctionConfigurationCommand 329 | ); 330 | } catch (err) { 331 | return next(err); 332 | } 333 | }; 334 | 335 | try { 336 | // resolve all promises before moving to next 337 | await Promise.all(failedFunctions.map((func) => disconnect(func))); 338 | return next(); 339 | } catch (err) { 340 | return next(err); 341 | } 342 | }, 343 | 344 | } 345 | export default testController; 346 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | // import bcrypt 3 | const saltRounds: number = 10; 4 | // import jwt 5 | import jwt from 'jsonwebtoken'; 6 | // import db 7 | import User from '../models/userModel'; 8 | import { IUser } from '../models/userModel'; 9 | // MUST CREATE .env FILE WITH SECRET KEY FOR JWT 10 | // Ex: ACCESS_TOKEN_SECRET= 11 | // import env config 12 | import dotenv from 'dotenv'; 13 | dotenv.config(); 14 | 15 | import { Request, Response, NextFunction } from 'express'; 16 | import ErrorMessage from '../models/notificationModel'; 17 | import { IError } from '../models/notificationModel'; 18 | import HistoryLog from '../models/historyLogModel'; 19 | import { IHistory } from '../models/historyLogModel'; 20 | 21 | const userController: any = { 22 | createUser: async (req: Request, res: Response, next: NextFunction) => { 23 | // pull user/pass/ARN off req.body 24 | //const { username, password, ARN } = req.body; 25 | const username: string = req.body.username; 26 | const password: string = req.body.password; 27 | const region: string = req.body.region; 28 | const ARN: string = req.body.ARN; 29 | try { 30 | // use bcrypt.hash to hash password 31 | bcrypt.hash(password, saltRounds, async (err, hashedPassword) => { 32 | if (err) { 33 | console.log(err); 34 | return next(err); 35 | } 36 | // insert into db using user, hash and arn 37 | try { 38 | await User.create({username: username, password: hashedPassword, ARN: ARN, region: region}) 39 | res.locals.username = username; 40 | res.locals.region = region; 41 | res.locals.ARN = ARN; 42 | return next(); 43 | } catch(err) { 44 | console.log('error creating user'); 45 | return next(err) 46 | }; 47 | // store user or arn on cookies or locals to pull and populate role arn on controllers? 48 | }); 49 | } catch (err) { 50 | console.log(err); 51 | return next(err); 52 | } 53 | }, 54 | 55 | verifyUser: async (req: Request, res: Response, next: NextFunction): Promise => { 56 | // pull user/pass off req.body 57 | try { 58 | const username: string = req.body.username; 59 | const password: string = req.body.password; 60 | // find user in db 61 | const user = await User.findOne({username: username}) as IUser; 62 | let hashedPassword: string; 63 | // if user doesn't exist, set an empty hashedPassword 64 | if(!user) { 65 | hashedPassword = ''; 66 | } 67 | // otherwise grab hashed pass 68 | else { 69 | hashedPassword = user.password; 70 | } 71 | try { 72 | // use bcrypt.compare to check password 73 | const match: boolean = await bcrypt.compare(password, hashedPassword); 74 | // if it doesnt match 75 | if (!match) { 76 | // return next with err message 77 | console.log('no match') 78 | return next({ 79 | log: 80 | `Failed to login.`, 81 | status: 400, 82 | //basic message to user 83 | message: {err: 'Failed to login'}, 84 | }) 85 | } 86 | res.locals.username = username; 87 | res.locals.ARN = user.ARN; 88 | res.locals.region = user.region; 89 | // return next 90 | return next(); 91 | } catch (err) { 92 | console.log(err); 93 | return next(err); 94 | } 95 | } catch (err) { 96 | console.log(err); 97 | return next(err); 98 | } 99 | }, 100 | createToken: async (req: Request, res: Response, next: NextFunction): Promise => { 101 | try { 102 | // pull user off res.locals 103 | const username: string = res.locals.username; 104 | const ARN: string = res.locals.ARN; 105 | const region: string = res.locals.region; 106 | // find user in db 107 | const user = await User.findOne({username: username}) as IUser; 108 | // use jwt.sign on user obj with secret env key 109 | const token = await jwt.sign({username: user.username}, process.env.ACCESS_TOKEN_SECRET as jwt.Secret, { 110 | expiresIn: 60 * 60// Expires in one hour 111 | }) 112 | // create cookie with token 113 | await res.cookie('token', token, { 114 | maxAge: (60 * 60 * 1000), // Expires in one hour 115 | httpOnly: true 116 | }) 117 | // create cookie with arn 118 | await res.cookie('ARN', ARN); 119 | await res.cookie('region', region); 120 | // give this an expiration to persist session? 121 | // ex. delete when they logout 122 | // and delete after an hour 123 | return next(); 124 | } catch (err) { 125 | console.log(err); 126 | return next(err); 127 | } 128 | }, 129 | verifyToken: async (req: Request, res: Response, next: NextFunction): Promise => { 130 | // pull token from cookies 131 | const token: string = req.cookies.token; 132 | try { 133 | // use jwt.verify to check if token is valid with secret env key 134 | await jwt.verify(token, process.env.ACCESS_TOKEN_SECRET as string, (err, success) => { 135 | if (err) { 136 | console.log(err); 137 | return next(err) 138 | } 139 | return next(); 140 | }) 141 | } catch (err) { 142 | console.log(err); 143 | return next(err); 144 | } 145 | }, 146 | 147 | deleteToken: (req: Request, res: Response, next: NextFunction): void => { 148 | try { 149 | // use res.clearCookie to delete all cookies 150 | res.clearCookie('token'); 151 | res.clearCookie('ARN'); 152 | res.clearCookie('region'); 153 | return next(); 154 | } catch (err) { 155 | console.log(err); 156 | return next(err); 157 | } 158 | }, 159 | 160 | getNotifications: async (req: Request, res: Response, next: NextFunction) => { 161 | try { 162 | // pull arn from cookie 163 | const ARN: string = req.cookies['ARN']; 164 | const notifications: string[] = [] 165 | // search notification db for notifications with corresponding ARN 166 | // send back all notifications 167 | 168 | const notificationLog = await ErrorMessage.find({ARN: ARN}); 169 | if(!notificationLog){ 170 | return next({ 171 | log: 'Error in getNotifications conditional', 172 | status: 400, 173 | message: 'Failed to retrieve notifications' 174 | }) 175 | } else { 176 | res.locals.notificationLog = notificationLog; 177 | return next() 178 | }; 179 | } catch (err) { 180 | console.log(err); 181 | return next(err); 182 | } 183 | }, 184 | getHistoryLog: async (req: Request, res: Response, next: NextFunction) => { 185 | try { 186 | // pull arn from cookie 187 | const ARN: string = req.cookies['ARN']; 188 | // search notification db for notifications with corresponding ARN 189 | // send back all notifications 190 | 191 | const historyLog = await HistoryLog.find({ARN: ARN}); 192 | if(!historyLog){ 193 | return next({ 194 | log: 'Error in historyLog conditional', 195 | status: 400, 196 | message: 'Failed to retrieve history log' 197 | }) 198 | } else { 199 | //notifications.push(notificationLog.message, ) 200 | res.locals.historyLog = historyLog; 201 | return next(); 202 | }; 203 | }catch(err){ 204 | console.log(err); 205 | return next(err); 206 | } 207 | }, 208 | 209 | changeInfo: async(req: Request, res: Response, next: NextFunction) => { 210 | try{ 211 | const updateInfo = req.body; 212 | const ARN: string = req.cookies['ARN']; 213 | // if password update, hash 214 | if (updateInfo.password) { 215 | const newPassword: string = updateInfo.password; 216 | const hashedPassword = await bcrypt.hash(newPassword, saltRounds); 217 | updateInfo.password = hashedPassword; 218 | } 219 | // update user in db 220 | const updatedUser = await User.findOneAndUpdate({ARN: req.cookies['ARN']}, updateInfo, { 221 | new: true, 222 | }); 223 | 224 | 225 | return next(); 226 | } catch(err) { 227 | return next({ 228 | log: `there was an error in userController.changeInfo. Error: ${err}`, 229 | status: 400, 230 | message: 'There was a problem updating that info!' 231 | }); 232 | } 233 | } 234 | 235 | }; 236 | 237 | export default userController; 238 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | 5 | //connect to mongoDB - going to hide this 6 | const myURI = ''; // insert MongoDB here 7 | 8 | //set uri to passed in value 9 | const URI = process.env.MONGO_URI || myURI; 10 | 11 | 12 | 13 | const connectDB = () => { 14 | 15 | //attempt to connect to mongoDB using myURI string 16 | mongoose.connect(myURI, { 17 | useNewUrlParser: true, 18 | useUnifiedTopology: true 19 | }); 20 | 21 | //when connected display message to dev successful connection 22 | mongoose.connection.on('connected', () => { 23 | console.log('Connected to MongoDB Atlas'); 24 | }); 25 | 26 | //if connection fails message dev failure message 27 | mongoose.connection.on('error', (error) => { 28 | console.log('Error connecting to MongoDB Atlas. Error: ', error); 29 | }); 30 | }; 31 | 32 | //run function to conncet to db 33 | 34 | module.exports = connectDB; 35 | 36 | -------------------------------------------------------------------------------- /server/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | //connect to mongoDB - going to hide this 4 | const myURI: string = 5 | ''; // insert MongoDB here 6 | 7 | //set uri to passed in value 8 | const URI: string = process.env.MONGO_URI || myURI; 9 | 10 | const connectDB = () => { 11 | mongoose.connect(myURI); 12 | 13 | //when connected display message to dev successful connection 14 | mongoose.connection.on('connected', () => { 15 | console.log('Connected to MongoDB Atlas'); 16 | }); 17 | 18 | //if connection fails message dev failure message 19 | mongoose.connection.on('error', (error) => { 20 | console.log('Error connecting to MongoDB Atlas. Error: ', error); 21 | }); 22 | }; 23 | 24 | //run function to conncet to db 25 | 26 | export default connectDB; 27 | -------------------------------------------------------------------------------- /server/js/servers.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const layerRouter = require('./routes/layerRouter'); 4 | const functionRouter = require('./routes/functionRouter'); 5 | const userRouter = require('./routes/userRouter'); 6 | const connectDB = require('./db'); 7 | const cookieParser = require('cookie-parser'); 8 | connectDB(); 9 | // Initialize Express 10 | const app = express(); 11 | const PORT = 3000; 12 | 13 | // CORS 14 | const cors = require('cors'); 15 | app.use(cors({ origin: 'http://localhost:8080', credentials: true })); 16 | 17 | app.use(cookieParser()); 18 | app.use(express.json()); // for parsing application/json 19 | app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 20 | 21 | // grab arn from cookies to use for connection in middleware 22 | app.use((req, res, next) => { 23 | const ARN = req.cookies.ARN; 24 | app.locals.ARN = ARN; 25 | next(); 26 | }); 27 | app.use('/layers', layerRouter); 28 | app.use('/functions', functionRouter); 29 | app.use('/user', userRouter); 30 | 31 | //global error handler 32 | app.use((err, req, res, next) => { 33 | const defaultErr = { 34 | //detailed message to dev 35 | log: 36 | ('Express error handler caught unknown middleware error. Error: ', err), 37 | status: 400, 38 | //basic message to user 39 | message: { err: 'An error occurred' }, 40 | }; 41 | const errorObj = Object.assign({}, defaultErr, err); 42 | //send error message to frontend 43 | return res.status(errorObj.status).json(errorObj.message); 44 | }); 45 | 46 | // Start Express Server 47 | app.listen(PORT, () => { 48 | console.log(`Server is running on http://localhost:${PORT}`); 49 | }); 50 | -------------------------------------------------------------------------------- /server/models/historyLogModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from 'mongoose' 2 | 3 | export interface IHistory extends Document { 4 | message: string; 5 | postDate: string; 6 | ARN: string; 7 | } 8 | 9 | 10 | const formattedDate = () => { 11 | const currentTimestamp = Date.now() 12 | const currentDate = new Date(currentTimestamp) 13 | 14 | const year = currentDate.getFullYear() 15 | const month = currentDate.getMonth() + 1 16 | const day = currentDate.getDate() 17 | const hours = currentDate.getHours() 18 | const minutes = currentDate.getMinutes() 19 | const seconds = currentDate.getSeconds() 20 | return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}` 21 | } 22 | //create mongoose Schema of user 23 | //used to hold username and passwords for login 24 | //ARN will be used to connect to their AWS account 25 | const HistoryLogSchema: Schema = new Schema({ 26 | message: {type: String, required: true}, 27 | ARN: {type: String, required: true}, 28 | postDate: {type: String, default: formattedDate} 29 | }); 30 | 31 | 32 | //Export user schedma 33 | const HistoryLog: Model = mongoose.model('HistoryLog', HistoryLogSchema); 34 | export default HistoryLog; 35 | -------------------------------------------------------------------------------- /server/models/js/notificationModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const currentTimestamp = Date.now() 5 | const currentDate = new Date(currentTimestamp) 6 | 7 | const year = currentDate.getFullYear() 8 | const month = currentDate.getMonth() + 1 9 | const day = currentDate.getDate() 10 | const hours = currentDate.getHours() 11 | const minutes = currentDate.getMinutes() 12 | const seconds = currentDate.getSeconds() 13 | 14 | const formattedDate = `${year}/${month}/${day} ${hours}:${minutes}:${seconds}` 15 | 16 | //create mongoose Schema of user 17 | //used to hold username and passwords for login 18 | //ARN will be used to connect to their AWS account 19 | const ErrorMessage = new Schema({ 20 | message: {type: String, required: true}, 21 | postDate: {type: String, default: formattedDate} 22 | //user: {type: String, required: true} 23 | }); 24 | 25 | 26 | //Export user schedma 27 | module.exports = mongoose.model('ErrorMessage', ErrorMessage); -------------------------------------------------------------------------------- /server/models/js/userModels.js: -------------------------------------------------------------------------------- 1 | // const mongoose = require('mongoose'); 2 | // const Schema = mongoose.Schema; 3 | import { Schema, InferSchemaType } from 'mongoose'; 4 | 5 | 6 | 7 | 8 | //create mongoose Schema of user 9 | //used to hold username and passwords for login 10 | //ARN will be used to connect to their AWS account 11 | const User = new Schema({ 12 | username: {type: String, required: true, unique: true}, 13 | password: {type: String, required: true}, 14 | ARN: {type: String, required: true} 15 | }); 16 | 17 | 18 | //Export user schedma 19 | module.exports = mongoose.model('User', User); -------------------------------------------------------------------------------- /server/models/notificationModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from 'mongoose' 2 | 3 | export interface IError extends Document { 4 | message: string; 5 | postDate: string; 6 | ARN: string; 7 | } 8 | 9 | 10 | const formattedDate = () => { 11 | const currentTimestamp = Date.now() 12 | const currentDate = new Date(currentTimestamp) 13 | 14 | const year = currentDate.getFullYear() 15 | const month = currentDate.getMonth() + 1 16 | const day = currentDate.getDate() 17 | const hours = currentDate.getHours() 18 | const minutes = currentDate.getMinutes() 19 | const seconds = currentDate.getSeconds() 20 | return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}` 21 | } 22 | 23 | //create mongoose Schema of user 24 | //used to hold username and passwords for login 25 | //ARN will be used to connect to their AWS account 26 | const ErrorMessageSchema: Schema = new Schema({ 27 | message: {type: String, required: true}, 28 | ARN: {type: String, required: true}, 29 | postDate: {type: String, default: formattedDate} 30 | }); 31 | 32 | 33 | //Export user schedma 34 | const ErrorMessage: Model = mongoose.model('ErrorMessage', ErrorMessageSchema); 35 | export default ErrorMessage; 36 | -------------------------------------------------------------------------------- /server/models/userModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document, Model } from 'mongoose' 2 | // const mongoose = require('mongoose'); 3 | 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | 7 | export interface IUser extends Document { 8 | username: string; 9 | password: string; 10 | ARN: string; 11 | region: string; 12 | } 13 | 14 | const UserSchema: Schema = new Schema ({ 15 | username: {type: String, required: true, unique: true}, 16 | password: {type: String, required: true}, 17 | ARN: {type: String, required: true}, 18 | region: {type: String, required: true} 19 | }) 20 | 21 | const User: Model = mongoose.model('User', UserSchema); 22 | export default User; -------------------------------------------------------------------------------- /server/routes/functionRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Request, Response } from 'express'; 3 | import functionController from '../controllers/functionController'; 4 | 5 | const router = express.Router(); 6 | 7 | // returns list of all functions 8 | router.get("/list", 9 | functionController.assumeRole, 10 | functionController.getFunction, 11 | (req: Request, res: Response) => { 12 | res.status(200).json(res.locals.functions) 13 | }) 14 | 15 | // returns list of layers associated with specific function 16 | router.post('/layers', functionController.getLayers, (req: Request, res: Response) => { 17 | res.status(200).json(res.locals.layers); 18 | }) 19 | 20 | // // removes layer from function 21 | // // functionality removed for now. layer-function interactions will take place through Layers tab and /layers routes 22 | router.post('/remove', functionController.removeLayer, (req: Request, res: Response) => { 23 | res.status(200).json(res.locals.successful); 24 | }) 25 | 26 | 27 | // removes all layers from function 28 | router.post('/removeAll', functionController.removeAllLayers, (req: Request, res: Response) => { 29 | res.sendStatus(200); 30 | }); 31 | 32 | 33 | 34 | export default router; -------------------------------------------------------------------------------- /server/routes/js/functionRouters.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const functionController = require('../controllers/functionController.ts').default; 4 | const layerController = require('../controllers/layerController.ts').default; 5 | 6 | const router = express.Router(); 7 | 8 | // returns list of all functions 9 | router.get("/list", 10 | functionController.assumeRole, 11 | functionController.getFunction, 12 | (req, res) => { 13 | res.status(200).json(res.locals.functions) 14 | }) 15 | 16 | // returns list of layers associated with specific layer 17 | router.post('/layers', functionController.getLayers, (req, res) => { 18 | res.status(200).json(res.locals.layers); 19 | }) 20 | 21 | // removes layer from functoin 22 | router.post('/remove', functionController.removeLayer, (req, res) => { 23 | res.sendStatus(200); 24 | }) 25 | 26 | router.post('/add', (req, res) => { 27 | res.sendStatus(200); 28 | }); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /server/routes/js/layerRouters.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const layerController = require('../controllers/layerController.ts').default; 4 | const testController = require('../controllers/testController.ts').default; 5 | 6 | const router = express.Router(); 7 | 8 | // lists all layers and versions 9 | router.get( 10 | '/list', 11 | layerController.assumeRole, 12 | layerController.getLayer, 13 | layerController.getVersions, 14 | (req, res) => { 15 | res.status(200).json(res.locals.layersWithVersions); 16 | } 17 | ); 18 | 19 | // removes function from layer 20 | router.post('/remove', layerController.removeFunction, (req, res) => { 21 | res.sendStatus(200); 22 | }); 23 | 24 | // tests and adds compatible layer 25 | router.post( 26 | '/add', 27 | testController.assumeRole, 28 | testController.testRuntime, 29 | testController.getTest, 30 | layerController.addFunction, 31 | testController.testDependencies, 32 | testController.removeFailedFunc, 33 | (req, res) => { 34 | if (res.locals.addError.length) { 35 | res.status(409).json(res.locals.addError); 36 | } else { 37 | res.sendStatus(200); 38 | } 39 | } 40 | ); 41 | 42 | // lists all functions associated with specifc layer 43 | router.post('/functions', layerController.getFunctions, (req, res) => { 44 | res.status(200).json(res.locals.functionArray); 45 | }); 46 | 47 | module.exports = router; 48 | -------------------------------------------------------------------------------- /server/routes/js/userRouters.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const userController = require('../controllers/userController.ts').default; 4 | 5 | const router = express.Router(); 6 | console.log(userController); 7 | // creates account and jwt token 8 | router.post( 9 | '/signup', 10 | userController.createUser, 11 | userController.createToken, 12 | (req, res) => { 13 | res.sendStatus(200); 14 | } 15 | ); 16 | 17 | // logs in and creates jwt token 18 | router.post( 19 | '/login', 20 | userController.verifyUser, 21 | userController.createToken, 22 | (req, res) => { 23 | res.sendStatus(200); 24 | } 25 | ); 26 | 27 | // logs out and removes jwt token 28 | router.delete('/logout', userController.deleteToken, (req, res) => { 29 | res.sendStatus(200); 30 | }); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /server/routes/layerRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Request, Response } from 'express'; 3 | import layerController from '../controllers/layerController'; 4 | import testController from '../controllers/testController'; 5 | 6 | const router = express.Router(); 7 | 8 | // lists all layers and versions 9 | router.get( 10 | '/list', 11 | layerController.assumeRole, 12 | layerController.getLayer, 13 | layerController.getVersions, 14 | (req: Request, res: Response) => { 15 | // console.log(res.locals.layersWithVersions); 16 | res.status(200).json(res.locals.layersWithVersions); 17 | } 18 | ); 19 | 20 | // removes function from layer 21 | router.post('/remove', layerController.removeFunction, (req: Request, res: Response) => { 22 | res.status(200).json({message: 'Successfully Removed!'}); 23 | }); 24 | 25 | // tests and adds compatible layer 26 | router.post( 27 | '/add', 28 | testController.assumeRole, 29 | testController.testRuntime, 30 | testController.getTest, 31 | layerController.addFunction, 32 | testController.testDependencies, 33 | testController.removeFailedFunc, 34 | (req: Request, res: Response) => { 35 | if (res.locals.addError.length) { 36 | res.status(409).json(res.locals.addError); 37 | } else { 38 | res.status(200).json({message: 'Successfully added'}); 39 | } 40 | } 41 | ); 42 | 43 | // lists all functions associated with specifc layer 44 | router.post('/functions', layerController.getFunctions, (req: Request, res: Response) => { 45 | res.status(200).json(res.locals.functionArray); 46 | }); 47 | 48 | export default router; 49 | -------------------------------------------------------------------------------- /server/routes/userRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Request, Response } from 'express'; 3 | import userController from '../controllers/userController'; 4 | 5 | const router = express.Router(); 6 | 7 | // creates account and jwt token 8 | router.post( 9 | '/signup', 10 | userController.createUser, 11 | userController.createToken, 12 | (req: Request, res: Response) => { 13 | res.status(200).json({message: 'Successfully signed up'}) 14 | } 15 | ); 16 | 17 | // logs in and creates jwt token 18 | router.post( 19 | '/login', 20 | userController.verifyUser, 21 | userController.createToken, 22 | (req: Request, res: Response) => { 23 | res.status(200).json({message: 'Successfully signed up'}); 24 | } 25 | ); 26 | 27 | // logs out and removes jwt token 28 | router.delete('/logout', 29 | userController.deleteToken, 30 | (req: Request, res: Response) => { 31 | res.sendStatus(200); 32 | }); 33 | 34 | router.get('/notifications', userController.getNotifications, (req: Request, res: Response) => { 35 | res.status(200).send(res.locals.notificationLog); 36 | }) 37 | 38 | router.get('/historylog', userController.getHistoryLog, (req: Request, res: Response) => { 39 | res.status(200).send(res.locals.historyLog); 40 | }) 41 | 42 | router.patch('/changeinfo', userController.changeInfo, (req: Request, res: Response) => { 43 | res.status(200); 44 | }) 45 | 46 | export default router; 47 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | import express, { Request, Response, NextFunction } from 'express'; 3 | //const layerRouter = require('./routes/layerRouter'); 4 | import layerRouter from './routes/layerRouter'; 5 | //const functionRouter = require('./routes/functionRouter'); 6 | import functionRouter from './routes/functionRouter'; 7 | //const userRouter = require('./routes/userRouter'); 8 | import userRouter from './routes/userRouter'; 9 | //const connectDB = require('./db'); 10 | import connectDB from './db'; 11 | import path from 'path'; 12 | 13 | // const cookieParser = require('cookie-parser'); 14 | import cookieParser from 'cookie-parser'; 15 | 16 | // 17 | connectDB(); 18 | // Initialize Express 19 | const app = express(); 20 | const PORT = process.env.PORT || 3000; 21 | 22 | // CORS 23 | const cors = require('cors'); 24 | app.use(cors({ origin: 'https://lambda-peeler.onrender.com/', methods: ["POST", "GET"], credentials: true })); 25 | // app.use(cors()); 26 | 27 | 28 | // // app.use(express.static('assets')); 29 | app.use(express.static(path.join(__dirname, '../dist'))); 30 | // app.use('/assets', express.static(path.join(__dirname, '../public/assets'))); 31 | 32 | app.use(cookieParser()); 33 | app.use(express.json()); // for parsing application/json 34 | app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 35 | app.use('/', express.static(path.join(__dirname, '/index.html'))); 36 | 37 | 38 | 39 | app.use('/api/layers', layerRouter); 40 | app.use('/api/functions', functionRouter); 41 | app.use('/api/user', userRouter); 42 | 43 | //global error handler 44 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 45 | const defaultErr = { 46 | //detailed message to dev 47 | log: 48 | `Express error handler caught unknown middleware error. Error: ${err}`, 49 | status: 400, 50 | //basic message to user 51 | message: { err: 'An error occurred' }, 52 | }; 53 | const errorObj = Object.assign({}, defaultErr, err); 54 | //send error message to frontend 55 | return res.status(errorObj.status).json(errorObj.message); 56 | }); 57 | 58 | // Start Express Server 59 | app.listen(PORT, () => { 60 | console.log(`Server is running on http://localhost:${PORT}`); 61 | }); 62 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; 3 | import Main from './components/MainDisplay.jsx'; 4 | import Login from './components/Login.jsx'; 5 | import Splash from './components/Splash.jsx'; 6 | import NotificationContainer from './containers/NotificationContainer.jsx'; 7 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 8 | import { blue, red } from '@mui/material/colors'; 9 | // theme object created as an input for MUI 10 | const theme = createTheme({ 11 | palette: { 12 | primary: { 13 | main: '#fad0a0', // beige 14 | dark: '#000000', 15 | light: '#ffeeda', 16 | }, 17 | secondary: { 18 | main: '#3576ba', // blue 19 | }, 20 | tertiary: { 21 | main: '#808080', 22 | }, 23 | }, 24 | }); 25 | 26 | const App = () => { 27 | //set state of loggin status - defaults to false 28 | const [isLoggedIn, setIsLoggedIn] = useState(false); 29 | // conditional rendering for login page or main component 30 | return ( 31 | 32 | 33 | 34 | } /> 35 | 40 | ) : ( 41 | 42 | ) 43 | } 44 | /> 45 | 50 | ) : ( 51 | 52 | ) 53 | } 54 | /> 55 | } /> 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default App; 63 | -------------------------------------------------------------------------------- /src/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './styles.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/Display.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import axios from 'axios'; 3 | import { useState, useEffect } from 'react'; 4 | import LayersContainer from '../containers/LayersContainer.jsx'; 5 | import FunctionsContainer from '../containers/FunctionsContainer.jsx'; 6 | import NotificationContainer from '../containers/NotificationContainer.jsx'; 7 | import HistoryContainer from '../containers/HistoryContainer.jsx'; 8 | import { Button, ToggleButton, ToggleButtonGroup } from '@mui/material'; 9 | import { useTheme } from '@mui/material/styles'; 10 | import { Routes, Route, useNavigate } from 'react-router-dom'; 11 | import Settings from './Settings.jsx'; 12 | 13 | const Display = ({ setActiveTab, activeTab }) => { 14 | //initialize state for Layers and Functions 15 | //active tab state determines if list of Layers or list of Functions is displayed 16 | const [layers, setLayers] = useState([]); 17 | const [functions, setFunctions] = useState([]); 18 | // const [activeTab, setActiveTab] = useState('Notifications'); 19 | const [displayPage, setDisplayPage] = useState(); 20 | const theme = useTheme(); 21 | 22 | // When page first renders, updates layers state and functions state 23 | useEffect(() => { 24 | //get to layerRouter.js 25 | axios 26 | .get('https://lambda-peeler.onrender.com/api/layers/list', { 27 | withCredentials: true, 28 | }) 29 | .then((response) => { 30 | //update the Layers state with data from get request 31 | setLayers(response.data); 32 | }) 33 | .catch((err) => { 34 | console.log('Error:', err); 35 | }); 36 | 37 | //get to functionRouter.js 38 | axios 39 | .get('https://lambda-peeler.onrender.com/api/functions/list', { 40 | withCredentials: true, 41 | }) 42 | .then((response) => { 43 | //update the Functions state with data from get request 44 | setFunctions(response.data.Functions); 45 | }) 46 | .catch((err) => { 47 | console.log('Error:', err); 48 | }); 49 | }, []); 50 | 51 | return ( 52 |
53 | {(activeTab === 'Layers' || activeTab === 'Functions') && ( 54 | 60 | setActiveTab('Layers')} 64 | sx={{ 65 | width: '8em', 66 | backgroundColor: 67 | activeTab === 'Layers' ? theme.palette.primary : 'inherit', 68 | }} 69 | > 70 | Layers 71 | 72 | setActiveTab('Functions')} 76 | sx={{ 77 | width: '8em', 78 | backgroundColor: 79 | activeTab === 'Functions' ? theme.palette.primary : 'inherit', 80 | }} 81 | > 82 | Functions 83 | 84 | 85 | )} 86 | {/* Send data to LayersContainer or FunctionsContainer depending which button was clicked */} 87 | {activeTab === 'Layers' && ( 88 |
89 | {/* Pass Layers and Function data from get requests to LayersContainer component. 'function' variable names creates errors, so lambda used in place */} 90 | {} 91 |
92 | )} 93 | {activeTab === 'Functions' && ( 94 |
95 | {/* Pass Layers and Function data from get requests to LayersContainer component. 'function' variable names creates errors, so lambda used in place */} 96 | {} 97 |
98 | )} 99 | {(activeTab === 'Notifications' || activeTab === 'History') && ( 100 | 106 | setActiveTab('Notifications')} 110 | sx={{ 111 | width: '10em', 112 | backgroundColor: 113 | activeTab === 'Notifications' 114 | ? theme.palette.primary 115 | : 'inherit', 116 | }} 117 | > 118 | Error Log 119 | 120 | setActiveTab('History')} 124 | sx={{ 125 | width: '10em', 126 | backgroundColor: 127 | activeTab === 'History' ? theme.palette.primary : 'inherit', 128 | }} 129 | > 130 | Success Log 131 | 132 | 133 | )} 134 | {activeTab === 'Notifications' && ( 135 |
{}
136 | )} 137 | {activeTab === 'History' && ( 138 |
{}
139 | )} 140 | {activeTab === 'Settings' &&
{}
} 141 |
142 | ); 143 | }; 144 | 145 | export default Display; 146 | -------------------------------------------------------------------------------- /src/components/Function.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import axios from 'axios'; 4 | import LinkedLayers from './LinkedLayers'; 5 | import LayerModal from './LayersModal'; 6 | import CircularProgress from '@mui/material/CircularProgress'; 7 | import { Button, IconButton, Tooltip, Box } from '@mui/material'; 8 | import LibraryAddIcon from '@mui/icons-material/LibraryAdd'; 9 | //CircularProgress, Button, IconButton, Tooltip, Box, LibraryAddIcon all used for styling 10 | 11 | /*functionName(string), 12 | ARN(string) of Function ARN, 13 | functionLayersArn:(array) of Layer ARNs on functions 14 | layers(Array or objects) layer data from get request in Display.js 15 | */ 16 | const Function = ({ functionName, ARN, functionLayersARN, layers }) => { 17 | // isCollapsed is tracked for each displayed Layer. true (default) means the Layer display is collapsed, false means the Layer box has expanded 18 | const [isCollapsed, setIsCollapsed] = useState(true); 19 | // associatedLayers is the state variable for tracking which layers are connected to a given Function. 20 | // it is an array of objects in this format: 21 | // { 22 | // LayerName: layer.name, 23 | // LayerVersion: layer.versions[index], 24 | // LayerArn: layerARN 25 | // } 26 | const [associatedLayers, setAssociatedLayers] = useState([]); 27 | // isOpened is for the FunctionModal for each displayed Layer. true means the modal is opened, false (default) means the modal is not opened 28 | const [isOpened, setIsOpened] = useState(false); 29 | // isLoading is the state variable used to control the CircularProgress loading doodle 30 | const [isLoading, setIsLoading] = useState(false); 31 | 32 | // post request to fetch layers that are associated with a specific function 33 | const fetchAssociatedLayers = async () => { 34 | axios 35 | //post request to functionRouter.js 36 | .post( 37 | 'http://localhost:3000/api/functions/layers', 38 | //pass Function ARN and Layers Array to backend 39 | { ARN: ARN, layers: layers }, 40 | { 41 | withCredentials: true, 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | } 46 | ) 47 | .then((response) => { 48 | // wait for response to update assocatedLayers state with the response.data 49 | //Response.data is an array of layer objects. Each object contains specific layer information 50 | console.log('response.data000:', response.data); 51 | setAssociatedLayers(response.data); 52 | }) 53 | .catch((err) => { 54 | console.log('Error:', err); 55 | }); 56 | }; 57 | 58 | // rerender layers list whenever the state changes 59 | // isCollapsed/isOpened with be changed on button clicks 60 | useEffect(() => { 61 | if (!isCollapsed) { 62 | //get array of layer objects 63 | fetchAssociatedLayers(); 64 | } 65 | }, [isCollapsed, isOpened]); 66 | 67 | // opens and closes modal 68 | const openModal = () => { 69 | setIsOpened(true); 70 | }; 71 | 72 | const closeModal = () => { 73 | setIsOpened(false); 74 | }; 75 | 76 | // post request to link function and layer 77 | const linkLayers = async (event) => { 78 | //start the loading animation by changing isLoading state 79 | setIsLoading(true); 80 | event.preventDefault(); 81 | // pull form data and put into arr 82 | const formResponse = new FormData(event.target); 83 | const arrayOfCheckedLayers = []; 84 | //Take keys from fromResponse and push into arrayOfCheckedLayers 85 | //arrayOfCheckedLayers will be sent to backend 86 | for (const key of formResponse.keys()) { 87 | arrayOfCheckedLayers.push(key); 88 | } 89 | try { 90 | //functionality not yet set up 91 | const result = await axios.post( 92 | 'https://lambda-peeler.onrender.com/api/functions/add', 93 | { 94 | ARN: ARN, 95 | layerArray: arrayOfCheckedLayers, 96 | FunctionName: functionName, 97 | }, 98 | { 99 | withCredentials: true, 100 | headers: { 101 | 'Content-Type': 'application/json', 102 | }, 103 | } 104 | ); 105 | //update loading states for animation - when post request is receieved end the animation 106 | setIsLoading(false); 107 | setIsOpened(false); 108 | return; 109 | } catch (error) { 110 | //update loading states for animation - if post fails end animation 111 | console.log('error: ', error); 112 | setIsLoading(false); 113 | setIsOpened(false); 114 | 115 | // Pop up to alert user of errors 116 | const messages = []; 117 | if (typeof error.response.data === 'string') { 118 | alert(error.response.data); 119 | } else { 120 | const errorArr = error.response.data; 121 | errorArr.forEach((message) => { 122 | console.log('error message: ', message); 123 | alert(message); 124 | }); 125 | } 126 | } 127 | }; 128 | 129 | return ( 130 |
131 | {/* make button to open/close layer information */} 132 | 144 | {/* When button is clicked isCollapsed state changes - 145 | display layer information in dropdown */} 146 | {!isCollapsed && ( 147 |
148 |

Layers

149 | {isLoading && ( 150 |
157 | {/* loading animation */} 158 | 165 |
166 | )} 167 | {/* iterate through associatedLayers array */} 168 | {associatedLayers.map((element) => ( 169 |
170 | {/* Pass data to LinkedLayers to display details */} 171 | 181 |
182 | ))} 183 | {/* Renders a Box component from MUI that will contain the "add function" function*/} 184 | 189 | 190 | {/* When add function on the layer tab is clicked, a modal of all functions will pop up*/} 191 | 198 |
199 | )} 200 |
201 | ); 202 | }; 203 | 204 | export default Function; 205 | -------------------------------------------------------------------------------- /src/components/FunctionModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import { 4 | Modal, 5 | Box, 6 | Button, 7 | Checkbox, 8 | FormControlLabel, 9 | Typography, 10 | } from '@mui/material'; 11 | import CircularProgress from '@mui/material/CircularProgress'; 12 | import LinearProgress from '@mui/material/LinearProgress'; 13 | import axios from 'axios'; 14 | import { useTheme } from "@mui/material/styles"; 15 | 16 | // modal that pops up when the + (add functions) button is clicked on a specific layer in the layers tab. 17 | // contains checkboxes for all functions 18 | const FunctionModal = ({ 19 | open, 20 | closeFunction, 21 | functions, 22 | onSubmit, 23 | isLoading, 24 | }) => { 25 | // used for the loading bar 26 | const [progress, setProgress] = useState(0); 27 | // used for MUI styling 28 | const theme = useTheme(); 29 | 30 | useEffect(() => { 31 | // if loading, increment progress bar by 1 to visualize loading progress 32 | if (isLoading) { 33 | const interval = setInterval(() => { 34 | setProgress((prevProgress) => { 35 | if (prevProgress >= 100) { 36 | clearInterval(interval); 37 | return 100; 38 | } 39 | return prevProgress + 1.1; 40 | }); 41 | }, 100); 42 | } 43 | setProgress(0); 44 | }, [isLoading]); 45 | 46 | return ( 47 |
48 | 49 | 70 | {/* renders the function names with checkboxes. onSubmit is defined in Layer.jsx */} 71 |
72 | {functions.map((func) => ( 73 | } 76 | label={func.FunctionName} 77 | /> 78 | ))} 79 |
90 | {/* Link button (which submits the form). onSubmit is defined in Layer.jsx */} 91 | 96 | {/* Close button (closes the modal). closeFunction is defined in Layer.jsx */} 97 | 100 |
101 | 102 | {/* renders the loading bar after submission*/} 103 | {isLoading && ( 104 |
117 | 118 | Testing Compatibility 119 | 120 | 121 | 122 | 123 |
124 | )} 125 |
126 |
127 |
128 | ); 129 | }; 130 | 131 | export default FunctionModal; 132 | -------------------------------------------------------------------------------- /src/components/HistoryLog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HistoryLog = ( {historyLogMessage, historyLogDate}) => { 4 | 5 | 6 | 7 | return ( 8 |
9 |
10 |
    11 |
  • {`Event: ${historyLogMessage}`} 12 |
    13 | {`Date: ${historyLogDate}`}
  • 14 |
15 |
16 |
17 | ) 18 | } 19 | 20 | export default HistoryLog; -------------------------------------------------------------------------------- /src/components/Layer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import LinkedFunctions from './LinkedFunctions.jsx'; 4 | import FunctionModal from './FunctionModal.jsx'; 5 | import axios from 'axios'; 6 | import CircularProgress from '@mui/material/CircularProgress'; 7 | import { Button, IconButton, Tooltip, Box, Skeleton } from '@mui/material'; 8 | import LibraryAddIcon from '@mui/icons-material/LibraryAdd'; 9 | 10 | const Layer = ({ layerName, versionNumber, ARN, functions }) => { 11 | // isCollapsed is tracked for each displayed Layer. true (default) means the Layer display is collapsed, false means the Layer box has expanded 12 | const [isCollapsed, setIsCollapsed] = useState(true); 13 | // associatedFunctions keeps track of which functions are linked to a layer 14 | const [associatedFunctions, setAssociatedFunctions] = useState([]); 15 | // isOpened is for the FunctionModal for each displayed Layer. true means the modal is opened, false (default) means the modal is not opened 16 | const [isOpened, setIsOpened] = useState(false); 17 | const [isLoading, setIsLoading] = useState(false); 18 | 19 | // invoked when the state of isCollapsed or isOpened changes 20 | // post requeset to grab associated functions 21 | const fetchAssociatedFunctions = async () => { 22 | axios 23 | .post( 24 | 'https://lambda-peeler.onrender.com/api/layers/functions', 25 | { ARN: ARN }, 26 | { 27 | withCredentials: true, 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | } 32 | ) 33 | .then((response) => { 34 | setAssociatedFunctions(response.data); 35 | }) 36 | .catch((err) => { 37 | console.log('Error:', err); 38 | }); 39 | }; 40 | 41 | // this useEffect will be invoked whenever you click on a layer component 42 | //or when the pop up for the function is opened or closed 43 | useEffect(() => { 44 | // if the layer component is expanded, invoke this function 45 | if (!isCollapsed) { 46 | fetchAssociatedFunctions(); 47 | } 48 | }, [isCollapsed, isOpened]); 49 | 50 | // changes the state for when function Modal opens and closes 51 | // openModal is passed down to the add function button 52 | const openModal = () => { 53 | setIsOpened(true); 54 | }; 55 | // closeModal is passed down to the Function Modal component 56 | const closeModal = () => { 57 | setIsOpened(false); 58 | }; 59 | 60 | // function to get the array of function names which have been checked in the FunctionModal for a given Layer 61 | // passed down to FunctionModal 62 | const linkFunction = async (event) => { 63 | setIsLoading(true); 64 | event.preventDefault(); 65 | // pull form data and store in array to be sent to server 66 | const formResponse = new FormData(event.target); 67 | const arrayOfCheckedFunctions = []; 68 | for (const func of formResponse.keys()) { 69 | arrayOfCheckedFunctions.push(func); 70 | } 71 | try { 72 | const result = await axios.post( 73 | 'https://lambda-peeler.onrender.com/api/layers/add', 74 | { 75 | ARN: ARN, 76 | functionArray: arrayOfCheckedFunctions, 77 | layerName: layerName, 78 | }, 79 | { 80 | withCredentials: true, 81 | headers: { 82 | 'Content-Type': 'application/json', 83 | }, 84 | } 85 | ); 86 | setIsLoading(false); 87 | setIsOpened(false); 88 | return; 89 | } catch (error) { 90 | setIsLoading(false); 91 | setIsOpened(false); 92 | 93 | // alert user of any errors 94 | const messages = []; 95 | if (typeof error.response.data === 'string') { 96 | alert(error.response.data); 97 | } else { 98 | const errorArr = error.response.data; 99 | errorArr.forEach((message) => { 100 | console.log('error message: ', message); 101 | alert(message); 102 | }); 103 | } 104 | } 105 | }; 106 | 107 | return ( 108 |
109 | {/* Layer component renders a button that has an onClick, layer name, ver, ARN */} 110 | 123 | {/* if isCollapsed is false, show a div of functions*/} 124 | {!isCollapsed && ( 125 |
126 |

Functions

127 | {/* if isLoading is true, show the circule progress component from MUI*/} 128 | {isLoading && ( 129 |
136 | 143 |
144 | )} 145 | {/* Takes the functions from the state associatedFunctions and create seperate components called LinkedFunctions*/} 146 | {associatedFunctions.map((element, index) => ( 147 |
148 | 157 |
158 | ))} 159 | {/* Renders a Box component from MUI that will contain the "add function" function*/} 160 | 165 | 166 | openModal()}> 167 | 168 | 169 | 170 | 171 | {/* When add function on the layer tab is clicked, a modal of all functions will pop up*/} 172 | 179 |
180 | )} 181 |
182 | ); 183 | }; 184 | 185 | export default Layer; 186 | -------------------------------------------------------------------------------- /src/components/LayersModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import { Modal, Box, Button, Checkbox, FormControlLabel, Typography } from '@mui/material'; 4 | import CircularProgress from '@mui/material/CircularProgress'; 5 | import LinearProgress from '@mui/material/LinearProgress'; 6 | import axios from 'axios'; 7 | import { useTheme } from "@mui/material/styles"; 8 | 9 | const LayersModal = ({ 10 | open, closeFunction, isLoading, layers, onSubmit 11 | }) => { 12 | 13 | 14 | // used for the loading bar 15 | const [progress, setProgress] = useState(0); 16 | // used for MUI styling 17 | const theme = useTheme(); 18 | 19 | useEffect(() => { 20 | // if loading, increment progress bar by 1 to visualize loading progress 21 | if (isLoading) { 22 | const interval = setInterval(() => { 23 | setProgress((prevProgress) => { 24 | if (prevProgress >= 100) { 25 | clearInterval(interval); 26 | return 100; 27 | } 28 | return prevProgress + 1.1; 29 | }); 30 | }, 100); 31 | } 32 | setProgress(0); 33 | }, [isLoading]); 34 | 35 | 36 | return( 37 |
38 | 39 | 60 | {/* renders the layer names with checkboxes */} 61 |
62 | {layers.map((layer) => ( 63 | } 66 | label={layer.name} 67 | /> 68 | ))} 69 |
80 | 85 | 88 |
89 | 90 | {/* renders the loading bar after submission*/} 91 | {isLoading && ( 92 |
105 | 106 | Testing Compatibility 107 | 108 | 109 | 110 | 111 |
112 | )} 113 |
114 |
115 |
116 | ) 117 | 118 | } 119 | 120 | export default LayersModal; -------------------------------------------------------------------------------- /src/components/LinkedFunctions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import axios from 'axios'; 4 | import CircularProgress from '@mui/material/CircularProgress'; 5 | import { Button, IconButton, Tooltip } from '@mui/material'; 6 | import { Delete, LayersClearSharp } from '@mui/icons-material' 7 | 8 | // This is rendered when a layer component is clicked 9 | // These are the functions that are linked to a particuler layer 10 | const LinkedFunctions = ({ 11 | functionName, 12 | ARN, 13 | fetch, 14 | setIsLoading, 15 | layerName 16 | }) => { 17 | // post req to remove a function from layer 18 | // sends arn and name back to find the specific func 19 | // invoked by clicking the 'x' button 20 | const removeFunction = async () => { 21 | setIsLoading(true); 22 | try { 23 | const result = await axios.post( 24 | 'https://lambda-peeler.onrender.com/api/layers/remove', 25 | { ARN: ARN, functionName: functionName, layerName: layerName }, 26 | { 27 | withCredentials: true, 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | } 32 | ); 33 | setIsLoading(false); 34 | // refetch list of funcs 35 | fetch(); 36 | return; 37 | } catch (err) { 38 | setIsLoading(false); 39 | console.log(err) 40 | } 41 | }; 42 | 43 | return ( 44 |
45 |
46 |
    47 |
  • {functionName}
  • 48 |
49 |
50 | 51 | removeFunction()}> 52 | 53 | 54 | 55 |
56 | ); 57 | }; 58 | 59 | export default LinkedFunctions; 60 | -------------------------------------------------------------------------------- /src/components/LinkedLayers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import axios from 'axios'; 4 | import CircularProgress from '@mui/material/CircularProgress'; 5 | import {Button, Tooltip, IconButton } from '@mui/material'; 6 | import { LayersClearSharp } from '@mui/icons-material' 7 | 8 | // each layer linked to a particular function 9 | const LinkedLayers = ({ 10 | fetch, 11 | layerName, 12 | layerVersion, 13 | layerArn, 14 | setIsLoading, 15 | functionName 16 | }) => { 17 | 18 | const removeLayer = async () => { 19 | setIsLoading(true); 20 | try { 21 | const result = await axios.post( 22 | 'https://lambda-peeler.onrender.com/api/functions/remove', 23 | { ARN: layerArn, LayerName: layerName, layerVersion: layerVersion, functionName: functionName }, 24 | { 25 | withCredentials: true, 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | } 30 | ); 31 | setIsLoading(false); 32 | // refetch list of layers 33 | fetch(); 34 | return; 35 | } catch (err) { 36 | setIsLoading(false); 37 | console.log(err) 38 | } 39 | }; 40 | 41 | return( 42 |
43 |
44 |
    45 |
  • Layer: {layerName}, Ver: {layerVersion} 46 |

    47 | ARN: {layerArn} 48 |
  • 49 |
50 |
51 | 52 | removeLayer()}> 53 | 54 | 55 | 56 |
57 | ) 58 | } 59 | 60 | export default LinkedLayers; -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | TextField, 4 | Box, 5 | Button, 6 | IconButton, 7 | AppBar, 8 | Toolbar, 9 | } from '@mui/material'; 10 | import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; 11 | import { useTheme } from '@mui/material/styles'; 12 | import axios from 'axios'; 13 | import { Link } from 'react-router-dom'; 14 | 15 | const Login = ({ setIsLoggedIn }) => { 16 | const [username, setUser] = useState(); 17 | const [password, setPassword] = useState(); 18 | const [ARN, setARN] = useState(); 19 | const [region, setRegion] = useState(); 20 | const [signUp, setSignUp] = useState(false); 21 | const [message, setMessage] = useState(''); 22 | const [action, setAction] = useState('Login'); 23 | const theme = useTheme(); 24 | 25 | const handleLogin = async (e) => { 26 | if (signUp) { 27 | handleSignUp(); 28 | return; 29 | } 30 | // signup functionality here 31 | try { 32 | const result = await axios.post( 33 | 'https://lambda-peeler.onrender.com/api/user/login', 34 | { username: username, password: password, ARN: ARN }, 35 | { 36 | withCredentials: true, 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | } 41 | ); 42 | if (result.status === 200) { 43 | setIsLoggedIn(true); 44 | return; 45 | } else { 46 | console.log('incorrect username or password'); 47 | setMessage('Incorrect username or password. Try again!'); 48 | } 49 | } catch (error) { 50 | setMessage('Incorrect username or password. Try again!'); 51 | console.log(message); 52 | console.log(error); 53 | } 54 | }; 55 | 56 | const handleSignUp = async (e) => { 57 | if (!signUp) { 58 | setSignUp(true); 59 | setAction('Sign Up'); 60 | return; 61 | } 62 | // signup functionality here 63 | try { 64 | const result = await axios.post( 65 | 'https://lambda-peeler.onrender.com/api/user/signup', 66 | { username: username, password: password, ARN: ARN, region: region }, 67 | { 68 | withCredentials: true, 69 | headers: { 70 | 'Content-Type': 'application/json', 71 | }, 72 | } 73 | ); 74 | if (result.status === 200) { 75 | setIsLoggedIn(true); 76 | return; 77 | } else { 78 | setMessage('Error signing up. Try again!'); 79 | } 80 | } catch (error) { 81 | console.log(error); 82 | } 83 | }; 84 | 85 | return ( 86 |
87 | 96 | 97 |
104 |
105 | Home 106 | Docs 107 | Contact 108 |
109 |
110 |
111 |
112 |
122 |
123 |

134 | {message} 135 |

136 |
137 | 138 |
139 | 157 | { 169 | setSignUp(false); 170 | setAction('Login'); 171 | }} 172 | > 173 | 174 | 175 |

{action}

176 | setUser(e.target.value)} 185 | /> 186 | setPassword(e.target.value)} 192 | /> 193 | {signUp && ( 194 | setARN(e.target.value)} 200 | /> 201 | )} 202 | {signUp && ( 203 | setRegion(e.target.value)} 209 | /> 210 | )} 211 |
212 | 227 | 236 | Need an account?  237 | handleSignUp(e)}> 238 | Sign Up 239 | 240 | 241 |
242 |
243 |
244 | ); 245 | }; 246 | 247 | export default Login; 248 | -------------------------------------------------------------------------------- /src/components/MainDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Navbar from './Navbar.jsx'; 3 | import Display from './Display.jsx'; 4 | import {useState} from 'react'; 5 | 6 | const Main = ({setLogin}) => { 7 | const [activeTab, setActiveTab] = useState('Layers'); 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Main; -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route, useNavigate, Link } from 'react-router-dom'; 3 | import { useState, useEffect } from 'react'; 4 | import Display from './Display'; 5 | import axios from 'axios'; 6 | import { Badge } from '@mui/material'; 7 | import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; 8 | import HistoryIcon from '@mui/icons-material/History'; 9 | import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; 10 | import LogoutIcon from '@mui/icons-material/Logout'; 11 | 12 | const Navbar = ({ setLogin, setActiveTab }) => { 13 | const [displayedPage, setDisplayedPage] = useState(''); 14 | 15 | const handleLogout = async () => { 16 | try { 17 | // '/logout' 18 | setLogin(false); 19 | await axios.delete('https://lambda-peeler.onrender.com/api/user/logout', { 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | withCredentials: true, 24 | }); 25 | } catch (err) { 26 | console.log('Error:', err); 27 | } 28 | }; 29 | 30 | return ( 31 | 73 | ); 74 | }; 75 | 76 | export default Navbar; 77 | -------------------------------------------------------------------------------- /src/components/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Routes, Route, useNavigate } from 'react-router-dom'; 3 | 4 | 5 | const Notification = ( {notificationName, notificationDate}) => { 6 | 7 | 8 | 9 | return ( 10 |
11 |
12 |
    13 |
  • {`Error Message: ${notificationName}`} 14 |
    15 | {`Date: ${notificationDate}`}
  • 16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export default Notification; -------------------------------------------------------------------------------- /src/components/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import { TextField, Box, Button } from '@mui/material'; 3 | import { useTheme } from "@mui/material/styles"; 4 | import axios from 'axios'; 5 | 6 | const Settings = () => { 7 | const [username, setUser] = useState(); 8 | const [password, setPassword] = useState(); 9 | const [ARN, setARN] = useState(); 10 | const [operation, setOperation] = useState(); 11 | const theme = useTheme(); 12 | 13 | const handleUpdate = async (operation, value) => { 14 | const input = {[operation]: value} 15 | try { 16 | await axios.patch('https://lambda-peeler.onrender.com/api/user/changeinfo', input, {withCredentials: true}); 17 | } catch(err) { 18 | console.log(err); 19 | } 20 | 21 | } 22 | 23 | return ( 24 |
25 | 43 |

Update Account Details

44 | {/*
*/} 45 | setUser(e.target.value)} 52 | /> 53 | setPassword(e.target.value)} 59 | sx={{width: '100%', alignSelf: 'center'}} 60 | /> 61 | setARN(e.target.value)} 67 | sx={{width: '100%', alignSelf: 'center'}} 68 | /> 69 | 80 | {/*
*/} 81 |
82 |
83 | ) 84 | } 85 | 86 | export default Settings; -------------------------------------------------------------------------------- /src/components/Splash.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AppBar, IconButton, Toolbar, ThemeProvider } from '@mui/material'; 3 | import EastIcon from '@mui/icons-material/East'; 4 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 5 | import AddLinkIcon from '@mui/icons-material/AddLink'; 6 | import LinkOffIcon from '@mui/icons-material/LinkOff'; 7 | import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; 8 | import GitHubIcon from '@mui/icons-material/GitHub'; 9 | import { Link as RouterLink } from 'react-router-dom'; 10 | import { useTheme } from '@mui/material/styles'; 11 | 12 | const Splash = () => { 13 | const theme = useTheme(); 14 | 15 | return ( 16 |
17 | 25 | 26 |
33 |
34 | Home 35 | Docs 36 | Contact 37 |
38 |
39 | Get Started 40 |
41 |
42 |
43 |
44 |
45 |
46 | 50 |

LambdaPeeler

51 |
52 |

53 | Lambda Peeler is a web-based dashboard tailored for AWS Lambda 54 | developers. It is meticulously designed to bridge the gap between 55 | managing Lambda functions and layers, simplifying AWS cloud 56 | operations. 57 |

58 | 62 | Learn more 63 | 64 |
65 |
66 |

Features

67 |
68 |
69 |

Connecting a function

70 | 71 |

72 | Effortlessly link multiple functions through our sleek, user 73 | interface. Behind the scenes, Lambda Peeler diligently conducts 74 | assessments, ensuring runtime and dependency compatibility. 75 |

76 |
77 | 78 |
79 |
80 |
81 |

Removing a function

82 | 83 |

84 | Seamlessly disconnect functions with just a single click. Our 85 | intuitive dashboard ensures swift and effortless layer management, 86 | streamlining your AWS Lambda experience. 87 |

88 |
89 | 90 |
91 |
92 |
93 |

Failing Compatability

94 | 95 |

96 | Our built-in compatibility testing feature cross-examines 97 | functions with layers, reducing the chance of introducing code 98 | breaking changes. 99 |

100 |
101 | 102 |
103 |
104 |
105 |

Meet the Team

106 |
107 |
108 | 112 | Nhat Trinh 113 | 121 |
122 |
123 | 127 | Greg Osborn 128 | 136 |
137 |
138 | 142 | Michael Shand 143 | 151 |
152 |
153 | 157 | Zach Hamilton 158 | 166 |
167 |
168 |
169 |
170 | ); 171 | }; 172 | 173 | export default Splash; 174 | -------------------------------------------------------------------------------- /src/containers/FunctionsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Function from '../components/Function.jsx'; 3 | 4 | const FunctionsContainer = ({ data, lambda }) => { 5 | return ( 6 | // map the array of functions to individual Function components 7 | // functionLayersARN is the Layers array for a specific function, coming from GetFunction/GetFunctionConfiguration AWS commands 8 | // layers is the array of all layers coming from Display 9 |
10 | {lambda.map((element) => ( 11 | 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | export default FunctionsContainer; 24 | -------------------------------------------------------------------------------- /src/containers/HistoryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Routes, Route, useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import { useState, useEffect } from 'react'; 5 | import HistoryLog from '../components/HistoryLog'; 6 | 7 | const HistoryContainer = () => { 8 | 9 | const [associatedHistoryLog, setAssociatedHistoryLog] = useState([]); 10 | 11 | const getHistoryLog = async () => { 12 | try{ 13 | const HistoryLog = await axios.get('https://lambda-peeler.onrender.com/api/user/historylog', { 14 | withCredentials: true, 15 | }) 16 | const flippedHistoryLog = []; 17 | const historyLogArray = HistoryLog.data; 18 | for(let i = historyLogArray.length-1; i > 0; i--){ 19 | flippedHistoryLog.push(historyLogArray[i]); 20 | } 21 | setAssociatedHistoryLog(flippedHistoryLog); 22 | 23 | return; 24 | } catch(err){ 25 | console.log('Error trying to get History log'); 26 | } 27 | } 28 | 29 | useEffect(() => { 30 | getHistoryLog(); 31 | }, []) 32 | 33 | 34 | return ( 35 |
36 | {associatedHistoryLog.map((element, index) => ( 37 | 42 | 43 | ))} 44 |
45 | ) 46 | } 47 | 48 | export default HistoryContainer; -------------------------------------------------------------------------------- /src/containers/LayersContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layer from '../components/Layer.jsx'; 3 | 4 | const LayersContainer = ({ data, lambda }) => { 5 | return ( 6 | // map the array of layers to individual Layer components 7 | // functions/lambda is the array of all functions coming from Display 8 |
9 | {data.map((layer) => 10 | layer.versions.map((version, index) => ( 11 | 18 | )) 19 | )} 20 |
21 | ); 22 | }; 23 | 24 | export default LayersContainer; 25 | -------------------------------------------------------------------------------- /src/containers/NotificationContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Routes, Route, useNavigate } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import { useState, useEffect } from 'react'; 5 | import Notification from '../components/Notification'; 6 | 7 | const NotificationContainer = () => { 8 | 9 | const [associatedNotifications, setAssociatedNotifications] = useState([]); 10 | 11 | const getNotification = async () => { 12 | try{ 13 | const Notifications = await axios.get('https://lambda-peeler.onrender.com/api/user/notifications', { 14 | withCredentials: true, 15 | }) 16 | const flippedNotification = []; 17 | const NotifArray = Notifications.data; 18 | for(let i = NotifArray.length-1; i > 0; i--){ 19 | flippedNotification.push(NotifArray[i]); 20 | } 21 | setAssociatedNotifications(flippedNotification); 22 | 23 | 24 | return; 25 | } catch(err){ 26 | console.log('Error trying to get Notifications'); 27 | } 28 | } 29 | 30 | useEffect(() => { 31 | getNotification(); 32 | }, []) 33 | 34 | 35 | 36 | return ( 37 |
38 | {associatedNotifications.map((element, index) => ( 39 | 44 | 45 | ))} 46 |
47 | ) 48 | } 49 | 50 | export default NotificationContainer; -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;1,100;1,200;1,300;1,400'); 2 | 3 | body { 4 | margin: 0px; 5 | width: 100vw; 6 | height: 100vh; 7 | font-family: 'Poppins', sans-serif; 8 | background-color: #fad0a0; 9 | } 10 | 11 | #main { 12 | display: flex; 13 | flex-direction: column; 14 | gap: 25px; 15 | min-height: 100vh; 16 | background-color: white; 17 | } 18 | 19 | #LayersContainer { 20 | display: flex; 21 | flex-direction: column; 22 | padding: 5px; 23 | gap: 5px; 24 | width: 50vw; 25 | } 26 | 27 | .layer { 28 | padding: 5px; 29 | } 30 | 31 | #FunctionsContainer { 32 | display: flex; 33 | flex-direction: column; 34 | padding: 5px; 35 | gap: 5px; 36 | width: 50vw; 37 | } 38 | 39 | .function { 40 | padding: 5px; 41 | } 42 | 43 | #display { 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | #navbar { 51 | display: flex; 52 | justify-content: space-between; 53 | margin: 0; 54 | background-color: #fad0a0; 55 | color: #444; 56 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; 57 | } 58 | 59 | a { 60 | text-decoration: none; 61 | color: black; 62 | } 63 | 64 | #navbar > ul { 65 | display: flex; 66 | justify-content: space-between; 67 | list-style: none; 68 | padding: 0px 15px 0px 15px; 69 | margin: 10; 70 | width: 100%; 71 | } 72 | 73 | #dropdown { 74 | background-color: white; 75 | cursor: pointer; 76 | border-radius: 5px; 77 | text-align: left; 78 | outline: none; 79 | font-size: 15px; 80 | transition: background-color 1s ease-in-out; 81 | } 82 | 83 | /* add by michael */ 84 | 85 | /* for the .collapsible content */ 86 | .collapsible { 87 | background-color: #eee; 88 | color: #444; 89 | cursor: pointer; 90 | padding: 18px; 91 | width: 100%; 92 | border: none; 93 | border-radius: 5px; 94 | text-align: left; 95 | outline: none; 96 | font-size: 15px; 97 | transition: background-color 0.5s ease-in-out; 98 | } 99 | 100 | .active, 101 | .collapsible:hover { 102 | background-color: #b0b0b0; 103 | } 104 | .content { 105 | padding: 0 18px; 106 | display: none; 107 | overflow: hidden; 108 | } 109 | 110 | /* for the switch for functions */ 111 | /* The switch - the box around the slider */ 112 | .switch { 113 | position: relative; 114 | display: inline-block; 115 | width: 60px; 116 | height: 34px; 117 | } 118 | 119 | /* Hide default HTML checkbox */ 120 | .switch input { 121 | opacity: 0; 122 | width: 0; 123 | height: 0; 124 | } 125 | 126 | /* The slider */ 127 | .slider { 128 | position: absolute; 129 | cursor: pointer; 130 | top: 0; 131 | left: 0; 132 | right: 0; 133 | bottom: 0; 134 | background-color: #ccc; 135 | -webkit-transition: 0.4s; 136 | transition: 0.4s; 137 | } 138 | 139 | .slider:before { 140 | position: absolute; 141 | content: ''; 142 | height: 26px; 143 | width: 26px; 144 | left: 4px; 145 | bottom: 4px; 146 | background-color: white; 147 | -webkit-transition: 0.4s; 148 | transition: 0.4s; 149 | } 150 | 151 | input:checked + .slider { 152 | background-color: #2196f3; 153 | } 154 | 155 | input:focus + .slider { 156 | box-shadow: 0 0 1px #2196f3; 157 | } 158 | 159 | input:checked + .slider:before { 160 | -webkit-transform: translateX(26px); 161 | -ms-transform: translateX(26px); 162 | transform: translateX(26px); 163 | } 164 | 165 | .functionDropDown { 166 | display: flex; 167 | flex-direction: row; 168 | justify-content: space-between; 169 | } 170 | 171 | .layerDropDown { 172 | display: flex; 173 | flex-direction: row; 174 | justify-content: space-between; 175 | } 176 | 177 | #loginButtons { 178 | display: flex; 179 | flex-direction: column; 180 | gap: 10px; 181 | justify-content: center; 182 | } 183 | 184 | #login { 185 | height: 100vh; 186 | width: 100vw; 187 | margin: 0; 188 | /* background-color: #ffdcb4; */ 189 | background-color: #fad0a0; 190 | } 191 | 192 | .listItem { 193 | display: flex; 194 | align-items: center; 195 | } 196 | 197 | #imgid { 198 | display: flex; 199 | position: absolute; 200 | height: 150px; 201 | width: auto; 202 | left: 50%; 203 | top: 20%; 204 | transform: translate(-50%, -50%); 205 | } 206 | 207 | #update { 208 | display: flex; 209 | flex-direction: row; 210 | justify-content: space-between; 211 | align-items: center; 212 | gap: 3em; 213 | margin: 1em; 214 | } 215 | 216 | /* style={{ 217 | position: 'absolute', 218 | left: '50%', 219 | top: '10%', 220 | transform: 'translate(-50%, -50%)', 221 | }} */ 222 | 223 | #notificationDisplay { 224 | display: flex; 225 | width: 50%; 226 | } 227 | 228 | #historyDisplay { 229 | display: flex; 230 | width: 50%; 231 | } 232 | 233 | #link:hover { 234 | text-decoration: underline; 235 | } 236 | 237 | #splash { 238 | display: flex; 239 | flex-direction: column; 240 | justify-content: center; 241 | align-items: center; 242 | gap: 5em; 243 | width: 100%; 244 | background-color: #fad0a0; 245 | } 246 | 247 | #hero { 248 | display: flex; 249 | flex-direction: column; 250 | align-items: center; 251 | justify-content: center; 252 | text-align: center; 253 | width: 60%; 254 | height: 100%; 255 | padding-top: 10em; 256 | } 257 | 258 | #features { 259 | display: flex; 260 | flex-direction: column; 261 | justify-content: center; 262 | align-items: center; 263 | padding-top: 10em; 264 | } 265 | 266 | .feature { 267 | display: flex; 268 | justify-content: center; 269 | align-items: center; 270 | padding-top: 5em; 271 | padding-bottom: 10em; 272 | gap: 5em; 273 | } 274 | 275 | .featureDiscription { 276 | display: flex; 277 | flex-direction: column; 278 | justify-content: center; 279 | align-items: center; 280 | text-align: center; 281 | width: 30em; 282 | height: 20em; 283 | border-radius: 10px; 284 | background-color: #fad0a0; 285 | padding-left: 2em; 286 | padding-right: 2em; 287 | box-shadow: rgba(0, 0, 0, 0.35) 0px 2px 10px; 288 | } 289 | 290 | #gif { 291 | height: 20em; 292 | border-radius: 10px; 293 | box-shadow: rgba(0, 0, 0, 0.35) 0px 2px 10px; 294 | } 295 | 296 | #team { 297 | display: flex; 298 | flex-direction: column; 299 | justify-content: center; 300 | align-items: center; 301 | width: 80%; 302 | padding: 5em; 303 | gap: 3em; 304 | } 305 | 306 | #people { 307 | display: flex; 308 | justify-content: space-evenly; 309 | align-items: center; 310 | width: 100%; 311 | } 312 | 313 | .person { 314 | display: flex; 315 | flex-direction: column; 316 | justify-content: center; 317 | align-items: center; 318 | border-radius: 5px; 319 | height: 15em; 320 | width: 15em; 321 | margin: 3em; 322 | } 323 | 324 | #profileLinks { 325 | display: flex; 326 | } 327 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es6", 5 | "jsx": "react", 6 | "module": "commonJS", 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "checkJs": false, 10 | }, 11 | "include": ["src/**/*", "server/**/*"], 12 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/api/:path(.*)", "destination": "https://personal-lambda-peeler.vercel.app/api/:path" } 4 | ], 5 | "headers": [ 6 | { 7 | "source": "/(.*)", 8 | "headers": [ 9 | { "key": "Access-Control-Allow-Credentials", "value": "true" }, 10 | { "key": "Access-Control-Allow-Origin", "value": "*" }, 11 | { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" }, 12 | { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | test: { 7 | globals: true, 8 | }, 9 | plugins: [react()], 10 | server: { 11 | proxy: { 12 | '/api': { 13 | target: 'http://localhost:3000', 14 | changeOrigin: true, 15 | secure: false, 16 | }, 17 | }, 18 | port: 8080, 19 | }, 20 | }); 21 | --------------------------------------------------------------------------------