├── README.md ├── bug-pull-request-template.md ├── feature-pull-request-template.md ├── moat-dashboard ├── .gitignore ├── .vscode │ └── settings.json ├── client │ ├── App.tsx │ ├── assets │ │ └── moatIcon.png │ ├── components │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Menu.tsx │ │ └── SideNav.tsx │ ├── containers │ │ ├── LogsContainer.tsx │ │ ├── MainDashboard.tsx │ │ └── NodeGraphContainer.tsx │ ├── index.tsx │ ├── routes │ │ └── ErrorPage.tsx │ └── scss │ │ ├── styles.scss │ │ └── styles.scss.d.ts ├── index.html ├── package-lock.json ├── package.json ├── server │ ├── server.ts │ └── userModel.ts ├── tsconfig.json ├── types │ └── index.d.ts └── webpack.config.js ├── test-env-aws-eb-rds-deployment ├── .ebextensions │ └── 00_postgres_client.config ├── .gitignore ├── Dockerfile ├── Dockerfile-postgres ├── README.md ├── client │ ├── components │ │ ├── AppBar.jsx │ │ ├── Drawer.jsx │ │ ├── PostContainer.jsx │ │ └── PostCreator.jsx │ ├── containers │ │ ├── AppBarContainer.jsx │ │ └── DrawerContainer.jsx │ ├── index.js │ ├── misc │ │ ├── postTypes.js │ │ └── sortingDefs.js │ ├── reducers │ │ └── forgeReducer.js │ ├── routes │ │ ├── Login.jsx │ │ ├── Main.jsx │ │ └── signup.jsx │ ├── store.js │ └── styles │ │ ├── AppBar.css │ │ └── styles.css ├── docs │ ├── db │ │ └── codeforge-sql.png │ └── dev-setup │ │ └── DevSetup.md ├── index.html ├── package-lock.json ├── package.json ├── server │ ├── Controllers │ │ ├── cookieController.js │ │ ├── postController.js │ │ ├── sessionController.js │ │ └── userController.js │ ├── Models │ │ └── UserModel.js │ ├── Routes │ │ ├── PostRouter.js │ │ └── UserRouter.js │ ├── __tests__ │ │ ├── testsPostController.js │ │ └── testsUserController.js │ ├── db │ │ ├── buildDB_wTests.sh │ │ ├── sessionScheduler.sh │ │ ├── sql_scripts │ │ │ ├── buildDB.sql │ │ │ └── sessionRemove.sql │ │ └── test_data_scripts │ │ │ ├── dropTables.sql │ │ │ └── insertTestData.js │ └── server.js └── webpack.config.js └── test-env-manual-deployment ├── README.md ├── codeforge ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── client │ ├── components │ │ ├── AppBar.jsx │ │ ├── Drawer.jsx │ │ ├── PostContainer.jsx │ │ └── PostCreator.jsx │ ├── containers │ │ ├── AppBarContainer.jsx │ │ └── DrawerContainer.jsx │ ├── index.js │ ├── misc │ │ ├── postTypes.js │ │ └── sortingDefs.js │ ├── reducers │ │ └── forgeReducer.js │ ├── routes │ │ ├── Login.jsx │ │ ├── Main.jsx │ │ └── signup.jsx │ ├── store.js │ └── styles │ │ ├── AppBar.css │ │ └── styles.css ├── codeforge-deployment.yaml ├── codeforge-service.yaml ├── database-deployment.yaml ├── docs │ ├── db │ │ └── codeforge-sql.png │ └── dev-setup │ │ └── DevSetup.md ├── index.html ├── ingress-setup.yaml ├── package-lock.json ├── package.json ├── server │ ├── Controllers │ │ ├── cookieController.js │ │ ├── postController.js │ │ ├── sessionController.js │ │ └── userController.js │ ├── Models │ │ └── UserModel.js │ ├── Routes │ │ ├── PostRouter.js │ │ └── UserRouter.js │ ├── __tests__ │ │ ├── testsPostController.js │ │ └── testsUserController.js │ ├── db │ │ ├── buildDB_wTests.sh │ │ ├── sessionScheduler.sh │ │ ├── sql_scripts │ │ │ ├── buildDB.sql │ │ │ └── sessionRemove.sql │ │ └── test_data_scripts │ │ │ ├── dropTables.sql │ │ │ └── insertTestData.js │ └── server.js └── webpack.config.js ├── grafana └── grafana-configmap.yaml ├── ingress-deployment └── codeforge-ingress.yaml └── postgres-deployment ├── postgres-pvc-pv.yaml └── postgres.yaml /README.md: -------------------------------------------------------------------------------- 1 |

2 | moat logo 3 |

4 | 5 | 6 | 7 |
8 | 9 | [![Kubernetes](https://img.shields.io/badge/kubernetes-326ce5.svg?&style=for-the-badge&logo=kubernetes&logoColor=white)](https://kubernetes.io/) [![Helm](https://img.shields.io/badge/Helm-0F1689?style=for-the-badge&logo=Helm&labelColor=0F1689)](https://helm.sh/) [![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) [![AWS](https://img.shields.io/badge/Amazon_AWS-FF9900?style=for-the-badge&logo=amazonaws&logoColor=white)](https://aws.amazon.com/) [![Prometheus](https://img.shields.io/badge/Prometheus-000000?style=for-the-badge&logo=prometheus&labelColor=000000)](https://prometheus.io/) [![Grafana](https://img.shields.io/badge/grafana-393946.svg?&style=for-the-badge&logo=grafana&logoColor=orange)](https://grafana.com/) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![JavaScript](https://img.shields.io/badge/JavaScript-323330?style=for-the-badge&logo=javascript&logoColor=F7DF1E)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 10 | [![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/) [![Express](https://img.shields.io/badge/Express%20js-000000?style=for-the-badge&logo=express&logoColor=white)](https://expressjs.com/) [![Node.js](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/en) [![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Glossary/HTML5) [![NPM](https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/) [![Eslint](https://img.shields.io/badge/ESLint-4B3263?style=for-the-badge&logo=eslint&logoColor=white)](https://eslint.org/) 11 | 12 |
13 | 14 | # About moat 15 | moat is a monitoring tool that scans clusters for basic health metrics and malicious activity with Prometheus and AWS Cloudwatch. Users can view potential threats on a dashboard featuring Grafana panels to quickly pinpoint and remediate issues. We embarked upon this effort with a desire to learn more about Docker, Kubernetes, AWS, Prometheus, and Grafana. moat facilitated our explorations and helped us to deepen our understanding of these technologies, along with the importance of applying security best practices to any Kubernetes cluster. 16 | 17 | Our application is still in its development phase. Our efforts up to now have consisted of building a Kubernetes cluster using Amazon EKS to simulate a real-world scenario. Then we deployed Prometheus to scan our clusters and log essential metrics. We are currently displaying general health metrics from our kubernetes cluster on our dashboard through Grafana panels. Also, given there are many potential entry points to a cluster, we decided to focus our first security feature on identifying failed AWS login attempts, which could indicate a brute force attack. 18 | 19 | # How To Use moat 20 | The instructions below will guide you through many of the same steps we took to build out our test environment and monitoring system. It will involve creating a Kubernetes cluster on AWS, deploying an application to it, and scanning it with Prometheus. CloudWatch alerts will allow you to identify when a user exceeds a specified login attempt threshold. You can leverage our frontend dashboard to display your Grafana panels and view the data that Prometheus scrapes from your cluster. 21 | 22 | ## Initial Setup 23 | ### Folder Structure 24 | ```bash 25 | ├── root 26 | │ ├── moat-dashboard 27 | │ ├── test-env-aws-eb-rds-deployment 28 | │ ├── test-env-manual-deployment 29 | ``` 30 | 1. The `moat-dashboard` directory contains our front-end dashboard. This is where you will need to npm install the necessary dependencies to view the dashboard in your browser. 31 | 2. The `test-env-aws-eb-rds-deployment` directory contains the config files we used to launch our Kubernetes cluster in AWS using Elastic Beanstalk and RDS. Feel free to use them for your own deployments, services, etc. if you decide to take this route. 32 | 3. The `test-env-manual-deployment` directory contains the config files we used to launch a Kubernetes cluster manually. Feel free to use them for your own deployments, services, etc. if you decide to take this route. 33 | 34 | ### Launch moat Dashboard 35 | 1. Fork and clone this repo. 36 | 2. To view the moat dashboard in your browser, navigate to `moat-dashboard` directory. Install the dependencies there. 37 | ```bash 38 | npm install 39 | ``` 40 | 2. Launch moat from the command line: 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | ## Build Kubernetes Test Environment 46 | The following instructions are for building a test environment on AWS. Keep in mind, this will cost money. If you want a free alternative, try building a test environment locally with minikube. You can leverage the files in our test environment directories to deploy a cluster with the AWS CLI and YAML files OR with AWS Elastic Beanstalk and RDS: 47 | 48 | Manual configuration: test-env-manual-deployment 49 | AWS Elastic Beanstalk/RDS configuration: test-env-aws-eb-rds-deployment 50 | 51 | 52 | 1. Create an AWS account and set up your IAM roles. 53 | - While all actions *can* be done in the root user’s account, it is *NOT* recommended to use the root user for anything other than setting up IAM roles. 54 | - Set up a user group with the AdministratorAccess policy and set up any additional IAM user roles from there. Use the rule of [least-privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) for granting permissions. 55 | 2. Create a kubernetes cluster on AWS using EKS. Once the kubernetes cluster has been spun up, you will be use AWS’s console or the [AWS CLI](https://docs.aws.amazon.com/cli/) to add, edit, and stop deployments. 56 | 3. Create a Docker image of your application (or use the example CodeForge app in our repo). 57 | 4. Push your image to either ECR or Docker Hub. Remember the repo and organization name, it will be important when creating the .yaml files for deploying the EC2 instance to our k8s cluster. 58 | 5. Deploy your application to AWS EC2 instance using Elastic Beanstalk, or your preferred method. 59 | 6. Set up your database with RDS, deploy your own database to the cluster, or use your preferred method. 60 | 7. Deploy your EC2 instance and database to your Kubernetes cluster. 61 | 8. Configure nginx-ingress controller to serve EC2 instance through an external URI. Make sure that the ports being exposed are the same ports being used by your application (CodeForge uses port 3000). 62 | 9. After testing, *REMEMBER* to tear down all unused AWS resources, they *WILL* charge you. 63 | 64 | ## Command Line Setup 65 | 1. Install AWS CLI and Configure AWS Credentials. 66 | - After AWS CLI is installed, go to AWS account > security credentials > access keys > create new access key. 67 | - Create IAM Role with administration access and make sure all services and users have been assigned to use this Role. 68 | - Run the command aws configure and enter AWS credentials. (If running into permissions issues you can use the root users credentials to configure AWS but this is NOT recommended because it is not secure.) 69 | 2. Install and setup kubectl (Kubernetes Command Line Tool). 70 | 3. Install and setup Helm (Kubernetes Package Manager). 71 | 4. Install and setup eksctl (CLI for Amazon EKS). 72 | - After instilation, you can either create a cluster here or pull an existing one. 73 | - To pull an existing cluster, run the command `eksctl get cluster --name your-cluster-name --region your-region`. Make sure that everything on AWS is using the same region! 74 | 5. Update Kube Config and connect to EKS cluster. 75 | - Run the command `aws eks update-kubeconfig --name your-cluster-name --region your-region`. 76 | - You can verify this connection by running kubectl get nodes. 77 | 78 | ## Install Prometheus and Grafana 79 | The following commands will install the Prometheus and Grafana OSS (_Not Grafana Cloud_) as a sidecar container on your Kubernetes Cluster. 80 | 81 | 1. Add Helm Stable Charts for your local client: `helm repo add stable https://charts.helm.sh/stable` 82 | 2. Add Prometheus Helm repo: `helm repo add prometheus-community https://prometheus-community.github.io/helm-charts` 83 | 3. Create Prometheus namespace: `helm repo add prometheus-community https://prometheus-community.github.io/helm-charts` 84 | 4. Install Prometheus: `helm install stable prometheus-community/kube-prometheus-stack -n prometheus` 85 | 5. Verify by running `kubectl get pods -n prometheus` 86 | 6. Edit Prometheus and Grafana service files to use LoadBalancer: `kubectl get svc -n prometheus` 87 | - Grafana will be installed along with Prometheus, so no need to install it separately 88 | - `kubectl edit svc stable-kube-prometheus-sta-prometheus -n prometheus` 89 | - At the bottom of this service file, under spec, change type from ClusterIP to `type: LoadBalancer`. Under status, change to `loadBalancer: {}`. Save the file. 90 | 6. Verify type and status have been changed: `kubectl get svc -n prometheus` 91 | 7. Now do the same for Grafana: `kubectl edit svc stable-grafana -n prometheus` 92 | 8. Change type and status from ClusterIP to LoadBalancer: `kubectl get svc -n prometheus`. This will provide a URL to access both the Prometheus and Grafana Servers. 93 | 9. Grafana default login credentials: 94 | - **Username:** admin 95 | - **Password:** prom-operator 96 | 10. Access secrets by running `kubectl get svc -n prometheus` 97 | 11. Prometheus should already be configured as a data source in Grafana. 98 | 12. On the top search bar click Import Dashboard. 99 | 13. Under Import from Grafana.com, enter `15760`. 100 | 14. Select Prometheus as the data source. This will load the pre-configured dashboard Kubernetes/Views/Pods. 101 | 15. Create your own dashboard panels by querying Prometheus using PromQL, or view other pre-configured dashboards here. 102 | 16. Update Grafana configuration to allow embedding. 103 | 17. Navigate to the grafana-configmap.yaml in the repo, and run the following comand: 104 | `kubectl apply -f /path_to/grafana-configmap.yaml` 105 | 18. Verify changes are reflected in Grafana: Home > Administration > Settings > security > allow_embedding=true 106 | 19. Remove time stamp from embedded URL. 107 | 20. You can now embed dashboard panels on an external website! 108 | 109 | [**Source for installing Prometheus on EKS**](https://medium.com/@maheshbiradar8887/eks-monitoring-using-helm-prometheus-and-grafana-dashboard-e47093c08ece) 110 | 111 | ## Add Panels to Dashboard 112 | Once you have Grafana configured and your cluster data from Prometheus is being displayed in your dashboard, you should be able to embed iframes of key metrics into the moat dashboard. 113 | 114 | ## Set Up AWS CloudWatch Alerts for Failed Login Attempts 115 | 1. Set up a [CloudWatch alarm](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html#cloudwatch-alarms-for-cloudtrail-signin) for sign-in failures in AWS. 116 | 2. Create an [SNS (Simple Notification Service) topic](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarm-evaluation) in AWS. This topic will be used to send notifications when alarms are triggered. 117 | 3. In your CloudWatch alarm settings, configure actions to be taken when the alarm state changes to "ALARM." Add an action to publish a message to your SNS topic. 118 | 4. Set up a Lambda function that receives CloudWatch alarm notifications and exposes them in Grafana. Here is the python code we used: 119 | 120 | ```python 121 | import boto3 122 | import json 123 | 124 | def lambda_handler(event, context): 125 | # Extract information from the CloudWatch Alarm event 126 | try: 127 | alarm_name = event['detail']['Console sign-in failures alarm'] 128 | alarm_description = event['detail']['Raises alarms if more than 3 console sign-in failures occur in 5 minutes'] 129 | new_state = event['detail']['newState']['stateValue'] 130 | except KeyError as e: 131 | # Handle missing keys gracefully 132 | return { 133 | 'statusCode': 400, 134 | 'body': json.dumps(f'Error: Missing key {e} in event.') 135 | } 136 | 137 | # Check if the alarm has entered the ALARM state 138 | if new_state == 'ALARM': 139 | # Define the action to take when the alarm is triggered 140 | action_to_take = "Take action to address console sign-in failures." 141 | 142 | # You can add your custom logic or notifications here 143 | # For example, sending a notification using SNS 144 | sns_client = boto3.client('sns') 145 | topic_arn = 'YOUR-ARN' 146 | message = f"Alarm '{alarm_name}' triggered: {alarm_description}\nAction: {action_to_take}" 147 | 148 | sns_client.publish( 149 | TopicArn=topic_arn, 150 | Message=message, 151 | Subject=f"Alarm '{alarm_name}' Triggered" 152 | ) 153 | 154 | # You can also perform other actions or integrations as needed 155 | 156 | # Return a response 157 | response = { 158 | 'statusCode': 200, 159 | 'body': json.dumps('Alarm triggered successfully.') 160 | } 161 | else: 162 | # Return a response indicating that the alarm is not in ALARM state 163 | response = { 164 | 'statusCode': 200, 165 | 'body': json.dumps('Alarm is not in ALARM state.') 166 | } 167 | 168 | return response 169 | ``` 170 | 11. Install the CloudWatch Data Source plugin for Grafana. This plugin allows Grafana to fetch data from CloudWatch. 171 | - Go to the Grafana home page, click on the gear icon (⚙️) on the left sidebar to access the configuration. 172 | - Choose "Data Sources" and then click "Add data source." 173 | - Search for "CloudWatch" and select it. 174 | - Configure the CloudWatch data source with your AWS credentials and settings. 175 | 13. In Grafana, set up alerting rules for the panels on your dashboards. You can define alert conditions based on CloudWatch data. When an alert condition is met, Grafana can trigger actions, such as sending notifications or changing the state of a panel. 176 | 14. Test the entire setup by triggering a CloudWatch alarm with excessive login attempts. 177 | 178 | 179 | # The Team 180 | | Name | Github | LinkedIn | 181 | | ------------- | ------------- | ------------- | 182 | | Anil Kondaveeti | https://github.com/Akon530 | http://www.linkedin.com/in/anil-kondaveeti-23175320b | 183 | | Gayle Martin | https://github.com/gaylem | https://www.linkedin.com/in/gaylem/ | 184 | | Ivy Shmikler | https://github.com/ishmikler | http://www.linkedin.com/in/ivy-shmikler | 185 | | Max Weiner | https://github.com/maxweiner02 | https://www.linkedin.com/in/max-j-weiner/ | 186 | | Meredith Frazier Britt | https://github.com/mfrazb | https://www.linkedin.com/in/meredithfrazierbritt/ | 187 | 188 | # How to Contribute 189 | If you wish to contribute, or just learn from our progress, you are more then welcome! Please follow these guidelines: 190 | 191 | 1. Fork and clone the repository 192 | 2. CREATE BRANCH with the format: 193 | > [!IMPORTANT] 194 | > **category/your-branch-name-here** 195 | 196 | | Category | Description | 197 | | ------------- | ------------- | 198 | | hotfix | for quickly fixing critical issues, usually with a temporary solution | 199 | | bugfix | for fixing a bug | 200 | | feature | for adding, removing or modifying a feature | 201 | | test | for experimenting with something that is not an issue | 202 | 203 | 3. Guidelines for commit messages: 204 | - Capitalize first word 205 | - Use active voice: “Create sidebar component” 206 | - Give why/how context when helpful to other developers 207 | - Commit early and often 208 | - Use multi-author commits if you paired with another developer on your contribution 209 | 210 | 4. DID YOU ADD ANY SENSITIVE INFORMATION TO CODE? Before you commit, move your sensitive data to a .env file. and add .env to .gitignore file. 211 | 5. COMMIT when you make a meaningful change and use the guidelines. 212 | 6. When you are ready to push your code, pull down dev and merge your code BEFORE pushing. 213 | 7. Submit a pull request to the dev branch and fill out the pull request template (feature or bug). 214 | 215 | # To Do List 216 | Here's our wishlist of features and tasks, in case you wish to contribute to this project: 217 | 218 | | Task | Description | 219 | | ------------- | ------------- | 220 | | Front and back end testing | Add Cypress and jest tests for user navigation (front end) and server connection (back end)| 221 | | Sign-up, login, and authentication for moat | Implement basic sign-up functionality, as well as login and authentication, for the moat dashboard | 222 | | Lockout mechanism for excessive login attempts | Write back end middleware to lock out user from test environment application (CodeForge) after 3 login attempts | 223 | | Simulate brute force login attempts | Write script that simulates attempts to gain access to our cluster via our test environment's login portal and our dashboard login portal | 224 | -------------------------------------------------------------------------------- /bug-pull-request-template.md: -------------------------------------------------------------------------------- 1 | ## What was the bug behavior? 2 | Your summary here 3 | 4 | ## What is the behavior now? 5 | Your summary here 6 | 7 | ## What should the reviewer look at? (specific questions, particular files, etc) 8 | Your answer here 9 | 10 | ## Share a summary of your solution 11 | Your summary here 12 | 13 | ## Steps to Reproduce (if helpful for rest of team) 14 | Your steps here 15 | 16 | ## Code Author Checklist 17 | - [ ] I cleaned up my code before making my pull request. 18 | - [ ] I kept my pull request small, so code can be reviewed easier. 19 | - [ ] I updated the documentation and project hub as needed. 20 | 21 | -------------------------------------------------------------------------------- /feature-pull-request-template.md: -------------------------------------------------------------------------------- 1 | ## Why is this feature needed? 2 | Your summary here 3 | 4 | ## What should the reviewer look at? (specific questions, particular files, etc) 5 | Your thoughts here 6 | 7 | ## Share a summary of your approach (any tech/dependencies involved, tutorials that assisted you with build, AND/OR design patterns/algo strategies) 8 | Your summary here 9 | 10 | ## If you considered other solutions, why did you not implement them? 11 | Your thoughts here 12 | 13 | ## Does the build break? 14 | This is not an option, do not make pull request... 15 | 16 | 17 | ## Code Author Checklist 18 | - [ ] I cleaned up my code before making my pull request. 19 | - [ ] I kept my pull request small, so code can be reviewed easier. 20 | - [ ] I updated the documentation and project hub as needed. 21 | -------------------------------------------------------------------------------- /moat-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | postgres-configmap.yaml 4 | postgres-secret.yaml 5 | mongo-configmap.yaml 6 | mongo-secret.yaml -------------------------------------------------------------------------------- /moat-dashboard/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "githubPullRequests.ignoredPullRequestBranches": ["dev"] 3 | } 4 | -------------------------------------------------------------------------------- /moat-dashboard/client/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, MouseEvent, MouseEventHandler } from 'react'; 2 | import MainDashboard from './containers/MainDashboard'; 3 | import { getStyle } from './scss/styles.scss'; 4 | import Header from './components/Header'; 5 | import SideNav from './components/SideNav'; 6 | import Footer from './components/Footer'; 7 | import NodeGraphContainer from './containers/NodeGraphContainer'; 8 | import LogsContainer from './containers/LogsContainer'; 9 | 10 | const App: React.FC = () => { 11 | // REMEMBER: When passing state using TypeScript you need to setup an interface in the child component with the format of the props you want to pass in 12 | 13 | // add state handler for page navigation 14 | // render page based on state 15 | const [open, openDrawer] = useState(false); 16 | 17 | //Page navigation state 18 | const [selection, menuSelect] = useState('main'); 19 | 20 | const toggleDrawer = (event: MouseEvent) => { 21 | if (open === false) openDrawer(true); 22 | if (open === true) openDrawer(false); 23 | }; 24 | 25 | //Decides which page to render 26 | let page; 27 | switch(selection){ 28 | case 'main': 29 | page = 30 | break; 31 | case 'node': 32 | page = 33 | break; 34 | case 'logs': 35 | page = 36 | break; 37 | } 38 | 39 | return ( 40 |
41 |
42 | 43 | {page} 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /moat-dashboard/client/assets/moatIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/moat-dashboard/client/assets/moatIcon.png -------------------------------------------------------------------------------- /moat-dashboard/client/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CopyrightIcon from '@mui/icons-material/Copyright'; 3 | 4 | interface FooterProps { 5 | open: boolean; 6 | } 7 | 8 | function Footer(props: FooterProps) { 9 | return ( 10 |
11 | 12 |
Fantastic 5 Inc
13 |
14 | ); 15 | } 16 | 17 | export default Footer; 18 | -------------------------------------------------------------------------------- /moat-dashboard/client/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react'; 2 | import MenuIcon from '@mui/icons-material/Menu'; 3 | import NotificationsIcon from '@mui/icons-material/Notifications'; 4 | import { IconButton } from '@mui/material'; 5 | 6 | interface HeaderProps { 7 | open: boolean; 8 | toggleDrawer: MouseEventHandler; 9 | } 10 | 11 | function Header(props: HeaderProps) { 12 | const { toggleDrawer } = props; 13 | 14 | return ( 15 |
16 | 25 |
26 | ); 27 | } 28 | 29 | export default Header; 30 | -------------------------------------------------------------------------------- /moat-dashboard/client/components/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, MouseEventHandler, SetStateAction } from 'react'; 2 | import MenuIcon from '@mui/icons-material/Menu'; 3 | import NotificationsIcon from '@mui/icons-material/Notifications'; 4 | import { IconButton } from '@mui/material'; 5 | import { ButtonGroup } from '@mui/material'; 6 | import { Button } from '@mui/material'; 7 | 8 | const style = { 9 | button: { 10 | background: '#639db0', 11 | padding: '10px', 12 | boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.2)', 13 | }, 14 | }; 15 | 16 | interface MenuProps { 17 | menuSelect: Dispatch>; 18 | } 19 | 20 | function Menu(props: MenuProps) { 21 | const { menuSelect } = props; 22 | 23 | return ( 24 | 56 | ); 57 | } 58 | 59 | export default Menu; 60 | -------------------------------------------------------------------------------- /moat-dashboard/client/components/SideNav.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from 'react'; 2 | import { 3 | Drawer, 4 | IconButton, 5 | ListItem, 6 | ListItemButton, 7 | ListItemText, 8 | } from '@mui/material'; 9 | 10 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 11 | import HexagonIcon from '@mui/icons-material/Hexagon'; 12 | 13 | import Menu from './Menu'; 14 | 15 | interface NavProps { 16 | open: boolean; 17 | toggleDrawer: Function; 18 | menuSelect: Dispatch>; 19 | } 20 | 21 | function SideNav(props: NavProps) { 22 | const { toggleDrawer, open } = props; 23 | const { menuSelect } = props; 24 | return ( 25 | toggleDrawer}> 36 |
37 | toggleDrawer(e)}> 41 | 42 | 43 |
44 | 74 |
75 | ); 76 | } 77 | 78 | export default SideNav; 79 | -------------------------------------------------------------------------------- /moat-dashboard/client/containers/LogsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LogsContainerProps { 4 | open: boolean; 5 | } 6 | 7 | function LogsContainer(props: LogsContainerProps) { 8 | const { open } = props; 9 | 10 | return ( 11 |
12 | {/* PLACEHOLDER IMAGE UNTIL LOGS ARE READY*/} 13 | 14 |
15 | ); 16 | } 17 | 18 | export default LogsContainer; 19 | -------------------------------------------------------------------------------- /moat-dashboard/client/containers/MainDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // error with parsing image, React says correct loader is not installed on pkg 3 | import logo from '../assets/moatIcon.png'; 4 | 5 | // TODO: Make a dashboard item component that will modularize the iframes and pass the source as a prop so we can create a filter so users can customize their dashboard. Anago had a query so you can build a dashboard from your query so we can look into that. Max has ideas about this. 6 | interface MainDashboardProps { 7 | open: boolean; 8 | } 9 | 10 | function MainDashboard(props: MainDashboardProps) { 11 | const { open } = props; 12 | 13 | return ( 14 |
15 |
16 | 21 |

22 | Security-First Cluster Monitoring. Detect and mitigate threats with 23 | confidence. Powered by Prometheus and Grafana, moat scans clusters, 24 | finds vulnerabilities, and delivers real-time threat insights. 25 |

26 |
27 |
28 | {/* Alerts Panel */} 29 | 33 | {/* Incoming Event Logs */} 34 | 38 | {/* Scrape Duration */} 39 | 43 | {/*K8s cluster CPU Usage */} 44 | 48 | {/* K8s cluster Memory Usage */} 49 | 53 | {/*Node overall resource overview */} 54 | 58 | {/*NGINX Network Input */} 59 | 63 | {/* NGINX Network Output */} 64 | 68 | {/* Pod Network I/O */} 69 | 73 | {/* Pods with OOM killed containters */} 74 | 78 |
79 |
80 | ); 81 | } 82 | 83 | export default MainDashboard; 84 | -------------------------------------------------------------------------------- /moat-dashboard/client/containers/NodeGraphContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface NodeGraphContainerProps { 4 | open: boolean; 5 | } 6 | 7 | function NodeGraphContainer(props: NodeGraphContainerProps) { 8 | const { open } = props; 9 | 10 | return ( 11 |
12 | {/* Node Graph */} 13 | 15 |
16 | ); 17 | } 18 | 19 | export default NodeGraphContainer; 20 | -------------------------------------------------------------------------------- /moat-dashboard/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 4 | import App from './App'; 5 | import ErrorPage from './routes/ErrorPage'; 6 | import MainDashboard from './containers/MainDashboard'; 7 | 8 | const router = createBrowserRouter([ 9 | { 10 | path: '/', 11 | element: , 12 | errorElement: , 13 | }, 14 | ]); 15 | 16 | // with TS, check if root element exists before rendering 17 | const rootElement = document.getElementById('root'); 18 | if (!rootElement) throw new Error('Failed to find the root element'); 19 | const root = createRoot(rootElement); 20 | root.render( 21 | 22 | 23 | , 24 | ); 25 | -------------------------------------------------------------------------------- /moat-dashboard/client/routes/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRouteError } from 'react-router-dom'; 3 | 4 | const ErrorPage = () => { 5 | const error: any = useRouteError(); 6 | console.error(error); 7 | 8 | return ( 9 |
10 |

Oops!

11 |

Sorry, an unexpected error has occurred.

12 |

13 | {error.statusText || error.message} 14 |

15 |
16 | ); 17 | }; 18 | 19 | export default ErrorPage; 20 | -------------------------------------------------------------------------------- /moat-dashboard/client/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import url(https://db.onlinewebfonts.com/c/5c81010800152b142ea357ccbee8c40e?family=Euphemia); 2 | 3 | $headerColor: #326ce5; 4 | $lightBorder: 0.5px solid #a5a5a5; 5 | $lightBackground: #ffffff; 6 | $mediumBackground: #ddefff; 7 | $darkBackground: #1a1a1a; 8 | $accentColor: #87abf8; 9 | $secondaryColor: #639db0; 10 | $highlightColor: #edd60f; 11 | 12 | * { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | .container { 18 | max-width: 1200px; 19 | margin: 0 auto; 20 | } 21 | 22 | h1 { 23 | color: white; 24 | font-size: 60px; 25 | font-weight: 400; 26 | align-self: center; 27 | text-shadow: 1px 1px 3px 20px rgb(97, 97, 97); 28 | } 29 | 30 | iframe { 31 | box-shadow: 2px 2px 10px rgb(209, 208, 208); 32 | margin: 0.75rem; 33 | } 34 | 35 | /* APP-LEVEL CONTAINER */ 36 | #origin { 37 | display: flex; 38 | flex-direction: column; 39 | } 40 | 41 | .getStyle { 42 | background: $lightBackground; 43 | border: $lightBorder; 44 | } 45 | 46 | /* HEADER CSS */ 47 | #header-container { 48 | background: linear-gradient(90deg, #87abf8, #639db0); 49 | border: $lightBorder; 50 | padding: 0rem 1.5rem; 51 | box-shadow: 2px 2px 10px rgb(209, 208, 208); 52 | } 53 | 54 | #header { 55 | font-family: 'Euphemia'; 56 | letter-spacing: -0.2em; 57 | display: flex; 58 | justify-content: space-between; 59 | margin: 0 auto; 60 | } 61 | 62 | #menu_icon, 63 | #bell_icon { 64 | color: white; 65 | } 66 | 67 | /* SIDENAV CSS*/ 68 | #chevron { 69 | background: #87abf8; 70 | box-shadow: 2px 2px 10px rgb(209, 208, 208); 71 | text-align: right; 72 | padding: 0.4rem 1.5rem; 73 | } 74 | 75 | #nav-content { 76 | color: rgb(86, 86, 86); 77 | padding: 2rem; 78 | display: flex; 79 | flex-direction: column; 80 | justify-content: space-between; 81 | height: 100vh; 82 | } 83 | 84 | #menu-container { 85 | padding: 0rem 1.5rem; 86 | } 87 | 88 | #menu { 89 | display: flex; 90 | justify-content: space-between; 91 | margin: 0 auto; 92 | padding-bottom: 2rem; 93 | } 94 | 95 | .large-text { 96 | font-size: 1.2rem; 97 | line-height: 1.6rem; 98 | padding-bottom: 1rem; 99 | } 100 | 101 | .security-feature { 102 | padding-top: 1rem; 103 | } 104 | 105 | .small-text { 106 | font-size: 1.1rem; 107 | line-height: 1.5rem; 108 | padding-bottom: 1rem; 109 | } 110 | 111 | /* DASHBOARD CSS */ 112 | #dashboard-container { 113 | display: flex; 114 | flex-direction: column; 115 | justify-content: center; 116 | align-items: center; 117 | max-width: 1200px; 118 | margin: 0 auto; 119 | #intro { 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | padding: 2em; 124 | #logo { 125 | max-width: 15%; 126 | min-width: 15%; 127 | -webkit-filter: drop-shadow(5px 5px 5px #222); 128 | filter: drop-shadow(1px 1px 5px #7d7d7d); 129 | } 130 | .text-block { 131 | max-width: 50%; 132 | font-size: larger; 133 | line-height: 1.5em; 134 | padding: 2em; 135 | color: rgb(86, 86, 86); 136 | text-align: justify; 137 | text-justify: inter-word; 138 | } 139 | } 140 | } 141 | 142 | #dashboard { 143 | display: flex; 144 | flex-wrap: wrap; 145 | justify-content: center; 146 | align-items: center; 147 | font-family: Helvetica, sans-serif; 148 | background: $headerColor; 149 | background: linear-gradient(to top, #f0f0f0, #ffffff); 150 | } 151 | 152 | /* NODE GRAPH, LOGS CSS */ 153 | #node-graph-container, 154 | #logs-container { 155 | display: flex; 156 | align-items: center; 157 | justify-content: center; 158 | width: 100%; 159 | margin: 0 auto; 160 | } 161 | 162 | /* FOOTER */ 163 | .footer { 164 | position: sticky; 165 | background: linear-gradient(90deg, #87abf8, #639db0); 166 | color: white; 167 | border: $lightBorder; 168 | display: flex; 169 | align-items: center; 170 | justify-content: center; 171 | line-height: 2em; 172 | } 173 | 174 | /* MEDIA QUERIES */ 175 | @media (max-width: 768px) { 176 | #sidenav { 177 | display: none; 178 | } 179 | #dashboard-container { 180 | #intro { 181 | /* stack logo and dashboard description */ 182 | flex-direction: column; 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /moat-dashboard/client/scss/styles.scss.d.ts: -------------------------------------------------------------------------------- 1 | export const getStyle: any; -------------------------------------------------------------------------------- /moat-dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Moat 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /moat-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moat", 3 | "version": "1.0.0", 4 | "description": "

\r \"moat \r

", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production npx ts-node server/server.ts", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development concurrently \"npx webpack serve --open\" \"nodemon server/server.ts\"" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@emotion/react": "^11.11.1", 16 | "@emotion/styled": "^11.11.0", 17 | "@mui/icons-material": "^5.14.11", 18 | "@mui/material": "^5.14.11", 19 | "@mui/styled-engine": "^5.14.10", 20 | "@mui/styled-engine-sc": "^5.14.11", 21 | "dotenv": "^16.3.1", 22 | "express": "^4.18.2", 23 | "mongodb": "^6.1.0", 24 | "mongoose": "^7.5.3", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-router": "^6.16.0", 28 | "react-router-dom": "^6.16.0", 29 | "socket.io": "^4.7.2", 30 | "styled-components": "^5.3.11", 31 | "webpack": "^5.88.2" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.22.20", 35 | "@babel/plugin-transform-runtime": "^7.22.15", 36 | "@babel/preset-env": "^7.22.20", 37 | "@babel/preset-react": "^7.22.15", 38 | "@types/express": "^4.17.17", 39 | "@types/mongoose": "^5.11.97", 40 | "@types/node": "^20.6.3", 41 | "@types/react": "^18.2.22", 42 | "@types/react-dom": "^18.2.7", 43 | "@types/sass": "^1.45.0", 44 | "babel-loader": "^9.1.3", 45 | "concurrently": "^8.2.1", 46 | "css-loader": "^6.8.1", 47 | "file-loader": "^6.2.0", 48 | "html-loader": "^4.2.0", 49 | "html-webpack-plugin": "^5.5.3", 50 | "nodemon": "^3.0.1", 51 | "sass": "^1.67.0", 52 | "sass-loader": "^13.3.2", 53 | "style-loader": "^3.3.3", 54 | "ts-loader": "^9.4.4", 55 | "ts-node": "^10.9.1", 56 | "typed-scss-modules": "^7.1.4", 57 | "typescript": "^5.2.2", 58 | "webpack-cli": "^5.1.4", 59 | "webpack-dev-server": "^4.15.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /moat-dashboard/server/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import express, { Express, Request, Response, NextFunction } from 'express'; 3 | import { User } from './userModel'; 4 | import { join } from 'path'; 5 | 6 | const app: Express = express(); 7 | 8 | app.use(express.json()); 9 | //TODO: This line of code doesn't work, we need to figure out TS errors preventing the static fetching 10 | app.use(express.static(join(__dirname, '../client/assets'))); 11 | 12 | //TODO: Bundling is broken, we get an error when we try to display the home page 13 | // This only comes into play when we build the app and run it in production mode 14 | if (process.env.NODE_ENV === 'production') { 15 | // statically serve everything in the build folder on the route '/dist' 16 | app.use('/dist', express.static(join(__dirname, '../dist'))); 17 | // serve index.html on the route '/' 18 | app.get('/', (req: Request, res: Response) => res.status(200).sendFile(join(__dirname, '../index.html'))); 19 | } 20 | 21 | // A test route to confirm the server functions as expected 22 | app.get('/', (req: Request, res: Response) => { 23 | res.send('Express + TypeScript Server :)'); 24 | }); 25 | 26 | //TODO: Make controller and router files, and move all business logic into them 27 | // This makes a test user, NOTE: need to manually delete it each time 28 | app.post('/test', async (req: Request, res: Response, next: NextFunction) => { 29 | try { 30 | await User.create({ username: 'max', password: 'hello', email: 'socool@gmail.com' }); 31 | res.send('test user made :)'); 32 | } catch { 33 | return next({ message: { err: 'AHHHHHHH' }}) 34 | //res.send('oopsies there might be a user with that username already'); 35 | } 36 | }); 37 | 38 | // This tests that we can get info from the database 39 | app.get('/test', async (req: Request, res: Response) => { 40 | const result = await User.find({}); 41 | res.send(result); 42 | }); 43 | 44 | // Unknown route handler 45 | app.use((req: Request, res: Response) => res.sendStatus(404)); 46 | 47 | //TODO: Validate error handling is working correctly 48 | // Global Error Handler 49 | app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { 50 | const defaultErr = { 51 | log: err, 52 | status: 500, 53 | message: { err: 'An error occurred' }, 54 | }; 55 | const errorObj = { ...defaultErr, err }; 56 | console.log(errorObj.log); 57 | return res.status(errorObj.status).json(errorObj.message); 58 | }); 59 | 60 | app.listen(process.env.PORT, () => { 61 | console.log(`[server]: Server is running at http://localhost:${process.env.PORT}`); 62 | }); 63 | -------------------------------------------------------------------------------- /moat-dashboard/server/userModel.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { Schema, model, connect } from 'mongoose'; 3 | 4 | //TODO: Utilize .env file to protect the connection string 5 | 6 | interface IUser { 7 | username: string; 8 | password: string; 9 | email: string; 10 | } 11 | 12 | const userSchema = new Schema({ 13 | username: { type: String, required: true, unique: true }, 14 | password: { type: String, required: true }, 15 | email: { type: String, required: true, unique: true }, 16 | }); 17 | 18 | //TODO: Set up pre-functions to implement encryption with BCrypt 19 | 20 | const User = model('User', userSchema); 21 | 22 | run().catch(err => console.log(err)); 23 | 24 | //TODO: Move sensitive info into .env file 25 | 26 | async function run() { 27 | await connect(process.env.DB_URI!); 28 | console.log('Connected to Database'); 29 | } 30 | 31 | export { User }; -------------------------------------------------------------------------------- /moat-dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "./dist", 5 | "noImplicitAny": true, 6 | "strict": true, 7 | // "module": "es6", 8 | "target": "es5", 9 | "jsx": "react", 10 | "allowJs": true, 11 | "moduleResolution": "node", 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true 14 | // "types": ["react/next", "react-dom/next"] 15 | }, 16 | "include": ["./client/**/*", "./types/index.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /moat-dashboard/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TODO: This file was added to try and fix the error with loading images via express.static. It still isn't working. However, it seems like it's a file-loader issue, not related to anything wrong with this page. 2 | // https://dev.to/minompi/add-images-to-a-react-project-with-typescript-4gbm 3 | 4 | declare module '*.jpg' { 5 | const path: string; 6 | export default path; 7 | } 8 | 9 | declare module '*.png' { 10 | const path: string; 11 | export default path; 12 | } 13 | -------------------------------------------------------------------------------- /moat-dashboard/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './client/index.tsx', 6 | output: { 7 | path: path.join(__dirname, './dist'), 8 | publicPath: '/', 9 | filename: 'bundle.js', 10 | }, 11 | 12 | plugins: [ 13 | new HTMLWebpackPlugin({ 14 | template: './index.html', 15 | }), 16 | ], 17 | devServer: { 18 | static: { 19 | directory: path.join(__dirname, 'dist'), 20 | }, 21 | compress: true, 22 | port: 8000, 23 | hot: true, 24 | proxy: {}, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /jsx?$/, 30 | exclude: /node_modules/, 31 | loader: 'babel-loader', 32 | options: { 33 | presets: ['@babel/env', '@babel/react'], 34 | plugins: ['@babel/plugin-transform-runtime'], 35 | }, 36 | }, 37 | { 38 | test: /\.tsx?$/, 39 | use: 'ts-loader', 40 | exclude: /node_modules/, 41 | }, 42 | { 43 | test: /scss$/, 44 | exclude: /node_modules/, 45 | use: ['style-loader', 'css-loader', 'sass-loader'], 46 | }, 47 | { test: /\\.(png|jp(e*)g|svg|gif)$/, use: ['file-loader'] }, 48 | ], 49 | }, 50 | resolve: { 51 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/.ebextensions/00_postgres_client.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | amazon-linux-extras: [] 4 | 5 | commands: 6 | 01_postgres_activate: 7 | command: sudo amazon-linux-extras enable postgresql10 8 | 02_postgres_install: 9 | command: sudo yum install -y postgresql-server -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | .DS_Store 5 | coverage/ -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.17 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3000 7 | ENTRYPOINT ["npm", "start"] -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/Dockerfile-postgres: -------------------------------------------------------------------------------- 1 | FROM postgres:16.0 2 | COPY ./server/db/sql_scripts/buildDB.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/README.md: -------------------------------------------------------------------------------- 1 | # Codeforge 2 | An educational coding website for beginners to intermediate. 3 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/components/AppBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // TO DO - consider switching legacy mui/styles to mui/system 3 | import { styled } from '@mui/material/styles'; 4 | // MUI COMPONENTS 5 | import { AppBar, Toolbar, IconButton, Button, Typography } from '@mui/material'; 6 | // MUI ICONS 7 | import MenuIcon from '@mui/icons-material/Menu'; 8 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 9 | 10 | // APPBAR STYLES dependent on state - theme, drawerOpen, drawerWidth 11 | const AppBarCF = styled(AppBar, { 12 | shouldForwardProp: prop => prop !== 'drawerOpen', 13 | })(({ theme, drawerOpen, drawerWidth }) => ({ 14 | zIndex: theme.zIndex.drawer + 1, 15 | transition: theme.transitions.create(['width', 'margin'], { 16 | easing: theme.transitions.easing.sharp, 17 | duration: theme.transitions.duration.leavingScreen, 18 | }), 19 | ...(drawerOpen && { 20 | marginLeft: `${drawerWidth}px`, 21 | width: `calc(100% - ${drawerWidth}px)`, 22 | transition: theme.transitions.create(['width', 'margin'], { 23 | easing: theme.transitions.easing.sharp, 24 | duration: theme.transitions.duration.enteringScreen, 25 | }), 26 | }), 27 | })); 28 | 29 | export default function AppBarUsage(props) { 30 | const { 31 | drawerOpen, 32 | drawerWidth, 33 | curUser, 34 | curPage, 35 | toggleDrawer, 36 | handlePostWindow, 37 | } = props; 38 | return ( 39 | 44 | 48 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | {curUser.username} 69 | 70 | 71 | {curPage} 72 | 73 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/components/Drawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // TO DO - consider switching legacy mui/styles to mui/system 3 | import { styled } from '@mui/material/styles'; 4 | // MUI COMPONENTS 5 | import { 6 | Drawer, 7 | Toolbar, 8 | Divider, 9 | IconButton, 10 | List, 11 | ListItemButton, 12 | ListItemIcon, 13 | ListItemText, 14 | Typography, 15 | } from '@mui/material'; 16 | 17 | // MUI ICONS 18 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 19 | import ConstructionIcon from '@mui/icons-material/Construction'; 20 | import GridOnIcon from '@mui/icons-material/GridOn'; 21 | import FilterVintageIcon from '@mui/icons-material/FilterVintage'; 22 | import LogoutIcon from '@mui/icons-material/Logout'; 23 | import FunctionsIcon from '@mui/icons-material/Functions'; 24 | 25 | // DRAWER STYLES dependent on state - theme, drawerOpen, drawerWidth 26 | const DrawerCF = styled(Drawer, { 27 | shouldForwardProp: prop => prop !== 'open', 28 | })(({ theme, open, drawerWidth }) => ({ 29 | '& .muiDrawer-paper': { 30 | position: 'relative', 31 | whiteSpace: 'nowrap', 32 | width: `${drawerWidth}px`, 33 | transition: theme.transitions.create('width', { 34 | easing: theme.transitions.easing.sharp, 35 | duration: theme.transitions.duration.enteringScreen, 36 | }), 37 | boxSizing: 'border-box', 38 | ...(!open && { 39 | overflowX: 'hidden', 40 | transition: theme.transitions.create('width', { 41 | easing: theme.transitions.easing.sharp, 42 | duration: theme.transitions.duration.leavingScreen, 43 | }), 44 | width: theme.spacing(7), 45 | [theme.breakpoints.up('sm')]: { 46 | width: 0, 47 | }, 48 | }), 49 | }, 50 | })); 51 | 52 | export default function DrawerUsage(props) { 53 | const { 54 | drawerOpen, 55 | drawerWidth, 56 | curUser, 57 | curPage, 58 | toggleDrawer, 59 | selectPage, 60 | handleLogout, 61 | } = props; 62 | 63 | return ( 64 | 65 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {`Welcome,`} 79 | 80 | 81 | {`${curUser.username}`} 82 | 83 | 93 | selectPage('Algorithms')} 95 | sx={{ maxHeight: 75 }}> 96 | 97 | 98 | 99 | 100 | 101 | selectPage('React')} 103 | sx={{ maxHeight: 75 }}> 104 | 105 | 106 | 107 | 108 | 109 | selectPage('Redux')} 111 | sx={{ maxHeight: 75 }}> 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | ); 132 | } 133 | 134 | // export default DrawerCF; 135 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/components/PostContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | Box, 5 | FormControl, 6 | InputLabel, 7 | Select, 8 | MenuItem, 9 | Button, 10 | } from '@mui/material'; 11 | import { RENDER_TEST, CHANGE_FILTER } from '../reducers/forgeReducer'; 12 | import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined'; 13 | import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt'; 14 | import filterSortingDefs from '../misc/sortingDefs'; 15 | 16 | const PostContainer = () => { 17 | const dispatch = useDispatch(); 18 | // Pull State of Page into post container 19 | const curPage = useSelector(state => state.forge.currentPage); 20 | const Posts = useSelector(state => state.forge.curPosts); 21 | const filter = useSelector(state => state.forge.filter); 22 | const newPostWindow = useSelector(state => state.forge.newPostWindow); 23 | const handleChange = event => { 24 | dispatch(CHANGE_FILTER(event.target.value)); 25 | }; 26 | const getPostData = async () => { 27 | const serverResponse = await fetch('/post/getposts', 28 | ).catch(err => { 29 | console.log(err); 30 | }); 31 | const parsedResponse = await serverResponse.json(); 32 | const filteredResponse = parsedResponse.data.filter((post) => post.category === curPage); 33 | filteredResponse.sort(filterSortingDefs[filter]); 34 | console.log(filteredResponse); 35 | dispatch(RENDER_TEST(filteredResponse)); 36 | }; 37 | const handleThumbsUp = async event => { 38 | console.log(event.target.value); 39 | const serverResponse = await fetch('/post/vote', { 40 | method: 'PATCH', 41 | headers: { 'Content-Type': 'application/json' }, 42 | body: JSON.stringify({ vote: 'up', link: event.target.value }), 43 | }).catch(err => { 44 | console.log(err); 45 | }); 46 | getPostData(); 47 | }; 48 | const handleThumbsDown = async event => { 49 | console.log(event.target.value); 50 | const serverResponse = await fetch('/post/vote', { 51 | method: 'PATCH', 52 | headers: { 'Content-Type': 'application/json' }, 53 | body: JSON.stringify({ vote: 'down', link: event.target.value }), 54 | }).catch(err => { 55 | console.log(err); 56 | }); 57 | getPostData(); 58 | }; 59 | React.useEffect(() => { 60 | getPostData(); 61 | }, [curPage, filter, newPostWindow]); 62 | 63 | const postArr = []; 64 | Posts.forEach((post, index) => { 65 | let date = new Date(post.date_submitted); 66 | postArr.push( 67 |
76 | {post.title} 77 | 80 |

{post.description}

81 |

82 | Link: {`${post.link}`} 83 |

84 |
90 | 91 | 99 | 107 |
108 |
, 109 | ); 110 | }); 111 | const filterArr = []; 112 | Object.keys(filterSortingDefs).forEach(filterType => { 113 | filterArr.push({filterType}); 114 | }); 115 | 116 | return ( 117 | 118 | 119 | 120 | Filter 121 | 130 | 131 | 132 | {postArr} 133 | 134 | ); 135 | }; 136 | 137 | export default PostContainer; 138 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/components/PostCreator.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TOGGLE_POST_WINDOW } from '../reducers/forgeReducer.js'; 3 | // HOOKS 4 | import { useDispatch } from 'react-redux'; 5 | // MUI COMPONENTS 6 | import { 7 | Box, 8 | Button, 9 | TextField, 10 | MenuItem, 11 | Dialog, 12 | DialogActions, 13 | DialogContent, 14 | DialogContentText, 15 | DialogTitle, 16 | } from '@mui/material'; 17 | // MISC 18 | import postType from './../misc/postTypes.js'; 19 | 20 | const PostCreator = props => { 21 | const { postWindow, handlePostWindow, curPage } = props; 22 | const dispatch = useDispatch(); 23 | const handleNewPost = async event => { 24 | event.preventDefault(); 25 | const data = new FormData(event.currentTarget); 26 | const title = data.get('title'); 27 | const description = data.get('description'); 28 | const link = data.get('link'); 29 | const contentType = data.get('content'); 30 | const request = { 31 | title, 32 | type: contentType, 33 | category: curPage, 34 | link, 35 | description, 36 | }; 37 | console.log(title, description, link, contentType); 38 | const serverResponse = await fetch( 39 | '/post/createpost', 40 | { 41 | method: 'POST', 42 | headers: { 'Content-Type': 'application/json' }, 43 | body: JSON.stringify(request), 44 | }, 45 | ).catch(err => { 46 | console.log(err); 47 | }); 48 | dispatch(TOGGLE_POST_WINDOW()); 49 | }; 50 | 51 | return ( 52 |
53 | 54 | Create New Post 55 | 56 | 57 | Please enter in the following information regarding your new post: 58 | 59 | 60 | 71 | 84 | 95 | 108 | {postType.map(option => ( 109 | 110 | {option.label} 111 | 112 | ))} 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
122 | ); 123 | }; 124 | 125 | export default PostCreator; 126 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/containers/AppBarContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // REACT HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | // COMPONENTS 5 | import AppBar from '../components/AppBar.jsx'; 6 | 7 | const AppBarContainer = props => { 8 | const { 9 | drawerOpen, 10 | drawerWidth, 11 | curPage, 12 | curUser, 13 | toggleDrawer, 14 | handlePostWindow, 15 | } = props; 16 | 17 | return ( 18 |
19 | 27 |
28 | ); 29 | }; 30 | 31 | export default AppBarContainer; 32 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/containers/DrawerContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | // REDUCERS 6 | import { SET_PAGE, RENDER_TEST } from '../reducers/forgeReducer'; 7 | // COMPONENTS 8 | import Drawer from '../components/Drawer.jsx'; 9 | 10 | const DrawerContainer = props => { 11 | const { drawerOpen, drawerWidth, curPage, curUser, toggleDrawer } = props; 12 | 13 | const navigate = useNavigate(); 14 | const dispatch = useDispatch(); 15 | 16 | // SELECT CATEGORY - set posts to new category 17 | const selectPage = page => { 18 | if (page === curPage) return; 19 | // dispatch(RENDER_TEST()); 20 | dispatch(SET_PAGE(page)); 21 | }; 22 | 23 | // LOGOUT - redirect to login page 24 | // TO DO - address sessions in handler 25 | const handleLogout = async() => { 26 | await fetch('/user/logout'); 27 | navigate('/'); 28 | }; 29 | 30 | return ( 31 |
32 | 41 |
42 | ); 43 | }; 44 | 45 | export default DrawerContainer; 46 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from './store'; 6 | import Login from './routes/Login'; 7 | import Main from './routes/Main'; 8 | import Signup from './routes/signup'; 9 | 10 | // import UserProvider from './UserProvider.js'; Maybe useful for having global state if we need it 11 | import './styles/styles.css'; 12 | 13 | 14 | const root = createRoot(document.getElementById('root')); 15 | 16 | root.render( 17 | 18 | 19 | 20 | } /> 21 | } /> 22 | } /> 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/misc/postTypes.js: -------------------------------------------------------------------------------- 1 | const postType = [ 2 | { 3 | value: 'article', 4 | label: 'Article', 5 | }, 6 | { 7 | value: 'video', 8 | label: 'Video', 9 | }, 10 | { 11 | value: 'tutorial', 12 | label: 'Tutorial', 13 | }, 14 | ]; 15 | 16 | export default postType; 17 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/misc/sortingDefs.js: -------------------------------------------------------------------------------- 1 | const filterSortingDefs = {}; 2 | 3 | filterSortingDefs.Popular = (a, b) => { 4 | return b.upvotes - a.upvotes; 5 | }; 6 | filterSortingDefs.Recent = (a, b) => { 7 | return Date.parse(b.date_submitted) - Date.parse(a.date_submitted); 8 | }; 9 | filterSortingDefs.Type = (a, b) => { 10 | if (a.type[0] < b.type[0]) { 11 | return -1; 12 | } 13 | if (a.type[0] > b.type[0]) { 14 | return 1; 15 | } 16 | return 0; 17 | }; 18 | // filterSortingDefs.Test = (a,b) => { 19 | // return 20 | // } 21 | 22 | export default filterSortingDefs; 23 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/reducers/forgeReducer.js: -------------------------------------------------------------------------------- 1 | //codeRep 2 | //number of upvotes/downvotes given from profile 3 | //profilePic? 4 | 5 | import { createSlice } from '@reduxjs/toolkit'; 6 | 7 | const initialState = { 8 | //add more tabs at a later point as necessary 9 | // currentPosts: [ 10 | // { 11 | // //need to add postID; needs to increment every time new one gets generated 12 | // title: '', 13 | // content_type: '', 14 | // poster: '', 15 | // link: '', 16 | // description: '', 17 | // date: '', 18 | // }, 19 | // ], 20 | curPosts: [], 21 | drawerOpen: false, 22 | filter: 'Popular', 23 | loggedIn: false, 24 | newPostWindow: false, 25 | currentUser: { username: ' ' }, 26 | currentPage: 'Algorithms', 27 | }; 28 | 29 | //possibly move logic into database//for right now, creating array of objects specific to each topic 30 | //database: table for each tab ----| posts based on each topic 31 | 32 | //add actions (logistics) for each reducer method 33 | //inside each action, modify the portion of state that needs to be edited and return the state 34 | 35 | //logic for individual profiles initial state? 36 | 37 | export const forgeSlice = createSlice({ 38 | name: 'forge', 39 | initialState, 40 | reducers: { 41 | CHANGE_FILTER: (state, action) => { 42 | //updates filter string to coordinate further organizing reducer 43 | state.filter = action.payload; 44 | }, 45 | // payload [{title, link}] 46 | RENDER_TEST: (state, action) => { 47 | // ? Does this remember the previous posts while additionally adding the new one 48 | state.curPosts = action.payload; 49 | }, 50 | // * SKIP 51 | RENDER_POSTS: (state, action) => { 52 | //switch/case /fetch-> get array of objects -> use dispatcher to go through posts 53 | //assume that action.payload is going to be an array of objects 54 | //each object will have all of the properties of each post 55 | //we will do the fetch request BEFORE calling the dispatcher, so we will have all the post info already 56 | 57 | const resp = 'Popular'; 58 | switch (resp) { 59 | case 'Recent': 60 | for (const dateVal in action.payload) { 61 | console.log(Math.min(...dateVal)); 62 | } //arrange results in order based off least greatest accumulated time 63 | case 'Type': //database: ---| unit ---| type 64 | console.log(action.type()); 65 | case 'Popular': 66 | for (const upvotes in action.payload) { 67 | console.log(Math.max(...upvotes)); //arrange results in order based off highest number of upvotes (Math.max?) 68 | } 69 | } 70 | }, 71 | // * SKIP 72 | SET_USER: (state, action) => { 73 | console.log('reducer: SET_USER', action.payload); 74 | const { username } = action.payload; 75 | state.currentUser.username = username; 76 | }, 77 | // * SKIP 78 | SET_PAGE: (state, action) => { 79 | state.currentPage = action.payload; 80 | }, 81 | // * SKIP 82 | TOGGLE_POST_WINDOW: (state, action) => { 83 | const curState = state.newPostWindow; 84 | state.newPostWindow = !curState; 85 | }, 86 | // * SKIP 87 | TOGGLE_DRAWER: (state, action) => { 88 | const curState = state.drawerOpen; 89 | state.drawerOpen = !curState; 90 | }, 91 | // CREATE_POST: (state, action) => { 92 | // //type the function here 93 | // const { title, content_type, poster, link, description, date } = action.payload; 94 | // // state.currentPost = action.payload 95 | // return { 96 | // ...state, 97 | // currentPost: { 98 | // title, 99 | // content_type, 100 | // poster, 101 | // link, 102 | // description, 103 | // date 104 | // } 105 | // }; 106 | // }, 107 | DELETE_POST: (state, action) => { 108 | return state; 109 | }, 110 | //LOAD_POST 111 | //EDIT_POST 112 | ADD_PROFILE: (state, action) => { 113 | return state; 114 | }, 115 | DELETE_PROFILE: (state, action) => { 116 | return state; 117 | }, 118 | default: () => { 119 | return state; 120 | }, 121 | //UPDATE_PROFILE 122 | }, 123 | }); 124 | 125 | //backend sends array of posts broken down by topic, then iterate through each topic array to find correct post 126 | 127 | export const { 128 | CHANGE_FILTER, 129 | TOGGLE_DRAWER, 130 | TOGGLE_POST_WINDOW, 131 | SET_PAGE, 132 | SET_USER, 133 | RENDER_TEST, 134 | } = forgeSlice.actions; 135 | 136 | export default forgeSlice.reducer; 137 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/routes/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { useEffect } from 'react'; 6 | //REDUCERS 7 | import { SET_USER } from '../reducers/forgeReducer'; 8 | // MUI STYLES 9 | import CSSBaseline from '@mui/material/CssBaseline'; 10 | // MUI COMPONENTS 11 | import { 12 | Avatar, 13 | Box, 14 | Button, 15 | Container, 16 | Link, 17 | TextField, 18 | Typography, 19 | } from '@mui/material'; 20 | // MUI ICONS 21 | import LoginIcon from '@mui/icons-material/Login'; 22 | 23 | //container--| username 24 | // --| password 25 | // --| login button 26 | // --| signup button 27 | 28 | const Login = () => { 29 | const navigate = useNavigate(); 30 | const dispatch = useDispatch(); 31 | 32 | // useEffect(() => { 33 | // // Checks if user is signed in, if so automatically redirects 34 | // async function isSignedIn(){ 35 | // const result = await fetch('/user/isloggedin'); 36 | // const res = await result.json(); 37 | // await console.log(res) 38 | // if (res.isLoggedIn) { 39 | // navigate('/main'); 40 | // } 41 | // } 42 | // isSignedIn(); 43 | // },[]) 44 | 45 | const handleSubmit = async event => { 46 | event.preventDefault(); 47 | const data = new FormData(event.currentTarget); 48 | //Save the username and password values on-click to these variables 49 | const username = data.get('username'); 50 | const password = data.get('password'); 51 | //Send the info to the database 52 | const serverResponse = await fetch('/user/login', { 53 | method: 'POST', 54 | headers: { 'Content-Type': 'application/json' }, 55 | // credentials: "include", 56 | body: JSON.stringify({ username, password }), 57 | }).catch(err => { 58 | console.log(err); 59 | }); 60 | console.log('server response', serverResponse) 61 | const parsedResponse = await serverResponse.json(); 62 | console.log(parsedResponse); 63 | if (serverResponse.status === 200) { 64 | dispatch(SET_USER(parsedResponse)); 65 | return navigate('/main'); 66 | } else { 67 | console.log('show an error'); 68 | } 69 | }; 70 | 71 | return ( 72 | 73 | 74 | 81 | 93 | 94 | CodeForge 95 | 96 | 97 | 98 | 99 | 100 | 101 | Sign in 102 | 103 | 104 | 114 | 124 | {/* If we want to implement the ability to 'remember' a user's username and password info } 126 | label="Remember me" 127 | /> */} 128 | 136 | 137 | 138 | {"Don't have an account? Sign Up"} 139 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | export default Login; 146 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/routes/Main.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | // HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | // REDUCERS 6 | import { 7 | SET_USER, 8 | TOGGLE_DRAWER, 9 | TOGGLE_POST_WINDOW, 10 | } from '../reducers/forgeReducer'; 11 | // MUI STYLES 12 | import CssBaseline from '@mui/material/CssBaseline'; 13 | // MUI COMPONENTS 14 | import Box from '@mui/material/Box'; 15 | import Container from '@mui/material/Container'; 16 | // CONTAINERS 17 | import AppBarContainer from '../containers/AppBarContainer.jsx'; 18 | import DrawerContainer from '../containers/DrawerContainer.jsx'; 19 | import PostContainer from '../components/PostContainer'; 20 | // COMPONENTS 21 | import PostCreator from '../components/PostCreator.jsx'; 22 | 23 | const main = () => { 24 | const drawerWidth = 360; 25 | 26 | const dispatch = useDispatch(); 27 | const navigate = useNavigate(); 28 | React.useEffect(() => { 29 | /** 30 | * Fetch userinfo when page loads 31 | */ 32 | async function getCurrentUser() { 33 | const result = await fetch('/user/currentuser'); 34 | const info = await result.json(); 35 | if (info.isLoggedIn) { 36 | // set dispatch userdata 37 | dispatch(SET_USER({ username: info.data.username })); 38 | console.log('setting_user:', info.data); 39 | } else { 40 | navigate('/'); 41 | } 42 | } 43 | getCurrentUser(); 44 | }, []); 45 | // STATE 46 | const curUser = useSelector(state => state.forge.currentUser); 47 | const curPage = useSelector(state => state.forge.currentPage); 48 | const postWindow = useSelector(state => state.forge.newPostWindow); 49 | const drawerOpen = useSelector(state => state.forge.drawerOpen); 50 | 51 | // open and close CREATE NEW POST window 52 | const handlePostWindow = () => { 53 | dispatch(TOGGLE_POST_WINDOW()); 54 | }; 55 | 56 | // open and close left drawer 57 | const toggleDrawer = () => { 58 | dispatch(TOGGLE_DRAWER()); 59 | }; 60 | 61 | // MOVE TO DRAWER 62 | // SELECT CATEGORY - set posts to new category 63 | const selectPage = page => { 64 | if (page === curPage) return; 65 | dispatch(SET_PAGE(page)); 66 | }; 67 | 68 | return ( 69 | 70 | 71 | 79 | 86 | 90 | theme.palette.mode === 'light' 91 | ? theme.palette.grey[100] 92 | : theme.palette.grey[900], 93 | flexGrow: 1, 94 | height: '100vh', 95 | overflow: 'auto', 96 | }}> 97 | 98 | 99 | 105 | 106 | 107 | 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default main; 114 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/routes/signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useEffect } from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | // MUI STYLES 6 | import CSSBaseline from '@mui/material/CssBaseline'; 7 | // MUI COMPONENTS 8 | import { 9 | Avatar, 10 | Box, 11 | Button, 12 | Container, 13 | Link, 14 | TextField, 15 | Typography, 16 | } from '@mui/material'; 17 | // MUI ICONS 18 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 19 | 20 | //container--| username 21 | // --| password 22 | // --| login button 23 | // --| signup button 24 | 25 | const Signup = () => { 26 | const navigate = useNavigate(); 27 | 28 | // React.useEffect(() => { 29 | // // Checks if user is signed in, if so automatically redirects 30 | // async function isSignedIn(){ 31 | // const result = await fetch('/user/isloggedin'); 32 | // const res = await result.json(); 33 | // await console.log(res) 34 | // if (res.isLoggedIn) { 35 | // navigate('/main'); 36 | // } 37 | // } 38 | // isSignedIn(); 39 | // },[]) 40 | 41 | const handleSubmit = async event => { 42 | event.preventDefault(); 43 | const data = new FormData(event.currentTarget); 44 | const username = data.get('username'); 45 | const password = data.get('password'); 46 | const email = data.get('email'); 47 | //Send the info to the database 48 | const serverResponse = await fetch('/user/signup', { 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify({ email, username, password }), 52 | }).catch(err => { 53 | console.log(err); 54 | }); 55 | const parsedResponse = await serverResponse.json(); 56 | console.log(parsedResponse); 57 | if (serverResponse.status === 200) { 58 | return navigate('/main'); 59 | } else { 60 | console.log('show an error'); 61 | } 62 | }; 63 | 64 | return ( 65 | 66 | 67 | 74 | 86 | 87 | CodeForge 88 | 89 | 90 | 91 | 92 | 93 | 94 | Create an Account 95 | 96 | 97 | 107 | 116 | 126 | {/* If we want to implement the ability to 'remember' a user's username and password info } 128 | label="Remember me" 129 | /> */} 130 | 138 | 139 | 140 | {'Already have an account? Log in'} 141 | 142 | 143 | 144 | ); 145 | }; 146 | 147 | export default Signup; 148 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import forgeReducer from './reducers/forgeReducer'; 3 | 4 | const store = configureStore({ 5 | reducer: { 6 | forge: forgeReducer 7 | } 8 | }); 9 | 10 | export default store; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/styles/AppBar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-aws-eb-rds-deployment/client/styles/AppBar.css -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/client/styles/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-aws-eb-rds-deployment/client/styles/styles.css -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/docs/db/codeforge-sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-aws-eb-rds-deployment/docs/db/codeforge-sql.png -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/docs/dev-setup/DevSetup.md: -------------------------------------------------------------------------------- 1 | # How to setup dev environement 2 | 3 | ### Environment Variables 4 | Move the ***example.env*** file to the root directory of this project. Rename it to ***.env*** and add the path to your postgreSQL database setting it equal to the variable ***"PG_URI"***. -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Codeforge 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeforge", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production concurrently \"node server/server.js\" ", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development concurrently \"webpack serve --open\" \"nodemon server/server.js\" ", 10 | "build_sql_tests": "./server/db/buildDB_wTests.sh", 11 | "tests": "./server/db/buildDB_wTests.sh; jest --verbose", 12 | "test_coverage": "jest --coverage" 13 | }, 14 | "author": "CodeforgeLLC", 15 | "dependencies": { 16 | "@emotion/react": "^11.11.1", 17 | "@emotion/styled": "^11.11.0", 18 | "@fontsource/roboto": "^5.0.8", 19 | "@fortawesome/fontawesome-svg-core": "^1.2.15", 20 | "@fortawesome/free-regular-svg-icons": "^5.7.2", 21 | "@fortawesome/free-solid-svg-icons": "^5.7.2", 22 | "@fortawesome/react-fontawesome": "^0.1.4", 23 | "@mui/icons-material": "^5.14.7", 24 | "@mui/material": "^5.14.7", 25 | "@reduxjs/toolkit": "^1.9.5", 26 | "@types/jest": "^29.5.4", 27 | "bcrypt": "^5.1.1", 28 | "cookie-parser": "^1.4.6", 29 | "d3": "^3.5.17", 30 | "dotenv": "^16.3.1", 31 | "express": "^4.16.3", 32 | "express-session": "^1.17.3", 33 | "node-fetch": "^2.3.0", 34 | "pg": "^8.11.3", 35 | "pg-format": "^1.0.4", 36 | "pg-promise": "^11.5.4", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-redux": "^8.1.2", 40 | "react-router": "^4.3.1", 41 | "react-router-dom": "^6.15.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.1.2", 45 | "@babel/preset-env": "^7.1.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-loader": "^8.2.3", 48 | "concurrently": "^5.0.0", 49 | "cross-env": "^6.0.3", 50 | "css-loader": "^6.5.1", 51 | "eslint": "^7.32.0", 52 | "eslint-plugin-react": "^7.21.5", 53 | "file-loader": "^6.2.0", 54 | "gh-pages": "^6.0.0", 55 | "html-webpack-plugin": "^5.5.0", 56 | "jest": "^29.6.4", 57 | "nodemon": "^1.18.9", 58 | "sass-loader": "^12.3.0", 59 | "style-loader": "^3.3.1", 60 | "webpack": "^5.64.1", 61 | "webpack-cli": "^4.9.1", 62 | "webpack-dev-server": "^4.5.0", 63 | "webpack-hot-middleware": "^2.24.3" 64 | }, 65 | "nodemonConfig": { 66 | "ignore": [] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | cookieController = {}; 2 | function makeid(length) { 3 | let result = ''; 4 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | const charactersLength = characters.length; 6 | let counter = 0; 7 | while (counter < length) { 8 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 9 | counter += 1; 10 | } 11 | return result; 12 | } 13 | 14 | cookieController.setSSIDCookie =(req, res, next) => { 15 | // generate session ID 16 | const number = makeid(32); 17 | res.cookie('ssid', number, {httpOnly:true}); 18 | res.locals.token = number; 19 | return next() 20 | } 21 | 22 | cookieController.removeSSIDCookie = (req, res, next) => { 23 | res.clearCookie('ssid'); 24 | return next(); 25 | } 26 | 27 | module.exports = cookieController; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Controllers/postController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | const express = require('express'); 3 | 4 | 5 | const postController = {}; 6 | 7 | // Takes post request properties and sets up postgres query 8 | postController.createPost = async (req, res, next) => { 9 | try { 10 | // deconstruction from req.body pulling the below properties 11 | const { title, type, category, link, description } = req.body; 12 | // creating a variable to use for the postgres DB query 13 | const createPostQuery = `INSERT INTO posts (user_id, title, link, description, category, type) VALUES ($1, $2, $3, $4, $5, $6);`; 14 | // db.query method pulls below parameters from DB and inserts them into posts 15 | const params = [res.locals.userId, title, link, description, category, type]; 16 | await db.query(createPostQuery, params); 17 | return next(); 18 | // if there is an error log and return string back to 19 | } catch(err) { 20 | // if the parameters don't exist or there is an error 21 | return next({ 22 | log: `postController.createPost: Error ${err}`, 23 | message: { err: 'Error occurred in postController.createPost'} 24 | }); 25 | } 26 | } 27 | 28 | // 29 | postController.getPosts = async (req, res, next) => { 30 | try { 31 | const getPostsQuery = `SELECT *, users.username FROM posts LEFT JOIN users ON posts.user_id=users.id`; 32 | const allPosts = await db.query(getPostsQuery); 33 | res.locals.allPosts = allPosts.rows; 34 | return next(); 35 | } catch (err) { 36 | return next({ 37 | log: `postController.getPosts: Error ${err}`, 38 | message: { err: 'Error occurred in postController.getPosts'} 39 | }); 40 | } 41 | } 42 | // unfinished and untested route 43 | postController.votePost = async (req, res, next) => { 44 | try { 45 | const { vote, link } = req.body; 46 | let getPostQuery; 47 | if (vote === 'up') { 48 | getPostQuery = `UPDATE posts SET upvotes = upvotes + 1 WHERE link = $1 RETURNING *` 49 | } 50 | if (vote === 'down') { 51 | getPostQuery = `UPDATE posts SET upvotes = upvotes - 1 WHERE link = $1 RETURNING *` 52 | } 53 | const params = [link]; 54 | const updatedPost = await db.query(getPostQuery, params) 55 | res.locals.votes = updatedPost 56 | return next(); 57 | } catch(err) { 58 | return next({ 59 | log: `postController.getPosts: Error ${err}`, 60 | message: { err: 'Error occurred in postController.getPosts'} 61 | }); 62 | } 63 | } 64 | 65 | module.exports = postController; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | 3 | sessionController = {}; 4 | 5 | /** 6 | * isLoggedIn - find the appropriate session for this request in the database, then 7 | * verify whether or not the session is still valid. 8 | */ 9 | // sessionController.isLoggedIn = async (req, res, next) => { 10 | // try{ 11 | // const SSID = req.cookies.ssid; 12 | // console.log(SSID, req.cookies) 13 | // const text = 'SELECT id FROM user_sessions WHERE session_token=$1'; 14 | // const response = await db.query(text, [SSID]); 15 | // //if there is no session verify user is needed! 16 | // // if there is a response, we can kind of bypass verify user 17 | // if (response.rows.length > 0) return res.status(301).json({isLoggedIn:true}); 18 | // else next(); 19 | // } catch(err){ 20 | // next({ 21 | // log: `sessionController.isLoggedIn: Error ${err}`, 22 | // message: { err: 'Error occurred in sessionController.isLoggedIn'} 23 | // }) 24 | // } 25 | 26 | // }; 27 | 28 | /** 29 | * startSession - create and save a new Session into the database. 30 | */ 31 | sessionController.startSession = async (req, res, next) => { 32 | try { 33 | const text = 'INSERT INTO user_sessions (session_token, users_id) VALUES ($1, $2)'; 34 | const params = [res.locals.token, res.locals.userId]; 35 | await db.query(text, params); 36 | next(); 37 | } 38 | catch(err){ 39 | next({ 40 | log: `sessionController.startSession: Error ${err}`, 41 | message: { err: 'Error occurred in sessionController.startSession'} 42 | }) 43 | } 44 | // Session.create() 45 | }; 46 | 47 | /** 48 | * 49 | * Should check the cookie and pass the userId to the next piece of middleware 50 | * 51 | * @param {Object} req.cookies 52 | * @param {Number} req.cookies.ssid ssid cookie for a user 53 | * @param {Object} res.locals 54 | * @returns 55 | * @param {Number} res.locals.userId The userId of the user associated with the given session 56 | */ 57 | sessionController.checkSession = async(req, res, next) => { 58 | // 59 | try { 60 | const ssid = req.cookies.ssid; 61 | const text = 'SELECT users_id FROM user_sessions WHERE session_token = $1'; 62 | const response = await db.query(text, [ssid]); 63 | await console.log( 'this is it', response.rows[0]) 64 | if (response.rows.length) { 65 | res.locals.userId = response.rows[0].users_id; 66 | return next(); 67 | 68 | } 69 | /** 70 | * { 71 | * isLoggedIn: false 72 | * data: [] 73 | * } 74 | * 75 | */ 76 | else { 77 | console.log('ran false') 78 | return res.status(301).json({'isLoggedIn': false}); 79 | } 80 | } catch (err) { 81 | next({ 82 | log: `sessionController.checkSession: Error ${err}`, 83 | message: { err: 'Error occurred in sessionController.checkSession'} 84 | }) 85 | } 86 | } 87 | 88 | // sessionController.startSession = async (req, res, next) => { 89 | // //write code here 90 | // const exists = await Session.findOne({cookieId: res.locals.id}).then(results => {if(results) {return true} else {return false}}) 91 | // if(!exists){ 92 | // await Session.create({cookieId: `${res.locals.id}`}) 93 | // console.log("created session"); 94 | // } 95 | // next(); 96 | // }; 97 | 98 | // module.exports = sessionController; 99 | 100 | module.exports = sessionController; 101 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | const bcrypt = require('bcrypt'); 3 | require('dotenv').config(); 4 | 5 | const SALT_WORK_FACTOR = Number(process.env.SALTROUNDS); 6 | 7 | const userController = {}; 8 | 9 | 10 | /** 11 | * 12 | * @param {Object} req.body 13 | * @param {String} req.body.username Username to add, must be unique, required 14 | * @param {String} req.body.password Password to add, required 15 | * @param {String} req.body.email Email to add, required 16 | * @param {Object} res.locals 17 | * @param {Number} res.locals.userId User id created from inserting into DB 18 | * @param {Function} next When invoked without an argument, moves to next middleware 19 | * @returns undefined | error object 20 | */ 21 | userController.createUser = async (req, res, next) => { 22 | try { 23 | const { username, password, email } = req.body; 24 | const hashedPW = await bcrypt.hash(password, SALT_WORK_FACTOR); 25 | const params = [username, hashedPW, email]; 26 | const insertUserQuery = ` 27 | INSERT INTO users (username, password, email) 28 | VALUES ($1, $2, $3) 29 | RETURNING id;` 30 | const result = await db.query(insertUserQuery, params); 31 | res.locals.userId = await result.rows[0].id; 32 | return next(); 33 | } catch(err) { 34 | return next({ 35 | log: `userController.createUser: Error ${err}`, 36 | message: { err: 'Error occurred in userController.createUser'} 37 | }); 38 | } 39 | } 40 | 41 | /** 42 | * 43 | * 44 | * This middleware checks a users username and password from a POST request. If 45 | * the user is found, it stores the user_id on res.locals.userId and moves to next middleware. 46 | * Otherwise it throws an error. 47 | * 48 | * @param {Object} req.body 49 | * @param {String} req.body.username 50 | * @param {String} req.body.password 51 | * @param {Object} res.locals 52 | * @param {Number} res.locals.userId User id created from inserting into DB 53 | * @param {Function} next When invoked without an argument, moves to next middleware 54 | * @returns undefined | error object 55 | */ 56 | userController.verifyUser = async (req, res, next) => { 57 | try { 58 | const { username, password } = req.body; 59 | const params = [username]; 60 | const verifyUserQuery = ` 61 | SELECT * 62 | FROM users 63 | WHERE username = $1;` 64 | const databasePW = await db.query(verifyUserQuery, params); 65 | 66 | if(databasePW.rows.length < 1) { 67 | await bcrypt.compare(password, password); 68 | return res.status(409).json({ message: 'Username or password incorrect.'}); 69 | } 70 | const match = await bcrypt.compare(password, databasePW.rows[0].password); 71 | // res.locals.userInfo = {user: databasePW.rows} 72 | res.locals.userId = databasePW.rows[0].id; 73 | if (match) { 74 | return next(); 75 | } else { 76 | return res.status(409).json({message: 'Username or password incorrect.'}) 77 | } 78 | } catch(err) { 79 | return next({ 80 | log: `userController.verifyUser: Error ${err}`, 81 | message: { err: 'Error occurred in userController.verifyUser'} 82 | }); 83 | } 84 | } 85 | 86 | /** 87 | * Gets the user info from a logged in users Id 88 | * @param {Integer} res.locals.userId 89 | * @returns 90 | * @param {Object} res.locals.user 91 | * @param {String} res.locals.user.username 92 | */ 93 | userController.getUsername = async (req, res, next) =>{ 94 | try { 95 | const query = 'SELECT username FROM users WHERE id=$1' 96 | const params = [res.locals.userId]; 97 | 98 | const dbquery = await db.query(query, params); 99 | res.locals.user = dbquery.rows[0]; 100 | return next(); 101 | } catch (err) { 102 | return next({ 103 | log: `userController.getUsername Error ${err}`, 104 | message: { err: 'Error occurred in userController.getUsername'} 105 | }); 106 | } 107 | } 108 | 109 | 110 | module.exports = userController; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Models/UserModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | // Declare a config object to pass into new Pool() 5 | 6 | const config = { 7 | max: 5, 8 | idleTimeoutMillis: 30000 9 | }; 10 | 11 | // user - 'ydpwqckz' 12 | // database - 'ydpwqckz' 13 | // password - 'zfIxtEVDAxWvTo8tdJs63HaotaoCU1g4' 14 | // server - peanut.db.elephantsql.com AKA 'host' 15 | 16 | // uri - postgres://ydpwqckz:zfIxtEVDAxWvTo8tdJs63HaotaoCU1g4@peanut.db.elephantsql.com/ydpwqckz 17 | 18 | // Add properties to config depending on NODE_ENV value 19 | if(process.env.NODE_ENV === 'development') { 20 | config.user = process.env.DEV_USER; 21 | config.database = process.env.DEV_DATABASE; 22 | config.password = process.env.DEV_PASSWORD; 23 | config.host = process.env.DEV_HOST; 24 | config.port = process.env.DEV_PORT; 25 | } else if (process.env.NODE_ENV === 'production') { 26 | config.user = process.env.RDS_USERNAME; 27 | config.database = process.env.RDS_DB_NAME; 28 | config.password = process.env.RDS_PASSWORD; 29 | config.host = process.env.RDS_HOSTNAME; 30 | config.port = process.env.RDS_PORT; 31 | } 32 | 33 | //PG_URI = process.env.PG_URI 34 | //console.log('pgUri', PG_URI) 35 | 36 | // console.log('process.env is:') 37 | // console.log(process.env); 38 | 39 | const pool = new Pool(config); 40 | 41 | pool.on('error', function (err, client) { 42 | console.error('idle client error', err.message, err.stack); 43 | }); 44 | 45 | module.exports = { 46 | query: (text, params, callback) => { 47 | return pool.query(text, params, callback); 48 | } 49 | }; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Routes/PostRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const postController = require('../Controllers/postController'); 4 | 5 | const router = express.Router(); 6 | 7 | //when there is a post request to the /creatpost endpoint and invoke the create post method on post controller 8 | // add return statement as best practice 9 | router.post('/createpost', postController.createPost, (req, res) => { 10 | return res.status(200).json({ message: 'Created post!'}) 11 | }) 12 | 13 | router.get('/getposts', postController.getPosts, (req, res) => { 14 | return res.status(200).json({'isLoggedIn': true, 'data': res.locals.allPosts}) 15 | }) 16 | 17 | // add return statement as best practice 18 | router.patch('/vote', postController.votePost, (req, res) => { 19 | return res.status(200).json({ message: 'Voted post!'}) 20 | }) 21 | 22 | 23 | 24 | module.exports = router; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/Routes/UserRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const userController = require('../Controllers/userController'); 4 | const cookieController = require('../Controllers/cookieController'); 5 | const sessionController = require('../Controllers/sessionController') 6 | 7 | const router = express.Router(); 8 | 9 | // adding return as best practice 10 | // go to login page -> check if a session exists based on your browser cookie -> if it does exist (add some conditionals) verify user middleware and go to main page 11 | 12 | router.post('/login', userController.verifyUser, cookieController.setSSIDCookie, sessionController.startSession, 13 | (req, res) => { 14 | return res.status(200).json({redirect:true}) 15 | }) 16 | 17 | // adding return as best practice 18 | router.post('/signup', userController.createUser, cookieController.setSSIDCookie, sessionController.startSession, 19 | (req, res) => { 20 | return res.status(200).json({redirect:true}); 21 | }) 22 | 23 | router.get('/currentuser', sessionController.checkSession, userController.getUsername, (req, res) =>{ 24 | return res.status(200).json({isLoggedIn: true, 'data': res.locals.user}); 25 | }) 26 | 27 | router.get('/logout', cookieController.removeSSIDCookie, (req,res) => { 28 | return res.status(200).json('Cleared Cookies!'); 29 | }) 30 | 31 | // router.get('/isloggedin', sessionController.isLoggedIn, (req, res) => { 32 | // return res.status(200).json({isLoggedIn: false}) 33 | // }) 34 | 35 | 36 | module.exports = router; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/__tests__/testsPostController.js: -------------------------------------------------------------------------------- 1 | const postController = require('./../Controllers/postController'); 2 | const pool = require('./../Models/UserModel'); 3 | 4 | describe('postController', () => { 5 | describe('postController.getPosts', () => { 6 | let req; 7 | let res; 8 | let next; 9 | beforeEach(()=>{ 10 | req = {query: {category: undefined}}; 11 | res = {locals: {}}; 12 | next = jest.fn(); 13 | }) 14 | 15 | it('Should get all posts when category is not specified', async() => { 16 | const text = ` 17 | SELECT * 18 | FROM posts; 19 | ` 20 | const results = await pool.query(text, []); 21 | const rows = await results.rows; 22 | await postController.getPosts(req, res, next) 23 | expect(res.locals.allPosts).not.toBe(undefined); 24 | expect(res.locals.allPosts.length).toEqual(rows.length); 25 | expect(next.mock.calls[0][0]).toEqual(undefined); //Checks next was called with no args 26 | expect(next).toBeCalledTimes(1); 27 | }) 28 | 29 | it('Should get all posts belonging to a correctly specified category', async()=>{ 30 | const categories = ['algorithms'] 31 | req = {query: {category: categories[0]}}; 32 | const text = ` 33 | SELECT * 34 | FROM posts 35 | WHERE category=($1); 36 | `; 37 | const values = [categories[0]]; 38 | const results = await pool.query(text, values); 39 | const rows = await results.rows; 40 | await postController.getPosts(req, res, next); 41 | expect(res.locals.allPosts).not.toBe(undefined); 42 | expect(res.locals.allPosts.length).toEqual(rows.length); 43 | expect(next.mock.calls[0][0]).toEqual(undefined); 44 | expect(next).toBeCalledTimes(1); 45 | 46 | }) 47 | 48 | it('Should return an error if the category is not a defined category', async() => { 49 | const category = 'FakeCategory'; 50 | req = {query: {category: category}} 51 | await pool.query(req, res, next); 52 | expect(res.locals.allPosts).toBe(undefined); 53 | expect(next.mock.calls[0][0]).toBeInstanceOf(object); 54 | }) 55 | }) 56 | 57 | describe('postController.votePost', () => { 58 | let req; 59 | let res; 60 | let next; 61 | let text; 62 | beforeEach(() => { 63 | req = {body: {postId: 1}}; 64 | res = {locals: {userId: 3}}; 65 | next = jest.fn(); 66 | text = ` 67 | SELECT * 68 | FROM posts 69 | WHERE id=($1); 70 | ` 71 | }) 72 | it('Should increment voting when user votes', async() => { 73 | const results = await pool.query(text, [req.body.postId]); 74 | const voteCount = await results.rows[0].upvotes; 75 | await postController.votePost(req, res, next); 76 | const results2 = await pool.query(text, [req.body.postId]); 77 | const voteCount2 = await results2.rows[0].upvotes; 78 | 79 | expect(voteCount2).toEqual(voteCount+1); 80 | }) 81 | 82 | it('Should remove voting when user votes again', async() => { 83 | const results = await pool.query(text, [req.body.postId]); 84 | const voteCount = await results.rows[0].upvotes; 85 | await postController.votePost(req, res, next); 86 | const results2 = await pool.query(text, [req.body.postId]); 87 | const voteCount2 = await results2.rows[0].upvotes; 88 | 89 | expect(voteCount2).toEqual(voteCount-1); 90 | }) 91 | 92 | it('Should return an error if the postId does not exist', async() => { 93 | let req = {body: {postId: 10000}}; 94 | await postController.votePost(req, res, next); 95 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 96 | }) 97 | 98 | }) 99 | 100 | describe('postController.createPost', () => { 101 | it('Should return an error if properties passed in are undefined', async()=>{ 102 | const req = { 103 | body: { 104 | title: 'Test title', 105 | category:'Test category', 106 | link: 'http://fakeurl.com', 107 | description: 'Test description' 108 | } 109 | } 110 | const res = {locals: {userId: '1'}}; 111 | const next = jest.fn(); 112 | await postController.createPost(req, res, next); 113 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 114 | }) 115 | 116 | it('Should add post to the database', async()=>{ 117 | const req = { 118 | body: { 119 | title: 'Test title', 120 | type: 'Test type', 121 | category:'Test category', 122 | link: 'http://fakeurl.com', 123 | description: 'Test description' 124 | } 125 | } 126 | const res = {locals: {userId: '1'}}; 127 | const next = jest.fn(); 128 | 129 | const text = ` 130 | SELECT * 131 | FROM posts; 132 | ` 133 | const results = await pool.query(text, []); 134 | const rows = await results.rows; 135 | 136 | await postController.createPost(req, res, next); 137 | 138 | const results2 = await pool.query(text, []); 139 | const rows2 = await results.rows; 140 | 141 | expect(next.mock.calls[0][0]).toBe(undefined); 142 | expect(next).toBeCalledTimes(1); 143 | expect(rows2.length).toEqual(rows.length + 1); 144 | 145 | }) 146 | 147 | }) 148 | 149 | 150 | }) -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/__tests__/testsUserController.js: -------------------------------------------------------------------------------- 1 | const pool = require('./../Models/UserModel'); 2 | const userController = require('./../Controllers/userController'); 3 | 4 | describe('userController', () => { 5 | 6 | describe('userController.createUser', () => { 7 | let req; 8 | let res; 9 | let next; 10 | beforeEach(() => { 11 | req = {body: {username: 'chad', password: 'pass1', email: 'testemail@gmail.com'}} 12 | res = {locals: {}}; 13 | next = jest.fn(); 14 | }) 15 | it('Should create a new user in the database', async() => { 16 | const text = ` 17 | SELECT * 18 | FROM users;` 19 | const results = await pool.query(text, []); 20 | const rowsLength = await results.rows.length; 21 | await userController.createUser(req, res, next); 22 | const resultPost = await pool.query(text, []); 23 | const rowsPostLength = await resultPost.rows.length; 24 | expect(rowsPostLength).toEqual(rowsLength+1); 25 | }) 26 | 27 | it('Should return an error if a user by the user_id already exists', async() => { 28 | req.body.email = 'uniqueemail@gmail.com'; 29 | const text = ` 30 | SELECT * 31 | FROM users;` 32 | const results = await pool.query(text, []); 33 | const rowsLength = await results.rows.length; 34 | await userController.createUser(req, res, next); 35 | const resultPost = await pool.query(text, []); 36 | const rowsPostLength = await resultPost.rows.length; 37 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 38 | expect(rowsPostLength).toEqual(rowsLength); 39 | }) 40 | 41 | it('Should return an error if a user with that email already exists', async() => { 42 | req.body.username = 'uniqueUser'; 43 | const text = ` 44 | SELECT * 45 | FROM users;` 46 | const results = await pool.query(text, []); 47 | const rowsLength = await results.rows.length; 48 | await userController.createUser(req, res, next); 49 | const resultPost = await pool.query(text, []); 50 | const rowsPostLength = await resultPost.rows.length; 51 | 52 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 53 | expect(rowsPostLength).toEqual(rowsLength); 54 | }) 55 | 56 | it('Should return an error if the required fields are not sent', async() => { 57 | req = {body: {username: 'username123', password: 'password123'}}; 58 | await userController.createUser(req, res, next); 59 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 60 | }) 61 | }) 62 | 63 | describe('userController.verifyUser', () => { 64 | let req; 65 | let res; 66 | let next; 67 | beforeEach(() => { 68 | req = {body: {}}; 69 | res = {locals: {}}; 70 | next = jest.fn(); 71 | }) 72 | it('Should store the user_id on res.locals.userId and go to next middleware', async() => { 73 | const username = 'user1'; 74 | const password = '1234'; 75 | req.body.username = username; 76 | req.body.password = password; 77 | await userController.verifyUser(req, res, next); 78 | expect(res.locals.userId).toBe(4); 79 | expect(next.mock.calls[0][0]).toBe(undefined) 80 | }) 81 | 82 | it('Should throw an error for incorrect username or password', async() => { 83 | const username = 'user1'; 84 | const password = 'password'; 85 | req.body.username = username; 86 | req.body.password = password; 87 | await userController.verifyUser(req, res, next); 88 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 89 | expect(next).toBeCalledTimes(1); 90 | }) 91 | 92 | it('Should throw an error for missing a field', async() => { 93 | const username = 'user1'; 94 | req.body.username = username; 95 | await userController.verifyUser(req, res, next); 96 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 97 | expect(next).toBeCalledTimes(1); 98 | }) 99 | }) 100 | }) -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/buildDB_wTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Import variables from .env 4 | source .env 5 | echo "Connecting to DB: $PG_URI" 6 | 7 | echo "Dropping tables..." 8 | psql -d ${PG_URI} -f ./server/db/test_data_scripts/dropTables.sql 9 | echo "Done dropping tables" 10 | 11 | echo "Building tables..." 12 | psql -d ${PG_URI} -f ./server/db/sql_scripts/buildDB.sql 13 | echo "Done building tables" 14 | 15 | echo "Add test data..." 16 | node ./server/db/test_data_scripts/insertTestData.js 17 | echo "Done adding test data" -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/sessionScheduler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ######################################### 4 | 5 | # About: Run this script concurrently 6 | # with the server to drop user 7 | # sessions 8 | 9 | 10 | ######################################### 11 | 12 | # mport variables from .env 13 | source .env 14 | echo "Connecting to DB: $PG_URI" 15 | echo "Will drop out of date user sessions every ${SLEEPTIME} seconds" 16 | 17 | while [ true ] 18 | do 19 | sleep ${SLEEPTIME} 20 | echo "Dropping expired sessions" 21 | psql -d ${PG_URI} -f ./server/db/sql_scripts/sessionRemove.sql 22 | done -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/sql_scripts/buildDB.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE users ( 3 | id SERIAL PRIMARY KEY, 4 | username TEXT NOT NULL UNIQUE, 5 | password TEXT NOT NULL, 6 | email TEXT NOT NULL UNIQUE, 7 | photo TEXT 8 | ); 9 | 10 | --User session generated for a user. Session comes from randomly generated bytes 11 | CREATE TABLE user_sessions ( 12 | id SERIAL PRIMARY KEY, 13 | session_token TEXT NOT NULL UNIQUE, 14 | users_id bigint NOT NULL REFERENCES users(id), 15 | date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP 16 | ); 17 | 18 | CREATE TABLE posts ( 19 | id SERIAL PRIMARY KEY, 20 | user_id bigint REFERENCES users(id), -- assuming your users table is named "users" 21 | upvotes INTEGER DEFAULT 0, 22 | title TEXT NOT NULL, 23 | link TEXT NOT NULL UNIQUE, 24 | description TEXT NOT NULL, 25 | category TEXT NOT NULL, 26 | date_submitted TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 27 | type TEXT NOT NULL 28 | ); -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/sql_scripts/sessionRemove.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM user_sessions 2 | WHERE date_created + interval '3600 seconds' < CURRENT_TIMESTAMP; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/test_data_scripts/dropTables.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE posts, user_sessions, users; -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/db/test_data_scripts/insertTestData.js: -------------------------------------------------------------------------------- 1 | const pool = require('../../Models/UserModel'); 2 | const format = require('pg-format'); 3 | const bcrypt = require('bcrypt'); 4 | require('dotenv').config(); 5 | 6 | console.log(pool.query()); 7 | 8 | // Should move this to separate file, .env perhaps? 9 | const SALT_ROUNDS = Number(process.env.SALTROUNDS); 10 | 11 | // Setup test data for users 12 | const text = ` 13 | INSERT INTO users(username, password, email) 14 | VALUES %L;`; 15 | 16 | const values = [ 17 | ['john$456', '1234', 'thebestemail@gmail.com'], 18 | ['yeezuspeezus', '789', 'emailnumerodos@gmail.com'], 19 | ['testuser', 'pass', 'testuser@gmail.com'], 20 | ['user1', '1234', 'user1@gmail.com'], 21 | ]; 22 | 23 | /** 24 | * Perform hashing on the passwords 25 | */ 26 | values.forEach((element, index, array) => { 27 | array[index][1] = bcrypt.hashSync(element[1], SALT_ROUNDS); 28 | }); 29 | 30 | // Setup test data for posts 31 | const postsQuery = ` 32 | INSERT INTO posts(user_id, title, link, description, category, type) 33 | VALUES %L;`; 34 | 35 | const postsValues = [ 36 | [ 37 | '1', 38 | 'JavaScript Algorithms and Data Structures tutorial from FreeCodeCamp', 39 | 'https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/', 40 | 'A step by step tutorial on algorithms and data structures.', 41 | 'Algorithms', 42 | 'tutorial', 43 | ], 44 | [ 45 | '1', 46 | 'JavaScript Algorithms and Data Structures tutorial from FreeCodeCamp', 47 | 'https://www.youtube.com/watch?v=hQAHSlTtcmY', 48 | 'A 30 minute YouTube video on React.', 49 | 'React', 50 | 'video', 51 | ], 52 | [ 53 | '1', 54 | 'Test', 55 | 'https://mui.com/material-ui/react-text-field/', 56 | "Help learning react, it's a pain", 57 | 'Algorithms', 58 | 'article', 59 | ], 60 | [ 61 | '2', 62 | 'Learn React', 63 | 'https://mui.com/material-ui/react-dialog/', 64 | "Help learning react, it's a pain", 65 | 'Algorithms', 66 | 'article', 67 | ], 68 | ]; 69 | 70 | // Define async function for inserting data 71 | // need users to be inserted first 72 | // otherwise we violate foreign key required constraint 73 | async function performQueries() { 74 | await pool.query(format(text, values), []); 75 | await pool.query(format(postsQuery, postsValues), []); 76 | } 77 | 78 | // Call function 79 | performQueries(); 80 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const dotenv = require('dotenv').config(); 4 | // const cors = require('cors'); 5 | const cookieParser = require('cookie-parser'); 6 | const app = express(); 7 | 8 | const UserRouter = require('./Routes/UserRouter'); 9 | const PostRouter = require('./Routes/PostRouter'); 10 | 11 | const sessionController = require('./Controllers/sessionController'); 12 | // const corsOptions = { 13 | // origin: true, //included origin as true 14 | // credentials: true, //included credentials as true 15 | // }; 16 | 17 | const PORT = process.env.PORT || 3000; 18 | app.use(cookieParser()); 19 | // app.use(cors(corsOptions)); 20 | app.use(express.json()); 21 | 22 | app.use(express.urlencoded({ extended: true })); 23 | 24 | if (process.env.NODE_ENV === 'production') { 25 | // changed to ./dist instead of ../dist 26 | app.use(express.static(path.join(__dirname, '../dist'))); 27 | } 28 | 29 | app.use('/user', UserRouter); 30 | 31 | app.use('/post', sessionController.checkSession, PostRouter); 32 | 33 | app.use((req, res) => res.status(404).send('Oops! This is not the right page')); 34 | 35 | app.use((err, req, res, next) => { 36 | const defaultError = { 37 | log: 'Express error handler caught unknown middleware error', 38 | status: 400, 39 | message: { err: 'An error occurred' }, 40 | }; 41 | const errorObj = Object.assign({}, defaultError, err); 42 | console.log(errorObj.log); 43 | res.status(errorObj.status).send(errorObj.message); 44 | }); 45 | 46 | app.listen(PORT, () => { 47 | console.log(`Server listening on port: ${PORT}`); 48 | }); 49 | 50 | module.exports = app; 51 | -------------------------------------------------------------------------------- /test-env-aws-eb-rds-deployment/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: [ 8 | // entry point of our app 9 | './client/index.js', 10 | ], 11 | output: { 12 | // change to /dist instead of dist 13 | path: path.resolve(__dirname, 'dist'), 14 | publicPath: '/', 15 | filename: 'bundle.js', 16 | }, 17 | devtool: 'eval-source-map', 18 | mode: process.env.NODE_ENV, 19 | devServer: { 20 | compress: true, 21 | open: true, 22 | // enable HMR on the devServer 23 | hot: true, 24 | // fallback to root for other urls 25 | historyApiFallback: true, 26 | 27 | static: { 28 | // match the output path 29 | // change to /dist instead of dist 30 | directory: path.resolve(__dirname, 'dist'), 31 | // match the output 'publicPath' 32 | publicPath: '/', 33 | }, 34 | 35 | // headers: { 'Access-Control-Allow-Origin': '*' }, 36 | /** 37 | * proxy is required in order to make api calls to 38 | * express server while using hot-reload webpack server 39 | * routes api fetch requests from localhost:8080/api/* (webpack dev server) 40 | * to localhost:3000/api/* (where our Express server is running) 41 | */ 42 | proxy: { 43 | '/post/': { 44 | target: 'http://localhost:4000/', 45 | secure: false, 46 | }, 47 | '/user/': { 48 | target: 'http://localhost:4000/', 49 | secure: false, 50 | }, 51 | // '/styles.css/**': { 52 | // target: 'http://localhost:4000/', 53 | // secure: false, 54 | // }, 55 | }, 56 | }, 57 | module: { 58 | rules: [ 59 | { 60 | test: /.(js|jsx)$/, 61 | exclude: /node_modules/, 62 | use: { 63 | loader: 'babel-loader', 64 | options: { 65 | presets: ['@babel/preset-env', '@babel/preset-react'], 66 | }, 67 | }, 68 | }, 69 | { 70 | test: /.(css|scss)$/, 71 | exclude: /node_modules/, 72 | use: ['style-loader', 'css-loader'], 73 | }, 74 | ], 75 | }, 76 | plugins: [ 77 | new HtmlWebpackPlugin({ 78 | template: './index.html', 79 | }), 80 | ], 81 | resolve: { 82 | // Enable importing JS / JSX files without specifying their extension 83 | extensions: ['.js', '.jsx'], 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /test-env-manual-deployment/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-manual-deployment/README.md -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | build 4 | .git 5 | *.md 6 | .gitignore -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | .DS_Store 5 | coverage/ -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15.3-alpine 2 | WORKDIR /codeforge 3 | COPY package.json ./ 4 | RUN npm install 5 | COPY . ./ 6 | RUN npm run build 7 | 8 | CMD ["npm", "run", "build"] 9 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/README.md: -------------------------------------------------------------------------------- 1 | # Codeforge 2 | An educational coding website for beginners to intermediate. 3 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/components/AppBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // TO DO - consider switching legacy mui/styles to mui/system 3 | import { styled } from '@mui/material/styles'; 4 | // MUI COMPONENTS 5 | import { AppBar, Toolbar, IconButton, Button, Typography } from '@mui/material'; 6 | // MUI ICONS 7 | import MenuIcon from '@mui/icons-material/Menu'; 8 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 9 | 10 | // APPBAR STYLES dependent on state - theme, drawerOpen, drawerWidth 11 | const AppBarCF = styled(AppBar, { 12 | shouldForwardProp: prop => prop !== 'drawerOpen', 13 | })(({ theme, drawerOpen, drawerWidth }) => ({ 14 | zIndex: theme.zIndex.drawer + 1, 15 | transition: theme.transitions.create(['width', 'margin'], { 16 | easing: theme.transitions.easing.sharp, 17 | duration: theme.transitions.duration.leavingScreen, 18 | }), 19 | ...(drawerOpen && { 20 | marginLeft: `${drawerWidth}px`, 21 | width: `calc(100% - ${drawerWidth}px)`, 22 | transition: theme.transitions.create(['width', 'margin'], { 23 | easing: theme.transitions.easing.sharp, 24 | duration: theme.transitions.duration.enteringScreen, 25 | }), 26 | }), 27 | })); 28 | 29 | export default function AppBarUsage(props) { 30 | const { 31 | drawerOpen, 32 | drawerWidth, 33 | curUser, 34 | curPage, 35 | toggleDrawer, 36 | handlePostWindow, 37 | } = props; 38 | return ( 39 | 44 | 48 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | {curUser.username} 69 | 70 | 71 | {curPage} 72 | 73 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/components/Drawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // TO DO - consider switching legacy mui/styles to mui/system 3 | import { styled } from '@mui/material/styles'; 4 | // MUI COMPONENTS 5 | import { 6 | Drawer, 7 | Toolbar, 8 | Divider, 9 | IconButton, 10 | List, 11 | ListItemButton, 12 | ListItemIcon, 13 | ListItemText, 14 | Typography, 15 | } from '@mui/material'; 16 | 17 | // MUI ICONS 18 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 19 | import ConstructionIcon from '@mui/icons-material/Construction'; 20 | import GridOnIcon from '@mui/icons-material/GridOn'; 21 | import FilterVintageIcon from '@mui/icons-material/FilterVintage'; 22 | import LogoutIcon from '@mui/icons-material/Logout'; 23 | import FunctionsIcon from '@mui/icons-material/Functions'; 24 | 25 | // DRAWER STYLES dependent on state - theme, drawerOpen, drawerWidth 26 | const DrawerCF = styled(Drawer, { 27 | shouldForwardProp: prop => prop !== 'open', 28 | })(({ theme, open, drawerWidth }) => ({ 29 | '& .muiDrawer-paper': { 30 | position: 'relative', 31 | whiteSpace: 'nowrap', 32 | width: `${drawerWidth}px`, 33 | transition: theme.transitions.create('width', { 34 | easing: theme.transitions.easing.sharp, 35 | duration: theme.transitions.duration.enteringScreen, 36 | }), 37 | boxSizing: 'border-box', 38 | ...(!open && { 39 | overflowX: 'hidden', 40 | transition: theme.transitions.create('width', { 41 | easing: theme.transitions.easing.sharp, 42 | duration: theme.transitions.duration.leavingScreen, 43 | }), 44 | width: theme.spacing(7), 45 | [theme.breakpoints.up('sm')]: { 46 | width: 0, 47 | }, 48 | }), 49 | }, 50 | })); 51 | 52 | export default function DrawerUsage(props) { 53 | const { 54 | drawerOpen, 55 | drawerWidth, 56 | curUser, 57 | curPage, 58 | toggleDrawer, 59 | selectPage, 60 | handleLogout, 61 | } = props; 62 | 63 | console.log('drawerWidth:', drawerWidth); 64 | 65 | return ( 66 | 67 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {`Welcome,`} 81 | 82 | 83 | {`${curUser.username}`} 84 | 85 | 95 | selectPage('Algorithms')} 97 | sx={{ maxHeight: 75 }}> 98 | 99 | 100 | 101 | 102 | 103 | selectPage('React')} 105 | sx={{ maxHeight: 75 }}> 106 | 107 | 108 | 109 | 110 | 111 | selectPage('Redux')} 113 | sx={{ maxHeight: 75 }}> 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ); 134 | } 135 | 136 | // export default DrawerCF; 137 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/components/PostContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | Box, 5 | FormControl, 6 | InputLabel, 7 | Select, 8 | MenuItem, 9 | Button, 10 | } from '@mui/material'; 11 | import { RENDER_TEST, CHANGE_FILTER } from '../reducers/forgeReducer'; 12 | import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined'; 13 | import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt'; 14 | import filterSortingDefs from '../misc/sortingDefs'; 15 | 16 | const PostContainer = () => { 17 | const dispatch = useDispatch(); 18 | // Pull State of Page into post container 19 | const curPage = useSelector(state => state.forge.currentPage); 20 | const Posts = useSelector(state => state.forge.curPosts); 21 | const filter = useSelector(state => state.forge.filter); 22 | const newPostWindow = useSelector(state => state.forge.newPostWindow); 23 | const handleChange = event => { 24 | dispatch(CHANGE_FILTER(event.target.value)); 25 | }; 26 | const getPostData = async () => { 27 | const serverResponse = await fetch('/post/getposts', 28 | ).catch(err => { 29 | console.log(err); 30 | }); 31 | const parsedResponse = await serverResponse.json(); 32 | const filteredResponse = parsedResponse.data.filter((post) => post.category === curPage); 33 | filteredResponse.sort(filterSortingDefs[filter]); 34 | console.log(filteredResponse); 35 | dispatch(RENDER_TEST(filteredResponse)); 36 | }; 37 | const handleThumbsUp = async event => { 38 | console.log(event.target.value); 39 | const serverResponse = await fetch('/post/vote', { 40 | method: 'PATCH', 41 | headers: { 'Content-Type': 'application/json' }, 42 | body: JSON.stringify({ vote: 'up', link: event.target.value }), 43 | }).catch(err => { 44 | console.log(err); 45 | }); 46 | getPostData(); 47 | }; 48 | const handleThumbsDown = async event => { 49 | console.log(event.target.value); 50 | const serverResponse = await fetch('/post/vote', { 51 | method: 'PATCH', 52 | headers: { 'Content-Type': 'application/json' }, 53 | body: JSON.stringify({ vote: 'down', link: event.target.value }), 54 | }).catch(err => { 55 | console.log(err); 56 | }); 57 | getPostData(); 58 | }; 59 | React.useEffect(() => { 60 | getPostData(); 61 | }, [curPage, filter, newPostWindow]); 62 | 63 | const postArr = []; 64 | Posts.forEach((post, index) => { 65 | let date = new Date(post.date_submitted); 66 | postArr.push( 67 |
76 | {post.title} 77 | 80 |

{post.description}

81 |

82 | Link: {`${post.link}`} 83 |

84 |
90 | 91 | 99 | 107 |
108 |
, 109 | ); 110 | }); 111 | const filterArr = []; 112 | Object.keys(filterSortingDefs).forEach(filterType => { 113 | filterArr.push({filterType}); 114 | }); 115 | 116 | return ( 117 | 118 | 119 | 120 | Filter 121 | 130 | 131 | 132 | {postArr} 133 | 134 | ); 135 | }; 136 | 137 | export default PostContainer; 138 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/components/PostCreator.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TOGGLE_POST_WINDOW } from '../reducers/forgeReducer.js'; 3 | // HOOKS 4 | import { useDispatch } from 'react-redux'; 5 | // MUI COMPONENTS 6 | import { 7 | Box, 8 | Button, 9 | TextField, 10 | MenuItem, 11 | Dialog, 12 | DialogActions, 13 | DialogContent, 14 | DialogContentText, 15 | DialogTitle, 16 | } from '@mui/material'; 17 | // MISC 18 | import postType from './../misc/postTypes.js'; 19 | 20 | const PostCreator = props => { 21 | const { postWindow, handlePostWindow, curPage } = props; 22 | const dispatch = useDispatch(); 23 | const handleNewPost = async event => { 24 | event.preventDefault(); 25 | const data = new FormData(event.currentTarget); 26 | const title = data.get('title'); 27 | const description = data.get('description'); 28 | const link = data.get('link'); 29 | const contentType = data.get('content'); 30 | const request = { 31 | title, 32 | type: contentType, 33 | category: curPage, 34 | link, 35 | description, 36 | }; 37 | console.log(title, description, link, contentType); 38 | const serverResponse = await fetch( 39 | '/post/createpost', 40 | { 41 | method: 'POST', 42 | headers: { 'Content-Type': 'application/json' }, 43 | body: JSON.stringify(request), 44 | }, 45 | ).catch(err => { 46 | console.log(err); 47 | }); 48 | dispatch(TOGGLE_POST_WINDOW()); 49 | }; 50 | 51 | return ( 52 |
53 | 54 | Create New Post 55 | 56 | 57 | Please enter in the following information regarding your new post: 58 | 59 | 60 | 71 | 84 | 95 | 108 | {postType.map(option => ( 109 | 110 | {option.label} 111 | 112 | ))} 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
122 | ); 123 | }; 124 | 125 | export default PostCreator; 126 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/containers/AppBarContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // REACT HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | // COMPONENTS 5 | import AppBar from '../components/AppBar.jsx'; 6 | 7 | const AppBarContainer = props => { 8 | const { 9 | drawerOpen, 10 | drawerWidth, 11 | curPage, 12 | curUser, 13 | toggleDrawer, 14 | handlePostWindow, 15 | } = props; 16 | 17 | return ( 18 |
19 | 27 |
28 | ); 29 | }; 30 | 31 | export default AppBarContainer; 32 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/containers/DrawerContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | // REDUCERS 6 | import { SET_PAGE, RENDER_TEST } from '../reducers/forgeReducer'; 7 | // COMPONENTS 8 | import Drawer from '../components/Drawer.jsx'; 9 | 10 | const DrawerContainer = props => { 11 | const { drawerOpen, drawerWidth, curPage, curUser, toggleDrawer } = props; 12 | 13 | const navigate = useNavigate(); 14 | const dispatch = useDispatch(); 15 | 16 | // SELECT CATEGORY - set posts to new category 17 | const selectPage = page => { 18 | if (page === curPage) return; 19 | // dispatch(RENDER_TEST()); 20 | dispatch(SET_PAGE(page)); 21 | }; 22 | 23 | // LOGOUT - redirect to login page 24 | // TO DO - address sessions in handler 25 | const handleLogout = async() => { 26 | await fetch('/user/logout'); 27 | navigate('/'); 28 | }; 29 | 30 | return ( 31 |
32 | 41 |
42 | ); 43 | }; 44 | 45 | export default DrawerContainer; 46 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from './store'; 6 | import Login from './routes/Login'; 7 | import Main from './routes/Main'; 8 | import Signup from './routes/signup'; 9 | 10 | // import UserProvider from './UserProvider.js'; Maybe useful for having global state if we need it 11 | import './styles/styles.css'; 12 | 13 | 14 | const root = createRoot(document.getElementById('root')); 15 | 16 | root.render( 17 | 18 | 19 | 20 | } /> 21 | } /> 22 | } /> 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/misc/postTypes.js: -------------------------------------------------------------------------------- 1 | const postType = [ 2 | { 3 | value: 'article', 4 | label: 'Article', 5 | }, 6 | { 7 | value: 'video', 8 | label: 'Video', 9 | }, 10 | { 11 | value: 'tutorial', 12 | label: 'Tutorial', 13 | }, 14 | ]; 15 | 16 | export default postType; 17 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/misc/sortingDefs.js: -------------------------------------------------------------------------------- 1 | const filterSortingDefs = {}; 2 | 3 | filterSortingDefs.Popular = (a, b) => { 4 | return b.upvotes - a.upvotes; 5 | }; 6 | filterSortingDefs.Recent = (a, b) => { 7 | return Date.parse(b.date_submitted) - Date.parse(a.date_submitted); 8 | }; 9 | filterSortingDefs.Type = (a, b) => { 10 | if (a.type[0] < b.type[0]) { 11 | return -1; 12 | } 13 | if (a.type[0] > b.type[0]) { 14 | return 1; 15 | } 16 | return 0; 17 | }; 18 | // filterSortingDefs.Test = (a,b) => { 19 | // return 20 | // } 21 | 22 | export default filterSortingDefs; 23 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/reducers/forgeReducer.js: -------------------------------------------------------------------------------- 1 | //codeRep 2 | //number of upvotes/downvotes given from profile 3 | //profilePic? 4 | 5 | import { createSlice } from '@reduxjs/toolkit'; 6 | 7 | const initialState = { 8 | //add more tabs at a later point as necessary 9 | // currentPosts: [ 10 | // { 11 | // //need to add postID; needs to increment every time new one gets generated 12 | // title: '', 13 | // content_type: '', 14 | // poster: '', 15 | // link: '', 16 | // description: '', 17 | // date: '', 18 | // }, 19 | // ], 20 | curPosts: [], 21 | drawerOpen: false, 22 | filter: 'Popular', 23 | loggedIn: false, 24 | newPostWindow: false, 25 | currentUser: { username: ' ' }, 26 | currentPage: 'Algorithms', 27 | }; 28 | 29 | //possibly move logic into database//for right now, creating array of objects specific to each topic 30 | //database: table for each tab ----| posts based on each topic 31 | 32 | //add actions (logistics) for each reducer method 33 | //inside each action, modify the portion of state that needs to be edited and return the state 34 | 35 | //logic for individual profiles initial state? 36 | 37 | export const forgeSlice = createSlice({ 38 | name: 'forge', 39 | initialState, 40 | reducers: { 41 | CHANGE_FILTER: (state, action) => { 42 | //updates filter string to coordinate further organizing reducer 43 | state.filter = action.payload; 44 | }, 45 | // payload [{title, link}] 46 | RENDER_TEST: (state, action) => { 47 | // ? Does this remember the previous posts while additionally adding the new one 48 | state.curPosts = action.payload; 49 | }, 50 | // * SKIP 51 | RENDER_POSTS: (state, action) => { 52 | //switch/case /fetch-> get array of objects -> use dispatcher to go through posts 53 | //assume that action.payload is going to be an array of objects 54 | //each object will have all of the properties of each post 55 | //we will do the fetch request BEFORE calling the dispatcher, so we will have all the post info already 56 | 57 | const resp = 'Popular'; 58 | switch (resp) { 59 | case 'Recent': 60 | for (const dateVal in action.payload) { 61 | console.log(Math.min(...dateVal)); 62 | } //arrange results in order based off least greatest accumulated time 63 | case 'Type': //database: ---| unit ---| type 64 | console.log(action.type()); 65 | case 'Popular': 66 | for (const upvotes in action.payload) { 67 | console.log(Math.max(...upvotes)); //arrange results in order based off highest number of upvotes (Math.max?) 68 | } 69 | } 70 | }, 71 | // * SKIP 72 | SET_USER: (state, action) => { 73 | console.log('reducer', action.payload); 74 | const { username } = action.payload; 75 | state.currentUser.username = username; 76 | }, 77 | // * SKIP 78 | SET_PAGE: (state, action) => { 79 | state.currentPage = action.payload; 80 | }, 81 | // * SKIP 82 | TOGGLE_POST_WINDOW: (state, action) => { 83 | const curState = state.newPostWindow; 84 | state.newPostWindow = !curState; 85 | }, 86 | // * SKIP 87 | TOGGLE_DRAWER: (state, action) => { 88 | const curState = state.drawerOpen; 89 | state.drawerOpen = !curState; 90 | }, 91 | // CREATE_POST: (state, action) => { 92 | // //type the function here 93 | // const { title, content_type, poster, link, description, date } = action.payload; 94 | // // state.currentPost = action.payload 95 | // return { 96 | // ...state, 97 | // currentPost: { 98 | // title, 99 | // content_type, 100 | // poster, 101 | // link, 102 | // description, 103 | // date 104 | // } 105 | // }; 106 | // }, 107 | DELETE_POST: (state, action) => { 108 | return state; 109 | }, 110 | //LOAD_POST 111 | //EDIT_POST 112 | ADD_PROFILE: (state, action) => { 113 | return state; 114 | }, 115 | DELETE_PROFILE: (state, action) => { 116 | return state; 117 | }, 118 | default: () => { 119 | return state; 120 | }, 121 | //UPDATE_PROFILE 122 | }, 123 | }); 124 | 125 | //backend sends array of posts broken down by topic, then iterate through each topic array to find correct post 126 | 127 | export const { 128 | CHANGE_FILTER, 129 | TOGGLE_DRAWER, 130 | TOGGLE_POST_WINDOW, 131 | SET_PAGE, 132 | SET_USER, 133 | RENDER_TEST, 134 | } = forgeSlice.actions; 135 | 136 | export default forgeSlice.reducer; 137 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/routes/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { useEffect } from 'react'; 6 | //REDUCERS 7 | import { SET_USER } from '../reducers/forgeReducer'; 8 | // MUI STYLES 9 | import CSSBaseline from '@mui/material/CssBaseline'; 10 | // MUI COMPONENTS 11 | import { 12 | Avatar, 13 | Box, 14 | Button, 15 | Container, 16 | Link, 17 | TextField, 18 | Typography, 19 | } from '@mui/material'; 20 | // MUI ICONS 21 | import LoginIcon from '@mui/icons-material/Login'; 22 | 23 | //container--| username 24 | // --| password 25 | // --| login button 26 | // --| signup button 27 | 28 | const Login = () => { 29 | const navigate = useNavigate(); 30 | const dispatch = useDispatch(); 31 | 32 | // useEffect(() => { 33 | // // Checks if user is signed in, if so automatically redirects 34 | // async function isSignedIn(){ 35 | // const result = await fetch('/user/isloggedin'); 36 | // const res = await result.json(); 37 | // await console.log(res) 38 | // if (res.isLoggedIn) { 39 | // navigate('/main'); 40 | // } 41 | // } 42 | // isSignedIn(); 43 | // },[]) 44 | 45 | const handleSubmit = async event => { 46 | event.preventDefault(); 47 | const data = new FormData(event.currentTarget); 48 | //Save the username and password values on-click to these variables 49 | const username = data.get('username'); 50 | const password = data.get('password'); 51 | //Send the info to the database 52 | const serverResponse = await fetch('/user/login', { 53 | method: 'POST', 54 | headers: { 'Content-Type': 'application/json' }, 55 | // credentials: "include", 56 | body: JSON.stringify({ username, password }), 57 | }).catch(err => { 58 | console.log(err); 59 | }); 60 | console.log('server response', serverResponse) 61 | const parsedResponse = await serverResponse.json(); 62 | console.log(parsedResponse); 63 | if (serverResponse.status === 200) { 64 | dispatch(SET_USER(parsedResponse)); 65 | return navigate('/main'); 66 | } else { 67 | console.log('show an error'); 68 | } 69 | }; 70 | 71 | return ( 72 | 73 | 74 | 81 | 93 | 94 | CodeForge 95 | 96 | 97 | 98 | 99 | 100 | 101 | Sign in 102 | 103 | 104 | 114 | 124 | {/* If we want to implement the ability to 'remember' a user's username and password info } 126 | label="Remember me" 127 | /> */} 128 | 136 | 137 | 138 | {"Don't have an account? Sign Up"} 139 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | export default Login; 146 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/routes/Main.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | // HOOKS 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { useEffect } from 'react'; 6 | // REDUCERS 7 | import { SET_USER, TOGGLE_DRAWER, TOGGLE_POST_WINDOW } from '../reducers/forgeReducer'; 8 | // MUI STYLES 9 | import CssBaseline from '@mui/material/CssBaseline'; 10 | // MUI COMPONENTS 11 | import Box from '@mui/material/Box'; 12 | import Container from '@mui/material/Container'; 13 | // CONTAINERS 14 | import AppBarContainer from '../containers/AppBarContainer.jsx'; 15 | import DrawerContainer from '../containers/DrawerContainer.jsx'; 16 | import PostContainer from '../components/PostContainer'; 17 | // COMPONENTS 18 | import Drawer from '../components/Drawer.jsx'; 19 | import PostCreator from '../components/PostCreator.jsx'; 20 | 21 | // DRAWER subcomponents 22 | import Toolbar from '@mui/material/Toolbar'; 23 | import IconButton from '@mui/material/IconButton'; 24 | import Button from '@mui/material/Button'; 25 | import List from '@mui/material/List'; 26 | import Typography from '@mui/material/Typography'; 27 | import Divider from '@mui/material/Divider'; 28 | import ListItemButton from '@mui/material/ListItemButton'; 29 | import ListItemIcon from '@mui/material/ListItemIcon'; 30 | import ListItemText from '@mui/material/ListItemText'; 31 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 32 | import FunctionsIcon from '@mui/icons-material/Functions'; 33 | import ConstructionIcon from '@mui/icons-material/Construction'; 34 | import GridOnIcon from '@mui/icons-material/GridOn'; 35 | import FilterVintageIcon from '@mui/icons-material/FilterVintage'; 36 | import LogoutIcon from '@mui/icons-material/Logout'; 37 | 38 | const main = () => { 39 | const drawerWidth = 360; 40 | 41 | const dispatch = useDispatch(); 42 | const navigate = useNavigate(); 43 | React.useEffect(() => { 44 | /** 45 | * Fetch userinfo when page loads 46 | */ 47 | async function getCurrentUser(){ 48 | const result = await fetch('/user/currentuser') 49 | const info = await result.json(); 50 | if (info.isLoggedIn) { 51 | // set dispatch userdata 52 | 53 | dispatch(SET_USER({'username':info.data.username})) 54 | } else { 55 | navigate('/'); 56 | } 57 | } 58 | getCurrentUser(); 59 | },[]) 60 | // STATE 61 | const curUser = useSelector(state => state.forge.currentUser); 62 | const curPage = useSelector(state => state.forge.currentPage); 63 | const postWindow = useSelector(state => state.forge.newPostWindow); 64 | const drawerOpen = useSelector(state => state.forge.drawerOpen); 65 | // open and close CREATE NEW POST window 66 | const handlePostWindow = () => { 67 | dispatch(TOGGLE_POST_WINDOW()); 68 | }; 69 | 70 | // open and close left drawer 71 | const toggleDrawer = () => { 72 | dispatch(TOGGLE_DRAWER()); 73 | }; 74 | 75 | // MOVE TO DRAWER 76 | // SELECT CATEGORY - set posts to new category 77 | const selectPage = page => { 78 | if (page === curPage) return; 79 | // dispatch(RENDER_TEST()); 80 | dispatch(SET_PAGE(page)); 81 | }; 82 | 83 | // MOVE TO DRAWER 84 | // LOGOUT - redirect to login page 85 | // TO DO - address sessions in handler 86 | // const handleLogout = async() => { 87 | // await fetch('/user/logout'); 88 | // navigate('/'); 89 | // }; 90 | 91 | return ( 92 | 93 | 94 | 102 | 109 | {/* 113 | 120 | 121 | 122 | 123 | 124 | 125 | 130 | {`Welcome`} 131 | 132 | 133 | {`${curUser.name}`} 134 | 135 | 145 | newPage('Algorithms')} 147 | sx={{ maxHeight: 75 }}> 148 | 149 | 150 | 151 | 152 | 153 | newPage('React')} 155 | sx={{ maxHeight: 75 }}> 156 | 157 | 158 | 159 | 160 | 161 | newPage('Redux')} 163 | sx={{ maxHeight: 75 }}> 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | */} 183 | 184 | 188 | theme.palette.mode === 'light' 189 | ? theme.palette.grey[100] 190 | : theme.palette.grey[900], 191 | flexGrow: 1, 192 | height: '100vh', 193 | overflow: 'auto', 194 | }}> 195 | 196 | 197 | 203 | 204 | 205 | 206 | 207 | 208 | ); 209 | }; 210 | 211 | export default main; 212 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/routes/signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // HOOKS 3 | import { useEffect } from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | // MUI STYLES 6 | import CSSBaseline from '@mui/material/CssBaseline'; 7 | // MUI COMPONENTS 8 | import { 9 | Avatar, 10 | Box, 11 | Button, 12 | Container, 13 | Link, 14 | TextField, 15 | Typography, 16 | } from '@mui/material'; 17 | // MUI ICONS 18 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 19 | 20 | //container--| username 21 | // --| password 22 | // --| login button 23 | // --| signup button 24 | 25 | const Signup = () => { 26 | const navigate = useNavigate(); 27 | 28 | // React.useEffect(() => { 29 | // // Checks if user is signed in, if so automatically redirects 30 | // async function isSignedIn(){ 31 | // const result = await fetch('/user/isloggedin'); 32 | // const res = await result.json(); 33 | // await console.log(res) 34 | // if (res.isLoggedIn) { 35 | // navigate('/main'); 36 | // } 37 | // } 38 | // isSignedIn(); 39 | // },[]) 40 | 41 | const handleSubmit = async event => { 42 | event.preventDefault(); 43 | const data = new FormData(event.currentTarget); 44 | const username = data.get('username'); 45 | const password = data.get('password'); 46 | const email = data.get('email'); 47 | //Send the info to the database 48 | const serverResponse = await fetch('/user/signup', { 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify({ email, username, password }), 52 | }).catch(err => { 53 | console.log(err); 54 | }); 55 | const parsedResponse = await serverResponse.json(); 56 | console.log(parsedResponse); 57 | if (serverResponse.status === 200) { 58 | return navigate('/main'); 59 | } else { 60 | console.log('show an error'); 61 | } 62 | }; 63 | 64 | return ( 65 | 66 | 67 | 74 | 86 | 87 | CodeForge 88 | 89 | 90 | 91 | 92 | 93 | 94 | Create an Account 95 | 96 | 97 | 107 | 116 | 126 | {/* If we want to implement the ability to 'remember' a user's username and password info } 128 | label="Remember me" 129 | /> */} 130 | 138 | 139 | 140 | {'Already have an account? Log in'} 141 | 142 | 143 | 144 | ); 145 | }; 146 | 147 | export default Signup; 148 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import forgeReducer from './reducers/forgeReducer'; 3 | 4 | const store = configureStore({ 5 | reducer: { 6 | forge: forgeReducer 7 | } 8 | }); 9 | 10 | export default store; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/styles/AppBar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-manual-deployment/codeforge/client/styles/AppBar.css -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/client/styles/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-manual-deployment/codeforge/client/styles/styles.css -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/codeforge-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: codeforge 6 | --- 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | metadata: 10 | namespace: codeforge 11 | name: deployment-codeforge 12 | spec: 13 | replicas: 2 14 | selector: 15 | matchLabels: 16 | app.kubernetes.io/name: codeforge-app 17 | replicas: 5 18 | template: 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: codeforge-app 22 | spec: 23 | containers: 24 | - name: codeforge-app 25 | image: moatdev/cf-prod 26 | imagePullPolicy: Always 27 | ports: 28 | - containerPort: 5000 29 | env: 30 | - name: POSTGRES_DB 31 | valueFrom: 32 | secretKeyRef: 33 | name: postgres-secret 34 | key: postgres-db 35 | - name: POSTGRES_USER 36 | valueFrom: 37 | secretKeyRef: 38 | name: postgres-secret 39 | key: postgres-user 40 | - name: POSTGRES_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: postgres-secret 44 | key: postgres-password 45 | - name: POSTGRES_HOST 46 | valueFrom: 47 | secretKeyRef: 48 | name: postgres-secret 49 | key: postgres-host 50 | - name: POSTGRES_PORT 51 | valueFrom: 52 | secretKeyRef: 53 | name: postgres-secret 54 | key: postgres-port 55 | nodeSelector: 56 | kubernetes.io/os: linux 57 | 58 | --- 59 | kind: Service 60 | apiVersion: v1 61 | metadata: 62 | namespace: codeforge 63 | name: service-codeforge 64 | spec: 65 | type: NodePort 66 | selector: 67 | app.kubernetes.io/name: codeforge-app 68 | ports: 69 | - protocol: TCP 70 | name: http 71 | port: 80 72 | targetPort: 3000 73 | --- 74 | kind: Ingress 75 | apiVersion: networking.k8s.io/v1 76 | metadata: 77 | namespace: codeforge 78 | name: ingress-codeforge 79 | spec: 80 | ingressClassName: nginx 81 | rules: 82 | - http: 83 | paths: 84 | - path: / 85 | pathType: Prefix 86 | backend: 87 | service: 88 | name: service-codeforge 89 | port: 90 | number: 80 91 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/codeforge-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: codeforge-service 5 | labels: 6 | app: codeforge-app 7 | spec: 8 | selector: 9 | app: codeforge-app 10 | ports: 11 | - protocol: TCP 12 | port: 80 13 | targetPort: 80 14 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/database-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: database-service 6 | name: database-service 7 | spec: 8 | externalName: cfdb-prod-instance-1.czshkme9oisa.us-east-1.rds.amazonaws.com 9 | selector: 10 | app: database-service 11 | type: ExternalName 12 | ports: 13 | - port: 5432 14 | protocol: TCP 15 | targetPort: 5432 16 | status: 17 | loadBalancer: {} -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/docs/db/codeforge-sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/moat/ed67cf895e0807f548f413da5bc5cc38ac4d60c2/test-env-manual-deployment/codeforge/docs/db/codeforge-sql.png -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/docs/dev-setup/DevSetup.md: -------------------------------------------------------------------------------- 1 | # How to setup dev environement 2 | 3 | ### Environment Variables 4 | Move the ***example.env*** file to the root directory of this project. Rename it to ***.env*** and add the path to your postgreSQL database setting it equal to the variable ***"PG_URI"***. -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Codeforge 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/ingress-setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-setup 5 | annotations: 6 | # ingress.kubernetes.io/rewrite-target: / 7 | nginx.ingress.kubernetes.io/ssl-redirect: 'false' 8 | nginx.ingress.kubernetes.io/force-ssl-redirect: 'false' 9 | nginx.ingress.kubernetes.io/rewrite-target: / 10 | spec: 11 | rules: 12 | - http: 13 | paths: 14 | - path: / 15 | pathType: ImplementationSpecific 16 | backend: 17 | service: 18 | name: codeforge 19 | port: 20 | number: 5678 21 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeforge", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production nodemon server/server.js", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development webpack serve --open --hot & NODE_ENV=development nodemon server/server.js", 10 | "build_sql_tests": "./server/db/buildDB_wTests.sh", 11 | "tests": "./server/db/buildDB_wTests.sh; jest --verbose", 12 | "test_coverage": "jest --coverage" 13 | }, 14 | "author": "CodeforgeLLC", 15 | "dependencies": { 16 | "@emotion/react": "^11.11.1", 17 | "@emotion/styled": "^11.11.0", 18 | "@fontsource/roboto": "^5.0.8", 19 | "@fortawesome/fontawesome-svg-core": "^1.2.15", 20 | "@fortawesome/free-regular-svg-icons": "^5.7.2", 21 | "@fortawesome/free-solid-svg-icons": "^5.7.2", 22 | "@fortawesome/react-fontawesome": "^0.1.4", 23 | "@mui/icons-material": "^5.14.7", 24 | "@mui/material": "^5.14.7", 25 | "@reduxjs/toolkit": "^1.9.5", 26 | "@types/jest": "^29.5.4", 27 | "bcrypt": "^5.1.1", 28 | "cookie-parser": "^1.4.6", 29 | "d3": "^3.5.17", 30 | "dotenv": "^16.3.1", 31 | "express": "^4.16.3", 32 | "express-session": "^1.17.3", 33 | "node-fetch": "^2.3.0", 34 | "pg": "^8.11.3", 35 | "pg-format": "^1.0.4", 36 | "pg-promise": "^11.5.4", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-redux": "^8.1.2", 40 | "react-router": "^4.3.1", 41 | "react-router-dom": "^6.15.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.1.2", 45 | "@babel/preset-env": "^7.1.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-loader": "^8.2.3", 48 | "concurrently": "^5.0.0", 49 | "cross-env": "^6.0.3", 50 | "css-loader": "^6.5.1", 51 | "eslint": "^7.32.0", 52 | "eslint-plugin-react": "^7.21.5", 53 | "file-loader": "^6.2.0", 54 | "gh-pages": "^6.0.0", 55 | "html-webpack-plugin": "^5.5.0", 56 | "jest": "^29.6.4", 57 | "nodemon": "^1.18.9", 58 | "sass-loader": "^12.3.0", 59 | "style-loader": "^3.3.1", 60 | "webpack": "^5.88.2", 61 | "webpack-cli": "^4.10.0", 62 | "webpack-dev-server": "^4.5.0", 63 | "webpack-hot-middleware": "^2.24.3" 64 | }, 65 | "nodemonConfig": { 66 | "ignore": [] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | cookieController = {}; 2 | 3 | function makeid(length) { 4 | let result = ''; 5 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 6 | const charactersLength = characters.length; 7 | let counter = 0; 8 | while (counter < length) { 9 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 10 | counter += 1; 11 | } 12 | return result; 13 | } 14 | 15 | cookieController.setSSIDCookie =(req, res, next) => { 16 | // generate session ID 17 | const number = makeid(32); 18 | res.cookie('ssid', number, {httpOnly:true}); 19 | res.locals.token = number; 20 | return next() 21 | } 22 | 23 | cookieController.removeSSIDCookie = (req, res, next) => { 24 | res.clearCookie('ssid'); 25 | return next(); 26 | } 27 | 28 | module.exports = cookieController; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Controllers/postController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | const express = require('express'); 3 | 4 | 5 | const postController = {}; 6 | 7 | // Takes post request properties and sets up postgres query 8 | postController.createPost = async (req, res, next) => { 9 | try { 10 | // deconstruction from req.body pulling the below properties 11 | const { title, type, category, link, description } = req.body; 12 | // creating a variable to use for the postgres DB query 13 | const createPostQuery = `INSERT INTO posts (user_id, title, link, description, category, type) VALUES ($1, $2, $3, $4, $5, $6);`; 14 | // db.query method pulls below parameters from DB and inserts them into posts 15 | const params = [res.locals.userId, title, link, description, category, type]; 16 | await db.query(createPostQuery, params); 17 | return next(); 18 | // if there is an error log and return string back to 19 | } catch(err) { 20 | // if the parameters don't exist or there is an error 21 | return next({ 22 | log: `postController.createPost: Error ${err}`, 23 | message: { err: 'Error occurred in postController.createPost'} 24 | }); 25 | } 26 | } 27 | 28 | // 29 | postController.getPosts = async (req, res, next) => { 30 | try { 31 | const getPostsQuery = `SELECT *, users.username FROM posts LEFT JOIN users ON posts.user_id=users.id`; 32 | const allPosts = await db.query(getPostsQuery); 33 | res.locals.allPosts = allPosts.rows; 34 | return next(); 35 | } catch (err) { 36 | return next({ 37 | log: `postController.getPosts: Error ${err}`, 38 | message: { err: 'Error occurred in postController.getPosts'} 39 | }); 40 | } 41 | } 42 | // unfinished and untested route 43 | postController.votePost = async (req, res, next) => { 44 | try { 45 | const { vote, link } = req.body; 46 | let getPostQuery; 47 | if (vote === 'up') { 48 | getPostQuery = `UPDATE posts SET upvotes = upvotes + 1 WHERE link = $1 RETURNING *` 49 | } 50 | if (vote === 'down') { 51 | getPostQuery = `UPDATE posts SET upvotes = upvotes - 1 WHERE link = $1 RETURNING *` 52 | } 53 | const params = [link]; 54 | const updatedPost = await db.query(getPostQuery, params) 55 | res.locals.votes = updatedPost 56 | return next(); 57 | } catch(err) { 58 | return next({ 59 | log: `postController.getPosts: Error ${err}`, 60 | message: { err: 'Error occurred in postController.getPosts'} 61 | }); 62 | } 63 | } 64 | 65 | module.exports = postController; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | 3 | sessionController = {}; 4 | 5 | /** 6 | * isLoggedIn - find the appropriate session for this request in the database, then 7 | * verify whether or not the session is still valid. 8 | */ 9 | // sessionController.isLoggedIn = async (req, res, next) => { 10 | // try{ 11 | // const SSID = req.cookies.ssid; 12 | // console.log(SSID, req.cookies) 13 | // const text = 'SELECT id FROM user_sessions WHERE session_token=$1'; 14 | // const response = await db.query(text, [SSID]); 15 | // //if there is no session verify user is needed! 16 | // // if there is a response, we can kind of bypass verify user 17 | // if (response.rows.length > 0) return res.status(301).json({isLoggedIn:true}); 18 | // else next(); 19 | // } catch(err){ 20 | // next({ 21 | // log: `sessionController.isLoggedIn: Error ${err}`, 22 | // message: { err: 'Error occurred in sessionController.isLoggedIn'} 23 | // }) 24 | // } 25 | 26 | // }; 27 | 28 | /** 29 | * startSession - create and save a new Session into the database. 30 | */ 31 | sessionController.startSession = async (req, res, next) => { 32 | try { 33 | const text = 'INSERT INTO user_sessions (session_token, users_id) VALUES ($1, $2)'; 34 | const params = [res.locals.token, res.locals.userId]; 35 | await db.query(text, params); 36 | next(); 37 | } 38 | catch(err){ 39 | next({ 40 | log: `sessionController.startSession: Error ${err}`, 41 | message: { err: 'Error occurred in sessionController.startSession'} 42 | }) 43 | } 44 | // Session.create() 45 | }; 46 | 47 | /** 48 | * 49 | * Should check the cookie and pass the userId to the next piece of middleware 50 | * 51 | * @param {Object} req.cookies 52 | * @param {Number} req.cookies.ssid ssid cookie for a user 53 | * @param {Object} res.locals 54 | * @returns 55 | * @param {Number} res.locals.userId The userId of the user associated with the given session 56 | */ 57 | sessionController.checkSession = async(req, res, next) => { 58 | // 59 | try { 60 | const ssid = req.cookies.ssid; 61 | const text = 'SELECT users_id FROM user_sessions WHERE session_token = $1'; 62 | const response = await db.query(text, [ssid]); 63 | await console.log( 'this is it', response.rows[0]) 64 | if (response.rows.length) { 65 | res.locals.userId = response.rows[0].users_id; 66 | return next(); 67 | 68 | } 69 | /** 70 | * { 71 | * isLoggedIn: false 72 | * data: [] 73 | * } 74 | * 75 | */ 76 | else { 77 | console.log('ran false') 78 | return res.status(301).json({'isLoggedIn': false}); 79 | } 80 | } catch (err) { 81 | next({ 82 | log: `sessionController.checkSession: Error ${err}`, 83 | message: { err: 'Error occurred in sessionController.checkSession'} 84 | }) 85 | } 86 | } 87 | 88 | // sessionController.startSession = async (req, res, next) => { 89 | // //write code here 90 | // const exists = await Session.findOne({cookieId: res.locals.id}).then(results => {if(results) {return true} else {return false}}) 91 | // if(!exists){ 92 | // await Session.create({cookieId: `${res.locals.id}`}) 93 | // console.log("created session"); 94 | // } 95 | // next(); 96 | // }; 97 | 98 | // module.exports = sessionController; 99 | 100 | module.exports = sessionController; 101 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require('../Models/UserModel.js'); 2 | const bcrypt = require('bcrypt'); 3 | require('dotenv').config(); 4 | 5 | const SALT_WORK_FACTOR = Number(process.env.SALTROUNDS); 6 | 7 | const userController = {}; 8 | 9 | 10 | /** 11 | * 12 | * @param {Object} req.body 13 | * @param {String} req.body.username Username to add, must be unique, required 14 | * @param {String} req.body.password Password to add, required 15 | * @param {String} req.body.email Email to add, required 16 | * @param {Object} res.locals 17 | * @param {Number} res.locals.userId User id created from inserting into DB 18 | * @param {Function} next When invoked without an argument, moves to next middleware 19 | * @returns undefined | error object 20 | */ 21 | userController.createUser = async (req, res, next) => { 22 | try { 23 | const { username, password, email } = req.body; 24 | const hashedPW = await bcrypt.hash(password, SALT_WORK_FACTOR); 25 | const params = [username, hashedPW, email]; 26 | const insertUserQuery = ` 27 | INSERT INTO users (username, password, email) 28 | VALUES ($1, $2, $3) 29 | RETURNING id;` 30 | const result = await db.query(insertUserQuery, params); 31 | res.locals.userId = await result.rows[0].id; 32 | return next(); 33 | } catch(err) { 34 | return next({ 35 | log: `userController.createUser: Error ${err}`, 36 | message: { err: 'Error occurred in userController.createUser'} 37 | }); 38 | } 39 | } 40 | 41 | /** 42 | * 43 | * 44 | * This middleware checks a users username and password from a POST request. If 45 | * the user is found, it stores the user_id on res.locals.userId and moves to next middleware. 46 | * Otherwise it throws an error. 47 | * 48 | * @param {Object} req.body 49 | * @param {String} req.body.username 50 | * @param {String} req.body.password 51 | * @param {Object} res.locals 52 | * @param {Number} res.locals.userId User id created from inserting into DB 53 | * @param {Function} next When invoked without an argument, moves to next middleware 54 | * @returns undefined | error object 55 | */ 56 | userController.verifyUser = async (req, res, next) => { 57 | try { 58 | const { username, password } = req.body; 59 | const params = [username]; 60 | const verifyUserQuery = ` 61 | SELECT * 62 | FROM users 63 | WHERE username = $1;` 64 | const databasePW = await db.query(verifyUserQuery, params); 65 | 66 | if(databasePW.rows.length < 1) { 67 | await bcrypt.compare(password, password); 68 | return res.status(409).json({ message: 'Username or password incorrect.'}); 69 | } 70 | const match = await bcrypt.compare(password, databasePW.rows[0].password); 71 | // res.locals.userInfo = {user: databasePW.rows} 72 | res.locals.userId = databasePW.rows[0].id; 73 | if (match) { 74 | return next(); 75 | } else { 76 | return res.status(409).json({message: 'Username or password incorrect.'}) 77 | } 78 | } catch(err) { 79 | return next({ 80 | log: `userController.verifyUser: Error ${err}`, 81 | message: { err: 'Error occurred in userController.verifyUser'} 82 | }); 83 | } 84 | } 85 | 86 | /** 87 | * Gets the user info from a logged in users Id 88 | * @param {Integer} res.locals.userId 89 | * @returns 90 | * @param {Object} res.locals.user 91 | * @param {String} res.locals.user.username 92 | */ 93 | userController.getUsername = async (req, res, next) =>{ 94 | try { 95 | const query = 'SELECT username FROM users WHERE id=$1' 96 | const params = [res.locals.userId]; 97 | 98 | const dbquery = await db.query(query, params); 99 | res.locals.user = dbquery.rows[0]; 100 | return next(); 101 | } catch (err) { 102 | return next({ 103 | log: `userController.getUsername Error ${err}`, 104 | message: { err: 'Error occurred in userController.getUsername'} 105 | }); 106 | } 107 | } 108 | 109 | 110 | module.exports = userController; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Models/UserModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | const pool = new Pool({ 5 | user: process.env.POSTGRES_USER, 6 | host: process.env.POSTGRES_HOST, 7 | database: process.env.POSTGRES_DB, 8 | password: process.env.POSTGRES_PASSWORD, 9 | port: process.env.POSTGRES_PORT, 10 | }); 11 | 12 | module.exports = { 13 | query: (text, params, callback) => { 14 | return pool.query(text, params, callback); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Routes/PostRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const postController = require('../Controllers/postController'); 4 | 5 | const router = express.Router(); 6 | 7 | //when there is a post request to the /creatpost endpoint and invoke the create post method on post controller 8 | // add return statement as best practice 9 | router.post('/createpost', postController.createPost, (req, res) => { 10 | return res.status(200).json({ message: 'Created post!'}) 11 | }) 12 | 13 | router.get('/getposts', postController.getPosts, (req, res) => { 14 | return res.status(200).json({'isLoggedIn': true, 'data': res.locals.allPosts}) 15 | }) 16 | 17 | // add return statement as best practice 18 | router.patch('/vote', postController.votePost, (req, res) => { 19 | return res.status(200).json({ message: 'Voted post!'}) 20 | }) 21 | 22 | 23 | 24 | module.exports = router; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/Routes/UserRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const userController = require('../Controllers/userController'); 4 | const cookieController = require('../Controllers/cookieController'); 5 | const sessionController = require('../Controllers/sessionController') 6 | 7 | const router = express.Router(); 8 | 9 | // adding return as best practice 10 | // go to login page -> check if a session exists based on your browser cookie -> if it does exist (add some conditionals) verify user middleware and go to main page 11 | 12 | router.post('/login', userController.verifyUser, cookieController.setSSIDCookie, sessionController.startSession, 13 | (req, res) => { 14 | return res.status(200).json({redirect:true}) 15 | }) 16 | 17 | // adding return as best practice 18 | router.post('/signup', userController.createUser, cookieController.setSSIDCookie, sessionController.startSession, 19 | (req, res) => { 20 | return res.status(200).json({redirect:true}); 21 | }) 22 | 23 | router.get('/currentuser', sessionController.checkSession, userController.getUsername, (req, res) =>{ 24 | return res.status(200).json({isLoggedIn: true, 'data': res.locals.user}); 25 | }) 26 | 27 | router.get('/logout', cookieController.removeSSIDCookie, (req,res) => { 28 | return res.status(200).json('Cleared Cookies!'); 29 | }) 30 | 31 | // router.get('/isloggedin', sessionController.isLoggedIn, (req, res) => { 32 | // return res.status(200).json({isLoggedIn: false}) 33 | // }) 34 | 35 | 36 | module.exports = router; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/__tests__/testsPostController.js: -------------------------------------------------------------------------------- 1 | const postController = require('./../Controllers/postController'); 2 | const pool = require('./../Models/UserModel'); 3 | 4 | describe('postController', () => { 5 | describe('postController.getPosts', () => { 6 | let req; 7 | let res; 8 | let next; 9 | beforeEach(()=>{ 10 | req = {query: {category: undefined}}; 11 | res = {locals: {}}; 12 | next = jest.fn(); 13 | }) 14 | 15 | it('Should get all posts when category is not specified', async() => { 16 | const text = ` 17 | SELECT * 18 | FROM posts; 19 | ` 20 | const results = await pool.query(text, []); 21 | const rows = await results.rows; 22 | await postController.getPosts(req, res, next) 23 | expect(res.locals.allPosts).not.toBe(undefined); 24 | expect(res.locals.allPosts.length).toEqual(rows.length); 25 | expect(next.mock.calls[0][0]).toEqual(undefined); //Checks next was called with no args 26 | expect(next).toBeCalledTimes(1); 27 | }) 28 | 29 | it('Should get all posts belonging to a correctly specified category', async()=>{ 30 | const categories = ['algorithms'] 31 | req = {query: {category: categories[0]}}; 32 | const text = ` 33 | SELECT * 34 | FROM posts 35 | WHERE category=($1); 36 | `; 37 | const values = [categories[0]]; 38 | const results = await pool.query(text, values); 39 | const rows = await results.rows; 40 | await postController.getPosts(req, res, next); 41 | expect(res.locals.allPosts).not.toBe(undefined); 42 | expect(res.locals.allPosts.length).toEqual(rows.length); 43 | expect(next.mock.calls[0][0]).toEqual(undefined); 44 | expect(next).toBeCalledTimes(1); 45 | 46 | }) 47 | 48 | it('Should return an error if the category is not a defined category', async() => { 49 | const category = 'FakeCategory'; 50 | req = {query: {category: category}} 51 | await pool.query(req, res, next); 52 | expect(res.locals.allPosts).toBe(undefined); 53 | expect(next.mock.calls[0][0]).toBeInstanceOf(object); 54 | }) 55 | }) 56 | 57 | describe('postController.votePost', () => { 58 | let req; 59 | let res; 60 | let next; 61 | let text; 62 | beforeEach(() => { 63 | req = {body: {postId: 1}}; 64 | res = {locals: {userId: 3}}; 65 | next = jest.fn(); 66 | text = ` 67 | SELECT * 68 | FROM posts 69 | WHERE id=($1); 70 | ` 71 | }) 72 | it('Should increment voting when user votes', async() => { 73 | const results = await pool.query(text, [req.body.postId]); 74 | const voteCount = await results.rows[0].upvotes; 75 | await postController.votePost(req, res, next); 76 | const results2 = await pool.query(text, [req.body.postId]); 77 | const voteCount2 = await results2.rows[0].upvotes; 78 | 79 | expect(voteCount2).toEqual(voteCount+1); 80 | }) 81 | 82 | it('Should remove voting when user votes again', async() => { 83 | const results = await pool.query(text, [req.body.postId]); 84 | const voteCount = await results.rows[0].upvotes; 85 | await postController.votePost(req, res, next); 86 | const results2 = await pool.query(text, [req.body.postId]); 87 | const voteCount2 = await results2.rows[0].upvotes; 88 | 89 | expect(voteCount2).toEqual(voteCount-1); 90 | }) 91 | 92 | it('Should return an error if the postId does not exist', async() => { 93 | let req = {body: {postId: 10000}}; 94 | await postController.votePost(req, res, next); 95 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 96 | }) 97 | 98 | }) 99 | 100 | describe('postController.createPost', () => { 101 | it('Should return an error if properties passed in are undefined', async()=>{ 102 | const req = { 103 | body: { 104 | title: 'Test title', 105 | category:'Test category', 106 | link: 'http://fakeurl.com', 107 | description: 'Test description' 108 | } 109 | } 110 | const res = {locals: {userId: '1'}}; 111 | const next = jest.fn(); 112 | await postController.createPost(req, res, next); 113 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 114 | }) 115 | 116 | it('Should add post to the database', async()=>{ 117 | const req = { 118 | body: { 119 | title: 'Test title', 120 | type: 'Test type', 121 | category:'Test category', 122 | link: 'http://fakeurl.com', 123 | description: 'Test description' 124 | } 125 | } 126 | const res = {locals: {userId: '1'}}; 127 | const next = jest.fn(); 128 | 129 | const text = ` 130 | SELECT * 131 | FROM posts; 132 | ` 133 | const results = await pool.query(text, []); 134 | const rows = await results.rows; 135 | 136 | await postController.createPost(req, res, next); 137 | 138 | const results2 = await pool.query(text, []); 139 | const rows2 = await results.rows; 140 | 141 | expect(next.mock.calls[0][0]).toBe(undefined); 142 | expect(next).toBeCalledTimes(1); 143 | expect(rows2.length).toEqual(rows.length + 1); 144 | 145 | }) 146 | 147 | }) 148 | 149 | 150 | }) -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/__tests__/testsUserController.js: -------------------------------------------------------------------------------- 1 | const pool = require('./../Models/UserModel'); 2 | const userController = require('./../Controllers/userController'); 3 | 4 | describe('userController', () => { 5 | 6 | describe('userController.createUser', () => { 7 | let req; 8 | let res; 9 | let next; 10 | beforeEach(() => { 11 | req = {body: {username: 'chad', password: 'pass1', email: 'testemail@gmail.com'}} 12 | res = {locals: {}}; 13 | next = jest.fn(); 14 | }) 15 | it('Should create a new user in the database', async() => { 16 | const text = ` 17 | SELECT * 18 | FROM users;` 19 | const results = await pool.query(text, []); 20 | const rowsLength = await results.rows.length; 21 | await userController.createUser(req, res, next); 22 | const resultPost = await pool.query(text, []); 23 | const rowsPostLength = await resultPost.rows.length; 24 | expect(rowsPostLength).toEqual(rowsLength+1); 25 | }) 26 | 27 | it('Should return an error if a user by the user_id already exists', async() => { 28 | req.body.email = 'uniqueemail@gmail.com'; 29 | const text = ` 30 | SELECT * 31 | FROM users;` 32 | const results = await pool.query(text, []); 33 | const rowsLength = await results.rows.length; 34 | await userController.createUser(req, res, next); 35 | const resultPost = await pool.query(text, []); 36 | const rowsPostLength = await resultPost.rows.length; 37 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 38 | expect(rowsPostLength).toEqual(rowsLength); 39 | }) 40 | 41 | it('Should return an error if a user with that email already exists', async() => { 42 | req.body.username = 'uniqueUser'; 43 | const text = ` 44 | SELECT * 45 | FROM users;` 46 | const results = await pool.query(text, []); 47 | const rowsLength = await results.rows.length; 48 | await userController.createUser(req, res, next); 49 | const resultPost = await pool.query(text, []); 50 | const rowsPostLength = await resultPost.rows.length; 51 | 52 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 53 | expect(rowsPostLength).toEqual(rowsLength); 54 | }) 55 | 56 | it('Should return an error if the required fields are not sent', async() => { 57 | req = {body: {username: 'username123', password: 'password123'}}; 58 | await userController.createUser(req, res, next); 59 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 60 | }) 61 | }) 62 | 63 | describe('userController.verifyUser', () => { 64 | let req; 65 | let res; 66 | let next; 67 | beforeEach(() => { 68 | req = {body: {}}; 69 | res = {locals: {}}; 70 | next = jest.fn(); 71 | }) 72 | it('Should store the user_id on res.locals.userId and go to next middleware', async() => { 73 | const username = 'user1'; 74 | const password = '1234'; 75 | req.body.username = username; 76 | req.body.password = password; 77 | await userController.verifyUser(req, res, next); 78 | expect(res.locals.userId).toBe(4); 79 | expect(next.mock.calls[0][0]).toBe(undefined) 80 | }) 81 | 82 | it('Should throw an error for incorrect username or password', async() => { 83 | const username = 'user1'; 84 | const password = 'password'; 85 | req.body.username = username; 86 | req.body.password = password; 87 | await userController.verifyUser(req, res, next); 88 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 89 | expect(next).toBeCalledTimes(1); 90 | }) 91 | 92 | it('Should throw an error for missing a field', async() => { 93 | const username = 'user1'; 94 | req.body.username = username; 95 | await userController.verifyUser(req, res, next); 96 | expect(next.mock.calls[0][0]).toBeInstanceOf(Object); 97 | expect(next).toBeCalledTimes(1); 98 | }) 99 | }) 100 | }) -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/buildDB_wTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Import variables from .env 4 | source .env 5 | echo "Connecting to DB: $PG_URI" 6 | 7 | echo "Dropping tables..." 8 | psql -d ${PG_URI} -f ./server/db/test_data_scripts/dropTables.sql 9 | echo "Done dropping tables" 10 | 11 | echo "Building tables..." 12 | psql -d ${PG_URI} -f ./server/db/sql_scripts/buildDB.sql 13 | echo "Done building tables" 14 | 15 | echo "Add test data..." 16 | node ./server/db/test_data_scripts/insertTestData.js 17 | echo "Done adding test data" -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/sessionScheduler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ######################################### 4 | 5 | # About: Run this script concurrently 6 | # with the server to drop user 7 | # sessions 8 | 9 | 10 | ######################################### 11 | 12 | # mport variables from .env 13 | source .env 14 | echo "Connecting to DB: $PG_URI" 15 | echo "Will drop out of date user sessions every ${SLEEPTIME} seconds" 16 | 17 | while [ true ] 18 | do 19 | sleep ${SLEEPTIME} 20 | echo "Dropping expired sessions" 21 | psql -d ${PG_URI} -f ./server/db/sql_scripts/sessionRemove.sql 22 | done -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/sql_scripts/buildDB.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE users ( 3 | id SERIAL PRIMARY KEY, 4 | username TEXT NOT NULL UNIQUE, 5 | password TEXT NOT NULL, 6 | email TEXT NOT NULL UNIQUE, 7 | photo TEXT 8 | ); 9 | 10 | --User session generated for a user. Session comes from randomly generated bytes 11 | CREATE TABLE user_sessions ( 12 | id SERIAL PRIMARY KEY, 13 | session_token TEXT NOT NULL UNIQUE, 14 | users_id bigint NOT NULL REFERENCES users(id), 15 | date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP 16 | ); 17 | 18 | CREATE TABLE posts ( 19 | id SERIAL PRIMARY KEY, 20 | user_id bigint REFERENCES users(id), -- assuming your users table is named "users" 21 | upvotes INTEGER DEFAULT 0, 22 | title TEXT NOT NULL, 23 | link TEXT NOT NULL UNIQUE, 24 | description TEXT NOT NULL, 25 | category TEXT NOT NULL, 26 | date_submitted TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 27 | type TEXT NOT NULL 28 | ); -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/sql_scripts/sessionRemove.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM user_sessions 2 | WHERE date_created + interval '3600 seconds' < CURRENT_TIMESTAMP; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/test_data_scripts/dropTables.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE posts, user_sessions, users; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/db/test_data_scripts/insertTestData.js: -------------------------------------------------------------------------------- 1 | const pool = require('../../Models/UserModel'); 2 | const format = require('pg-format') 3 | const bcrypt = require('bcrypt'); 4 | require('dotenv').config(); 5 | 6 | // Should move this to separate file, .env perhaps? 7 | const SALT_ROUNDS = Number(process.env.SALTROUNDS); 8 | 9 | // Setup test data for users 10 | const text = ` 11 | INSERT INTO users(username, password, email) 12 | VALUES %L;` 13 | 14 | const values = [ 15 | ['john$456', '1234', 'thebestemail@gmail.com'], 16 | ['yeezuspeezus', '789', 'emailnumerodos@gmail.com'], 17 | ['testuser', 'pass', 'testuser@gmail.com'], 18 | ['user1', '1234', 'user1@gmail.com'], 19 | ] 20 | 21 | /** 22 | * Perform hashing on the passwords 23 | */ 24 | values.forEach((element, index, array) => { 25 | array[index][1] = bcrypt.hashSync(element[1], SALT_ROUNDS); 26 | }) 27 | 28 | // Setup test data for posts 29 | const postsQuery = ` 30 | INSERT INTO posts(user_id, title, link, description, category, type) 31 | VALUES %L;` 32 | 33 | const postsValues = [ 34 | ['1', 'JavaScript Algorithms and Data Structures tutorial from FreeCodeCamp', 'https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/', 'A step by step tutorial on algorithms and data structures.', 'Algorithms', 'tutorial'], 35 | ['1', 'JavaScript Algorithms and Data Structures tutorial from FreeCodeCamp', 'https://www.youtube.com/watch?v=hQAHSlTtcmY', 'A 30 minute YouTube video on React.', 'React', 'video'], 36 | ['1', 'Test', 'https://mui.com/material-ui/react-text-field/', 'Help learning react, it\'s a pain', 'Algorithms', 'article'], 37 | ['2', 'Learn React', 'https://mui.com/material-ui/react-dialog/', 'Help learning react, it\'s a pain', 'Algorithms', 'article'], 38 | ] 39 | 40 | // Define async function for inserting data 41 | // need users to be inserted first 42 | // otherwise we violate foreign key required constraint 43 | async function performQueries(){ 44 | await pool.query(format(text, values), []); 45 | await pool.query(format(postsQuery, postsValues), []) 46 | } 47 | 48 | // Call function 49 | performQueries(); 50 | -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const dotenv = require('dotenv').config(); 4 | // const cors = require('cors'); 5 | const cookieParser = require('cookie-parser'); 6 | const app = express(); 7 | 8 | const UserRouter = require('./Routes/UserRouter'); 9 | const PostRouter = require('./Routes/PostRouter'); 10 | 11 | const sessionController = require('./Controllers/sessionController'); 12 | // const corsOptions = { 13 | // origin: true, //included origin as true 14 | // credentials: true, //included credentials as true 15 | // }; 16 | 17 | const PORT = process.env.PORT || 3000; 18 | app.use(cookieParser()); 19 | // app.use(cors(corsOptions)); 20 | app.use(express.json()); 21 | 22 | app.use(express.urlencoded({ extended: true })); 23 | 24 | if (process.env.NODE_ENV === 'production') { 25 | app.use(express.static(path.join(__dirname, '../dist'))); 26 | } 27 | 28 | app.use('/user', UserRouter); 29 | 30 | app.use('/post', sessionController.checkSession , PostRouter); 31 | 32 | // app.use((req, res) => res.status(404).send('Oops! This is not the right page')); 33 | 34 | app.use((err, req, res, next) =>{ 35 | const defaultError = { 36 | log: 'Express error handler caught unknown middleware error', 37 | status: 400, 38 | message: { err: 'An error occurred' }, 39 | }; 40 | const errorObj = Object.assign({},defaultError, err); 41 | console.log(errorObj.log); 42 | res.status(errorObj.status).send(errorObj.message); 43 | }); 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Server listening on port: ${PORT}`); 47 | }); 48 | 49 | module.exports = app; -------------------------------------------------------------------------------- /test-env-manual-deployment/codeforge/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: process.env.NODE_ENV, 7 | entry: './client/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | publicPath: '/', 11 | filename: 'bundle.js', 12 | }, 13 | devtool: 'eval-source-map', 14 | devServer: { 15 | compress: true, 16 | open: true, 17 | // enable HMR on the devServer 18 | hot: true, 19 | // fallback to root for other urls 20 | historyApiFallback: true, 21 | 22 | static: { 23 | // match the output path 24 | directory: path.resolve(__dirname, 'dist'), 25 | // match the output 'publicPath' 26 | publicPath: '/', 27 | }, 28 | 29 | // headers: { 'Access-Control-Allow-Origin': '*' }, 30 | /** 31 | * proxy is required in order to make api calls to 32 | * express server while using hot-reload webpack server 33 | * routes api fetch requests from localhost:8080/api/* (webpack dev server) 34 | * to localhost:3000/api/* (where our Express server is running) 35 | */ 36 | proxy: { 37 | '/post/': { 38 | target: 'http://localhost:3000/', 39 | secure: false, 40 | }, 41 | '/user/': { 42 | target: 'http://localhost:3000/', 43 | secure: false, 44 | }, 45 | // '/styles.css/**': { 46 | // target: 'http://localhost:3000/', 47 | // secure: false, 48 | // }, 49 | }, 50 | }, 51 | module: { 52 | rules: [ 53 | { 54 | test: /.(js|jsx)$/, 55 | exclude: /node_modules/, 56 | use: { 57 | loader: 'babel-loader', 58 | options: { 59 | presets: ['@babel/preset-env', '@babel/preset-react'], 60 | }, 61 | }, 62 | }, 63 | { 64 | test: /.(css|scss)$/, 65 | exclude: /node_modules/, 66 | use: ['style-loader', 'css-loader'], 67 | }, 68 | ], 69 | }, 70 | plugins: [ 71 | new HtmlWebpackPlugin({ 72 | template: './index.html', 73 | }), 74 | ], 75 | resolve: { 76 | // Enable importing JS / JSX files without specifying their extension 77 | extensions: ['.js', '.jsx'], 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /test-env-manual-deployment/grafana/grafana-configmap.yaml: -------------------------------------------------------------------------------- 1 | # Please edit the object below. Lines beginning with a '#' will be ignored, 2 | # and an empty file will abort the edit. If an error occurs while saving this file will be 3 | # reopened with the relevant failures. 4 | # 5 | # configmaps "stable-grafana-config-dashboards" was not valid: 6 | # * patch: Invalid value: "map[data:map[provider.yaml:[map[allowUiUpdates:false disableDeletion:false folder: name:sidecarProvider options:map[allow_embedding:true auth.anonymous:map[enabled:true org_name:Main Org. org_role:Viewer] foldersFromFilesStructure:false path:/tmp/dashboards] orgId:1 type:file updateIntervalSeconds:30]]] metadata:map[creationTimestamp:]]": unrecognized type: string 7 | # 8 | 9 | apiVersion: v1 10 | kind: ConfigMap 11 | metadata: 12 | annotations: 13 | meta.helm.sh/release-name: stable 14 | meta.helm.sh/release-namespace: prometheus 15 | labels: 16 | app.kubernetes.io/instance: stable 17 | app.kubernetes.io/managed-by: Helm 18 | app.kubernetes.io/name: grafana 19 | app.kubernetes.io/version: 10.1.1 20 | helm.sh/chart: grafana-6.59.4 21 | name: stable-grafana-config-dashboards 22 | namespace: prometheus 23 | resourceVersion: '661734' 24 | uid: 'UID' 25 | data: 26 | provider.yaml: 27 | | #Not sure if this slash needs to be there or not. Embarassing, I know. 28 | - name: 'sidecarProvider' 29 | orgId: 1 30 | folder: '' 31 | type: file 32 | disableDeletion: false 33 | allowUiUpdates: false 34 | updateIntervalSeconds: 30 35 | options: 36 | foldersFromFilesStructure: false 37 | path: /tmp/dashboards 38 | allow_embedding: true 39 | auth.anonymous: 40 | enabled: true 41 | org_name: 'Main Org.' 42 | org_role: Viewer 43 | -------------------------------------------------------------------------------- /test-env-manual-deployment/ingress-deployment/codeforge-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: codeforge-ingress 5 | labels: 6 | app: codeforge 7 | spec: 8 | containers: 9 | - name: codeforge-ingress 10 | image: 'YOUR-DOCKER-IMAGE' 11 | 12 | --- 13 | 14 | kind: Service 15 | apiVersion: v1 16 | metadata: 17 | name: codeforge-service 18 | spec: 19 | selector: 20 | app: codeforge 21 | ports: 22 | - port: 5678 23 | -------------------------------------------------------------------------------- /test-env-manual-deployment/postgres-deployment/postgres-pvc-pv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: postgres-pv-volume 5 | labels: 6 | type: local 7 | app: postgres 8 | spec: 9 | storageClassName: manual 10 | capacity: 11 | storage: 5Gi 12 | accessModes: 13 | - ReadWriteMany 14 | hostPath: 15 | path: '/mnt/data' 16 | --- 17 | kind: PersistentVolumeClaim 18 | apiVersion: v1 19 | metadata: 20 | name: postgres-pv-claim 21 | labels: 22 | app: postgres 23 | spec: 24 | storageClassName: manual 25 | accessModes: 26 | - ReadWriteMany 27 | resources: 28 | requests: 29 | storage: 5Gi 30 | -------------------------------------------------------------------------------- /test-env-manual-deployment/postgres-deployment/postgres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres-deployment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: postgres 10 | template: 11 | metadata: 12 | labels: 13 | app: postgres 14 | spec: 15 | containers: 16 | - name: postgres 17 | image: postgres:10.1 18 | imagePullPolicy: 'IfNotPresent' 19 | ports: 20 | - name: postgres 21 | containerPort: 5432 22 | - name: postgres-exp 23 | containerPort: 9187 24 | env: 25 | - name: POSTGRES_DB 26 | valueFrom: 27 | secretKeyRef: 28 | name: postgres-secret 29 | key: postgres-db 30 | - name: POSTGRES_USER 31 | valueFrom: 32 | secretKeyRef: 33 | name: postgres-secret 34 | key: postgres-user 35 | - name: POSTGRES_PASSWORD 36 | valueFrom: 37 | secretKeyRef: 38 | name: postgres-secret 39 | key: postgres-password 40 | volumes: 41 | - name: postgres-db 42 | persistentVolumeClaim: 43 | claimName: postgres-pv-claim 44 | --- 45 | apiVersion: v1 46 | kind: Service 47 | metadata: 48 | name: postgres-service 49 | spec: 50 | selector: 51 | app: postgres 52 | ports: 53 | - name: postgres 54 | protocol: TCP 55 | port: 5432 56 | targetPort: 5432 57 | - name: postgres-exp 58 | protocol: TCP 59 | port: 9187 60 | targetPort: 9187 61 | --------------------------------------------------------------------------------