├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── AppInfoModal.test.tsx ├── ProjectRepoListItem.test.tsx ├── SignIn.test.tsx ├── __snapshots__ │ └── ProjectRepoListItem.test.tsx.snap └── setupTest.ts ├── build ├── icon.icns ├── icon.ico └── icon.png ├── demoScreenshot.png ├── env.ts ├── images ├── Env_File_Sreenshot.png ├── add-project.png ├── add-repos.png ├── clone-repos.png ├── demoScreenshot.png ├── phlippy_icon.png └── success.png ├── jest.config.js ├── package-lock.json ├── package.json ├── phlippy_icon.png ├── src ├── App.sass ├── app.tsx ├── client │ ├── components │ │ ├── addRepos │ │ │ ├── AddRepos.tsx │ │ │ └── RepoSearchListItem.tsx │ │ ├── appInfo │ │ │ ├── AppInfoList.tsx │ │ │ └── AppInfoModal.tsx │ │ ├── getStarted │ │ │ └── GetStarted.tsx │ │ ├── home │ │ │ ├── Routes.tsx │ │ │ └── home.tsx │ │ ├── projects │ │ │ ├── CloningReposModal.tsx │ │ │ ├── ComposeFileModal.tsx │ │ │ ├── ProjectPage.tsx │ │ │ ├── ProjectRepoListItem.tsx │ │ │ └── ProjectSidebar.tsx │ │ ├── sidebar │ │ │ ├── Sidebar.tsx │ │ │ └── SidebarButton.tsx │ │ └── signIn │ │ │ └── SignIn.tsx │ └── helpers │ │ ├── constants.ts │ │ ├── cookieClientHelper.ts │ │ ├── fetchRepoHelper.ts │ │ └── projectHelper.ts ├── index.css ├── index.html ├── index.ts ├── renderer.ts ├── scripts │ ├── cloneRepo.sh │ ├── findDockerfiles.sh │ ├── sshKeyDelete.sh │ └── sshKeygen.sh ├── server │ ├── config │ │ └── passport-setup.ts │ ├── controllers │ │ ├── authController.ts │ │ ├── configController.ts │ │ ├── dockerController.ts │ │ ├── gitController.ts │ │ ├── helpers │ │ │ └── shellHelper.ts │ │ └── sshKeyController.ts │ ├── routes │ │ ├── api-route.ts │ │ ├── auth-route.ts │ │ ├── config-route.ts │ │ └── docker-route.ts │ └── server.ts └── types │ └── types.tsx ├── tsconfig.json ├── tslint.json ├── user-projects └── empty ├── webpack.main.config.js ├── webpack.plugins.js ├── webpack.renderer.config.js └── webpack.rules.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/errors", 12 | "plugin:import/warnings" 13 | ], 14 | "parser": "@typescript-eslint/parser" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Electron-Forge 89 | out/ 90 | 91 | # VS Code 92 | .vscode/ 93 | 94 | # Project directories 95 | /myProjects 96 | 97 | # ssh agent directories 98 | tmpAgent/ 99 | tmpKeys/ 100 | 101 | # Saved user projects 102 | user-projects/projects.json 103 | myProjects/ 104 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | install: 5 | - npm install 6 | script: 7 | - npm run test -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![DockerLocal Demo](/images/phlippy_icon.png) 3 | # DockerLocal 4 | 5 | DockerLocal is a GUI application that allows you to keep an up-to-date version of the docker compose file for interconnected repositories while doing development work on a single repository. 6 | 7 | ![DockerLocal Demo](/demoScreenshot.png) 8 | 9 | ## Getting Started 10 | 11 | These instructions will get you a copy of the project up and running on your local machine. 12 | 13 | ### Prerequisites 14 | 15 | What things you need to install the software and how to install them 16 | 17 | ``` 18 | Mac/Linux 19 | A Github Personal Access Token 20 | ``` 21 | 22 | ### Instructions 23 | 24 | A step by step series of examples that tell you how to get a development env running 25 | 26 | 1. Clone our repo 27 | 2. Get a personal access token from [Github](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) 28 | 3. Open the env.ts file in our root directory and paste your github user ID and access token 29 | 30 | ![DockerLocal Demo](images/Env_File_Sreenshot.png) 31 | 32 | 4. npm install and npm start 33 | 5. Add a Project 34 | 35 | ![DockerLocal Demo](images/add-project.png) 36 | 37 | 6. Add Repos to your project 38 | 39 | ![DockerLocal Demo](images/add-repos.png) 40 | 41 | 7. Choose which repos you'd like included, click Clone Repos 42 | 43 | ![DockerLocal Demo](images/clone-repos.png) 44 | 45 | 8. Click Compose when you're ready! 46 | 47 | ![DockerLocal Demo](images/success.png) 48 | 49 | 50 | ## Running the tests 51 | 52 | Run npm test in the terminal. 53 | 54 | ``` 55 | $ npm test 56 | ``` 57 | 58 | Currently, Jest CLI has set up to run all test suites and display individual test results with the test suite hierarchy. 59 | 60 | ### Testing React Components 61 | We're using: 62 | - Jest, a test runner 63 | - Enzyme, a testing utility for React 64 | 65 | In jest.config.js file: 66 | - ts-jest preset to compile Typescript to JavaScript 67 | - enzyme-to-json to convert Enzyme wrappers for Jest snappshot matcher. 68 | 69 | ## Deployment 70 | 71 | Add additional notes about how to deploy this on a live system 72 | 73 | ## Built With 74 | 75 | * [Typescript](https://www.typescriptlang.org/) - Language used 76 | * [Electron](https://www.electronjs.org/) - Native Desktop Application Framework 77 | * [React.js](https://reactjs.org/) - Front end library used 78 | * [Node.js](https://nodejs.org/en/) - The web framework used 79 | * [npm](https://www.npmjs.com/) - Package Manager 80 | * [Webpack](https://webpack.js.org/) - Dependency Management 81 | * [Bulma](https://bulma.io/) - CSS Framework 82 | * [TSlint](https://palantir.github.io/tslint/) - Linter 83 | 84 | 85 | ## Contributing 86 | 87 | Please read [CONTRIBUTING.md](https://github.com/oslabs-beta/DockerLocal/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 88 | 89 | ## Versioning 90 | 91 | For the versions available, see the [tags on this repository](https://github.com/your/project/tags). 92 | 93 | ## Authors 94 | 95 | * **Vivian Cermeno** - *Co-creator* - [Vcermeno](https://github.com/vcermeno) 96 | * **Kate Chanthakaew** - *Co-creator* - [KateChantha](https://github.com/KateChantha) 97 | * **Tom Lutz** - *Co-creator* - [tlutz888](https://github.com/tlutz888) 98 | * **Katty Polyak** - *Co-creator* - [KattyPolyak](https://github.com/KattyPolyak) 99 | * **Louis Xavier Sheid III** - *Co-creator* - [louisxsheid](https://github.com/louisxsheid) 100 | 101 | See also the list of [contributors](https://github.com/oslabs-beta/DockerLocal/contributors) who participated in this project. 102 | 103 | ## License 104 | 105 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 106 | 107 | ## Acknowledgments 108 | 109 | * Thank you to everyone who helped support the project. 110 | -------------------------------------------------------------------------------- /__tests__/AppInfoModal.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import AppInfoModal from '../src/client/components/appInfo/AppInfoModal'; 4 | 5 | 6 | 7 | describe(' unit testing', () => { 8 | //props when AppInfoModal button is clicked to be shown 9 | const propsShowModal = { 10 | showModal: true, 11 | setShowModal: jest.fn() 12 | } 13 | 14 | const wrapper = shallow(); 15 | 16 | wrapper 17 | .find('button') 18 | .first() 19 | .simulate('click'); 20 | 21 | it('calls setShowModal on click', () => { 22 | expect(propsShowModal.setShowModal).toHaveBeenCalled() 23 | 24 | }) 25 | 26 | // The first argument of the first call to the functon was false 27 | it('calls setShowModal to set showModal to be false on click ', () => { 28 | expect(propsShowModal.setShowModal.mock.calls[0][0]).toEqual(false); 29 | }) 30 | 31 | it('should render two 228 | 234 | 235 | 236 | 237 | 238 | 239 | ); 240 | }; 241 | 242 | export default AddRepos; 243 | -------------------------------------------------------------------------------- /src/client/components/addRepos/RepoSearchListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, Dispatch } from 'react'; 2 | import { RepoSearchListItemProps, Project } from '../../../types/types'; 3 | 4 | import { findActiveProject } from '../../helpers/projectHelper' 5 | 6 | 7 | const RepoSearchListItem: React.FC = ({ repo, selectedRepos, setSelectedRepos, projectList, activeProject }) => { 8 | const [isChecked, setIsChecked] = useState(false) 9 | const [isDisabled, setIsDisabled] = useState(false) 10 | 11 | // needed to account for switching back and forth between filters/tabs 12 | useEffect(() => { 13 | if (selectedRepos.includes(repo) && !isChecked) setIsChecked(true); 14 | const currentProject: Project = findActiveProject(projectList, activeProject); 15 | 16 | if (currentProject.projectRepos.some(({ repoId }) => repoId === repo.repoId)) setIsDisabled(true); 17 | }, [isChecked]) 18 | 19 | // adds/removes from selected repos list and toggles checkbox 20 | const toggleSelect = (): void => { 21 | if (isDisabled) return; 22 | if (!isChecked) setSelectedRepos([...selectedRepos, repo]); 23 | else setSelectedRepos(selectedRepos.filter(({ repoId }) => repoId !== repo.repoId )) 24 | setIsChecked(!isChecked) 25 | } 26 | 27 | return ( 28 | 29 | {/* checkbox is readonly because it rerenders on state change */} 30 | 31 | {`${repo.repoName} - ${repo.repoOwner}`} 32 | 33 | ) 34 | } 35 | 36 | export default RepoSearchListItem; -------------------------------------------------------------------------------- /src/client/components/appInfo/AppInfoList.tsx: -------------------------------------------------------------------------------- 1 | const AppInfoList = 2 | " Welcome to DockerLocal! A GUI application that automatically\ 3 | creates easily configurable compose files for each of your saved projects.\ 4 | Each project will contain public, private, and even organization repositiories of your choosing\ 5 | directly from your GitHub account and will update those repositories with every pull request so that you\ 6 | can run up-to-date containers to quickly test your working code locally.\n\n Just Sign In With GitHub To Get Started\ 7 | or Visit Our Website To Learn Even More!!"; 8 | 9 | export default AppInfoList; 10 | -------------------------------------------------------------------------------- /src/client/components/appInfo/AppInfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from "react"; 2 | 3 | import AppInfoList from "./AppInfoList"; 4 | 5 | // display AppInfoList component 6 | // constains 2 buttons listen to onClick to close modal 7 | const AppInfoModal: React.FC<{ 8 | setShowModal: Dispatch>; 9 | }> = ({ setShowModal }) => { 10 | return ( 11 |
12 |
13 |
14 |
15 |

About DockerLocal

16 | 23 |
24 |
{AppInfoList}
25 |
26 | To get started, please create a personal access token from on your github account. Instructions are on our github readme. 27 |
28 |
29 | 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default AppInfoModal; 39 | -------------------------------------------------------------------------------- /src/client/components/getStarted/GetStarted.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // import Home from "../home/home"; 4 | // import Sidebar from "../sidebar/Sidebar"; 5 | // import AddRepos from "../addRepos/AddRepos"; 6 | 7 | // place holder for GetStarted components 8 | //TODO: add logic to render child components 9 | const GetStarted = () => { 10 | return
Get Started Page
; 11 | }; 12 | 13 | export default GetStarted; 14 | -------------------------------------------------------------------------------- /src/client/components/home/Routes.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable import/no-unresolved */ 3 | import React, { useState, useEffect } from "react"; 4 | import Home from './home'; 5 | import SignIn from '../signIn/SignIn'; 6 | import { User } from '../../../types/types'; 7 | import { getUsernameAndToken } from "../../helpers/cookieClientHelper"; 8 | import { GITHUB_USERID } from "../../../../env" 9 | 10 | const Routes: React.FC = (props) => { 11 | // hooks to define state 12 | const [isLoggedIn, setIsLoggedIn] = useState(false); 13 | const [userInfo, setUserInfo] = useState({userName: GITHUB_USERID, userId: 'abc'}); 14 | const [rendered, setRendered] = useState(); 15 | 16 | 17 | // need to add type for userInfo if not null 18 | // const [userInfo, setUserInfo] = useState(null); 19 | 20 | // runs on render and checks 'isloggedin' display correct page depending on whether user is logged in 21 | // could add logic for 'hasprojects' 22 | // need to make prettier 23 | useEffect(()=> { 24 | // const checkIfLoggedIn = async (): Promise => { 25 | // const { username , accessToken } = await getUsernameAndToken(); 26 | // if (accessToken){ 27 | // setUserInfo({userName: username, userId: 'abc'}) 28 | // setIsLoggedIn(true); 29 | // } 30 | // } 31 | // checkIfLoggedIn(); 32 | if (isLoggedIn) { 33 | setRendered( 34 | 38 | ) 39 | } else setRendered() 40 | }, [isLoggedIn]) 41 | return ( 42 |
43 | {/* */} 44 | {rendered} 45 |
46 | ); 47 | }; 48 | 49 | export default Routes; 50 | -------------------------------------------------------------------------------- /src/client/components/home/home.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import React, { 3 | useState, 4 | Dispatch, 5 | SetStateAction, 6 | useEffect, 7 | useReducer, 8 | } from 'react'; 9 | 10 | import Sidebar from '../sidebar/Sidebar'; 11 | import AddRepos from '../addRepos/AddRepos'; 12 | import ProjectPage from '../projects/ProjectPage'; 13 | 14 | import { 15 | Project, 16 | Repo, 17 | User, 18 | HomeProps, 19 | projectListActions, 20 | } from '../../../types/types'; 21 | import { saveProjectList } from '../../helpers/projectHelper'; 22 | import { EXPRESS_URL } from '../../helpers/constants'; 23 | import logo from '../../../../phlippy_icon.png' 24 | 25 | const initialProjectList: Project[] = []; 26 | 27 | function projectListReducer(state: readonly Project[], action: projectListActions): readonly Project[] { 28 | switch (action.type) { 29 | case 'addProject': 30 | return action.payload; 31 | case 'deleteProject': 32 | return action.payload; 33 | case 'addRepo': 34 | return action.payload; 35 | case 'deleteRepo': 36 | return action.payload; 37 | case 'toggleRepo': 38 | return action.payload; 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | const Home: React.FC = ({ userInfo, setUserInfo }) => { 45 | const [projectList, dispatch] = useReducer( 46 | projectListReducer, 47 | initialProjectList 48 | ); 49 | 50 | const [activeProject, setActiveProject] = useState(''); 51 | 52 | // run once on render 53 | // reads project list from local file and sets state 54 | useEffect(() => { 55 | fetch(`${EXPRESS_URL}/config`) 56 | .then((res) => res.json()) 57 | .then((res) => { 58 | if (res.projectList && res.activeProject) { 59 | dispatch({ type: 'addProject', payload: res.projectList }); 60 | setActiveProject(res.activeProject); 61 | } 62 | // else alert(`It looks like you haven't added any projects yet. Click Add Project to get started. `) ** this has bugs on windows 63 | }) 64 | .catch((err) => console.log('fail', err)); 65 | }, []); 66 | 67 | // saves project list to disk whenever either are modified 68 | useEffect(() => { 69 | if (projectList[0]) saveProjectList(projectList, activeProject); 70 | }, [projectList, activeProject]); 71 | 72 | return ( 73 |
74 |

{`${userInfo.userName}`}

75 | 76 |
77 |
81 | 89 |
90 |
99 | 107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default Home; 114 | -------------------------------------------------------------------------------- /src/client/components/projects/CloningReposModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CloningReposModalProps } from "../../../types/types"; 3 | 4 | const CloningReposModal: React.FC = ({ showCloningReposModal, setShowCloningReposModal }) => { 5 | 6 | 7 | return ( 8 |
9 |
10 |
11 |
12 |

Cloning Repos

13 | 14 |
15 |
16 | Cloning your selected repos. This may take a while. This modal will close when finished. Please be patient. 17 |
18 |
19 | {/* */} 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default CloningReposModal; 32 | -------------------------------------------------------------------------------- /src/client/components/projects/ComposeFileModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ComposeFileModalProps} from '../../../types/types' 3 | 4 | /** 5 | * @descriotion ComposeFileModal component 6 | * @param props drilling from ProjectPage component 7 | */ 8 | const ComposeFileModal: React.FC = ({ 9 | setShowComposeModal, 10 | activeProject, 11 | projectList, 12 | composeFileData 13 | }) => { 14 | 15 | const ymlText = composeFileData.text; 16 | const ymlFilePath = composeFileData.path; 17 | 18 | /** 19 | * RENDER: 1. ymlText or error messgae 20 | * 2. file path or error message 21 | * CONTAINS: 2 buttons 'open folder' and 'close' buttons 22 | */ 23 | return ( 24 |
25 |
26 |
27 |
28 |

docker-compose.yml

29 | 30 |
31 |
32 | {/* Display ymlText or error message */} 33 | {!ymlText && ( 34 |

35 | ERROR: cannot compose file 36 |

37 | )} 38 | {ymlText && ( 39 |
40 |               
41 |                 {ymlText}
42 |               
43 |             
44 | )} 45 |
46 |
47 |
48 | File Location: 49 | {/* Display file path or error message */} 50 | {!ymlFilePath && ( 51 |

52 | ERROR: file path not found 53 |

54 | )} 55 | {ymlFilePath && ( 56 |

57 | {ymlFilePath} 58 |

59 | )} 60 | {/* ============== TODO ================== */} 61 | {/* TODO: Open File or File Folder onClick */} 62 | {/* ====================================== */} 63 | 64 | 70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default ComposeFileModal; 78 | -------------------------------------------------------------------------------- /src/client/components/projects/ProjectPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import AddRepos from '../addRepos/AddRepos'; 4 | import { 5 | Project, 6 | ProjectPageProps, 7 | ComposeFile, 8 | ComposeFileModalProps, 9 | } from '../../../types/types'; 10 | import ProjectRepoListItem from './ProjectRepoListItem'; 11 | import ComposeFileModal from './ComposeFileModal'; 12 | import CloningReposModal from './CloningReposModal'; 13 | import { findActiveProject } from '../../helpers/projectHelper'; 14 | import { getUsernameAndToken } from '../../helpers/cookieClientHelper'; 15 | import { EXPRESS_URL } from '../../helpers/constants'; 16 | 17 | const ProjectPage: React.FC = ({ 18 | activeProject, 19 | userInfo, 20 | projectList, 21 | dispatch, 22 | }) => { 23 | const [showAddRepos, setShowAddRepos] = useState(false); 24 | const [projectRepoListItems, setprojectRepoListItems] = useState([]); 25 | 26 | const [showCloningReposModal, setShowCloningReposModal] = useState(false); 27 | const [showComposeModal, setShowComposeModal] = useState(false); 28 | const [composeFileData, setComposeFileData] = useState( 29 | null 30 | ); 31 | 32 | // populate repo list items when active project changes and when request from home.tsx comes back to update project list 33 | useEffect(() => { 34 | const currentProject: Project = findActiveProject( 35 | projectList, 36 | activeProject 37 | ); 38 | 39 | if (currentProject && currentProject.projectRepos) { 40 | const newList = currentProject.projectRepos.map((repo) => { 41 | return ( 42 | 46 | ); 47 | }); 48 | setprojectRepoListItems(newList); 49 | } 50 | }, [activeProject, projectList]); 51 | 52 | /** 53 | * @function onClick button Compose File button 54 | * @description send 'POST' request 55 | * send: projectName 56 | * receive: yml file and pathfile 57 | */ 58 | const composeFile = (): void => { 59 | const currentProject: Project = findActiveProject( 60 | projectList, 61 | activeProject 62 | ); 63 | 64 | const reposToClone = currentProject.projectRepos.filter( 65 | ({ isIncluded }) => isIncluded 66 | ); 67 | 68 | const body = { 69 | projectName: currentProject.projectName, 70 | repos: reposToClone, 71 | }; 72 | 73 | fetch(`${EXPRESS_URL}/docker`, { 74 | headers: { 75 | 'content-type': 'application/json; charset=UTF-8', 76 | }, 77 | body: JSON.stringify(body), 78 | method: 'POST', 79 | }) 80 | .then((data) => { 81 | return data.json(); 82 | }) 83 | .then((res) => { 84 | const newYmlData: ComposeFile = { 85 | text: res.file, 86 | path: res.path, 87 | }; 88 | setComposeFileData(newYmlData); 89 | setShowComposeModal(true); 90 | }) 91 | .catch((error) => console.log(error)); 92 | }; 93 | 94 | const cloneRepos = async (): Promise => { 95 | setShowCloningReposModal(true); 96 | 97 | const currentProject: Project = findActiveProject( 98 | projectList, 99 | activeProject 100 | ); 101 | 102 | // get selected repos that are checked to clone 103 | const reposToClone = currentProject.projectRepos.filter( 104 | ({ isIncluded }) => isIncluded 105 | ); 106 | 107 | // get username and token 108 | const { username, accessToken } = await getUsernameAndToken(); 109 | 110 | const body = JSON.stringify({ 111 | username, 112 | accessToken, 113 | repos: reposToClone, 114 | projectName: currentProject.projectName, 115 | }); 116 | 117 | fetch(`${EXPRESS_URL}/api/clonerepos`, { 118 | method: 'POST', 119 | body, 120 | headers: { 121 | 'Content-Type': 'application/json', 122 | }, 123 | }) 124 | .then((res) => { 125 | setShowCloningReposModal(false); 126 | return res.json(); 127 | }) 128 | .then((res) => console.log('success', res)) 129 | .catch((err) => console.log('fail', err)); 130 | }; 131 | 132 | return ( 133 |
134 |
Select your repositories:
135 | 142 | 149 | 150 | 157 | 158 | {projectRepoListItems} 159 | 160 | {/* shows this element if showAddRepos is true */} 161 | {showAddRepos && ( 162 | 171 | )} 172 | {showCloningReposModal && ( 173 | 176 | )} 177 | 178 | {showComposeModal && ( 179 | 188 | )} 189 |
190 | ); 191 | }; 192 | 193 | export default ProjectPage; 194 | -------------------------------------------------------------------------------- /src/client/components/projects/ProjectRepoListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import { ProjectRepoListItemProps, Project, Repo } from '../../../types/types'; 4 | import { findActiveProject } from '../../helpers/projectHelper'; 5 | 6 | const ProjectRepoListItem: React.FC = ({ 7 | repo, 8 | activeProject, 9 | projectList, 10 | dispatch, 11 | }) => { 12 | const [isChecked, setIsChecked] = useState(repo.isIncluded || false); 13 | 14 | useEffect(() => { 15 | setIsChecked(repo.isIncluded || false); 16 | }); 17 | 18 | const toggleIsIncluded = (): void => { 19 | // define current repo 20 | const currentProject: Project = findActiveProject( 21 | projectList, 22 | activeProject 23 | ); 24 | 25 | // need to make copy of states to include in new to not mutate state directly 26 | // make copy of repo with toggled isIncluded value 27 | const newRepo: Repo = { ...repo, isIncluded: !repo.isIncluded }; 28 | 29 | // make copy of active project repo list with new repo included 30 | const newProjectRepos = currentProject.projectRepos.map((repo) => 31 | repo.repoId === newRepo.repoId ? newRepo : repo 32 | ); 33 | 34 | // copy active project 35 | const newActiveProject: Project = { ...currentProject }; 36 | newActiveProject.projectRepos = newProjectRepos; 37 | 38 | // insert new project into new project list 39 | const newProjectList: Project[] = projectList.map((project) => 40 | project.projectId === newActiveProject.projectId 41 | ? newActiveProject 42 | : project 43 | ); 44 | 45 | // set new project list 46 | dispatch({ type: 'addRepo', payload: newProjectList }); 47 | }; 48 | 49 | return ( 50 |
51 | {/* checkbox is readonly because it rerenders on state change */} 52 | 53 | {repo.repoName} 54 |
55 | ); 56 | }; 57 | 58 | export default ProjectRepoListItem; 59 | -------------------------------------------------------------------------------- /src/client/components/projects/ProjectSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction, useState } from 'react'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { Project, ProjectSideBarProps } from '../../../types/types'; 4 | 5 | import { checkValidName } from '../../helpers/projectHelper'; 6 | 7 | const ProjectSideBar: React.FC = ({ 8 | setShowProjectSidebarModal, 9 | projectList, 10 | setActiveProject, 11 | dispatch, 12 | }) => { 13 | // State hook for new project name and boolean whether the name is valid 14 | const [newProject, setNewProject] = useState({ 15 | name: '', 16 | isValid: false, 17 | }); 18 | 19 | // State hook for message body and style 20 | const validBody = 21 | 'Project names can contain alphanumeric characters, hypens, or underscores. Project names must be unique, contain less than 26 characters, and cannot be blank.'; 22 | const invalidBody = `You have entered an invalid project name. ${validBody}`; 23 | const [newProjectStyle, setNewProjectStyle] = useState({ 24 | messageBody: validBody, 25 | messageStyle: 'is-info', 26 | }); 27 | 28 | /** 29 | * @function handleChange 30 | * @description updates newProject.name, newProject.isValid, newProjectStyle.messageBody, newProjectStyle.messageStyle 31 | * @param e each update to new project name field 32 | */ 33 | const handleChange = (e: React.ChangeEvent): void => { 34 | const isValid = checkValidName(e.target.value, projectList); 35 | setNewProject({ 36 | name: e.target.value, 37 | isValid, 38 | }); 39 | setNewProjectStyle({ 40 | messageBody: isValid ? validBody : invalidBody, 41 | messageStyle: isValid ? 'is-info' : 'is-danger', 42 | }); 43 | }; 44 | 45 | const handleSubmit = (): void => { 46 | // create a new project object 47 | if (checkValidName(newProject.name, projectList)) { 48 | const newProjectId: string = uuidv4(); 49 | 50 | const newProjectObject: Project = { 51 | projectId: newProjectId, 52 | projectName: newProject.name, 53 | projectRepos: [], 54 | }; 55 | 56 | // set copy of a new project 57 | dispatch({ 58 | type: 'addProject', 59 | payload: [...projectList, newProjectObject], 60 | }); 61 | setActiveProject(newProjectId); 62 | // then close the modal 63 | setShowProjectSidebarModal(false); 64 | } 65 | }; 66 | 67 | const errorIcon = ( 68 | 69 | 70 | 71 | ); 72 | 73 | // input for projectname 74 | return ( 75 |
76 |
77 |
78 |
79 |

Create A New Project

80 | 81 |
82 |
83 |
84 | 114 |
115 |
116 |
117 | 124 | 130 |
131 |
132 |
133 | ); 134 | }; 135 | 136 | export default ProjectSideBar; 137 | -------------------------------------------------------------------------------- /src/client/components/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, Dispatch, SetStateAction } from 'react'; 2 | 3 | import SidebarButton from './SidebarButton'; 4 | import ProjectSideBar from '../projects/ProjectSidebar'; 5 | import { Project, SidebarProps } from '../../../types/types'; 6 | 7 | const Sidebar: React.FC = ({ 8 | projectList, 9 | dispatch, 10 | activeProject, 11 | setActiveProject, 12 | }) => { 13 | const sidebarButtons = projectList.map((project) => ( 14 | 21 | )); 22 | 23 | const [showProjectSideBarModal, setShowProjectSidebarModal] = useState(false); 24 | // render a sidebar button for every project in list 25 | return ( 26 |
27 | 31 | 32 | 41 | {showProjectSideBarModal && ( 42 | 51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Sidebar; 57 | -------------------------------------------------------------------------------- /src/client/components/sidebar/SidebarButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, Dispatch, SetStateAction, useEffect, } from 'react'; 2 | 3 | import { Project, SidebarButtonProps } from '../../../types/types' 4 | 5 | 6 | const SidebarButton: React.FC = ({ projectName, projectId, projectRepos, activeProject, setActiveProject}) => { 7 | const [isActive, setIsActive] = useState(''); 8 | 9 | // listens for change in [activeProject], changes isActive to string to style element 10 | // come check if we should be listening for [activeProject or more efficient to listen for specific property] 11 | useEffect(() => { 12 | if (projectId === activeProject) { 13 | setIsActive('is-active') 14 | } else { 15 | setIsActive('') 16 | }}, [activeProject]) 17 | 18 | 19 | return ( 20 |
  • 21 | setActiveProject(projectId)}> 22 | {projectName} 23 | 24 |
  • 25 | ) 26 | 27 | } 28 | 29 | export default SidebarButton; -------------------------------------------------------------------------------- /src/client/components/signIn/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import AppInfoModal from "../appInfo/AppInfoModal"; 4 | import { EXPRESS_URL } from "../../helpers/constants"; 5 | import { getUsernameAndToken } from "../../helpers/cookieClientHelper"; 6 | import { SignInProps } from "../../../types/types" 7 | 8 | const SignIn: React.FC = ({ isLoggedIn, setIsLoggedIn, setUserInfo }) => { 9 | //initailize showModal to false 10 | const [showModal, setShowModal] = useState(false); 11 | 12 | //display signIn button and appInfo button 13 | return ( 14 |
    15 | 35 | 41 | 42 | {/* this will render AppInfoModal if showModal evaluates to true */} 43 | {showModal && } 44 |
    45 | ); 46 | }; 47 | 48 | export default SignIn; 49 | -------------------------------------------------------------------------------- /src/client/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXPRESS_URL = 'http://localhost:3001'; -------------------------------------------------------------------------------- /src/client/helpers/cookieClientHelper.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | import { GITHUB_USERID, GITHUB_ACCESS_TOKEN } from '../../../env' 3 | 4 | /** 5 | * @function getUsernameAndToken 6 | * @description a helper function that take in a document cookie and parses and decrypts it to seperate the username and token from the cookie 7 | * @description In the current version, this returns the username and token stored in .env variables 8 | * @param username user's github username 9 | * @param accessToken user's github access token 10 | * @returns boolean. True if no invalid characters 11 | */ 12 | 13 | export const getUsernameAndToken = async (): Promise<{ 14 | username: string; 15 | accessToken: string; 16 | }> => { 17 | /* 18 | // PARSE TOKEN AND USERNAME FROM COOKIES 19 | let username; 20 | let token; 21 | const cookie = document.cookie; 22 | // if there is no cookie yet since user has not signed in, do NOT run the function 23 | if (cookie === ''){ 24 | return {username, accessToken: token}; 25 | } 26 | const nameAndToken = cookie.split(";"); 27 | // the cookie comes in with either token or username first since we dont know when or why that happens 28 | // this function is supposed to check which one comes first and assign it respectively before parsing 29 | if (nameAndToken[0].slice(0,9) === "username"){ 30 | username = nameAndToken[0]; 31 | token = nameAndToken[1]; 32 | } else { 33 | username = nameAndToken[1]; 34 | token = nameAndToken[0]; 35 | } 36 | username = username.replace("username=", "").trim(); 37 | token = token.replace("token=", "").trim(); 38 | const parsedToken = decodeURIComponent(token); 39 | 40 | // DECRYPT TOKEN FROM COOKIES 41 | const decryptedToken = await CryptoJS.AES.decrypt( 42 | parsedToken, 43 | "super_secret" 44 | ).toString(CryptoJS.enc.Utf8); 45 | */ 46 | return { username: GITHUB_USERID, accessToken: GITHUB_ACCESS_TOKEN}; 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/helpers/fetchRepoHelper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { RepoResponseType } from "../../types/types"; 3 | import { getUsernameAndToken } from "../helpers/cookieClientHelper"; 4 | 5 | // dummy request and response 6 | export const fetchRepos = async (): Promise => { 7 | // *** need to add conditional if user is not logged in or if fetch error etc *** 8 | 9 | const { username, accessToken } = await getUsernameAndToken(); 10 | 11 | const response: RepoResponseType = { 12 | personal: [], 13 | organizations: [], 14 | collaborations: [], 15 | }; 16 | 17 | // WILL USE USERNAME AND ACCESS TOKEN FOR THESE REQUESTS 18 | // ADD HEADERS FOR POST REQUEST 19 | const myHeaders = new Headers(); 20 | myHeaders.append("Authorization", `Bearer ${accessToken}`); 21 | myHeaders.append("Content-Type", "application/json"); 22 | 23 | // PUPLIC REPO FETCH ////////////////////////////////////////////////// 24 | // Query to get the repo id, repo name, and repo owner for public repositories 25 | const publicRepoQuery = JSON.stringify({ 26 | query: `{ 27 | user(login: "${username}") { 28 | repositories(first: 100) { 29 | edges { 30 | node { 31 | id 32 | name 33 | owner { 34 | ... on User { 35 | login 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | }`, 43 | variables: {}, 44 | }); 45 | 46 | // Request options for fetches with appended headers and correct query 47 | const requestOptions: RequestInit = { 48 | method: "POST", 49 | headers: myHeaders, 50 | body: publicRepoQuery, 51 | redirect: "follow", 52 | }; 53 | 54 | // Send fetch request to Github GraphQL API with requestOptions object for Public Repos 55 | await fetch("https://api.github.com/graphql", requestOptions) 56 | .then((response) => response.text()) 57 | .then((res) => { 58 | const result = JSON.parse(res); 59 | let currentNode; 60 | const pubLength = result.data.user.repositories.edges.length; 61 | const pubRepos = []; 62 | for (let i = 0; i < pubLength; i++) { 63 | currentNode = result.data.user.repositories.edges[i].node; 64 | pubRepos.push({ 65 | repoId: currentNode.id, 66 | repoName: currentNode.name, 67 | repoOwner: currentNode.owner.login, 68 | }); 69 | } 70 | response.personal = pubRepos; 71 | }) 72 | .catch((error) => console.log("error", error)); 73 | 74 | // COLLABORATOR REPO FETCH////////////////////////////////////////////////// 75 | // gets the id, name and owner username from repos collaborated on 76 | const collabFetch = JSON.stringify({ 77 | query: `{ 78 | user(login: "${username}") { 79 | repositories(first: 100, affiliations: COLLABORATOR) { 80 | nodes { 81 | id 82 | name 83 | owner { 84 | login 85 | } 86 | } 87 | } 88 | } 89 | }`, 90 | variables: {}, 91 | }); 92 | 93 | // changes the query to be for collaborated repos (above) 94 | requestOptions.body = collabFetch; 95 | 96 | await fetch("https://api.github.com/graphql", requestOptions) 97 | .then((response) => response.text()) 98 | .then((res) => { 99 | const result = JSON.parse(res); 100 | let currentNode; 101 | const collabChain = result.data.user.repositories.nodes; 102 | const collabLength = collabChain.length; 103 | const collabRepos = []; 104 | for (let i = 0; i < collabLength; i++) { 105 | currentNode = result.data.user.repositories.nodes[i]; 106 | collabRepos.push({ 107 | repoId: currentNode.id, 108 | repoName: currentNode.name, 109 | repoOwner: currentNode.owner.login, 110 | }); 111 | } 112 | response.collaborations = collabRepos; 113 | }) 114 | .catch((error) => console.log("error", error)); 115 | 116 | // ORGANIZATION REPO FETCH////////////////////////////////////////////////// 117 | // gets the id, name and owner username from repos inside of organizations the user is a member of 118 | const orgFetch = JSON.stringify({ 119 | query: `{ 120 | user(login: "${username}") { 121 | organizations(first: 100) { 122 | edges { 123 | node { 124 | repositories(affiliations: ORGANIZATION_MEMBER, first: 100) { 125 | edges { 126 | node { 127 | name 128 | id 129 | owner { 130 | login 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | }`, 140 | variables: {}, 141 | }); 142 | 143 | // changes the query to be for organization repos (above) 144 | requestOptions.body = orgFetch; 145 | 146 | await fetch("https://api.github.com/graphql", requestOptions) 147 | .then((response) => response.text()) 148 | .then((res) => { 149 | const result = JSON.parse(res); 150 | let currentNode; 151 | let reposLength; 152 | const objChain = result.data.user.organizations.edges; 153 | const orgLength = objChain.length; 154 | const orgArr = []; 155 | // Iterate through the returned object and save the name, id, and owner information for each repo node 156 | for (let x = 0; x < orgLength; x++) { 157 | reposLength = objChain[x].node.repositories.edges.length; 158 | for (let i = 0; i < reposLength; i++) { 159 | currentNode = 160 | result.data.user.organizations.edges[x].node.repositories.edges[i] 161 | .node; 162 | orgArr.push({ 163 | repoName: currentNode.name, 164 | repoId: currentNode.id, 165 | repoOwner: currentNode.owner.login, 166 | }); 167 | } 168 | } 169 | response.organizations = orgArr; 170 | }) 171 | .catch((error) => console.log("error", error)); 172 | 173 | // logs all gathered and parsed repository data; 174 | // console.log("RESPONSE: ", response); 175 | return response; 176 | }; 177 | -------------------------------------------------------------------------------- /src/client/helpers/projectHelper.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '../../types/types'; 2 | import { EXPRESS_URL } from './constants'; 3 | 4 | /** 5 | * @function findActiveProject 6 | * @description finds the active project from the list of user's projects 7 | * @param projectList contains info about all projects 8 | * @param activeProject ID of active project 9 | * @returns project object whose ID matches the current active project's ID 10 | */ 11 | export const findActiveProject = ( 12 | projectList: readonly Project[], 13 | activeProject: string 14 | ): Project => { 15 | return { 16 | ...projectList.find((project) => project.projectId === activeProject), 17 | }; 18 | }; 19 | 20 | /** 21 | * @function saveProjectList 22 | * @description saves project list to disk 23 | * @param projectList contains info about all projects 24 | * @param activeProject ID of active project 25 | * 26 | */ 27 | 28 | export const saveProjectList = (projectList: readonly Project[], activeProject: string): void => { 29 | fetch(`${EXPRESS_URL}/config`, { 30 | method: "POST", 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify({ 35 | projectList: [...projectList], 36 | activeProject 37 | }), 38 | }) 39 | .catch((err) => console.log("fail", err)); 40 | }; 41 | 42 | /** 43 | * @function checkValidName 44 | * @description function that updates boolean in state that indicates whether project name entered is valid 45 | * @param projectNameValue string which will be checked for valid format 46 | * @returns boolean. True if no invalid characters 47 | */ 48 | export const checkValidName = ( 49 | projectNameValue: string, 50 | projectList: readonly Project[] 51 | ): boolean => { 52 | // RegExp to test if project name will be valid project/folder name. 53 | // Can have letters, numbers, hyphens, underscores only. Limited to 1 to 25 characters 54 | const regex = /^[\w-]{1,25}$/; 55 | const isValid: boolean = regex.test(projectNameValue); 56 | // Check to make sure we don't have duplicate project names. 57 | const nameArray = projectList.map((project) => 58 | project.projectName.toLowerCase() 59 | ); 60 | const isUnique = !nameArray.includes(projectNameValue.toLowerCase()); 61 | 62 | return isValid && isUnique; 63 | }; 64 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 3 | margin: auto; 4 | max-width: 38rem; 5 | padding: 2rem; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | DockerLocal 17 | 18 | 19 | 20 |
    21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, shell, session, WebPreferences } from 'electron'; 2 | import installExtension, { 3 | REACT_DEVELOPER_TOOLS, 4 | } from 'electron-devtools-installer'; 5 | import { EXPRESS_URL } from "./client/helpers/constants"; 6 | 7 | 8 | 9 | const express = require('./server/server.ts'); 10 | const path = require('path'); 11 | 12 | // global variable pointing to main_window under entrypoints in package.json 13 | // loads index.html and app.tsx 14 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string; 15 | 16 | // // use this to disable dark mode 17 | // const { nativeTheme } = require('electron') 18 | // console.log(nativeTheme.themeSource = 'light') 19 | // console.log('menuItems!! ', Menu) 20 | // const viewSettingsMenu = new Menu() 21 | 22 | 23 | // let mainWindow: BrowserWindow; 24 | 25 | // Handle creating/removing shortcuts on Windows when installing/uninstalling. 26 | // eslint-disable-line global-require 27 | if (require('electron-squirrel-startup')) app.quit(); 28 | 29 | const createWindow = (): void => { 30 | // // Create the browser window. 31 | const mainWindow = new BrowserWindow({ 32 | width: 1280, 33 | height: 720, 34 | autoHideMenuBar: true, 35 | useContentSize: true, 36 | resizable: true, 37 | webPreferences: { 38 | contextIsolation: true, 39 | enableRemoteModule: false 40 | 41 | } 42 | 43 | 44 | }); 45 | 46 | // set permission requests to false for all remote content - electron security checklist 47 | 48 | 49 | mainWindow 50 | .loadURL(MAIN_WINDOW_WEBPACK_ENTRY) 51 | // .then(() => mainWindow.webContents.openDevTools()); // uncomment to display dev tools 52 | mainWindow.focus(); 53 | }; 54 | 55 | 56 | 57 | app.whenReady().then(() => { 58 | // automatically deny all permission requests from remote content 59 | session.defaultSession.setPermissionRequestHandler((webcontents, permission, callback) => callback(false)); 60 | // session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 61 | // callback({ 62 | // responseHeaders: { 63 | // ...details.responseHeaders, 64 | // 'Content-Security-Policy': ['default-src \'self\''] 65 | // } 66 | // }) 67 | // }) 68 | 69 | 70 | installExtension(REACT_DEVELOPER_TOOLS) 71 | .then((devtool: any) => console.log(`Added Extension: ${devtool.name}`)) 72 | .catch((err: any) => console.log('An error occurred: ', err)); 73 | }); 74 | 75 | // This method will be called when Electron has finished 76 | // initialization and is ready to create browser windows. 77 | // Some APIs can only be used after this event occurs. 78 | app.on('ready', createWindow); 79 | 80 | // Quit when all windows are closed. 81 | app.on('window-all-closed', () => { 82 | // On OS X it is common for applications and their menu bar 83 | // to stay active until the user quits explicitly with Cmd + Q 84 | if (process.platform !== 'darwin') { 85 | app.quit(); 86 | } 87 | }); 88 | 89 | app.on('activate', () => { 90 | // On OS X it's common to re-create a window in the app when the 91 | // dock icon is clicked and there are no other windows open. 92 | if (BrowserWindow.getAllWindows().length === 0) { 93 | createWindow(); 94 | } 95 | }); 96 | 97 | 98 | // Verify WebView Options Before Creation - from electron security docs - https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation 99 | app.on('web-contents-created', (event, contents) => { 100 | contents.on('will-attach-webview', (event, webPreferences: WebPreferences & { preloadURL: string}, params) => { 101 | // Strip away preload scripts if unused or verify their location is legitimate 102 | delete webPreferences.preload; 103 | delete webPreferences.preloadURL; 104 | 105 | // Disable Node.js integration 106 | webPreferences.nodeIntegration = false; 107 | 108 | // No URLS should be loaded 109 | event.preventDefault(); 110 | 111 | }) 112 | }) 113 | 114 | const URL = require('url').URL 115 | 116 | // only allows navigation to our express server URL 117 | app.on('web-contents-created', (event, contents) => { 118 | contents.on('will-navigate', (event, navigationUrl) => { 119 | const parsedUrl = new URL(navigationUrl) 120 | if (parsedUrl.origin !== EXPRESS_URL) { 121 | event.preventDefault() 122 | } 123 | }) 124 | }) 125 | 126 | // doesnt allow new windows to be created 127 | app.on('web-contents-created', (event, contents) => { 128 | contents.on('new-window', async (event, navigationUrl) => { 129 | event.preventDefault() 130 | await shell.openExternal(navigationUrl) 131 | }) 132 | }) 133 | 134 | 135 | 136 | // In this file you can include the rest of your app's specific main process 137 | // code. You can also put them in separate files and import them here. 138 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file will automatically be loaded by webpack and run in the "renderer" context. 3 | * To learn more about the differences between the "main" and the "renderer" context in 4 | * Electron, visit: 5 | * 6 | * https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes 7 | * 8 | * By default, Node.js integration in this file is disabled. When enabling Node.js integration 9 | * in a renderer process, please be aware of potential security implications. You can read 10 | * more about security risks here: 11 | * 12 | * https://electronjs.org/docs/tutorial/security 13 | * 14 | * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration` 15 | * flag: 16 | * 17 | * ``` 18 | * // Create the browser window. 19 | * mainWindow = new BrowserWindow({ 20 | * width: 800, 21 | * height: 600, 22 | * webPreferences: { 23 | * nodeIntegration: true 24 | * } 25 | * }); 26 | * ``` 27 | */ 28 | 29 | // import './index.css'; 30 | // import * as React from 'react'; 31 | // import { render } from 'react-dom'; 32 | // import App from './App'; 33 | 34 | 35 | console.log('👋 This message is being logged by "renderer.js", included via webpack'); 36 | 37 | -------------------------------------------------------------------------------- /src/scripts/cloneRepo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to clone a github repository 3 | 4 | # handle incoming arguments, username and repoName 5 | username=$1 6 | repoName=$2 7 | projectName=$3 8 | 9 | # get the sock address of our ssh agent so that we can connect to it 10 | temp_sock=$(cat ./tmpAgent/agentSock) 11 | # add the ssh agent sock to the environment variables 12 | export SSH_AUTH_SOCK=$temp_sock 13 | 14 | # create a myProjects folder to store repositories if it doesn't already exist 15 | if [ ! -d "./myProjects" ] 16 | then 17 | mkdir ./myProjects 18 | fi 19 | cd myProjects 20 | # create a folder named after the current project if one doesn't exist already 21 | if [ ! -d "./$projectName" ] 22 | then 23 | mkdir $projectName 24 | fi 25 | 26 | cd $projectName 27 | 28 | # clone a git repository from github using ssh connection 29 | git clone git@github.com:$username/$repoName.git 30 | echo "We finished cloning" 31 | exit 0 -------------------------------------------------------------------------------- /src/scripts/findDockerfiles.sh: -------------------------------------------------------------------------------- 1 | # Find Dockerfiles in chosen repository 2 | projectFolder=$1 3 | FILE=*[dD]ocker* 4 | # Folder path WILL change in production 5 | find ./myProjects/"$projectFolder" -name "$FILE" -type f 6 | # allows for error output if no 7 | if test $(find ./myProjects/"$projectFolder" -name "$FILE" -type f| wc -c) -eq 0 8 | then 9 | echo missing repository with Dockerfile 10 | exit 1 11 | fi 12 | exit 0 13 | -------------------------------------------------------------------------------- /src/scripts/sshKeyDelete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to generate an SSH key and add github.com to known hosts 3 | 4 | # get the sock address of our ssh agent so that we can connect to it 5 | temp_sock=$(cat ./tmpAgent/agentSock) 6 | # add the ssh agent sock to the environment variables 7 | export SSH_AUTH_SOCK=$temp_sock 8 | 9 | # get the process id of the ssh-agent we created in sshKeygen.sh 10 | agent_pid=$(cat ./tmpAgent/agentPID) 11 | 12 | # Kill the ssh-agent which we created in sshKeygen.sh 13 | ssh-add -D $agent_pid >/dev/null 14 | 15 | # remove the ./tmpKeys and .tmpAgent folders and all contents 16 | rm -rf ./tmpKeys 17 | rm -rf ./tmpAgent 18 | 19 | exit 0 -------------------------------------------------------------------------------- /src/scripts/sshKeygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to generate an SSH key and add github.com to known hosts 3 | 4 | # remove temporary folders that store agent info and ssh keys, if they exist 5 | if [ -d './tmpKeys' ] 6 | then 7 | rm -rf ./tmpKeys 8 | elif [ -d './tmpAgent' ] 9 | then 10 | rm -rf ./tmpAgent 11 | fi 12 | 13 | # create temporary folders to store agent info and ssh keys 14 | mkdir ./tmpKeys 15 | mkdir ./tmpAgent 16 | 17 | # generate public and private SSH keys with no password 18 | ssh-keygen -t ed25519 -f ./tmpKeys/dockerKey -g -N "" 19 | # add github.com to ssh known hosts so that we can ssh connect to github.com 20 | ssh-keyscan -H github.com -y >> ~/.ssh/known_hosts 21 | 22 | # start ssh agent 23 | eval $(ssh-agent -s) 24 | 25 | # save ssh agent sock info and agent PID so that we can connect later 26 | echo $SSH_AUTH_SOCK > ./tmpAgent/agentSock 27 | echo $SSH_AGENT_PID > ./tmpAgent/agentPID 28 | 29 | # add ssh key to ssh agent 30 | ssh-add ./tmpKeys/dockerKey 31 | exit 0 -------------------------------------------------------------------------------- /src/server/config/passport-setup.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | 3 | require("dotenv").config(); 4 | const passport = require("passport"); 5 | const GithubStrategy = require("passport-github2"); 6 | const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env; 7 | // Configure the Github strategy for use by Passport. 8 | 9 | // OAuth 2.0-based strategies require a `verify` function which receives the 10 | // credential (`accessToken`) for accessing the Github API on the user's 11 | // behalf, along with the user's profile. The function must invoke callback 12 | // with a user object, which will be set at `req.user` in route handlers after 13 | // authentication. 14 | 15 | passport.use( 16 | new GithubStrategy( 17 | { 18 | // https://github.com/login/oauth/authorize 19 | // will be given through API. used to identify our app to github 20 | clientID: GITHUB_CLIENT_ID, 21 | // will be given through API. used to identify our app to github 22 | clientSecret: GITHUB_CLIENT_SECRET, 23 | // callback url that sends client to github login page 24 | callbackURL: "/auth/github/callback", 25 | }, 26 | ( 27 | accessToken: string, 28 | refreshToken: string, 29 | profile: any, 30 | done: Function 31 | ) => { 32 | // basic 4 params -> getting github profile information from auth-route 33 | /** passport callback fn 34 | * accessToken - is how we will make an API call on behalf of the user. It is sent to us by github in the response. 35 | * refreshToken - is a token that can refresh the access token if it 'times out'. 36 | * profile - is the user's record in Github. We associate this profile with a user record in our application database. 37 | * done - after getting successully authenticated - run this callback function 38 | * routes to 'authenticated page' w/ correct user information 39 | **/ 40 | const { username } = profile; 41 | 42 | // in case we want to use profile picture 43 | const { avatar_url:profilePic } = profile; 44 | 45 | const payload: object = { 46 | username, 47 | accessToken, 48 | }; 49 | done(null, payload); 50 | } 51 | ) 52 | ); 53 | 54 | /** Configure Passport authenticated session persistence. 55 | * 56 | * In order to restore authentication state across HTTP requests, Passport needs 57 | * to serialize users into and deserialize users out of the session. We 58 | * supply the user ID when serializing, and query the user record by ID 59 | * from the database when deserializing. 60 | **/ 61 | passport.serializeUser((payload: object, done: Function) => done(null, payload)); 62 | 63 | passport.deserializeUser((payload: string, done: Function) =>done(null, payload)); 64 | 65 | module.exports = passport.serializeUser, passport.deserializeUser; 66 | -------------------------------------------------------------------------------- /src/server/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | import { Request, Response, NextFunction } from "express"; 3 | import CryptoJS = require("crypto-js"); 4 | 5 | const authController: any = {}; 6 | 7 | // Middleware to get username and access token from passport-setup.js's done callback (payload) 8 | authController.saveAccessToken = ( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ): void => { 13 | // Destructure username/accessToken from req.user 14 | const { username, accessToken }: any = req.user; 15 | 16 | // CryptoJS -> encrypt accessToken with AES and a "super_secret" password 17 | const encrypted = CryptoJS.AES.encrypt( 18 | accessToken, 19 | "super_secret" 20 | ).toString(); 21 | // Save encrypted token as cookie 22 | res.cookie("token", encrypted, { maxAge: 360000 }); 23 | 24 | // Save username as cookie 25 | res.cookie("username", username, { maxAge: 360000 }); 26 | 27 | // error handling 28 | if (!(username || encrypted)){ 29 | return next({ 30 | log: `Error caught in authContoller.saveAccessToken: Missing username: ${username} or encrypted: ${Boolean(encrypted)}`, 31 | msg: { err: 'authContoller.saveAccessToken: ERROR: Check server logs for details'} 32 | }) 33 | } 34 | 35 | return next(); 36 | }; 37 | 38 | // Middleware to get username and access token from cookies and store each in locals 39 | authController.getNameAndTokenFromCookies = ( 40 | req: Request, 41 | res: Response, 42 | next: NextFunction 43 | ): void => { 44 | // Destructure username and token from cookies 45 | const { username, token }: {username: string; token: string} = req.cookies; 46 | 47 | // CryptoJS -> decrypt accessToken and convert back to original 48 | const decrypted = CryptoJS.AES.decrypt(token, "super_secret").toString( 49 | CryptoJS.enc.Utf8 50 | ); 51 | 52 | // Store decrypted access token in locals 53 | res.locals.accessToken = decrypted; 54 | 55 | // Store username in locals 56 | res.locals.username = username; 57 | 58 | // error handling 59 | if (!(decrypted || username)){ 60 | return next({ 61 | log: `Error caught in authContoller.getNameAndTokenFromCookies: Missing ${username}, decrypt: ${Boolean(decrypted)}`, 62 | msg: { err: 'authController.getNameAndTokenFromCookies: ERROR: Check server logs for details'} 63 | }); 64 | } 65 | 66 | return next(); 67 | }; 68 | 69 | // USE THIS MIDDLEWARE TO GET CHECKED REPO DATA 70 | authController.saveUserInfoAndRepos = ( 71 | req: Request, 72 | res: Response, 73 | next: NextFunction 74 | ): void => { 75 | // save username, access token and repos from request body 76 | const { username, accessToken, repos, projectName }: {username: string; accessToken: string; repos: string []; projectName: string} = req.body; 77 | res.locals.username = username; 78 | res.locals.accessToken = accessToken; 79 | res.locals.repos = repos; 80 | res.locals.projectName = projectName; 81 | 82 | // error handling 83 | if (!(username || accessToken || repos || projectName)){ 84 | return next({ 85 | log: `Error caught in authController.saveUserInfoAndRepos: Missing username: ${username}, accessToken:${Boolean(accessToken)} , repos:${repos}, or projectName ${projectName}`, 86 | msg: { err: `authController.saveUserInfoAndRepos: ERROR: Check server logs for details`} 87 | }); 88 | } 89 | 90 | return next(); 91 | }; 92 | 93 | module.exports = authController; -------------------------------------------------------------------------------- /src/server/controllers/configController.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import fs = require('fs'); 4 | import path = require('path'); 5 | 6 | const configController: any = {}; 7 | 8 | // for GET 9 | // check for folder 10 | configController.readJSONFromFile = async (req: Request, res: Response, next: NextFunction): Promise => { 11 | // Get JSON data from local file 12 | // save to res.locals.projects 13 | 14 | const filePath = path.join(__dirname, '../../user-projects/projects.json'); 15 | try { 16 | const data = fs.readFileSync(filePath, 'utf8') 17 | res.locals.projects = data; 18 | return next(); 19 | } catch (error) { 20 | return next({ 21 | log: `Error caught in configController- readJSONFromFile ${error}`, 22 | status: 500, 23 | msg: { 24 | err: 'configController.readFromJSONFile: ERROR: Check server log for details' 25 | } 26 | }) 27 | } 28 | 29 | }; 30 | 31 | // for POST 32 | configController.writeJSONToFile = async (req: Request, res: Response, next: NextFunction): Promise => { 33 | // takes in json from req.body; 34 | // writes req.body JSON to local file 35 | 36 | const filePath = path.join(__dirname, '../../user-projects/projects.json'); 37 | 38 | try{ 39 | fs.writeFileSync(filePath, JSON.stringify(req.body)); 40 | return next(); 41 | } catch (error) { 42 | return next({ 43 | log: `Error caught in configController- writeJSONToFile ${error}`, 44 | status: 500, 45 | msg: { 46 | err: 'configController.writeJSONToFile: ERROR: Check server log for details', 47 | } 48 | }) 49 | } 50 | }; 51 | 52 | module.exports = configController; -------------------------------------------------------------------------------- /src/server/controllers/dockerController.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import { exec } from 'child_process'; 4 | import fs = require('fs'); 5 | let portNo = 5001; 6 | let dockerPortNo = portNo; 7 | 8 | // WORKING ASSUMPTIONS: 9 | // All chosen directories will be stored in the same folder so that findDockerfiles.sh will access it 10 | // My Repos folder will exist before dockerController is run 11 | // Docker-Compose file will be stored in DockerLocal/myProjects 12 | // Dockerfiles will have docker in the name and no other files will have docker in the name 13 | // Dockerfiles will be located in the root folder of a project and so will be descriptive of the project(i.e. Container Name) 14 | 15 | const dockerController: any = {}; 16 | 17 | /** 18 | * @middlware getFilePaths 19 | * @description Insert and run (findDockerfile.sh) inside repo root directory to find all dockerfile build paths 20 | */ 21 | dockerController.getFilePaths = (req: Request, res: Response, next: NextFunction): void => { 22 | // the name of the project folder that you are storing all the repos inside DockerLocal/myProjects/ 23 | const projectFolder: string = req.body.projectName; 24 | const buildPathArray: string[] = []; 25 | const myShellScript = exec(`sh src/scripts/findDockerfiles.sh ${projectFolder}`); 26 | myShellScript.stdout.on('data', (data: string) => { 27 | const output = data; 28 | 29 | // checking for shell script error message output caused by lack of dockerfile inside active Project 30 | if (output === "missing repository with Dockerfile\n"){ 31 | return next({ 32 | log: `ERROR caught in dockerController.getFilePaths SHELL SCRIPT: ${data.slice(0, data.length-1)} in ${projectFolder}`, 33 | msg: { err: 'dockerContoller.getFilePaths: ERROR: Check server logs for details' } 34 | }); 35 | } 36 | 37 | // get filepaths from one long data string 38 | const filePathArray: string[] = output.split('\n').slice(0, -1); 39 | let buildPath: string; 40 | 41 | // make filepaths into buildpaths by removing the name of the file from the path 42 | // "src/server/happy/dockerfile" => "src/server/happy" 43 | for (const filePath of filePathArray) { 44 | for (let char = filePath.length - 1; char >= 0; char--) { 45 | if (filePath[char] === '/') { 46 | buildPath = filePath.substring(0, char); 47 | buildPathArray.push(buildPath); 48 | break; 49 | } 50 | } 51 | } 52 | res.locals.buildPathArray = buildPathArray; 53 | 54 | return next(); 55 | }); 56 | 57 | // shell script errror handling 58 | myShellScript.stderr.on('data', (data: string) => { 59 | return next({ 60 | log: `ERROR caught in dockerController.getFilePaths SHELL SCRIPT: ${data}`, 61 | msg: { err: 'dockerContoller.getFilePaths: ERROR: Check server logs for details' } 62 | }); 63 | }) 64 | 65 | } 66 | 67 | 68 | /** 69 | * @middlware getContainerNames 70 | * @description Use build paths to get Container Names 71 | */ 72 | dockerController.getContainerNames = (req: Request, res: Response, next: NextFunction): void => { 73 | const containerNameArray: string[] = []; 74 | const { buildPathArray } = res.locals; 75 | let containerName: string; 76 | // use folder names as the container name 77 | // "src/server/happy" => "happy" 78 | for (const buildPath of buildPathArray) { 79 | for (let char = buildPath.length - 1; char >= 0; char--) { 80 | if (buildPath[char] === '/') { 81 | containerName = buildPath.substring(char + 1); 82 | containerNameArray.push(containerName); 83 | break; 84 | } 85 | } 86 | } 87 | res.locals.containerNameArray = containerNameArray; 88 | 89 | // error handling 90 | if (!(buildPathArray || containerNameArray)){ 91 | return next({ 92 | log: `Error caught in dockerContoller.getContainerNames: Missing containerNameArray: ${Boolean(containerNameArray)} or buildPathArray: ${Boolean(buildPathArray)}`, 93 | msg: { err: `dockerController.getContainerNames: ERROR: Check server log for details. `} 94 | }); 95 | } 96 | 97 | return next(); 98 | } 99 | 100 | 101 | /** 102 | * @middlware getContainerNames 103 | * @description Use container names and build paths to create docker compose file 104 | */ 105 | dockerController.createDockerCompose = (req: Request, res: Response, next: NextFunction): void => { 106 | const projectFolder: string = req.body.projectName; 107 | const { buildPathArray } = res.locals; 108 | const { containerNameArray } = res.locals; 109 | let directory: string; 110 | let containerName: string; 111 | const composeFilePath = `./myProjects/${projectFolder}/docker-compose.yaml` 112 | 113 | /* writeFile will create a new docker compose file each time the controller is run 114 | so user can have leave-one-out functionality. Indentation is important in yaml files so it looks weird on purpose */ 115 | try { 116 | fs.writeFileSync(composeFilePath, `version: "3"\nservices:\n`); 117 | } catch(error){ 118 | return next({ 119 | log: `ERROR in writeFileSync in dockerController.createDockerCompose: ${error}`, 120 | msg: { err: 'dockerController.createDockerCompose: ERROR: Check server log for details' } 121 | }) 122 | } 123 | 124 | // Taking the 'checked' repositories and storing each name into an array 125 | const { repos } = res.locals; 126 | const repoArray = []; 127 | for (const repo of repos) { 128 | repoArray.push(repo.repoName); 129 | } 130 | 131 | // adding service information to docker compose file 132 | for (let i = 0; i < buildPathArray.length; i++) { 133 | directory = buildPathArray[i]; 134 | containerName = containerNameArray[i]; 135 | // only gets repos stored in the active Project that have dockerfiles (using buildPath to grab repo folder) 136 | const repoFolder = directory.slice(14 + projectFolder.length, directory.length - containerName.length - 1); 137 | 138 | // if the repo folder is in the 'checked' repositories array then add it to the docker compose file 139 | // will also ignore docker-compose file we create that is stored in root project folder 140 | if (repoArray.includes(repoFolder)) { 141 | portNo++; 142 | dockerPortNo++; 143 | 144 | // appending the file with the configurations for each service and error handling 145 | try{ 146 | fs.appendFileSync(composeFilePath, 147 | ` ${containerName}:\n build: "${directory}"\n ports:\n - ${portNo}:${dockerPortNo}\n`); 148 | } catch (error){ 149 | return next({ 150 | log: `ERROR in appendFileSync in dockerController.createDockerCompose: ${error}`, 151 | msg: { err: 'dockerController.createDockerCompose appendFile: ERROR: Check server log for details' } 152 | }); 153 | } 154 | 155 | } 156 | } 157 | 158 | return next(); 159 | } 160 | 161 | module.exports = dockerController; -------------------------------------------------------------------------------- /src/server/controllers/gitController.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | import { Request, Response, NextFunction } from "express"; 4 | import { Repo } from '../../types/types' 5 | 6 | // import helper function to execute shell scripts 7 | const execShellCommand = require("./helpers/shellHelper"); 8 | 9 | const gitController: any = {}; 10 | 11 | /** 12 | * @middleware Clone Github repositor(y/ies) using an SSH connection 13 | * @desc Clones git repository from github. Expects repository info to be in res.locals. 14 | */ 15 | gitController.cloneRepo = async ( 16 | req: Request, 17 | res: Response, 18 | next: NextFunction 19 | ): Promise => { 20 | 21 | const { repos, projectName }: {repos: Repo[]; projectName: string} = res.locals; 22 | const shellCommand = "./src/scripts/cloneRepo.sh"; 23 | 24 | // make an array of promises to clone all selected repos 25 | const promises = repos.map(async (currentRepo: {repoOwner: string; repoName: string}) => { 26 | const repoOwner = currentRepo.repoOwner; 27 | const repoName = currentRepo.repoName; 28 | 29 | if (!(repoOwner || repoName)){ 30 | return next({ 31 | log: `Error caught in gitContoller.cloneRepo: Missing repoOwner: ${repoOwner} or name ${repoName}`, 32 | msg: { err: 'gitContoller.cloneRepo: ERROR: Check server logs for more details'} 33 | }); 34 | } 35 | 36 | // shell script clones github repo using SSH connection 37 | const shellResp = await execShellCommand(shellCommand, [ 38 | repoOwner, 39 | repoName, 40 | projectName, 41 | ]); 42 | 43 | return shellResp; 44 | }); 45 | 46 | // execute cloning ALL repos 47 | await Promise.all(promises).catch( 48 | err => next({ 49 | log: `Error in shell scripts cloning repos in gitController.cloneRepo: ${err}`, 50 | msg: { 51 | err: 'gitContoller.cloneRepo: ERROR: Check server logs for more details' 52 | } 53 | }) 54 | ) 55 | 56 | return next(); 57 | }; 58 | 59 | module.exports = gitController; 60 | -------------------------------------------------------------------------------- /src/server/controllers/helpers/shellHelper.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | const exec = require("child_process").execFile; 4 | 5 | /** 6 | * @function Executes a shell command and return it as a Promise. 7 | * @param shellCommand {"./src/scripts/cloneRepo.sh"} 8 | * @param args {[repoOwner, repoName, projectName ]} 9 | * @return {Promise} 10 | */ 11 | 12 | function execShellCommand(shellCommand: string, args: string []): Promise { 13 | return new Promise((resolve, reject) => { 14 | exec( 15 | shellCommand, 16 | args, 17 | (error: string, stdout: string, stderr: string) => { 18 | if (error) { 19 | console.warn(error); 20 | } 21 | resolve(stdout? stdout : stderr); 22 | } 23 | ); 24 | }); 25 | } 26 | 27 | module.exports = execShellCommand; 28 | -------------------------------------------------------------------------------- /src/server/controllers/sshKeyController.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | import { Request, Response, NextFunction } from "express"; 4 | const fetch = require("node-fetch").default; 5 | const fs = require("fs"); 6 | 7 | // import helper function to execute shell scripts 8 | const execShellCommand = require("./helpers/shellHelper"); 9 | 10 | const sshKeyController: any = {}; 11 | 12 | /** 13 | * @middleware Create SSH key to be used for connection to clone/update github repos 14 | * @desc Executes sshKeygen.sh 15 | */ 16 | sshKeyController.createSSHkey = async( 17 | req: Request, 18 | res: Response, 19 | next: NextFunction 20 | ): Promise => { 21 | 22 | // shell script adds the github.com domain name to the known_hosts file using the ssh-keyscan command 23 | // script then clones github repo using SSH connection 24 | const shellCommand = "./src/scripts/sshKeygen.sh"; 25 | 26 | const shellResult = await execShellCommand(shellCommand, []) 27 | if (shellResult instanceof Error){ 28 | return next({ 29 | log: `Error caught in sshKeyController.createSSHkey: execShellCommand produces error: ${shellResult}`, 30 | msg: {err:`sshKeyController.createSSHkey: ERROR: Check server logs for details`}, 31 | }) 32 | } 33 | return next(); 34 | }; 35 | 36 | /** 37 | * @middleware Push SSH key to user's github account 38 | * @desc Uses a post request to github API 39 | */ 40 | sshKeyController.addSSHkeyToGithub = async ( 41 | req: Request, 42 | res: Response, 43 | next: NextFunction 44 | ): Promise => { 45 | const { accessToken, username } = res.locals; 46 | 47 | const sshKey = fs.readFileSync("./tmpKeys/dockerKey.pub", "utf8"); 48 | 49 | // create the request body which we will use to create the ssh key on Github 50 | const reqBody = JSON.stringify({ 51 | title: `${username}@DockerLocal`, 52 | key: sshKey, 53 | }); 54 | 55 | // send a post request to Github api to add the ssh key to user's keys 56 | const url = `https://api.github.com/user/keys`; 57 | const response = await fetch(url, { 58 | method: "POST", 59 | headers: { 60 | Authorization: `Bearer ${accessToken}`, 61 | "Content-type": "application/json", 62 | }, 63 | body: reqBody, 64 | }).catch((err: Error) => next({ 65 | log: `Error caught in sshKeyController.addSSHkeyToGithub: Issue sending post request to Github API: ${err}`, 66 | msg: {err:'sshKeyController.addSSHkeyToGithub: ERROR: Check server logs for details'} 67 | })); 68 | 69 | // converts the response body into JSON 70 | const jsonResponse = await response.json(); 71 | 72 | // save the key id from the response. this will be used to delete the key from Github after we are done using it 73 | const { id } = jsonResponse; 74 | res.locals.keyId = id; 75 | return next(); 76 | }; 77 | 78 | /** 79 | * @middleware Deletes public SSH key from user's github account and private/public keys from ./tmpKeys folder 80 | * @desc Uses a post request to github API 81 | */ 82 | sshKeyController.deleteSSHkey = async ( 83 | req: Request, 84 | res: Response, 85 | next: NextFunction 86 | ): Promise => { 87 | const { accessToken, keyId } = res.locals; 88 | 89 | const shellCommand = "./src/scripts/sshKeyDelete.sh"; 90 | 91 | await execShellCommand(shellCommand, []); 92 | 93 | const url = `https://api.github.com/user/keys/${keyId}`; 94 | await fetch(url, { 95 | method: "delete", 96 | headers: { 97 | Authorization: `Bearer ${accessToken}`, 98 | }, 99 | }).catch((err: Error) => next({ 100 | log: `Error caught in sshKeyController.deleteSSHkey: deleting key from github produces error: ${err}`, 101 | msg: {err: 'sshKeyController.deleteSSHkey: ERROR: Check server logs for details'} 102 | })); 103 | 104 | return next(); 105 | }; 106 | 107 | module.exports = sshKeyController; 108 | -------------------------------------------------------------------------------- /src/server/routes/api-route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | export {}; 3 | import express, { Request, Response } from "express"; 4 | const router = express.Router(); 5 | const authController = require("../controllers/authController"); 6 | const gitController = require("../controllers/gitController"); 7 | const sshKeyController = require("../controllers/sshKeyController"); 8 | require("dotenv/config"); 9 | 10 | router.post( 11 | "/clonerepos", 12 | authController.saveUserInfoAndRepos, 13 | sshKeyController.createSSHkey, 14 | sshKeyController.addSSHkeyToGithub, 15 | gitController.cloneRepo, 16 | sshKeyController.deleteSSHkey, 17 | (req: Request, res: Response) => res.status(201).json(res.locals.repos) 18 | ); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /src/server/routes/auth-route.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | import express, { Request, Response } from 'express'; 3 | const router = express.Router(); 4 | const passport = require('passport'); 5 | const authController = require('../controllers/authController'); 6 | declare const MAIN_WINDOW_WEBPACK_ENTRY: any; 7 | 8 | 9 | // using passport to authenticate the github 10 | router.get( 11 | '/github', 12 | passport.authenticate('github', { 13 | scope: ['user','repo','admin:public_key'], 14 | 'X-OAuth-Scopes': ['repo', 'user'], 15 | 'X-Accepted-OAuth-Scopes': ['repo','user'] 16 | })); 17 | 18 | // Github callback function (authentication) 19 | // if successful 20 | // save username/accessToken to cookies 21 | // redirect to /api/repos 22 | // if unsuccessful 23 | // redirect to /fail 24 | router.get( 25 | '/github/callback', 26 | passport.authenticate('github', { 27 | failureRedirect: '/fail' 28 | }), 29 | authController.saveAccessToken, 30 | (req: Request, res: Response) => { 31 | res.redirect(MAIN_WINDOW_WEBPACK_ENTRY) 32 | } 33 | ); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /src/server/routes/config-route.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | import express, { Request, Response } from 'express'; 3 | const router = express.Router(); 4 | const configController = require('../controllers/configController'); 5 | 6 | // /config endpoint to send back saved project data from local file 7 | router.get('/', 8 | configController.readJSONFromFile, 9 | (req: Request, res: Response) => res.status(200).send(JSON.parse(res.locals.projects))); 10 | 11 | // /config endpoint to write incoming json object to local file 12 | router.post('/', 13 | configController.writeJSONToFile, 14 | (req: Request, res: Response) => res.sendStatus(201)); 15 | 16 | module.exports = router; -------------------------------------------------------------------------------- /src/server/routes/docker-route.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | import express, { Request, Response } from 'express'; 3 | import path from 'path'; 4 | import fs = require('fs'); 5 | const router = express.Router(); 6 | const authController = require('../controllers/authController'); 7 | const dockerController = require('../controllers/dockerController'); 8 | 9 | // creates docker compose file using project folder names 10 | router.post( 11 | '/', 12 | authController.saveUserInfoAndRepos, 13 | dockerController.getFilePaths, 14 | dockerController.getContainerNames, 15 | dockerController.createDockerCompose, 16 | (req: Request, res: Response) => { 17 | // gets project folder name to provide docker compose correct file path to front end 18 | // so end user knows where to find their docker compose file in their system 19 | const projectFolder = req.body.projectName; 20 | // send docker compose filepath and yaml file to front end 21 | const composeFilePath = path.resolve(__dirname, `../../myProjects/${projectFolder}/docker-compose.yaml`); 22 | const fileAsJSON = fs.readFileSync(composeFilePath, "utf8"); 23 | const payload = { 24 | path: composeFilePath, 25 | file: fileAsJSON 26 | } 27 | res.status(200).send(payload); 28 | } 29 | ) 30 | 31 | module.exports = router; -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | export {}; 3 | import express, { Request, Response, NextFunction, ErrorRequestHandler } from "express"; 4 | import path = require("path"); 5 | // const passportSetup = require("../../src/server/config/passport-setup"); 6 | // import passport = require("passport"); 7 | require("dotenv/config"); 8 | const app = express(); 9 | const cookieParser = require("cookie-parser"); 10 | const cors = require("cors"); 11 | 12 | // disables 'powered by express' header 13 | app.disable('x-powered-by') 14 | 15 | // only allow CORS from react front end 16 | declare const MAIN_WINDOW_WEBPACK_ENTRY: string; 17 | const reactOrigin = MAIN_WINDOW_WEBPACK_ENTRY.substring(0, MAIN_WINDOW_WEBPACK_ENTRY.lastIndexOf("/")) 18 | const corsOptions = { origin: reactOrigin} 19 | app.use(cors(corsOptions)); 20 | 21 | // Bring in routes 22 | const authRoute = require('../../src/server/routes/auth-route'); 23 | const apiRoute = require('../../src/server/routes/api-route'); 24 | const dockerRoute = require('../../src/server/routes/docker-route'); 25 | const configRoute = require("../../src/server/routes/config-route"); 26 | 27 | // Body Parsing Middleware 28 | app.use(express.json()); 29 | app.use(express.urlencoded({ extended: false })); 30 | // app.use(passport.initialize()); 31 | app.use(cookieParser()); 32 | 33 | // Use routes 34 | app.use('/auth', authRoute); 35 | app.use('/api', apiRoute); 36 | app.use('/docker', dockerRoute); 37 | app.use("/config", configRoute); 38 | 39 | // Serve static files 40 | app.use(express.static("assets")); 41 | 42 | // Home endpoint 43 | app.get("/", (req: Request, res: Response) => 44 | res.sendFile(path.resolve(__dirname, "../../src/index.html")) 45 | ); 46 | 47 | // Handle redirections 48 | app.get("*", (req: Request, res: Response) => res.sendStatus(200)); 49 | 50 | // Failed auth redirect 51 | app.get("/fail", (req: Request, res: Response) => 52 | res.status(200).send("❌ FAILURE TO AUTHENTICATE ❌") 53 | ); 54 | 55 | // Global Error handler 56 | app.use( 57 | ( 58 | err: ErrorRequestHandler, 59 | req: Request, 60 | res: Response, 61 | next: NextFunction 62 | ) => { 63 | // Set up default error 64 | const defaultError = { 65 | log: "Error caught in global error handler", 66 | status: 500, 67 | msg: { 68 | err, 69 | }, 70 | }; 71 | 72 | // Update default error message with provided error if there is one 73 | const output = Object.assign(defaultError, err); 74 | console.log(output.log); 75 | res.send(output.msg); 76 | } 77 | ); 78 | 79 | const PORT = 3001; 80 | 81 | app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); 82 | -------------------------------------------------------------------------------- /src/types/types.tsx: -------------------------------------------------------------------------------- 1 | import { SetStateAction, Dispatch } from 'react'; 2 | 3 | export type Project = { 4 | projectName: string; 5 | projectId: string; 6 | projectRepos: readonly Repo[]; 7 | }; 8 | 9 | export type projectListActions = { 10 | type: 11 | | 'addProject' 12 | | 'deleteProject' 13 | | 'addRepo' 14 | | 'deleteRepo' 15 | | 'toggleRepo'; 16 | payload: readonly Project[]; 17 | }; 18 | 19 | export type Repo = { 20 | repoName: string; 21 | repoId: string; 22 | repoOwner: string; 23 | isIncluded?: boolean; 24 | }; 25 | 26 | export type User = { 27 | userName: string; 28 | userId: string; 29 | }; 30 | 31 | export type ProjectReposType = { 32 | personal: readonly Repo[]; 33 | organizations: readonly Repo[]; 34 | collaborations: readonly Repo[]; 35 | }; 36 | 37 | export type RepoSearchListItemProps = { 38 | repo: Repo; 39 | selectedRepos: readonly Repo[]; 40 | setSelectedRepos: Dispatch>; 41 | projectList: readonly Project[]; 42 | activeProject: string; 43 | }; 44 | 45 | export type AddReposProps = { 46 | showAddRepos: boolean; 47 | setShowAddRepos: Dispatch>; 48 | activeProject: string; 49 | projectList: readonly Project[]; 50 | dispatch: React.Dispatch; 51 | }; 52 | 53 | export type ProjectPageProps = { 54 | activeProject: string; 55 | userInfo: User; 56 | projectList: readonly Project[]; 57 | dispatch: React.Dispatch; 58 | }; 59 | 60 | export type ProjectRepoListItemProps = { 61 | activeProject: string; 62 | repo: Repo; 63 | projectList: readonly Project[]; 64 | dispatch: React.Dispatch; 65 | }; 66 | 67 | export type RepoResponseType = { 68 | personal: readonly Repo[]; 69 | organizations: readonly Repo[]; 70 | collaborations: readonly Repo[]; 71 | }; 72 | 73 | export type ProjectSideBarProps = { 74 | setShowProjectSidebarModal: Dispatch>; 75 | projectList: readonly Project[]; 76 | dispatch: React.Dispatch; 77 | setActiveProject: Dispatch>; 78 | }; 79 | 80 | export type SidebarButtonProps = Project & { 81 | activeProject: string; 82 | setActiveProject: Dispatch>; 83 | }; 84 | 85 | export type SidebarProps = { 86 | projectList: readonly Project[]; 87 | dispatch: React.Dispatch; 88 | activeProject: string; 89 | setActiveProject: Dispatch>; 90 | }; 91 | 92 | export type HomeProps = { 93 | userInfo: User; 94 | setUserInfo: Dispatch>; 95 | }; 96 | 97 | export type CloningReposModalProps = { 98 | showCloningReposModal: boolean; 99 | setShowCloningReposModal: Dispatch>; 100 | }; 101 | 102 | export type ComposeFileModalProps = { 103 | setShowComposeModal: Dispatch>; 104 | activeProject: string; 105 | projectList: readonly Project[]; 106 | composeFileData: { text: string; path: string }; 107 | }; 108 | 109 | export type ComposeFile = { 110 | text: string; 111 | path: string; 112 | }; 113 | 114 | export type SignInProps = { 115 | isLoggedIn: boolean; 116 | setIsLoggedIn: Dispatch>; 117 | setUserInfo: Dispatch>; 118 | } 119 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "outDir": "dist", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "paths": { 15 | "*": [ 16 | "node_modules/*" 17 | ] 18 | } 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "trailing-comma": [ false ] 9 | }, 10 | "rulesDirectory": [] 11 | } -------------------------------------------------------------------------------- /user-projects/empty: -------------------------------------------------------------------------------- 1 | empty -------------------------------------------------------------------------------- /webpack.main.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * This is the main entry point for your application, it's the first file 4 | * that runs in the main process. 5 | */ 6 | entry: './src/index.ts', 7 | // Put your normal webpack config below here 8 | module: { 9 | rules: require('./webpack.rules'), 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'] 13 | }, 14 | }; -------------------------------------------------------------------------------- /webpack.plugins.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | 3 | module.exports = [ 4 | new ForkTsCheckerWebpackPlugin() 5 | ]; 6 | -------------------------------------------------------------------------------- /webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | const rules = require('./webpack.rules'); 2 | const plugins = require('./webpack.plugins'); 3 | 4 | rules.push({ 5 | test: /\.s[ac]ss$/i, 6 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'sass-loader' }], 7 | }); 8 | 9 | module.exports = { 10 | module: { 11 | rules, 12 | }, 13 | plugins: plugins, 14 | resolve: { 15 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'] 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /webpack.rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Add support for native node modules 3 | { 4 | test: /\.node$/, 5 | use: 'node-loader', 6 | }, 7 | { 8 | test: /\.(m?js|node)$/, 9 | parser: { amd: false }, 10 | use: { 11 | loader: '@marshallofsound/webpack-asset-relocator-loader', 12 | options: { 13 | outputAssetBase: 'native_modules', 14 | }, 15 | }, 16 | }, 17 | { 18 | test: /\.tsx?$/, 19 | exclude: /(node_modules|\.webpack)/, 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | transpileOnly: true 24 | } 25 | } 26 | }, 27 | { 28 | test: /\.(jpg|png)$/, 29 | use: { 30 | loader: 'url-loader', 31 | } 32 | }, 33 | ]; 34 | --------------------------------------------------------------------------------