├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── add-edit-team-meeting-time.md │ ├── add-remove-pm-admin-access.md │ ├── address-warnings-for-rule-.md │ ├── blank-issue.md │ ├── bug-report.yml │ └── update-package---name-of-package-.md ├── pull_request_template.md └── workflows │ ├── all-PRs.yaml │ ├── all-merges.yaml │ ├── aws-backend-deploy.yml │ ├── aws-frontend-deploy.yml │ ├── pr-instructions.yml │ ├── waiting-to-merge.yaml │ └── wr-pr-instructions.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .prettierrc ├── Dockerfile.api ├── Dockerfile.dev ├── Dockerfile.prod ├── README.md ├── app.js ├── config │ ├── auth.config.js │ ├── auth.config.test.js │ ├── database.config.js │ └── index.js ├── controllers │ ├── email.controller.js │ ├── email.controller.test.js │ ├── event.controller.js │ ├── event.controller.test.js │ ├── healthCheck.controller.js │ ├── index.js │ ├── project.controller.js │ ├── project.controller.test.js │ ├── recurringEvent.controller.js │ ├── user.controller.js │ └── user.controller.test.js ├── globalConfig.json ├── jest.config.js ├── jest.setup.js ├── middleware │ ├── auth.middleware.js │ ├── errorhandler.middleware.js │ ├── index.js │ ├── token.middleware.js │ └── user.middleware.js ├── models │ ├── checkIn.model.js │ ├── event.model.js │ ├── index.js │ ├── project.model.js │ ├── projectTeamMember.model.js │ ├── question.model.js │ ├── recurringEvent.model.js │ ├── role.model.js │ ├── timeTracker.model.js │ └── user.model.js ├── package.json ├── routers │ ├── auth.router.js │ ├── checkIns.router.js │ ├── checkUser.router.js │ ├── checkUser.router.test.js │ ├── events.router.js │ ├── grantpermission.router.js │ ├── healthCheck.router.js │ ├── projectTeamMembers.router.js │ ├── projects.router.js │ ├── questions.router.js │ ├── recurringEvents.router.js │ ├── slack.router.js │ ├── success.router.js │ ├── users.router.js │ └── users.router.test.js ├── server.js ├── setup-test.js ├── test │ └── old-tests │ │ ├── auth.router.test.js │ │ ├── events.router.test.js │ │ ├── projects.router.test.js │ │ └── users.router.test.js ├── validators │ ├── index.js │ └── user.api.validator.js ├── workers │ ├── closeCheckins.js │ ├── createRecurringEvents.js │ ├── createRecurringEvents.test.js │ ├── lib │ │ └── generateEventData.js │ ├── openCheckins.js │ └── slackbot.js └── yarn.lock ├── client ├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── Dockerfile.client ├── Dockerfile.dev ├── Dockerfile.prod ├── index.html ├── nginx │ └── default.conf ├── package.json ├── public │ ├── bg-image-pier.webp │ ├── bg-image-skyline.webp │ ├── bg-image-sunset.webp │ ├── favicon.ico │ ├── hflalogo.png │ ├── logo180.png │ ├── logo192.png │ ├── logo310.png │ ├── manifest.json │ ├── projectleaderdashboard │ │ ├── check.png │ │ ├── github.png │ │ ├── googledrive.png │ │ └── slack.png │ └── robots.txt ├── src │ ├── App.jsx │ ├── App.scss │ ├── api │ │ ├── EventsApiService.js │ │ ├── ProjectApiService.js │ │ ├── RecurringEventsApiService.js │ │ ├── UserApiService.js │ │ └── auth.js │ ├── common │ │ ├── datepicker │ │ │ └── index.scss │ │ └── tabs │ │ │ ├── index.jsx │ │ │ ├── index.scss │ │ │ └── tab.jsx │ ├── components │ │ ├── ChangesModal.jsx │ │ ├── DashboardUsers.jsx │ │ ├── ErrorContainer.jsx │ │ ├── Footer.jsx │ │ ├── Form.jsx │ │ ├── Header.jsx │ │ ├── Leaderboard.jsx │ │ ├── Leaderboard.test.jsx │ │ ├── Navbar.jsx │ │ ├── ProjectForm.jsx │ │ ├── ReadyEvents.jsx │ │ ├── __snapshots__ │ │ │ ├── Leaderboard.test.js.snap │ │ │ └── Leaderboard.test.jsx.snap │ │ ├── admin │ │ │ ├── dashboard │ │ │ │ ├── index.jsx │ │ │ │ └── index.scss │ │ │ ├── donutChart.jsx │ │ │ ├── donutChartContainer.jsx │ │ │ ├── donutChartLoading.jsx │ │ │ ├── eventOverview.jsx │ │ │ └── reports │ │ │ │ ├── index.jsx │ │ │ │ └── index.scss │ │ ├── auth │ │ │ ├── Auth.jsx │ │ │ └── HandleAuth.jsx │ │ ├── dashboard │ │ │ ├── AddTeamMember.jsx │ │ │ ├── AttendeeTable.jsx │ │ │ ├── AttendeeTableRow.jsx │ │ │ ├── DashboardButton.jsx │ │ │ ├── ProjectInfo.jsx │ │ │ ├── RosterTable.jsx │ │ │ └── RosterTableRow.jsx │ │ ├── data.js │ │ ├── manageProjects │ │ │ ├── addProject.jsx │ │ │ ├── createNewEvent.jsx │ │ │ ├── editMeetingTimes.jsx │ │ │ ├── editProject.jsx │ │ │ ├── editableField.jsx │ │ │ ├── editableMeeting.jsx │ │ │ ├── eventForm.jsx │ │ │ ├── selectProject.jsx │ │ │ └── utilities │ │ │ │ ├── addDurationToTime.js │ │ │ │ ├── findNextDayOccuranceOfDay.js │ │ │ │ ├── readableEvent.js │ │ │ │ ├── tests │ │ │ │ └── addDurationToTime.test.js │ │ │ │ ├── timeConvertFromForm.js │ │ │ │ ├── validateEditableField.js │ │ │ │ └── validateEventForm.js │ │ ├── parts │ │ │ ├── boxes │ │ │ │ └── TitledBox.jsx │ │ │ └── form │ │ │ │ └── ValidatedTextField.jsx │ │ ├── presentational │ │ │ ├── CheckInButtons.jsx │ │ │ ├── CreateNewProfileButton.jsx │ │ │ ├── DashboardReport.jsx │ │ │ ├── newUserForm.jsx │ │ │ ├── profile │ │ │ │ ├── ProfileOption.jsx │ │ │ │ ├── UserEvents.jsx │ │ │ │ ├── UserTable.jsx │ │ │ │ └── UserTeams.jsx │ │ │ ├── projectDashboardContainer.jsx │ │ │ ├── returnUserForm.jsx │ │ │ └── upcomingEvent.jsx │ │ └── user-admin │ │ │ ├── AddNewProject.jsx │ │ │ ├── EditUsers.jsx │ │ │ ├── UserManagement.jsx │ │ │ └── UserPermissionSearch.jsx │ ├── context │ │ ├── authContext.jsx │ │ ├── snackbarContext.jsx │ │ └── userContext.jsx │ ├── fonts │ │ ├── aliseo-noncommercial-webfont.woff │ │ └── aliseo-noncommercial-webfont.woff2 │ ├── hooks │ │ ├── useAuth.js │ │ └── withAuth.jsx │ ├── index.jsx │ ├── index.scss │ ├── logo.svg │ ├── pages │ │ ├── CheckInForm.jsx │ │ ├── EmailSent.jsx │ │ ├── Event.jsx │ │ ├── Events.jsx │ │ ├── HealthCheck.jsx │ │ ├── Home.jsx │ │ ├── ManageProjects.jsx │ │ ├── NewUser.jsx │ │ ├── ProjectLeaderDashboard.jsx │ │ ├── ProjectList.jsx │ │ ├── ReturningUser.jsx │ │ ├── SecretPassword.jsx │ │ ├── Success.jsx │ │ ├── UserAdmin.jsx │ │ ├── UserDashboard.jsx │ │ ├── UserPermission.jsx │ │ ├── UserPermissionSearch.jsx │ │ ├── UserProfile.jsx │ │ ├── UserWelcome.jsx │ │ └── Users.jsx │ ├── sass │ │ ├── AddNew.scss │ │ ├── AddTeamMember.scss │ │ ├── AdminLogin.scss │ │ ├── CheckIn.scss │ │ ├── Dashboard.scss │ │ ├── DashboardUsers.scss │ │ ├── ErrorContainer.scss │ │ ├── Event.scss │ │ ├── Events.scss │ │ ├── Footer.scss │ │ ├── Form.scss │ │ ├── Headers.scss │ │ ├── Home.scss │ │ ├── MagicLink.scss │ │ ├── ManageProjects.scss │ │ ├── Navbar.scss │ │ ├── ProjectLeaderDashboard.module.scss │ │ ├── ReadyEvents.scss │ │ ├── UserAdmin.scss │ │ ├── UserProfile.scss │ │ └── Users.scss │ ├── serviceWorker.js │ ├── services │ │ ├── projectTeamMember-api-service.js │ │ └── user.service.js │ ├── setupProxy.js │ ├── svg │ │ ├── 22.gif │ │ ├── Icon_Clock.svg │ │ ├── Icon_Edit.svg │ │ ├── Icon_Location.svg │ │ ├── Icon_Plus.svg │ │ ├── PlusIcon.svg │ │ ├── hflalogo.png │ │ ├── hflalogo.svg │ │ └── hflalogo_white.png │ ├── theme │ │ ├── index.js │ │ └── palette.js │ └── utils │ │ ├── authUtils.js │ │ ├── blacklist.js │ │ ├── createClockHours.js │ │ ├── endpoints.js │ │ ├── globalSettings.js │ │ └── stringUtils.js ├── vite.config.mjs └── yarn.lock ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── admin_login.js │ └── home_page.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docker-compose.yml ├── github-actions └── pr-instructions │ ├── create-instruction.js │ ├── post-comment.js │ └── pr-instructions-template.md ├── nginx ├── .dockerignore ├── Dockerfile.nginx └── default.conf ├── package.json ├── project-edit-info.md ├── utils └── local_db_backup.sh ├── vrms.md └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { "requireConfigFile": "false" }, 4 | "babelOptions": { "configFile": "./.babelrc" } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and handle it automatically 2 | * text=auto 3 | 4 | # Perform LF normalization for .scss files due to resolve-url-loader issue 5 | *.scss text eol=lf 6 | *.scss text eol=lf 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-edit-team-meeting-time.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Add/edit team meeting time 3 | about: Use this template to request a meeting time change for your project 4 | title: Meeting time change request for [Project Name] 5 | labels: '1 week change request, role: Product, time-sensitive' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Overview 11 | We need to add/change the team meeting time for [YOUR PROJECT NAME HERE] so it will appear correctly on hackforla.org 12 | 13 | ### Is your team meeting team already displayed on hackforla.org? 14 | - [ ] yes 15 | - [ ] no 16 | 17 | ### List all of your meeting times (PST time ONLY) (Name of project, name of team meeting, day of week, start time am/pm, duration): 18 | EXAMPLE: 19 | - Home Unite US, Management, Saturday 12:30pm, 1 hour 20 | - Home Unite US, Team meeting, Tuesday 6pm, 2 hours 21 | 22 | Note: Please explicitly state if a meeting time that is currently displayed is to be deleted. 23 | 24 | ### What is your project page URL? 25 | https://www.hackforla.org/projects/vrms 26 | 27 | ### Who should we contact on your team (via Slack) for user acceptance testing 28 | Slack Channel #name: 29 | Slack @handle: 30 |
31 | ### Instructions 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-remove-pm-admin-access.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Add/Remove PM Admin Access 3 | about: VRMS admin team members are currently adding or removing product managers manually. 4 | This issue is how PMs can request changes to access on VRMS. 5 | title: 'Add/Remove PM Admin Access: [name of project]' 6 | labels: 'p-feature: Add/Remove PM access, role: Product, time-sensitive' 7 | assignees: '' 8 | 9 | --- 10 | 11 | Person who is staying on the team or leaving a team, person adds ticket. 12 | 13 | ### Required information 14 | - Name of PM requesting changes (your name): 15 | - Slack handle: 16 | 17 | #### People you are removing 18 | - Name: 19 | - Slack Handle: 20 | - Email: 21 | 22 | #### People you are adding 23 | - Name: 24 | - Slack Handle: 25 | - Email: 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/address-warnings-for-rule-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Address warnings for rule:' 3 | about: Describe this issue template's purpose here. 4 | title: 'Address warnings for rule:' 5 | labels: 'role: Front End, size: 1pt' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # v0.4 Setup Infrastructure 11 | 12 | This ticket addresses eslint warnings for rules copied from the v0.3 branch. 13 | 14 | # Instructions 15 | - [ ] Review the rule documentation for the rule (see reference pages below) 16 | - [ ] Navigate the cli to the `client` folder 17 | - [ ] Run the lint script (e.g. `npm run lint`) 18 | - [ ] Review the results, noting all instances where the rule is referenced. 19 | - [ ] Address each warning using one of the following options, according to your best judgement: 20 | - use the `npx eslint --fix --rule {rule to fix}` command (not available for all rules) 21 | - correct the code according to the recommendations listed in the rule documentation 22 | - disable the rule for the reported line (NOTE: only use the disable instruction for a single line. Do NOT use the more global options). Include a comment for why this decision was made. 23 | - raise the rule as a topic to discuss with the group in the next meeting. We can then decide whether to edit or drop the rule, add more global disable flags, etc. 24 | 25 | # Acceptance Criteria 26 | Running the lint script should result in an output that does not mention this rule (meaning all instances either corrected or marked for ignore). 27 | 28 | # Reference 29 | - [eslint rules](https://eslint.org/docs/rules/) 30 | - [eslint cli reference](https://eslint.org/docs/user-guide/command-line-interface) 31 | - [jest plugin](https://github.com/jest-community/eslint-plugin-jest#readme) 32 | - [prettier plugin](https://github.com/prettier/eslint-config-prettier) 33 | - [Air Bnb plugin](https://github.com/airbnb/javascript) 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Issue 3 | about: Consistent formatting make Issues concise and easy to navigate 4 | title: '' 5 | labels: 'feature: missing, milestone: missing, role: Missing, size: missing' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Overview 11 | REPLACE THIS TEXT - Text here that clearly states the purpose of this issue in 2 sentences or less. 12 | 13 | ### Action Items 14 | REPLACE THIS TEXT - If this is the beginning of the task this is most likely something to be researched and documented. 15 | 16 | REPLACE THIS TEXT - If the issue has already been researched, and the course of action is clear, this will describe the steps. However, if the steps can be divided into tasks for more than one person, we recommend dividing it up into separate issues or assigning it as a pair programming task. 17 | 18 | ### Resources/Instructions 19 | REPLACE THIS TEXT - If there is a website that has documentation that helps with this issue provide the link(s) here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug Report' 2 | description: 'Bug report HackforLA issue form.' 3 | 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Describe the Bug 9 | description: Clearly state the bug 10 | validations: 11 | required: true 12 | - type: dropdown 13 | id: feature 14 | attributes: 15 | label: Feature 16 | description: Select a feature related to the bug 17 | options: 18 | - AWS 19 | - Brand 20 | - Login 21 | - Users 22 | - Agenda 23 | - Check in 24 | - Database 25 | - Dashboard 26 | - Marketing 27 | - Onboarding 28 | - Repo Update 29 | - Documentation 30 | - GitHub Actions 31 | - GitHub Hygiene 32 | - Infrastructure 33 | - Package Update 34 | - ESLint Warnings 35 | - Form validation 36 | - Recurring Events 37 | - Project Management 38 | - Account Setup Automation 39 | - Meeting Time Change Ticket 40 | - Add/Remove PM access 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: replicate 45 | attributes: 46 | label: How to Replicate 47 | description: Clearly state how to replicate the bug 48 | validations: 49 | required: true 50 | - type: dropdown 51 | id: notification 52 | attributes: 53 | label: Request notification after bug squashing 54 | description: Request notification after a bug is squashed 55 | options: 56 | - Yes 57 | - No 58 | validations: 59 | required: true 60 | - type: textarea 61 | id: resource 62 | attributes: 63 | label: Resources/Information 64 | description: Include any important resources/information -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/update-package---name-of-package-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Update package: [name of package]' 3 | about: Describe this issue template's purpose here. 4 | title: 'Update package: name of package' 5 | labels: 'housekeeping, role: Front End, size: 1pt' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Overview 11 | We need to update the [name of package] package that require major version changes to address security audit warnings 12 | 13 | ### Extra info 14 | We are doing this individually to manage scope of changes per PR 15 | 16 | ### Action Items 17 | - [ ] developer to update to the latest version of the package indicated 18 | - [ ] Ensure the project builds and runs 19 | - [ ] Ensure that all unit tests pass 20 | - [ ] Ensure that there are no lint errors 21 | 22 | ### Resources/Instructions 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes #replace_this_text_with_the_issue_number 2 | 3 | ### What changes did you make and why did you make them ? 4 | 5 | - 6 | - 7 | - 8 | 9 | ### Screenshots of Proposed Changes Of The Website (if any, please do not screen shot code changes) 10 | 11 | 12 | 13 |
14 | Visuals before changes are applied 15 | 16 | ![image](Paste_Your_Image_Link_Here_After_Attaching_Files) 17 | 18 |
19 | 20 |
21 | Visuals after changes are applied 22 | 23 | ![image](Paste_Your_Image_Link_Here_After_Attaching_Files) 24 | 25 |
26 | -------------------------------------------------------------------------------- /.github/workflows/all-merges.yaml: -------------------------------------------------------------------------------- 1 | name: Build VMRS App 2 | on: 3 | pull_request: 4 | branches: [development] 5 | paths-ignore: 6 | - "*.md" 7 | 8 | jobs: 9 | push-docker-container: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.merged == true 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v1 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | - name: Login to DockerHub 19 | uses: docker/login-action@v1 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | - name: Build and push client 24 | id: client_build 25 | uses: docker/build-push-action@v2 26 | with: 27 | context: ./client 28 | file: ./client/Dockerfile.client 29 | platforms: linux/amd64 30 | target: client-production 31 | stdin_open: true 32 | push: true 33 | tags: | 34 | vrmsdeploy/vrms:client 35 | - name: Client digest 36 | run: echo ${{ steps.client_build.outputs.digest }} 37 | - name: Build and push backend 38 | id: backend_build 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: ./backend 42 | file: ./backend/Dockerfile.api 43 | platforms: linux/amd64 44 | target: api-production 45 | stdin_open: true 46 | push: true 47 | tags: | 48 | vrmsdeploy/vrms:backend 49 | - name: Backend digest 50 | run: echo ${{ steps.backend_build.outputs.digest }} 51 | - name: Build and push nginx 52 | uses: docker/build-push-action@v2 53 | id: nginx_build 54 | with: 55 | context: ./nginx 56 | file: ./nginx/Dockerfile.nginx 57 | platforms: linux/amd64 58 | push: true 59 | tags: | 60 | vrmsdeploy/vrms:nginx 61 | - name: Nginx digest 62 | run: echo ${{ steps.nginx_build.outputs.digest }} 63 | -------------------------------------------------------------------------------- /.github/workflows/pr-instructions.yml: -------------------------------------------------------------------------------- 1 | name: Add Pull Request Instructions 2 | on: 3 | pull_request: 4 | types: [opened] 5 | branches: 6 | - 'development' 7 | 8 | jobs: 9 | Add-Pull-Request-Instructions: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | # Create the message to post 15 | - name: Create Instruction 16 | uses: actions/github-script@v4 17 | id: instruction 18 | with: 19 | script: | 20 | const script = require('./github-actions/pr-instructions/create-instruction.js') 21 | const instruction = script({g: github, c: context}) 22 | return JSON.stringify({ instruction: instruction, issueNum: context.payload.number }) 23 | 24 | # Create an artifact with the message 25 | - name: Create Artifacts 26 | run: | 27 | mkdir -p addingPrInstructions/artifact 28 | echo ${{ steps.instruction.outputs.result }} > addingPrInstructions/artifact/artifact.txt 29 | 30 | - name: Upload Artifacts 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: adding-pr-instructions-artifact 34 | path: addingPrInstructions/artifact/ 35 | -------------------------------------------------------------------------------- /.github/workflows/waiting-to-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Waiting to Merge 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened, labeled, unlabeled] 6 | 7 | jobs: 8 | do-not-merge: 9 | if: ${{ contains(github.event.*.labels.*.name, 'waiting to merge') }} 10 | name: Prevent Merging 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check for label 14 | run: | 15 | echo "Pull request is labeled as 'waiting to merge'" 16 | echo "This workflow fails so that the pull request cannot be merged" 17 | exit 1 18 | -------------------------------------------------------------------------------- /.github/workflows/wr-pr-instructions.yml: -------------------------------------------------------------------------------- 1 | name: WR Add Pull Request Instructions 2 | on: 3 | workflow_run: 4 | workflows: ["Add Pull Request Instructions"] 5 | types: [completed] 6 | 7 | jobs: 8 | Last-Workflow-Success: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 11 | steps: 12 | - name: Download artifact 13 | uses: actions/github-script@v4 14 | with: 15 | script: | 16 | // Retrieve metadata about the artifacts of the last workflow 17 | // https://octokit.github.io/rest.js/v18#actions-list-workflow-run-artifacts 18 | const artifacts = await github.actions.listWorkflowRunArtifacts({ 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | run_id: context.payload.workflow_run.id, 22 | }); 23 | const artifactData = artifacts.data.artifacts[0] 24 | 25 | // Download artifact with GET API 26 | // https://octokit.github.io/rest.js/v18#actions-download-artifact 27 | var download = await github.actions.downloadArtifact({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | artifact_id: artifactData.id, 31 | archive_format: 'zip', 32 | }); 33 | const fs = require('fs'); 34 | fs.writeFileSync('${{github.workspace}}/artifact.zip', Buffer.from(download.data)); 35 | - run: unzip artifact.zip 36 | 37 | - uses: actions/github-script@v4 38 | id: artifact 39 | with: 40 | script: | 41 | // Retrieve pull request and issue number from downloaded artifact 42 | const fs = require('fs') 43 | const artifact = fs.readFileSync('artifact.txt') 44 | const artifactJSON = JSON.parse(artifact); 45 | return artifactJSON 46 | 47 | - uses: actions/checkout@v4 48 | # Create the message to post 49 | - name: Post Comment 50 | uses: actions/github-script@v4 51 | with: 52 | script: | 53 | const artifact = ${{ steps.artifact.outputs.result }}; 54 | 55 | const script = require('./github-actions/pr-instructions/post-comment.js') 56 | script({g: github, c: context}, artifact) 57 | 58 | Last-Workflow-Failure: 59 | runs-on: ubuntu-latest 60 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 61 | steps: 62 | - name: Failed Run 63 | run: echo "The previous GitHub Action failed. Please check the logs for the previous action." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | npm-debug.log 4 | .DS_Store 5 | /*.env 6 | client/.env 7 | .env 8 | /.idea 9 | test.db 10 | videos/ 11 | screenshots/ 12 | vrms.code-workspace 13 | .vscode/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | VRMS is governed by the [Hack for LA Code of Conduct](https://www.hackforla.org/code-of-conduct/) which applies to any interaction on our VRMS slack channel (inside the HackforLA Slack workspace), direct slack messages, github org or repository, or any other communication medium. 2 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | .npm 4 | docker-compose*.yml 5 | npm-debug.log* 6 | Dockerfile.* 7 | node_modules 8 | *.sh -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Contact the VRMS team for the below values 2 | DATABASE_URL= 3 | CUSTOM_REQUEST_HEADER= 4 | SLACK_OAUTH_TOKEN= 5 | SLACK_BOT_TOKEN= 6 | SLACK_TEAM_ID= 7 | SLACK_CHANNEL_ID= 8 | SLACK_CLIENT_ID= 9 | SLACK_CLIENT_SECRET= 10 | SLACK_SIGNING_SECRET= 11 | BACKEND_PORT=4000 12 | REACT_APP_PROXY=http://localhost:${BACKEND_PORT} 13 | GMAIL_CLIENT_ID= 14 | GMAIL_SECRET_ID= 15 | GMAIL_REFRESH_TOKEN= 16 | GMAIL_EMAIL=vrms.signup@gmail.com 17 | MAILHOG_PORT=1025 18 | MAILHOG_USER=user 19 | MAILHOG_PASSWORD= 20 | NODE_ENV=test 21 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": false, 6 | "codeFrame": false 7 | }, 8 | "plugins": ["@babel"], 9 | "extends": ["airbnb", "prettier"], 10 | "env": { 11 | "browser": true, 12 | "jest": true 13 | }, 14 | "rules": { 15 | "max-len": ["error", { "code": 100 }], 16 | "prefer-promise-reject-errors": ["off"], 17 | "react/jsx-filename-extension": ["off"], 18 | "react/prop-types": ["warn"], 19 | "no-return-assign": ["off"] 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "999.999.999" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /backend/Dockerfile.api: -------------------------------------------------------------------------------- 1 | FROM node:18.12.0 AS api-development 2 | RUN mkdir /srv/backend 3 | WORKDIR /srv/backend 4 | RUN mkdir -p node_modules 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install --pure-lockfile 7 | COPY . . 8 | 9 | FROM node:18.12.0 AS api-test 10 | RUN mkdir /srv/backend 11 | WORKDIR /srv/backend 12 | COPY package.json yarn.lock ./ 13 | RUN yarn install --silent 14 | RUN mkdir -p node_modules 15 | 16 | FROM node:18.12.0-slim AS api-production 17 | EXPOSE 4000 18 | USER node 19 | WORKDIR /srv/backend 20 | COPY --from=api-development /srv/backend/node_modules ./node_modules 21 | COPY . . 22 | CMD ["npm", "run", "dev"] 23 | 24 | -------------------------------------------------------------------------------- /backend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18.12.0 AS api-development 2 | RUN mkdir /srv/backend 3 | WORKDIR /srv/backend 4 | RUN mkdir -p node_modules 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install --pure-lockfile 7 | COPY . . 8 | -------------------------------------------------------------------------------- /backend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:18.12.0 AS api-builder 2 | RUN mkdir /srv/backend 3 | WORKDIR /srv/backend 4 | RUN mkdir -p node_modules 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install --pure-lockfile 7 | COPY . . 8 | 9 | FROM node:18.12.0-slim AS api-production 10 | EXPOSE 4000 11 | USER node 12 | WORKDIR /srv/backend 13 | COPY --from=api-builder /srv/backend/node_modules ./node_modules 14 | COPY . . 15 | CMD ["npm", "run", "start"] 16 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | ## Running Tests 4 | 5 | To maintain consistency across the front end and back end development, we are using Jest 6 | as a test runner. 7 | 8 | ### Execute Tests 9 | 10 | You will need to be in the backend directory for this to work. 11 | 12 | 1. Run all tests: `npm test` 13 | 1. Run a single test: `npm test ` 14 | 15 | ### Writing Tests 16 | 17 | To maintain idempotent tests, we have opted to use in memory test databases. Jest, like 18 | most test runners, has hooks or methods for you to call before or after tests. We can 19 | can setup our db and tear it down by importing the `setupDB` module. 20 | 21 | ```js 22 | // You will need to require the db-handler file. 23 | const { setupDB } = require("../setup-test"); 24 | 25 | // You will need to name the in memory DB for this test. 26 | setupDB("api-auth"); 27 | ``` 28 | 29 | If you are unsure of where to start, then find a test that does something similar to your 30 | aims. Copy, tweak, and run that test until you have your desired outcome. Also make sure 31 | to give your test it's own name. 32 | 33 | ### Unit Tests 34 | 35 | Unit tests are tests to write around a single unit. These can be tests around validation 36 | of a Model, or testing the boundaries on a class. 37 | 38 | ### Integration Tests 39 | 40 | Integration Tests are tests that verify that differing components work together. A common 41 | example of an integration test is to verify that data is saved correctly in the database 42 | based on the use of the API. -------------------------------------------------------------------------------- /backend/config/auth.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | module.exports = { 3 | SECRET: 4 | 'c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079', 5 | CUSTOM_REQUEST_HEADER: process.env.CUSTOM_REQUEST_HEADER, 6 | TOKEN_EXPIRATION_SEC: 900, 7 | }; 8 | /* eslint-enable */ -------------------------------------------------------------------------------- /backend/config/auth.config.test.js: -------------------------------------------------------------------------------- 1 | test.skip('Environment variables are working as expected', () => { 2 | const backendUrl = process.env.REACT_APP_PROXY; 3 | expect(backendUrl).toBe(`http://localhost:${process.env.BACKEND_PORT}`); 4 | }); 5 | -------------------------------------------------------------------------------- /backend/config/database.config.js: -------------------------------------------------------------------------------- 1 | exports.PORT = process.env.BACKEND_PORT; 2 | exports.DATABASE_URL = process.env.DATABASE_URL; 3 | -------------------------------------------------------------------------------- /backend/config/index.js: -------------------------------------------------------------------------------- 1 | const CONFIG_AUTH = require('./auth.config'); 2 | const CONFIG_DB = require('./database.config'); 3 | 4 | module.exports = { 5 | CONFIG_AUTH, 6 | CONFIG_DB, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/controllers/email.controller.test.js: -------------------------------------------------------------------------------- 1 | const EmailController = require('./email.controller'); 2 | 3 | test('Can import the email controller', async () => { 4 | expect(EmailController).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/controllers/event.controller.js: -------------------------------------------------------------------------------- 1 | const { Event } = require('../models'); 2 | 3 | const EventController = {}; 4 | 5 | EventController.event_list = async function (req, res) { 6 | const { query } = req; 7 | 8 | try { 9 | const events = await Event.find(query).populate('project'); 10 | return res.status(200).send(events); 11 | } catch (err) { 12 | return res.sendStatus(400); 13 | } 14 | }; 15 | 16 | EventController.event_by_id = async function (req, res) { 17 | const { EventId } = req.params; 18 | 19 | try { 20 | const events = await Event.findById(EventId).populate('project'); 21 | return res.status(200).send(events); 22 | } catch (err) { 23 | return res.sendStatus(400); 24 | } 25 | }; 26 | 27 | EventController.create = async function (req, res) { 28 | const { body } = req; 29 | 30 | try { 31 | const event = await Event.create(body); 32 | return res.status(201).send(event); 33 | } catch (err) { 34 | return res.sendStatus(400); 35 | } 36 | }; 37 | 38 | EventController.destroy = async function (req, res) { 39 | const { EventId } = req.params; 40 | 41 | try { 42 | const event = await Event.findByIdAndDelete(EventId); 43 | return res.status(200).send(event); 44 | } catch (err) { 45 | return res.sendStatus(400); 46 | } 47 | }; 48 | 49 | EventController.update = async function (req, res) { 50 | const { EventId } = req.params; 51 | 52 | try { 53 | const event = await Event.findByIdAndUpdate(EventId, req.body); 54 | return res.status(200).send(event); 55 | } catch (err) { 56 | return res.sendStatus(400); 57 | } 58 | }; 59 | 60 | module.exports = EventController; 61 | -------------------------------------------------------------------------------- /backend/controllers/event.controller.test.js: -------------------------------------------------------------------------------- 1 | const EventController = require('./event.controller'); 2 | 3 | test('Can import the email controller', async () => { 4 | expect(EventController).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/controllers/healthCheck.controller.js: -------------------------------------------------------------------------------- 1 | const HealthCheckController = {}; 2 | 3 | HealthCheckController.isAlive = (_, res) => { 4 | res.status(200).send("I'm Alive!"); 5 | } 6 | 7 | module.exports = HealthCheckController; 8 | -------------------------------------------------------------------------------- /backend/controllers/index.js: -------------------------------------------------------------------------------- 1 | const EmailController = require('./email.controller'); 2 | const EventController = require('./event.controller'); 3 | const UserController = require('./user.controller'); 4 | const ProjectController = require('./project.controller'); 5 | const HealthCheckController = require('./healthCheck.controller'); 6 | const RecurringEventController = require('./recurringEvent.controller') 7 | 8 | module.exports = { 9 | EmailController, 10 | EventController, 11 | UserController, 12 | ProjectController, 13 | HealthCheckController, 14 | RecurringEventController 15 | }; 16 | -------------------------------------------------------------------------------- /backend/controllers/project.controller.js: -------------------------------------------------------------------------------- 1 | const { Project } = require('../models'); 2 | 3 | const ProjectController = {}; 4 | 5 | ProjectController.project_list = async function (req, res) { 6 | const { query } = req; 7 | 8 | try { 9 | const projects = await Project.find(query); 10 | return res.status(200).send(projects); 11 | } catch (err) { 12 | return res.sendStatus(400); 13 | } 14 | }; 15 | 16 | ProjectController.pm_filtered_projects = async function (req, res) { 17 | try { 18 | const projectList = await Project.find({}); 19 | const projects = projectList.filter((proj) => req.body.includes(proj._id.toString())); 20 | return res.status(200).send(projects); 21 | } catch (e) { 22 | return res.sendStatus(400); 23 | } 24 | }; 25 | 26 | ProjectController.create = async function (req, res) { 27 | const { body } = req; 28 | 29 | try { 30 | const newProject = await Project.create(body); 31 | return res.status(201).send(newProject); 32 | } catch (err) { 33 | return res.sendStatus(400); 34 | } 35 | }; 36 | 37 | ProjectController.project_by_id = async function (req, res) { 38 | const { ProjectId } = req.params; 39 | 40 | try { 41 | const project = await Project.findById(ProjectId); 42 | return res.status(200).send(project); 43 | } catch (err) { 44 | return res.sendStatus(400); 45 | } 46 | }; 47 | 48 | ProjectController.update = async function (req, res) { 49 | const { ProjectId } = req.params; 50 | try { 51 | const project = await Project.findOneAndUpdate({ _id: ProjectId }, req.body, { new: true }); 52 | return res.status(200).send(project); 53 | } catch (err) { 54 | return res.sendStatus(400); 55 | } 56 | }; 57 | 58 | ProjectController.destroy = async function (req, res) { 59 | const { ProjectId } = req.params; 60 | 61 | try { 62 | const project = await Project.findByIdAndDelete(ProjectId); 63 | return res.status(200).send(project); 64 | } catch (err) { 65 | return res.sendStatus(400); 66 | } 67 | }; 68 | 69 | module.exports = ProjectController; 70 | -------------------------------------------------------------------------------- /backend/controllers/project.controller.test.js: -------------------------------------------------------------------------------- 1 | const ProjectController = require('./project.controller'); 2 | 3 | test('Can import the project controller', async () => { 4 | expect(ProjectController).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/controllers/recurringEvent.controller.js: -------------------------------------------------------------------------------- 1 | const { RecurringEvent } = require('../models'); 2 | const expectedHeader = process.env.CUSTOM_REQUEST_HEADER; 3 | 4 | const RecurringEventController = {}; 5 | 6 | 7 | // // Add User with POST 8 | RecurringEventController.create = async function (req, res) { 9 | const { headers } = req; 10 | 11 | if (headers['x-customrequired-header'] !== expectedHeader) { 12 | return res.sendStatus(403); 13 | } 14 | 15 | try { 16 | const rEvent = await RecurringEvent.create(req.body); 17 | return res.status(200).send(rEvent); 18 | } catch (err) { 19 | return res.sendStatus(400); 20 | } 21 | }; 22 | 23 | // Update Recurring Event using PATCH 24 | RecurringEventController.update = async function (req, res) { 25 | 26 | const { headers } = req; 27 | const { RecurringEventId } = req.params; 28 | 29 | if (headers['x-customrequired-header'] !== expectedHeader) { 30 | return res.sendStatus(403); 31 | } 32 | 33 | const filter = {_id: RecurringEventId}; 34 | const update = req.body; 35 | 36 | try { 37 | const uEvent = await RecurringEvent.findOneAndUpdate(filter, update, {new: true}); 38 | return res.status(200).send(uEvent); 39 | } catch (err) { 40 | return res.sendStatus(400); 41 | } 42 | }; 43 | 44 | 45 | // Delete Recurring Event 46 | RecurringEventController.destroy = async function (req, res) { 47 | const { RecurringEventId } = req.params; 48 | 49 | try { 50 | const rEvent = await RecurringEvent.findByIdAndDelete(RecurringEventId); 51 | return res.status(200).send(rEvent); 52 | } catch (err) { 53 | return res.sendStatus(400); 54 | } 55 | }; 56 | 57 | module.exports = RecurringEventController; -------------------------------------------------------------------------------- /backend/controllers/user.controller.test.js: -------------------------------------------------------------------------------- 1 | const userContoller = require('./user.controller'); 2 | 3 | test('Can import the email controller', async () => { 4 | expect(userContoller).not.toBeUndefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /backend/globalConfig.json: -------------------------------------------------------------------------------- 1 | {"mongoUri":"mongodb://127.0.0.1:43943/jest?","mongoDBName":"jest"} -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | setupFilesAfterEnv: ['./jest.setup.js'], 4 | watchPathIgnorePatterns: ['globalConfig'], 5 | testPathIgnorePatterns: ['/test/old-tests/'], 6 | }; 7 | -------------------------------------------------------------------------------- /backend/jest.setup.js: -------------------------------------------------------------------------------- 1 | // Be able to use Env variables in Github Actions 2 | const dotenv = require('dotenv'); 3 | const dotenvExpand = require('dotenv-expand'); 4 | 5 | const myEnv = dotenv.config(); 6 | dotenvExpand(myEnv); 7 | 8 | 9 | jest.setTimeout(30000) 10 | 11 | // TODO: Refactor worker routes. These are setup to run cron jobs every time the app 12 | // is instantiated. These break any integration tests. 13 | jest.mock('./workers/openCheckins'); 14 | jest.mock('./workers/closeCheckins'); 15 | jest.mock('./workers/createRecurringEvents'); 16 | jest.mock('./workers/slackbot'); 17 | -------------------------------------------------------------------------------- /backend/middleware/auth.middleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { CONFIG_AUTH } = require('../config'); 3 | 4 | function verifyToken(req, res, next) { 5 | // Allow users to set token 6 | // eslint-disable-next-line dot-notation 7 | let token = req.headers['x-access-token'] || req.headers['authorization']; 8 | if (token.startsWith('Bearer ')) { 9 | // Remove Bearer from string 10 | token = token.slice(7, token.length); 11 | } 12 | if (!token) { 13 | return res.sendStatus(403); 14 | } 15 | 16 | try { 17 | const decoded = jwt.verify(token, CONFIG_AUTH.SECRET); 18 | res.cookie('token', token, { httpOnly: true }); 19 | req.userId = decoded.id; 20 | return next(); 21 | } catch (err) { 22 | return res.sendStatus(401); 23 | } 24 | } 25 | 26 | function verifyCookie(req, res, next) { 27 | jwt.verify(req.cookies.token, CONFIG_AUTH.SECRET, (err, decoded) => { 28 | if (err) { 29 | return res.sendStatus(401); 30 | } 31 | req.userId = decoded.id; 32 | req.role = decoded.accessLevel; 33 | 34 | next(); 35 | }); 36 | } 37 | 38 | const AuthUtil = { 39 | verifyToken, 40 | verifyCookie, 41 | }; 42 | module.exports = AuthUtil; 43 | -------------------------------------------------------------------------------- /backend/middleware/errorhandler.middleware.js: -------------------------------------------------------------------------------- 1 | function errorHandler (error, req, res, next) { 2 | error.status = error.status || 500; 3 | 4 | res.status(error.status).json({ 5 | error: { 6 | status: error.status, 7 | message: error.message || 'Internal Server Error', 8 | stack: error.stack || '' 9 | } 10 | }); 11 | } 12 | 13 | module.exports = errorHandler; 14 | -------------------------------------------------------------------------------- /backend/middleware/index.js: -------------------------------------------------------------------------------- 1 | const AuthUtil = require('./auth.middleware'); 2 | const verifyUser = require('./user.middleware'); 3 | const verifyToken = require('./token.middleware'); 4 | 5 | module.exports = { 6 | AuthUtil, 7 | verifyUser, 8 | verifyToken, 9 | }; 10 | -------------------------------------------------------------------------------- /backend/middleware/token.middleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { CONFIG_AUTH } = require('../config'); 3 | 4 | async function isTokenValid(req, res, next) { 5 | let token = req.headers['x-access-token'] || req.headers['authorization']; 6 | if (token.startsWith('Bearer ')) { 7 | // Remove Bearer from string 8 | token = token.slice(7, token.length); 9 | } 10 | 11 | if (!token) { 12 | return res.sendStatus(400); 13 | } 14 | 15 | try { 16 | jwt.verify(token, CONFIG_AUTH.SECRET); 17 | next(); 18 | } catch (err) { 19 | return res.sendStatus(403); 20 | } 21 | } 22 | 23 | const verifyToken = { 24 | isTokenValid, 25 | }; 26 | 27 | module.exports = verifyToken; 28 | -------------------------------------------------------------------------------- /backend/middleware/user.middleware.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../models'); 2 | 3 | function checkDuplicateEmail(req, res, next) { 4 | User.findOne({ email: req.body.email }).then((user) => { 5 | if (user) { 6 | return res.sendStatus(400); 7 | } 8 | next(); 9 | }); 10 | } 11 | 12 | function isAdminByEmail(req, res, next) { 13 | User.findOne({ email: req.body.email }).then((user) => { 14 | if (!user) { 15 | return res.sendStatus(400); 16 | } else { 17 | const role = user.accessLevel; 18 | if (role === 'admin' || role === 'superadmin' || user.managedProjects.length > 0) { 19 | next(); 20 | } else { 21 | next(res.sendStatus(401)); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | const verifyUser = { 28 | checkDuplicateEmail, 29 | isAdminByEmail, 30 | }; 31 | 32 | module.exports = verifyUser; 33 | -------------------------------------------------------------------------------- /backend/models/checkIn.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | mongoose.Promise = global.Promise; 4 | 5 | const checkInSchema = mongoose.Schema({ 6 | userId: { type: String }, 7 | eventId: { type: String }, 8 | checkedIn: { type: Boolean, default: true }, 9 | createdDate: { type: Date, default: Date.now }, 10 | }); 11 | 12 | checkInSchema.methods.serialize = function () { 13 | return { 14 | id: this._id, 15 | userId: this.userId, 16 | eventId: this.eventId, 17 | checkedIn: this.checkedIn, 18 | createdDate: this.createdDate, 19 | }; 20 | }; 21 | 22 | const CheckIn = mongoose.model("CheckIn", checkInSchema); 23 | 24 | module.exports = { CheckIn }; 25 | -------------------------------------------------------------------------------- /backend/models/index.js: -------------------------------------------------------------------------------- 1 | const { CheckIn } = require('./checkIn.model'); 2 | const { Event } = require('./event.model'); 3 | const { Project } = require('./project.model'); 4 | const { ProjectTeamMember } = require('./projectTeamMember.model'); 5 | const { Question } = require('./question.model'); 6 | const { RecurringEvent } = require('./recurringEvent.model'); 7 | const { Role } = require('./role.model'); 8 | const { User } = require('./user.model'); 9 | 10 | const mongoose = require("mongoose"); 11 | mongoose.Promise = global.Promise; 12 | 13 | module.exports = { 14 | CheckIn, 15 | Event, 16 | Project, 17 | ProjectTeamMember, 18 | Question, 19 | RecurringEvent, 20 | Role, 21 | User, 22 | }; 23 | -------------------------------------------------------------------------------- /backend/models/projectTeamMember.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.Promise = global.Promise; 4 | 5 | /* 6 | ProjectTeamMember: 7 | - projectID 8 | - user ID 9 | - teamMemberStatus (active/inactive) 10 | - roleOnProject 11 | - joinedDate 12 | - leftDate 13 | - leftReason (project completed, project paused, switched projects, no-show, etc.) 14 | Idea for the future: numberGithubContributions (pull this from github?) 15 | */ 16 | 17 | const projectTeamMemberSchema = mongoose.Schema({ 18 | userId: { type: String }, // id of the user 19 | projectId: { type: String }, // id of the project 20 | teamMemberStatus: { type: String }, // Active or Inactive 21 | vrmsProjectAdmin: { type: Boolean }, // does this team member have admin rights to the project in VRMS? 22 | roleOnProject: { type: String }, // Developer, Project Manager, UX, Data Science 23 | joinedDate: { type: Date, default: Date.now }, // date/time joined project 24 | leftDate: { type: Date }, // only if Status = Inactive, date/time went inactive 25 | leftReason: { type: String }, // project completed, project paused, switched projects, no-show, other 26 | githubPermissionLevel: { type: String }, // Write, Triage, Read, Maintainer, or Admin; pull from Github API? 27 | onProjectGithub: { type: Boolean, default: false }, // added to the project team on github? pull from github api? 28 | onProjectGoogleDrive: { type: Boolean, default: false} // added to the project team's google drive folder? 29 | }); 30 | projectTeamMemberSchema.methods.serialize = function() { 31 | return { 32 | id: this._id, 33 | userId: this.userId, 34 | projectId: this.projectId, 35 | teamMemberStatus: this.teamMemberStatus, 36 | vrmsProjectAdmin: this.vrmsProjectAdmin, 37 | roleOnProject: this.roleOnProject, 38 | joinedDate: this.joinedDate, 39 | leftDate: this.leftDate, 40 | leftReason: this.leftReason, 41 | githubPermissionLevel: this.githubPermissionLevel, 42 | onProjectGithub: this.onProjectGithub, 43 | onProjectGoogleDrive: this.onProjectGoogleDrive 44 | }; 45 | }; 46 | 47 | const ProjectTeamMember = mongoose.model('ProjectTeamMember', projectTeamMemberSchema); 48 | 49 | module.exports = { ProjectTeamMember }; 50 | -------------------------------------------------------------------------------- /backend/models/question.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.Promise = global.Promise; 4 | 5 | const questionSchema = mongoose.Schema({ 6 | questionText: { type: String }, 7 | htmlName: { type: String }, 8 | answers: { 9 | answerOneText: { type: String }, 10 | answerTwoText: { type: String }, 11 | answerThreeText: { type: String }, 12 | answerFourText: { type: String } 13 | } 14 | }); 15 | 16 | questionSchema.methods.serialize = function() { 17 | return { 18 | id: this._id, 19 | questionText: this.questionText, 20 | htmlName: this.htmlName, 21 | inputType: this.inputType, 22 | answers: { 23 | answerOneText: this.answers.answerOneText, 24 | answerTwoText: this.answers.answerTwoText, 25 | answerThreeText: this.answers.answerThreeText, 26 | answerFourText: this.answers.answerFourText 27 | } 28 | }; 29 | }; 30 | 31 | const Question = mongoose.model('Question', questionSchema); 32 | 33 | module.exports = { Question }; 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /backend/models/recurringEvent.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.Promise = global.Promise; 4 | 5 | const recurringEventSchema = mongoose.Schema({ 6 | name: { type: String }, 7 | location: { // should we include address here? 8 | city: { type: String }, 9 | state: { type: String }, 10 | country: { type: String } 11 | }, 12 | hacknight: { type: String }, // DTLA, Westside, South LA, Online 13 | brigade: { type: String, default: "Hack for LA" }, 14 | eventType: { type: String }, // Project Meeting, Orientation, Workshop 15 | description: { type: String }, 16 | project: { // only needed if it's type = Project Meeting 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: 'Project' 19 | }, 20 | date: { type: Date }, 21 | startTime: { type: Date }, // start date and time of the event 22 | endTime: { type: Date }, // end date and time of the event 23 | hours: { type: Number }, // length of the event in hours 24 | createdDate: { type: Date, default: Date.now }, // date/time event was created 25 | updatedDate: { type: Date, default: Date.now }, // date/time event was last updated 26 | checkInReady: { type: Boolean, default: false }, // is the event open for check-ins? 27 | videoConferenceLink: { type: String }, // can be same or different from project 28 | owner: { 29 | ownerId: { type: String, default: '123456' } // id of user who created event 30 | } 31 | }); 32 | 33 | recurringEventSchema.methods.serialize = function() { 34 | return { 35 | id: this._id, 36 | name: this.name, 37 | location: { 38 | city: this.location.city, 39 | state: this.location.state, 40 | country: this.location.country 41 | }, 42 | hacknight: [this.hacknight], 43 | brigade: this.brigade, 44 | eventType: this.eventType, 45 | description: this.eventDescription, 46 | project: this.project, 47 | date: this.date, 48 | startTime: this.startTime, 49 | endTime: this.endTime, 50 | hours: this.hours, 51 | createdDate: this.createdDate, 52 | checkInReady: this.checkInReady, 53 | videoConferenceLink: this.videoConferenceLink, 54 | owner: { 55 | ownerId: this.owner.ownerId 56 | } 57 | }; 58 | }; 59 | 60 | const RecurringEvent = mongoose.model('RecurringEvent', recurringEventSchema); 61 | 62 | module.exports = { RecurringEvent }; 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /backend/models/role.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Role = mongoose.model( 4 | "Role", 5 | new mongoose.Schema({ 6 | name: { type: String }, 7 | }) 8 | ); 9 | 10 | module.exports = { Role }; 11 | -------------------------------------------------------------------------------- /backend/models/timeTracker.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | mongoose.Promise = global.Promise; 4 | 5 | const timeTrackerSchema = mongoose.Schema({ 6 | user: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "User", 9 | }, 10 | project: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: "Project", 13 | }, 14 | category: { type: String }, // picklist with 4 options: Development, Design/UX, Product/Project Management, Other 15 | notes: { type: String }, 16 | startDate: { type: Date }, 17 | endDate: { type: Date }, 18 | }); 19 | 20 | timeTrackerSchema.methods.serialize = function () { 21 | return { 22 | id: this._id, 23 | user: { 24 | userId: this.user.userId, 25 | }, 26 | selectedAnswer: this.selectedAnswer, 27 | startDate: this.startDate, 28 | endDate: this.endDate, 29 | }; 30 | }; 31 | 32 | const TimeTracker = mongoose.model("TimeTracker", timeTrackerSchema); 33 | 34 | module.exports = { TimeTracker }; 35 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vrms-server", 3 | "version": "0.3.0", 4 | "description": "VRMS Backend", 5 | "main": "server.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "format": "prettier --check .", 9 | "test": "jest", 10 | "test:watch": "jest --watch", 11 | "start": "node server.js", 12 | "dev": "nodemon server.js", 13 | "client": "npm run start --prefix client", 14 | "heroku-postbuild": "cd client && npm install && npm run build" 15 | }, 16 | "author": "sarL3y", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@babel/core": "^7.15.0", 20 | "@babel/eslint-parser": "^7.15.0", 21 | "@babel/eslint-plugin": "^7.14.5", 22 | "concurrently": "^5.1.0", 23 | "debug": "^4.3.1", 24 | "eslint": "^7.9.0", 25 | "eslint-config-airbnb": "^18.2.0", 26 | "eslint-config-airbnb-base": "^14.2.0", 27 | "eslint-config-prettier": "^6.11.0", 28 | "eslint-plugin-import": "^2.22.0", 29 | "eslint-plugin-jsx-a11y": "^6.3.1", 30 | "eslint-plugin-react": "^7.20.6", 31 | "jest": "^26.4.0", 32 | "mockdate": "^3.0.5", 33 | "nodemon": "^2.0.2", 34 | "prettier": "^2.1.1", 35 | "pretty-quick": "^3.0.2", 36 | "supertest": "^4.0.2", 37 | "why-is-node-running": "^2.2.0" 38 | }, 39 | "dependencies": { 40 | "@slack/bolt": "^2.2.3", 41 | "assert-env": "^0.6.0", 42 | "async": "^3.2.2", 43 | "cookie-parser": "^1.4.5", 44 | "cors": "^2.8.5", 45 | "dotenv": "^8.2.0", 46 | "dotenv-expand": "^5.1.0", 47 | "express": "^4.20.0", 48 | "express-validator": "^6.6.1", 49 | "googleapis": "^59.0.0", 50 | "helmet": "^3.22.0", 51 | "jsonwebtoken": "^8.5.1", 52 | "mongodb-memory-server": "^6.9.0", 53 | "mongoose": "^5.10.0", 54 | "morgan": "^1.10.0", 55 | "node-cron": "^2.0.3", 56 | "node-fetch": "^2.6.7", 57 | "nodemailer": "^6.6.1" 58 | }, 59 | "directories": { 60 | "test": "test" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/routers/auth.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { AuthUtil, verifyUser, verifyToken } = require('../middleware'); 3 | const { UserController } = require('../controllers/'); 4 | const { authApiValidator } = require('../validators'); 5 | 6 | const router = express.Router(); 7 | 8 | // eslint-disable-next-line func-names 9 | router.use(function (req, res, next) { 10 | res.header('Access-Control-Allow-Headers', 'x-access-token, Origin, Content-Type, Accept'); 11 | next(); 12 | }); 13 | 14 | // The root is /api/auth 15 | router.post( 16 | '/signup', 17 | [authApiValidator.validateCreateUserAPICall, verifyUser.checkDuplicateEmail], 18 | UserController.createUser, 19 | ); 20 | 21 | router.post( 22 | '/signin', 23 | [authApiValidator.validateSigninUserAPICall, verifyUser.isAdminByEmail], 24 | UserController.signin, 25 | ); 26 | 27 | router.post('/verify-signin', [verifyToken.isTokenValid], UserController.verifySignIn); 28 | 29 | router.post('/me', [AuthUtil.verifyCookie], UserController.verifyMe); 30 | 31 | router.post('/logout', [AuthUtil.verifyCookie], UserController.logout); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /backend/routers/checkIns.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const { CheckIn } = require('../models/checkIn.model'); 5 | 6 | // GET /api/checkins/ 7 | router.get('/', (req, res) => { 8 | CheckIn.find() 9 | .then((checkIns) => { 10 | res.status(200).send(checkIns); 11 | }) 12 | .catch((err) => { 13 | console.log(err); 14 | res.sendStatus(400); 15 | }); 16 | }); 17 | 18 | router.get("/:id", (req, res) => { 19 | CheckIn.findById(req.params.id) 20 | .then((checkIn) => { 21 | res.status(200).send(checkIn); 22 | }) 23 | .catch((err) => { 24 | console.log(err); 25 | res.sendStatus(400); 26 | }); 27 | }); 28 | 29 | router.get("/findEvent/:id", (req, res) => { 30 | CheckIn.find({ eventId: req.params.id, userId: { $ne: "undefined" } }) 31 | .populate({ 32 | path: "userId", 33 | model: "User", 34 | }) 35 | .then((checkIns) => { 36 | res.status(200).send(checkIns); 37 | }) 38 | .catch((err) => { 39 | console.log(err); 40 | res.sendStatus(400); 41 | }); 42 | }); 43 | 44 | router.post("/", (req, res) => { 45 | CheckIn.create(req.body) 46 | .then((checkIn) => { 47 | res.sendStatus(201); 48 | }) 49 | .catch((err) => { 50 | console.log(err); 51 | res.sendStatus(400); 52 | }); 53 | }); 54 | 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /backend/routers/checkUser.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { User } = require('../models/user.model'); 5 | 6 | // TODO: Refactor checkuser and test. Consider moving to auth. 7 | 8 | // GET /api/checkuser/ 9 | router.post('/', (req, res) => { 10 | const { email, auth_origin } = req.body; 11 | console.log(email); 12 | console.log(`auth_origin: ${auth_origin}`); 13 | 14 | if (email === 'undefined') { 15 | return res.sendStatus(400); 16 | } 17 | 18 | if (email) { 19 | User.findOne({ email }) 20 | .then((user) => { 21 | if (!user) { 22 | return res.sendStatus(400); 23 | } else { 24 | return res.status(200).send({ user: user, auth_origin: auth_origin }); 25 | } 26 | }) 27 | .catch((err) => { 28 | console.log(err); 29 | 30 | return res.sendStatus(400); 31 | }); 32 | } else { 33 | // TODO: Refactor as path is not called, or tested. 34 | res.json({ message: 'Enter the email address you used to check-in last time.' }); 35 | } 36 | }); 37 | 38 | router.get('/:id', (req, res) => { 39 | // TODO: Refactor and test 40 | User.findById(req.params.id) 41 | .then((user) => { 42 | return res.status(200).send(user); 43 | }) 44 | .catch((err) => { 45 | console.log(err); 46 | res.sendStatus(400); 47 | }); 48 | }); 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /backend/routers/checkUser.router.test.js: -------------------------------------------------------------------------------- 1 | // Mock and import User Model 2 | jest.mock('../models/user.model'); 3 | const { User } = require('../models'); 4 | 5 | // Import checkUser router 6 | const express = require('express'); 7 | const supertest = require('supertest'); 8 | const checkUserRouter = require('./checkUser.router'); 9 | 10 | // Create a new Express application for testing 11 | const testapp = express(); 12 | // express.json() is a body parser needed for POST API requests 13 | testapp.use(express.json()); 14 | testapp.use('/api/checkuser', checkUserRouter); 15 | const request = supertest(testapp); 16 | 17 | describe('Unit tests for checkUser router', () => { 18 | // Mock user for test 19 | const id = '123'; 20 | const mockUser = { 21 | id, 22 | name: { 23 | firstName: 'mock', 24 | lastName: 'user', 25 | }, 26 | accessLevel: 'user', 27 | skillsToMatch: [], 28 | projects: [], 29 | textingOk: false, 30 | managedProjects: [], 31 | isActive: true, 32 | email: 'mockuser@gmail.com', 33 | currentRole: 'Product Owner', 34 | desiredRole: 'Product Owner', 35 | newMember: false, 36 | firstAttended: 'NOV 2015', 37 | createdDate: '2020-01-14T02:14:22.407Z', 38 | attendanceReason: 'Civic Engagement', 39 | currentProject: 'Undebate', 40 | }; 41 | 42 | const auth_origin = 'test-origin'; 43 | 44 | // Clear all mocks after each test 45 | afterEach(() => { 46 | jest.clearAllMocks(); 47 | }); 48 | 49 | describe('CREATE', () => { 50 | it('should authenticate user with POST /api/checkuser', async (done) => { 51 | // Mock Mongoose method 52 | User.findOne.mockResolvedValue(mockUser); 53 | 54 | const response = await request 55 | .post('/api/checkuser') 56 | .send({ email: 'mockuser@gmail.com', auth_origin }); 57 | 58 | // Tests 59 | expect(User.findOne).toHaveBeenCalledWith({ email: 'mockuser@gmail.com' }); 60 | expect(response.status).toBe(200); 61 | expect(response.body).toEqual({ user: mockUser, auth_origin: auth_origin }); 62 | 63 | // Marks completion of tests 64 | done(); 65 | }); 66 | }); 67 | 68 | describe('READ', () => { 69 | it('should return a user by id with GET /api/checkuser/:id', async (done) => { 70 | // Mock Mongoose method 71 | User.findById.mockResolvedValue(mockUser); 72 | 73 | const response = await request.get(`/api/checkuser/${id}`); 74 | 75 | // Tests 76 | expect(User.findById).toHaveBeenCalledWith(id); 77 | expect(response.status).toBe(200); 78 | expect(response.body).toEqual(mockUser); 79 | 80 | // Marks completion of tests 81 | done(); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /backend/routers/events.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const { Event } = require('../models/event.model'); 5 | const { EventController } = require('../controllers'); 6 | 7 | // The root is /api/events 8 | router.get('/', EventController.event_list); 9 | 10 | router.post('/', EventController.create); 11 | 12 | router.get('/:EventId', EventController.event_by_id); 13 | 14 | router.delete('/:EventId', EventController.destroy); 15 | 16 | router.patch('/:EventId', EventController.update); 17 | 18 | // TODO: Refactor and remove 19 | router.get("/nexteventbyproject/:id", (req, res) => { 20 | Event.find({ project: req.params.id }) 21 | .populate("project") 22 | .then((events) => { 23 | res.status(200).json(events[events.length - 1]); 24 | }) 25 | .catch((err) => { 26 | console.log(err); 27 | res.sendStatus(500); 28 | }); 29 | }); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /backend/routers/healthCheck.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const { HealthCheckController } = require('../controllers'); 5 | 6 | // The root is /api/healthcheck 7 | router.get('/', HealthCheckController.isAlive); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routers/projectTeamMembers.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const { ProjectTeamMember } = require('../models/projectTeamMember.model'); 5 | 6 | // GET /api/projectteammembers/ 7 | router.get("/", (req, res) => { 8 | ProjectTeamMember.find() 9 | .populate("userId") 10 | .then((teamMembers) => { 11 | return res.status(200).send(teamMembers); 12 | }) 13 | .catch((err) => { 14 | console.log(err); 15 | return res.sendStatus(400); 16 | }); 17 | }); 18 | 19 | router.get("/:id", (req, res) => { 20 | ProjectTeamMember.find({ projectId: req.params.id }) 21 | .populate("userId") 22 | .then((teamMembers) => { 23 | return res.status(200).send(teamMembers); 24 | }) 25 | .catch((err) => { 26 | console.log(err); 27 | return res.sendStatus(400); 28 | }); 29 | }); 30 | 31 | router.get("/project/:id/:userId", (req, res) => { 32 | ProjectTeamMember.find({ 33 | projectId: req.params.id, 34 | userId: req.params.userId, 35 | }) 36 | .populate("userId") 37 | .then((teamMember) => { 38 | if (!teamMember.length) { 39 | return res.sendStatus(400); 40 | } else { 41 | return res.status(200).send(teamMember); 42 | } 43 | }) 44 | .catch((err) => { 45 | console.log(err); 46 | return res.sendStatus(400); 47 | }); 48 | }); 49 | 50 | router.get("/projectowner/:id", (req, res) => { 51 | const id = req.params.id; 52 | 53 | ProjectTeamMember.findOne({ userId: id }) 54 | .populate("userId") 55 | .populate("projectId") 56 | .then((teamMember) => { 57 | teamMember.vrmsProjectAdmin === true 58 | ? res.status(200).send(teamMember) 59 | : res.status(200).send(false); 60 | }) 61 | .catch((err) => { 62 | res.status(400).send(err); 63 | }); 64 | }); 65 | 66 | router.post("/", (req, res) => { 67 | ProjectTeamMember.create(req.body) 68 | .then((teamMember) => { 69 | return res.status(201).send(teamMember); 70 | }) 71 | .catch((err) => { 72 | console.log(err); 73 | return res.sendStatus(400); 74 | }); 75 | }); 76 | 77 | router.patch("/:id", (req, res) => { 78 | ProjectTeamMember.findByIdAndUpdate(req.params.id, req.body) 79 | .then((edit) => res.json(edit)) 80 | .catch((err) => 81 | res.sendStatus(400)); 82 | // }; 83 | }); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /backend/routers/projects.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { ProjectController } = require('../controllers'); 5 | const { AuthUtil } = require('../middleware'); 6 | 7 | // The base is /api/projects 8 | router.get('/', ProjectController.project_list); 9 | 10 | // Its a put because we have to send the PM projects to be filtered here 11 | router.put('/', ProjectController.pm_filtered_projects); 12 | 13 | router.post('/', AuthUtil.verifyCookie, ProjectController.create); 14 | 15 | router.get('/:ProjectId', ProjectController.project_by_id); 16 | 17 | router.put('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update); 18 | 19 | router.patch('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /backend/routers/questions.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { Question } = require('../models/question.model'); 5 | 6 | 7 | // GET /api/questions/ 8 | router.get('/', (req, res) => { 9 | // const { query } = req; 10 | 11 | Question 12 | .find() 13 | .then(questions => { 14 | return res.status(200).send(questions); 15 | }) 16 | .catch(err => { 17 | console.log(err); 18 | return res.sendStatus(400); 19 | }); 20 | }); 21 | 22 | router.post('/', (req, res) => { 23 | 24 | Question 25 | .create(req.body) 26 | .then(question => { 27 | return res.sendStatus(201); 28 | }) 29 | .catch(err => { 30 | console.log(err); 31 | return res.sendStatus(400); 32 | }); 33 | }); 34 | 35 | router.get('/:id', (req, res) => { 36 | 37 | Question 38 | .findById(req.params.id) 39 | .then(event => { 40 | return res.status(200).send(event); 41 | }) 42 | .catch(err => { 43 | console.log(err); 44 | return res.sendStatus(400); 45 | }); 46 | }); 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /backend/routers/recurringEvents.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const cors = require('cors'); 4 | 5 | const { RecurringEvent } = require('../models/recurringEvent.model'); 6 | const { RecurringEventController } = require('../controllers/'); 7 | const { AuthUtil } = require('../middleware'); 8 | 9 | // GET /api/recurringevents/ 10 | router.get('/', cors(), (req, res) => { 11 | // const { query } = req; 12 | 13 | RecurringEvent 14 | // .find(query.checkInReady === 'true' ? query : undefined) 15 | .find() 16 | // This will deselect the video conference link field 17 | .select("-videoConferenceLink") 18 | .populate('project') 19 | .then(recurringEvents => { 20 | return res.status(200).send(recurringEvents); 21 | }) 22 | .catch(err => { 23 | console.log(err); 24 | return res.sendStatus(400); 25 | }); 26 | }); 27 | 28 | router.get("/internal", (req, res) => { 29 | RecurringEvent 30 | .find() 31 | .populate('project') 32 | .then(recurringEvents => { 33 | return res.status(200).send(recurringEvents) 34 | }) 35 | .catch(err => { 36 | console.error(err) 37 | return res.status(400); 38 | }) 39 | } ) 40 | 41 | router.get('/:id', (req, res) => { 42 | RecurringEvent 43 | .findById(req.params.id) 44 | .then(recurringEvent => { 45 | return res.status(200).send(recurringEvent); 46 | }) 47 | .catch(err => { 48 | console.log(err); 49 | return res.sendStatus(400); 50 | }); 51 | }); 52 | 53 | router.post('/', AuthUtil.verifyCookie, RecurringEventController.create); 54 | 55 | router.patch('/:RecurringEventId', AuthUtil.verifyCookie, RecurringEventController.update); 56 | 57 | router.delete('/:RecurringEventId', AuthUtil.verifyCookie, RecurringEventController.destroy); 58 | 59 | module.exports = router; 60 | -------------------------------------------------------------------------------- /backend/routers/success.router.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const cors = require("cors"); 4 | 5 | const { Event } = require('../models/event.model'); 6 | 7 | // GET /api/recurringevents/ 8 | router.get("/", cors(), (req, res) => { 9 | // const { query } = req; 10 | 11 | Event.find() 12 | .then((event) => { 13 | res.status(200).send(event); 14 | }) 15 | .catch((err) => { 16 | console.log(err); 17 | res.sendStatus(400); 18 | }); 19 | 20 | router.get("/:id", (req, res) => { 21 | Event.findById(req.params.id) 22 | .then((event) => { 23 | res.status(200).send(event); 24 | }) 25 | .catch((err) => { 26 | console.log(err); 27 | res.sendStatus(400); 28 | }); 29 | }); 30 | }); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /backend/routers/users.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { UserController } = require('../controllers'); 5 | 6 | // The base is /api/users 7 | router.get('/', UserController.user_list); 8 | 9 | router.get('/admins', UserController.admin_list); 10 | 11 | router.get('/projectManagers', UserController.projectLead_list); 12 | 13 | router.post('/', UserController.create); 14 | 15 | router.get('/:UserId', UserController.user_by_id); 16 | 17 | router.patch('/:UserId', UserController.update); 18 | 19 | router.delete('/:UserId', UserController.delete); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | const mongoose = require("mongoose"); 3 | 4 | const { Role } = require("./models"); 5 | 6 | // Load config variables 7 | const { CONFIG_DB } = require('./config/'); 8 | 9 | // Required convention for mongoose - https://stackoverflow.com/a/51862948/5900471 10 | mongoose.Promise = global.Promise; 11 | 12 | let server; 13 | async function runServer(databaseUrl = CONFIG_DB.DATABASE_URL, port = CONFIG_DB.PORT) { 14 | await mongoose 15 | .connect(databaseUrl, { 16 | useNewUrlParser: true, 17 | useCreateIndex: true, 18 | useUnifiedTopology: true, 19 | useFindAndModify: false, 20 | }) 21 | .catch((err) => err); 22 | 23 | server = app 24 | .listen(port, () => { 25 | console.log( 26 | `Mongoose connected from runServer() and is listening on ${port}` 27 | ); 28 | }) 29 | .on("error", (err) => { 30 | mongoose.disconnect(); 31 | return err; 32 | }); 33 | } 34 | 35 | async function closeServer() { 36 | await mongoose.disconnect().then(() => { 37 | return new Promise((resolve, reject) => { 38 | console.log("Closing Mongoose connection. Bye"); 39 | 40 | server.close((err) => { 41 | if (err) { 42 | return reject(err); 43 | } 44 | 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | function initial() { 52 | Role.collection.estimatedDocumentCount((err, count) => { 53 | if (!err && count === 0) { 54 | new Role({ 55 | name: "APP_USER", 56 | }).save((err) => { 57 | if (err) { 58 | console.log("error", err); 59 | } 60 | 61 | console.log("added 'user' to roles collection"); 62 | }); 63 | 64 | new Role({ 65 | name: "APP_ADMIN", 66 | }).save((err) => { 67 | if (err) { 68 | console.log("error", err); 69 | } 70 | 71 | console.log("added 'moderator' to roles collection"); 72 | }); 73 | } 74 | }); 75 | } 76 | 77 | if (require.main === module) { 78 | runServer().catch((err) => console.error(err)); 79 | initial(); 80 | } 81 | 82 | module.exports = { app, runServer, closeServer }; 83 | -------------------------------------------------------------------------------- /backend/setup-test.js: -------------------------------------------------------------------------------- 1 | // test-setup.js 2 | const mongoose = require("mongoose"); 3 | mongoose.set("useCreateIndex", true); 4 | mongoose.promise = global.Promise; 5 | 6 | const { MongoMemoryServer } = require("mongodb-memory-server"); 7 | 8 | async function removeAllCollections() { 9 | const mongooseCollections = mongoose.connection.collections; 10 | const collections = Object.keys(mongooseCollections); 11 | for (const collectionName of collections) { 12 | const collection = mongoose.connection.collections[collectionName]; 13 | collection.deleteMany(); 14 | } 15 | } 16 | 17 | async function dropAllCollections() { 18 | const collections = Object.keys(mongoose.connection.collections); 19 | for (const collectionName of collections) { 20 | const collection = mongoose.connection.collections[collectionName]; 21 | try { 22 | await collection.drop(); 23 | } catch (error) { 24 | // Sometimes this error happens, but you can safely ignore it 25 | if (error.message === "ns not found") return; 26 | // This error occurs when you use it.todo. You can 27 | // safely ignore this error too 28 | if (error.message.includes("a background operation is currently running")) 29 | return; 30 | console.log(error.message); 31 | } 32 | } 33 | } 34 | let mongoServer; 35 | module.exports = { 36 | setupIntegrationDB(databaseName) { 37 | // Connect to Mongoose 38 | beforeAll(async () => { 39 | mongoServer = new MongoMemoryServer({ 40 | instance: { dbName: databaseName }, 41 | }); 42 | const mongoUri = await mongoServer.getUri(); 43 | const opts = { 44 | useNewUrlParser: true, 45 | useFindAndModify: false, 46 | useCreateIndex: true, 47 | useUnifiedTopology: true, 48 | }; 49 | await mongoose.connect(mongoUri, opts, (err) => { 50 | if (err) console.error(err); 51 | }); 52 | }); 53 | 54 | // Disconnect Mongoose 55 | afterAll(async () => { 56 | await dropAllCollections(); 57 | await mongoose.connection.close(); 58 | await mongoServer.stop(); 59 | }); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /backend/validators/index.js: -------------------------------------------------------------------------------- 1 | const authApiValidator = require('./user.api.validator'); 2 | 3 | module.exports = { 4 | authApiValidator, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/validators/user.api.validator.js: -------------------------------------------------------------------------------- 1 | const { body, validationResult } = require('express-validator'); 2 | 3 | async function validateCreateUserAPICall(req, res, next) { 4 | await body('name.firstName').not().isEmpty().trim().escape().run(req); 5 | await body('name.lastName').not().isEmpty().trim().escape().run(req); 6 | await body('email', 'Invalid email') 7 | .exists() 8 | .isEmail() 9 | .normalizeEmail({ gmail_remove_dots: false }) 10 | .run(req); 11 | 12 | // Finds the validation errors in this request and wraps them in an object with handy functions 13 | const errors = validationResult(req); 14 | 15 | if (!errors.isEmpty()) { 16 | return res.status(403).json({ errors: errors.array() }); 17 | } 18 | return next(); 19 | } 20 | 21 | async function validateSigninUserAPICall(req, res, next) { 22 | await body('email', 'Invalid email') 23 | .exists() 24 | .isEmail() 25 | .normalizeEmail({ gmail_remove_dots: false }) 26 | .run(req); 27 | 28 | // Finds the validation errors in this request and wraps them in an object with handy functions 29 | const errors = validationResult(req); 30 | 31 | if (!errors.isEmpty()) { 32 | return res.status(403).json({ errors: errors.array() }); 33 | } 34 | return next(); 35 | } 36 | 37 | const authApiValidator = { 38 | validateCreateUserAPICall, 39 | validateSigninUserAPICall, 40 | }; 41 | 42 | module.exports = authApiValidator; 43 | -------------------------------------------------------------------------------- /backend/workers/lib/generateEventData.js: -------------------------------------------------------------------------------- 1 | function generateEventData(eventObj, TODAY_DATE = new Date()) { 2 | /** 3 | * Generates event data based on the provided event object and date. 4 | * In the cron job this function normally runs in, it is expected that eventObj.date is the same as TODAY_DATE. 5 | */ 6 | const eventDate = new Date(eventObj.startTime); 7 | // Create new event 8 | const hours = eventDate.getHours(); 9 | const minutes = eventDate.getMinutes(); 10 | const seconds = eventDate.getSeconds(); 11 | const milliseconds = eventDate.getMilliseconds(); 12 | 13 | const yearToday = TODAY_DATE.getFullYear(); 14 | const monthToday = TODAY_DATE.getMonth(); 15 | const dateToday = TODAY_DATE.getDate(); 16 | 17 | const newEventDate = new Date(yearToday, monthToday, dateToday, hours, minutes, seconds, milliseconds); 18 | 19 | const newEndTime = new Date(yearToday, monthToday, dateToday, hours + eventObj.hours, minutes, seconds, milliseconds) 20 | 21 | const eventToCreate = { 22 | name: eventObj.name && eventObj.name, 23 | hacknight: eventObj.hacknight && eventObj.hacknight, 24 | eventType: eventObj.eventType && eventObj.eventType, 25 | description: eventObj.eventDescription && eventObj.eventDescription, 26 | project: eventObj.project && eventObj.project, 27 | date: eventObj.date && newEventDate, 28 | startTime: eventObj.startTime && newEventDate, 29 | endTime: eventObj.endTime && newEndTime, 30 | hours: eventObj.hours && eventObj.hours 31 | } 32 | 33 | if (eventObj.hasOwnProperty("location")) { 34 | eventToCreate.location = { 35 | city: eventObj.location.city ? eventObj.location.city : 'REMOTE', 36 | state: eventObj.location.state ? eventObj.location.state : 'REMOTE', 37 | country: eventObj.location.country ? eventObj.location.country : 'REMOTE' 38 | }; 39 | } 40 | 41 | return eventToCreate 42 | }; 43 | 44 | module.exports = { generateEventData }; -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | docker-compose*.yml 4 | npm-debug.log* 5 | Dockerfile.* 6 | node_modules 7 | *.sh 8 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | # Contact the VRMS team for the below values 2 | CLIENT_PORT=3000 3 | CLIENT_URL=http://localhost:${CLIENT_PORT} 4 | BACKEND_HOST=localhost 5 | BACKEND_PORT=4000 6 | REACT_APP_PROXY=http://${BACKEND_HOST}:${BACKEND_PORT} 7 | REACT_APP_CUSTOM_REQUEST_HEADER= 8 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2017": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "airbnb", 9 | "airbnb-base", 10 | "plugin:prettier/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 2017, 18 | "sourceType": "module" 19 | }, 20 | "plugins": ["react"], 21 | "rules": { 22 | "react/jsx-filename-extension": [ 23 | 1, 24 | { 25 | "extensions": [".js", ".jsx"] 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | package-lock.json 28 | -------------------------------------------------------------------------------- /client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /client/Dockerfile.client: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS client-development 2 | RUN mkdir /srv/client && chown node:node /srv/client 3 | WORKDIR /srv/client 4 | USER node 5 | RUN mkdir -p node_modules 6 | COPY --chown=node:node package.json package.json ./ 7 | RUN npm install --silent 8 | 9 | FROM node:20-alpine AS client-builder 10 | USER node 11 | WORKDIR /srv/client 12 | COPY --from=client-development /srv/client/node_modules node_modules 13 | COPY . . 14 | USER root 15 | RUN npm run build 16 | 17 | FROM nginx as client-production 18 | EXPOSE 3000 19 | COPY /nginx/default.conf /etc/nginx/conf.d/default.conf 20 | COPY --from=client-builder /srv/client/build /usr/share/nginx/html/ 21 | CMD ["nginx", "-g", "daemon off;"] 22 | -------------------------------------------------------------------------------- /client/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS client-development 2 | RUN mkdir /srv/client && chown node:node /srv/client 3 | WORKDIR /srv/client 4 | USER node 5 | RUN mkdir -p node_modules 6 | COPY --chown=node:node package.json package.json ./ 7 | RUN npm install --silent 8 | -------------------------------------------------------------------------------- /client/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS node-modules-install 2 | RUN mkdir /srv/client && chown node:node /srv/client 3 | WORKDIR /srv/client 4 | USER node 5 | RUN mkdir -p node_modules 6 | COPY --chown=node:node package.json package.json ./ 7 | RUN npm install --no-update-notifier 8 | 9 | FROM node:20-alpine AS client-builder 10 | USER node 11 | WORKDIR /srv/client 12 | COPY --from=node-modules-install /srv/client/node_modules node_modules 13 | COPY . . 14 | USER root 15 | ARG CUSTOM_REQUEST_HEADER nAb3kY-S%qE#4!d 16 | ENV REACT_APP_CUSTOM_REQUEST_HEADER $CUSTOM_REQUEST_HEADER 17 | RUN npm run build 18 | 19 | FROM nginx as client-production 20 | EXPOSE 3000 21 | COPY /nginx/default.conf /etc/nginx/conf.d/default.conf 22 | COPY --from=client-builder /srv/client/build /usr/share/nginx/html/ 23 | CMD ["nginx", "-g", "daemon off;"] 24 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 31 | VRMS 32 | 33 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /client/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 3000; 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vrms-client", 3 | "version": "0.3.0", 4 | "description": "VRMS Client", 5 | "homepage": "https://www.vrms.io", 6 | "dependencies": { 7 | "@babel/plugin-transform-react-jsx-self": "^7.10.4", 8 | "@emotion/react": "^11.13.3", 9 | "@emotion/styled": "^11.13.0", 10 | "@mui/icons-material": "^5.14.19", 11 | "@mui/material": "^5.16.7", 12 | "@mui/x-date-pickers": "^7.21.0", 13 | "@vitejs/plugin-react": "^4.0.3", 14 | "@vitejs/plugin-react-swc": "^3.3.2", 15 | "classnames": "^2.2.6", 16 | "cross-env": "^7.0.2", 17 | "cross-var": "^1.1.0", 18 | "d3": "^5.15.1", 19 | "dotenv-cli": "^3.2.0", 20 | "http-proxy-middleware": "^2.0.9", 21 | "js-cookie": "^2.2.1", 22 | "local-storage": "^2.0.0", 23 | "mathjs": "^7.5.1", 24 | "minimist": "^1.2.6", 25 | "moment": "^2.29.2", 26 | "moment-recur": "^1.0.7", 27 | "react": "^18.2.0", 28 | "react-datepicker": "^4.16.0", 29 | "react-dom": "^18.2.0", 30 | "react-hook-form": "^7.44.3", 31 | "react-router-dom": "^5.1.2", 32 | "validator": "^13.7.0", 33 | "vite": "^4.5.14", 34 | "vite-plugin-svgr": "^3.2.0" 35 | }, 36 | "scripts": { 37 | "vite": "vite", 38 | "dev": "vite", 39 | "start": "dotenv -e .env -e ../backend/.env -- cross-var cross-env PORT=%CLIENT_PORT% vite", 40 | "build": "vite build", 41 | "test": "vitest run", 42 | "preview": "vite preview", 43 | "heroku-postbuild": "npm run build" 44 | }, 45 | "eslintConfig": { 46 | "extends": "react-app" 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "author": "sarL3y", 61 | "license": "ISC", 62 | "devDependencies": { 63 | "@testing-library/dom": "^10.3.2", 64 | "@testing-library/react": "^16.0.0", 65 | "eslint-config-airbnb": "^18.2.0", 66 | "eslint-config-airbnb-base": "^14.2.0", 67 | "eslint-config-prettier": "^6.11.0", 68 | "eslint-plugin-import": "^2.22.0", 69 | "eslint-plugin-jsx-a11y": "^6.3.1", 70 | "eslint-plugin-prettier": "^3.1.4", 71 | "eslint-plugin-react": "^7.20.6", 72 | "eslint-plugin-react-hooks": "^4.1.2", 73 | "jsdom": "^24.1.0", 74 | "prettier": "^2.1.1", 75 | "sass": "^1.49.7", 76 | "vitest": "^1.6.1" 77 | }, 78 | "engines": { 79 | "node": "<=18.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client/public/bg-image-pier.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-pier.webp -------------------------------------------------------------------------------- /client/public/bg-image-skyline.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-skyline.webp -------------------------------------------------------------------------------- /client/public/bg-image-sunset.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-sunset.webp -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/hflalogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/hflalogo.png -------------------------------------------------------------------------------- /client/public/logo180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo180.png -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo310.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "VRMS", 3 | "name": "Volunteer Relationship Management System", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo180.png", 12 | "type": "image/png", 13 | "sizes": "180x180" 14 | }, 15 | { 16 | "src": "logo192.png", 17 | "type": "image/png", 18 | "sizes": "192x192" 19 | }, 20 | { 21 | "src": "logo310.png", 22 | "type": "image/png", 23 | "sizes": "310x310" 24 | } 25 | ], 26 | "start_url": ".", 27 | "display": "standalone", 28 | "theme_color": "#000000", 29 | "background_color": "#ffffff" 30 | } 31 | -------------------------------------------------------------------------------- /client/public/projectleaderdashboard/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/check.png -------------------------------------------------------------------------------- /client/public/projectleaderdashboard/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/github.png -------------------------------------------------------------------------------- /client/public/projectleaderdashboard/googledrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/googledrive.png -------------------------------------------------------------------------------- /client/public/projectleaderdashboard/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/slack.png -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/src/App.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | width: 100vw; 4 | display: flex; 5 | justify-content: center; 6 | align-content: center; 7 | overflow: hidden; 8 | max-height: 90vh; 9 | margin: 5vh 0; 10 | } 11 | 12 | .app-container { 13 | position: relative; 14 | max-width: 500px; 15 | width: 100%; 16 | background-color: white; 17 | overflow: hidden; 18 | border-radius: 10px; 19 | padding: 15px; 20 | } 21 | 22 | .main { 23 | height: calc(90vh - 160px); 24 | overflow-y: scroll; 25 | } 26 | 27 | .flexcenter-container { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-content: center; 32 | text-align: center; 33 | height: 100%; 34 | width: 100%; 35 | } 36 | 37 | .flex-container { 38 | display: flex; 39 | flex-direction: column; 40 | height: 100%; 41 | width: 100%; 42 | } 43 | 44 | @media (max-width: 500px) { 45 | .app-container { 46 | margin: 0 15px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/api/EventsApiService.js: -------------------------------------------------------------------------------- 1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings'; 2 | 3 | class EventsApiService { 4 | constructor() { 5 | this.headers = { 6 | 'Content-Type': 'application/json', 7 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER, 8 | }; 9 | this.baseUrl = '/api/events/'; 10 | } 11 | 12 | async fetchEvents() { 13 | try { 14 | const threeWeeksInMilliseconds = 3 * 7 * 24 * 60 * 60 * 1000; // 3 weeks in milliseconds 15 | const threeWeeksAgo = new Date(); 16 | threeWeeksAgo.setTime(threeWeeksAgo.getTime() - threeWeeksInMilliseconds); 17 | 18 | const dateQuery = `?date[$gt]=${threeWeeksAgo.toISOString().split('.')[0]+"Z"}`; 19 | 20 | const res = await fetch(this.baseUrl + dateQuery, { 21 | headers: this.headers, 22 | }); 23 | return await res.json(); 24 | } catch (error) { 25 | console.log(`fetchEvents error: ${error}`); 26 | alert('Server not responding. Please refresh the page.'); 27 | return []; 28 | } 29 | } 30 | 31 | async createNewEvent(eventToCreate) { 32 | const requestOptions = { 33 | method: 'POST', 34 | headers: this.headers, 35 | body: JSON.stringify(eventToCreate), 36 | }; 37 | 38 | try { 39 | return await fetch(this.baseUrl, requestOptions); 40 | } catch (error) { 41 | console.error(`Add event error: `, error); 42 | alert('Server not responding. Please try again.'); 43 | return undefined; 44 | } 45 | } 46 | 47 | async deleteEvent(recurringEventID) { 48 | const requestOptions = { 49 | method: 'DELETE', 50 | headers: this.headers, 51 | }; 52 | 53 | try { 54 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions); 55 | } catch (error) { 56 | console.error(`Delete event error: `, error); 57 | alert('Server not responding. Please try again.'); 58 | return undefined; 59 | } 60 | } 61 | 62 | async updateEvent(eventToUpdate, eventID) { 63 | const requestOptions = { 64 | method: 'PATCH', 65 | headers: this.headers, 66 | body: JSON.stringify(eventToUpdate), 67 | }; 68 | try { 69 | return await fetch(`${this.baseUrl}${eventID}`, requestOptions); 70 | } catch (error) { 71 | console.error(`Update event error: `, error); 72 | alert('Server not responding. Please try again.'); 73 | return undefined; 74 | } 75 | } 76 | } 77 | 78 | export default EventsApiService; 79 | -------------------------------------------------------------------------------- /client/src/api/RecurringEventsApiService.js: -------------------------------------------------------------------------------- 1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings'; 2 | 3 | class RecurringEventsApiService { 4 | constructor() { 5 | this.headers = { 6 | 'Content-Type': 'application/json', 7 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER, 8 | }; 9 | this.baseUrl = '/api/recurringEvents/'; 10 | } 11 | 12 | async fetchRecurringEvents() { 13 | try { 14 | const res = await fetch(`${this.baseUrl}/internal`, { 15 | headers: this.headers, 16 | }); 17 | return await res.json(); 18 | } catch (error) { 19 | console.log(`fetchRecurringEvents error: ${error}`); 20 | alert('Server not responding. Please refresh the page.'); 21 | return []; 22 | } 23 | } 24 | 25 | async createNewRecurringEvent(eventToCreate) { 26 | const requestOptions = { 27 | method: 'POST', 28 | headers: this.headers, 29 | body: JSON.stringify(eventToCreate), 30 | }; 31 | 32 | try { 33 | return await fetch(this.baseUrl, requestOptions); 34 | } catch (error) { 35 | console.error(`Add recurring event error: `, error); 36 | alert('Server not responding. Please try again.'); 37 | return undefined; 38 | } 39 | } 40 | 41 | async deleteRecurringEvent(recurringEventID) { 42 | const requestOptions = { 43 | method: 'DELETE', 44 | headers: this.headers, 45 | }; 46 | 47 | try { 48 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions); 49 | } catch (error) { 50 | console.error(`Delete recurring event error: `, error); 51 | alert('Server not responding. Please try again.'); 52 | return undefined; 53 | } 54 | } 55 | 56 | async updateRecurringEvent(eventToUpdate, recurringEventID) { 57 | const requestOptions = { 58 | method: 'PATCH', 59 | headers: this.headers, 60 | body: JSON.stringify(eventToUpdate), 61 | }; 62 | try { 63 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions); 64 | } catch (error) { 65 | console.error(`Update recurring event error: `, error); 66 | alert('Server not responding. Please try again.'); 67 | return undefined; 68 | } 69 | } 70 | } 71 | 72 | export default RecurringEventsApiService; 73 | -------------------------------------------------------------------------------- /client/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings'; 2 | 3 | const BASE_URL = '/api/auth'; 4 | 5 | const DEFAULT_HEADERS = { 6 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER 7 | }; 8 | 9 | export const fetchLogout = async () => { 10 | return await fetch(BASE_URL + '/logout', { 11 | method: 'POST', 12 | headers: DEFAULT_HEADERS, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/common/datepicker/index.scss: -------------------------------------------------------------------------------- 1 | .datepicker-section { 2 | color: #000000; 3 | .datepicker-header { 4 | font-size: 15px; 5 | font-weight: bold; 6 | } 7 | } 8 | 9 | .datepicker-wrap { 10 | display: block; 11 | max-width: 100%; 12 | text-align: center; 13 | 14 | .datepicker-name { 15 | display: inline-flex; 16 | line-height: 1; 17 | width: 40px; 18 | max-width: 40px; 19 | } 20 | 21 | .react-datepicker-wrapper { 22 | .react-datepicker__input-container{ 23 | input[type=text] { 24 | max-width: 242px; 25 | min-width: 242px; 26 | width: 242px; 27 | height: auto; 28 | color: #000000; 29 | font-size: 14px; 30 | font-weight: normal; 31 | line-height: 1; 32 | margin: 0; 33 | border-bottom: 1px solid #000000; 34 | text-align: center; 35 | &:focus { 36 | border-bottom: 1px solid #fa114f; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/src/common/tabs/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import "./index.scss"; 3 | 4 | const TabsContainer = (props) => { 5 | 6 | const [activeTab, selectTab] = useState(props.active); 7 | 8 | function handleChange(index) { 9 | selectTab(index); 10 | } 11 | 12 | return ( 13 |
14 | 28 |
{props.children[activeTab]}
29 |
30 | ); 31 | }; 32 | 33 | export default TabsContainer; 34 | -------------------------------------------------------------------------------- /client/src/common/tabs/index.scss: -------------------------------------------------------------------------------- 1 | .tab-header{ 2 | display: flex; 3 | justify-content: center; 4 | list-style: none; 5 | padding: 0; 6 | margin-bottom: 0; 7 | li{ 8 | width: 50%; 9 | text-align: center; 10 | font-size: 15px; 11 | line-height: 1; 12 | margin-left: 0; 13 | padding: 10px; 14 | border-bottom: 2px solid #3D5A6C; 15 | transition: all .3s; 16 | font-weight: 300; 17 | cursor: pointer; 18 | &.selected{ 19 | border-bottom: 2px solid #3D5A6C; 20 | color: #FFFFFF; 21 | font-weight: bold; 22 | background: #3D5A6C; 23 | border-radius: 10px 10px 0 0; 24 | } 25 | } 26 | } 27 | 28 | .tab{ 29 | width: 100%; 30 | padding: 10px 0; 31 | font-family: 'Open Sans', sans-serif; 32 | color: #444; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/common/tabs/tab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "./index.scss"; 3 | 4 | const Tab = (props) => { 5 | return( 6 |
{props.children}
7 | ) 8 | }; 9 | 10 | export default Tab; 11 | -------------------------------------------------------------------------------- /client/src/components/ChangesModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Modal, 4 | Box, 5 | Typography, 6 | Grid 7 | } from '@mui/material' 8 | import { StyledButton } from './ProjectForm'; 9 | import { Link } from 'react-router-dom'; 10 | import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; 11 | 12 | 13 | const style = { 14 | position: 'absolute', 15 | top: '50%', 16 | left: '50%', 17 | transform: 'translate(-50%, -50%)', 18 | width: 400, 19 | bgcolor: 'background.paper', 20 | border: '2px solid #000', 21 | boxShadow: 24, 22 | p: 4, 23 | }; 24 | 25 | 26 | export default function ChangesModal({open, onClose, handleClose, destination }) { 27 | return ( 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Wait! You made some changes. 42 | 43 | 44 | Are you sure you want to exit without saving? 45 | 46 | 47 | 48 | 55 | Yes 56 | 57 | 58 | 59 | 66 | No 67 | 68 | 69 | 70 | 71 | 72 | ) 73 | } -------------------------------------------------------------------------------- /client/src/components/DashboardUsers.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import '../sass/DashboardUsers.scss'; 4 | 5 | const DashboardUsers = (props) => { 6 | const [users, setUsers] = useState(null); 7 | 8 | async function fetchData() { 9 | try { 10 | const res = await fetch("/api/users"); 11 | const resJson = await res.json(); 12 | setUsers(resJson); 13 | } catch(error) { 14 | alert(error); 15 | } 16 | console.log(users); 17 | } 18 | 19 | useEffect(() => { 20 | fetchData(); 21 | }, []); 22 | 23 | return ( 24 |
25 |
26 |
    27 | {users !== null && users.map((user, index) => ( 28 |
  • 29 |
    30 |
    31 |
    {user.name.firstName} {user.name.lastName}
    32 |
    33 |
    34 |

    Current Role: {user.currentRole}

    35 |

    Desired Role: {user.desiredRole}

    36 |
    37 |
    38 |
  • 39 | ))} 40 |
41 |
42 |
43 | ) 44 | }; 45 | 46 | export default DashboardUsers; 47 | -------------------------------------------------------------------------------- /client/src/components/ErrorContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "../sass/ErrorContainer.scss"; 4 | 5 | export function ErrorContainer({ className, ...props }) { 6 | return
{props.children}
; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import pkg from '../../package.json'; 5 | import useAuth from '../hooks/useAuth'; 6 | import { Button, Box, Typography } from '@mui/material'; 7 | 8 | import '../sass/Footer.scss'; 9 | 10 | const Footer = () => { 11 | const { auth, logout } = useAuth(); 12 | 13 | const handleLogout = async (e) => { 14 | e.preventDefault(); 15 | await logout(); 16 | return ; 17 | }; 18 | 19 | return ( 20 | // 21 | 26 | {`v${pkg.version} "Alpha"`} 30 | 31 | {auth?.user && ( 32 | 43 | {`Hi ${auth.user.name.firstName}`} 52 | 59 | 60 | )} 61 | 62 | ); 63 | }; 64 | 65 | export default Footer; 66 | -------------------------------------------------------------------------------- /client/src/components/Form.jsx: -------------------------------------------------------------------------------- 1 | // Form.jsx contains several unused components, including abstractions for button and form elements. 2 | // They are not currently being used in the codebase. 3 | 4 | import React from "react"; 5 | 6 | import "../sass/Form.scss"; 7 | 8 | /*********************************************** 9 | * LABEL 10 | ***********************************************/ 11 | export function Label({ className, isRadioParent, ...props }) { 12 | return