├── .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 | 
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 | 
10 | 
11 | 
12 | 
13 | 
14 | 
15 | 
16 | 
17 | 
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 | 
43 |
44 | **Removing a function from a layer**
45 | 
46 |
47 | **Failed Compatibility**
48 | 
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 | 
54 |
55 | ## Connecting to our App
56 | 1. Navigate to your IAM dashboard on your AWS account and create a new role.
57 | 
58 |
59 | 2. Select AWS Account as the trusted entity and enter our tool's ARN number: 524403604286.
60 | 
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 | 
64 |
65 | 4. The role name has to be OSPTool.
66 | 
67 |
68 | 5. Click on OSPTool under roles and copy the ARN number.
69 | 
70 |
71 | 6. Your trust relationships under your OSPTool role should also look like this.
72 | 
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 | 
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 |
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 |
setIsCollapsed(!isCollapsed)}
135 | >
136 |
137 | {' '}
138 | {/* Display Each function name and ARN inside the button */}
139 | Function: {functionName}
140 |
141 | ARN: {ARN}
142 |
143 |
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 |
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 |
setIsCollapsed(!isCollapsed)}
113 | >
114 |
115 | {' '}
116 | {layerName}
117 |
118 | Version: {versionNumber}
119 |
120 | ARN: {ARN}
121 |
122 |
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 |
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 |
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 |
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 | handleUpdate('ARN', ARN)}
71 | variant="contained"
72 | fullWidth='true'
73 | sx={{ height: '3em', backgroundColor: theme.palette.primary.main, '&:hover': {
74 | backgroundColor: theme.palette.primary.main
75 | }}}
76 |
77 | >
78 | Submit
79 |
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 |
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 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
127 | Greg Osborn
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
142 | Michael Shand
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
157 | Zach Hamilton
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
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 |
--------------------------------------------------------------------------------