├── .dockerignore ├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── Dockerfile-postgres ├── LICENSE ├── README.Docker.md ├── README.md ├── client ├── index.html ├── src │ ├── App.tsx │ ├── assets │ │ ├── RAILGUIDE.png │ │ └── Untitled design (2).png │ ├── components │ │ ├── Card.tsx │ │ ├── EventCard.tsx │ │ ├── IpAccessCombined.tsx │ │ ├── Modal.tsx │ │ ├── Navbar.tsx │ │ └── charts │ │ │ ├── AccessPerIp.tsx │ │ │ ├── AnomalyChart.tsx │ │ │ ├── EventSource.tsx │ │ │ ├── EventType.tsx │ │ │ ├── HeatMap.tsx │ │ │ ├── IpAccessOverTime.tsx │ │ │ ├── PieChart.tsx │ │ │ └── UserActivity.tsx │ ├── index.scss │ ├── main.tsx │ ├── pages │ │ ├── EventsDashboard.tsx │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── Profile.tsx │ │ └── SignUp.tsx │ ├── profile.css │ └── types.ts ├── tsconfig.app.json └── vite-env.d.ts ├── compose-dev.yml ├── compose-node_modules.yml ├── compose.yml ├── eslint.config.js ├── package-lock.json ├── package.json ├── readmeAssets ├── aws-credential.png ├── log-in.png ├── sign-up.png └── trailguide-readme-main.webp ├── scripts └── db_init.sql ├── server ├── controllers │ ├── awsController.js │ ├── ipLocController.js │ └── userController.js ├── models │ ├── eventsModel.js │ ├── ipsModel.js │ └── usersModel.js ├── server.js └── utils │ └── timeBuckets.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | # **/charts 23 | **/docker-compose* 24 | **/compose.y*ml 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/lint-format.yml 2 | 3 | name: Lint and Format 4 | 5 | # Run this workflow on pull requests targeting 'main' or 'dev' branches 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | - dev 11 | 12 | jobs: 13 | lint-and-format: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | # Step 1: Check out the code 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | # Step 2: Set up Node.js (specify the Node version if required) 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '20' # Adjust the version if necessary 26 | 27 | # Step 3: Install dependencies 28 | - name: Install dependencies 29 | run: npm install 30 | 31 | # Step 4: Run ESLint to check for linting issues 32 | - name: Run ESLint 33 | run: npm run lint -- --fix # Make sure you have a lint script in package.json 34 | 35 | - name: List Changes 36 | run: git status --porcelain 37 | 38 | # Step 6: Check for changes after formatting 39 | - name: Check for formatting changes 40 | run: | 41 | if [[ `git status --porcelain` ]]; then 42 | echo "There are formatting changes." 43 | echo "Please commit the changes locally or configure auto-formatting in Prettier." 44 | exit 1 45 | else 46 | echo "No formatting changes needed." 47 | fi 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | *.tsbuildinfo 15 | .env 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | ARG NODE_VERSION=20.16.0 3 | 4 | ################################################################################ 5 | # Use node image for base image for all stages. 6 | FROM node:${NODE_VERSION}-alpine AS base 7 | 8 | # Set working directory for all build stages. 9 | WORKDIR /usr/src/app 10 | 11 | 12 | ################################################################################ 13 | # Create a stage for installing production dependecies. 14 | FROM base AS deps 15 | 16 | # Download dependencies as a separate step to take advantage of Docker's caching. 17 | # Leverage a cache mount to /root/.npm to speed up subsequent builds. 18 | # Leverage bind mounts to package.json and package-lock.json to avoid having to copy them 19 | # into this layer. 20 | RUN --mount=type=bind,source=package.json,target=package.json \ 21 | # workaround for npm optional dependencies bug: https://github.com/npm/cli/issues/4828 22 | # --mount=type=bind,source=package-lock.json,target=package-lock.json \ 23 | --mount=type=cache,target=/root/.npm \ 24 | npm i --omit=dev 25 | 26 | ################################################################################ 27 | # Create a stage for building the application. 28 | FROM deps AS dev-deps 29 | 30 | 31 | # Download additional development dependencies before building, as some projects require 32 | # "devDependencies" to be installed to build. If you don't need this, remove this step. 33 | RUN --mount=type=bind,source=package.json,target=package.json \ 34 | # workaround for npm optional dependencies bug: https://github.com/npm/cli/issues/4828 35 | # --mount=type=bind,source=package-lock.json,target=package-lock.json \ 36 | --mount=type=cache,target=/root/.npm \ 37 | npm i 38 | 39 | FROM dev-deps AS build 40 | # Copy the rest of the source files into the image. 41 | COPY . . 42 | # Run the build script. 43 | RUN npx tsc -b 44 | RUN npx vite build 45 | 46 | ################################################################################ 47 | # Create a new stage to run the application with minimal runtime dependencies 48 | # where the necessary files are copied from the build stage. 49 | FROM base AS final 50 | 51 | # Use production node environment by default. 52 | ENV NODE_ENV=production 53 | 54 | # Run the application as a non-root user. 55 | USER node 56 | 57 | # Copy package.json so that package manager commands can be used. 58 | COPY package.json . 59 | 60 | # Copy the production dependencies from the deps stage and also 61 | # the built application from the build stage into the image. 62 | COPY --from=deps /usr/src/app/node_modules ./node_modules 63 | COPY --from=build /usr/src/app/server ./ 64 | COPY --from=build /usr/src/app/dist ./dist 65 | 66 | 67 | 68 | # Expose the port that the application listens on. 69 | EXPOSE 8080 70 | 71 | # Run the application. 72 | CMD [ "node", "server.js" ] 73 | -------------------------------------------------------------------------------- /Dockerfile-postgres: -------------------------------------------------------------------------------- 1 | FROM postgres:12.8 2 | COPY ./scripts/db_init.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.Docker.md: -------------------------------------------------------------------------------- 1 | ### Building and running your application 2 | 3 | When you're ready, start your application by running: 4 | `docker compose up --build`. 5 | 6 | Your application will be available at http://localhost:8080. 7 | 8 | ### Deploying your application to the cloud 9 | 10 | First, build your image, e.g.: `docker build -t myapp .`. 11 | If your cloud uses a different CPU architecture than your development 12 | machine (e.g., you are on a Mac M1 and your cloud provider is amd64), 13 | you'll want to build the image for that platform, e.g.: 14 | `docker build --platform=linux/amd64 -t myapp .`. 15 | 16 | Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. 17 | 18 | Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) 19 | docs for more detail on building and pushing. 20 | 21 | ### References 22 | * [Docker's Node.js guide](https://docs.docker.com/language/nodejs/) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrailGuide 2 | 3 | [TrailGuide](https://oslabs-beta.github.io/TrailGuideIO/) is a open source AWS cloud security solution for developers who need their cloud security reassured. 4 | 5 | We built TrailGuide because we are passionate in solving the data overloading problem in the cloud. Join us! 6 | 7 | - Track key management events: Quickly view events related to creating, modifying, or deleting AWS resources. 8 | - Visualize CloudTrail data: Present data in easy-to-read formats, such as pie charts for event distribution and heatmaps for geographical IP access. 9 | - Analyze recent events based on various criteria, such as IP addresses, event types, associated users, and timestamps. 10 | 11 | Every single part is fully open source! Fork it, extend it, or deploy it to your own server. 12 | 13 | List View Screenshot 14 | 15 | # Installation and Spin-Up 16 | 17 | - Make sure you have docker installed 18 | - Create your compose.yml file 19 | - (see our starter version in [Docker Hub](https://hub.docker.com/r/trailguide/trailguide-prod), or copy the one from this repo ) 20 | - run `docker compose up` on the command line 21 | 22 | # Getting Start: 23 | 24 | 1. Use the signup link to create user 25 | 26 | List View Screenshot 27 | 28 | 2. Login 29 | 30 | List View Screenshot 31 | 32 | 3. Copy paste the aws credentials in the fields in the profile 33 | 34 | List View Screenshot 35 | 36 | ## Shoutouts :tada: 37 | 38 | Omnivore takes advantage of some great open source software: 39 | 40 | - [TypeScript](https://www.typescriptlang.org/) - Most of our backend and frontend are written in TypeScript. 41 | - [PostgreSQL](https://www.postgresql.org/)- For managing complex queries and storing event data, PostgreSQL is our go-to. Its reliability and performance are key to managing and analyzing extensive data, enhancing the robustness of our monitoring and analytics features. 42 | - [Docker](https://www.docker.com/)- Thanks to Docker, deploying our platform is seamless and consistent, whether locally or on the cloud. Docker allows us to containerize our ML models and backend services, ensuring reliable and scalable performance for our users. 43 | - [AWS](https://aws.amazon.com/)- AWS forms the backbone of TrailGuide, providing the infrastructure and data streams that allow us to offer real-time monitoring and security insights for AWS environments. CloudTrail logs enable us to dive deep into user activity and detect anomalies as they happen. 44 | - [Scikit-learn](https://scikit-learn.org/)- TrailGuide’s anomaly detection thrives with Scikit-learn's Isolation Forest, enabling real-time detection of unusual activity in CloudTrail logs with efficiency and accuracy. 45 | - And many more awesome libraries, just checkout our package files to see what we are using. 46 | 47 | ## Requirements for development 48 | 49 | TraildeGuide is written in TypeScript and JavaScript. 50 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | TrailGuide 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useCallback, useEffect, useState } from 'react'; 2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 3 | 4 | import Navbar from './components/Navbar'; 5 | import Profile from './pages/Profile'; 6 | import Home from './pages/Home'; 7 | import EventsDashboard from './pages/EventsDashboard'; 8 | import Login from './pages/Login'; 9 | import SignUp from './pages/SignUp'; 10 | import { AWSCredentials, UserDetails } from './types'; 11 | 12 | const App: React.FC = () => { 13 | const [isDarkMode, setIsDarkMode] = useState(false); // Dark mode state 14 | const [user, setUser] = useState(null); 15 | 16 | const updateCredentials = useCallback( 17 | function (credentials: AWSCredentials): void { 18 | const locallyStoredUser: UserDetails = JSON.parse( 19 | window.localStorage.getItem('user')! 20 | ) as UserDetails; 21 | fetch('/credentials', { 22 | method: 'POST', 23 | body: JSON.stringify({ 24 | ...credentials, 25 | username: 26 | user?.username ?? locallyStoredUser.username ?? 'No Active User', 27 | }), 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | }) 32 | .then((response) => { 33 | if (!response.ok) 34 | throw Error('Server Error while updating aws credentials'); 35 | return response.json(); 36 | }) 37 | .then((data: UserDetails) => { 38 | setUser(data); 39 | window.localStorage.setItem('user', JSON.stringify(data)); 40 | }) 41 | .catch((error: Error) => { 42 | console.error(error); 43 | }); 44 | }, 45 | // we don't want to update on user update, because it would create an infinte loop, only on app reload 46 | // eslint-disable-next-line react-hooks/exhaustive-deps 47 | [] 48 | ); 49 | 50 | // check for a user session and update the user if found 51 | useEffect(() => { 52 | if (window.localStorage.getItem('user')) { 53 | const locallyStoredUser: UserDetails = JSON.parse( 54 | window.localStorage.getItem('user')! 55 | ) as UserDetails; 56 | setUser(locallyStoredUser); 57 | } 58 | }, []); 59 | 60 | const toggleDarkMode = () => { 61 | setIsDarkMode((prev) => !prev); 62 | document.body.classList.toggle('dark-mode', !isDarkMode); // Toggle class based on state 63 | }; 64 | 65 | function checkLogin(component: ReactNode): ReactNode { 66 | return user ? component :

You must login to see this page

; 67 | } 68 | 69 | function checkAWSCreds(component: ReactNode): ReactNode { 70 | if ( 71 | user?.aws_access_key?.length && 72 | user?.aws_region?.length > 0 && 73 | user?.aws_secret_access_key?.length > 0 74 | ) { 75 | return component; 76 | } 77 | return ( 78 |

79 | You must enter your AWS credentials in the profile page to see any data 80 | here. 81 |

82 | ); 83 | } 84 | 85 | return ( 86 | 87 | 93 | 94 | } /> 95 | } /> 96 | } /> 97 | 98 | ))} 101 | /> 102 | 110 | )} 111 | /> 112 | ) 116 | )} 117 | /> 118 | 119 | 120 | ); 121 | }; 122 | 123 | export default App; 124 | -------------------------------------------------------------------------------- /client/src/assets/RAILGUIDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/client/src/assets/RAILGUIDE.png -------------------------------------------------------------------------------- /client/src/assets/Untitled design (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/client/src/assets/Untitled design (2).png -------------------------------------------------------------------------------- /client/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CardProps } from '../types'; 3 | 4 | const Card: React.FC = ({ title, children, isDarkMode }) => { 5 | return ( 6 |
7 |
8 |

{title}

9 |
10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default Card; 16 | -------------------------------------------------------------------------------- /client/src/components/EventCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EventCardProps } from '../types'; // Ensure this matches the updated structure 3 | 4 | const EventCard: React.FC = ({ event, onViewDetails }) => { 5 | // Ensure event.time is a valid Date object 6 | const eventDate = new Date(event.time); 7 | 8 | // Check if the date is valid 9 | const isValidDate = !isNaN(eventDate.getTime()); 10 | 11 | return ( 12 |
13 |

Event: {event.name || 'N/A'}

14 |

15 | Timestamp:{' '} 16 | {isValidDate ? eventDate.toLocaleString() : 'Invalid Date'} 17 |

18 |

19 | User: {event.username ?? 'Unknown User'} 20 |

21 | 22 |
23 | ); 24 | }; 25 | 26 | export default EventCard; 27 | -------------------------------------------------------------------------------- /client/src/components/IpAccessCombined.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import AccessPerIpChart from './charts/AccessPerIp'; 3 | import IpAccessOverTimeChart from './charts/IpAccessOverTime'; 4 | import { CountedEvent, IPLocation, IpAccessCombinedProps } from '../types'; // Import the interface from types.ts 5 | 6 | export default function IpAccessCombined({ 7 | currentIp, 8 | setCurrentIp, 9 | }: IpAccessCombinedProps): JSX.Element { 10 | const [ipLocCounts, setIpLocCounts] = useState<(IPLocation & CountedEvent)[]>( 11 | [] 12 | ); 13 | 14 | useEffect(() => { 15 | fetch('/events?countOn=source_ip&includeLocation=true') 16 | .then((response) => { 17 | if (response.ok) return response.json(); 18 | throw new Error(response.status + ': ' + response.statusText); 19 | }) 20 | .then((data: (IPLocation & CountedEvent)[] | { err: string }) => 21 | setIpLocCounts(() => data as (IPLocation & CountedEvent)[]) 22 | ) 23 | .catch((error) => 24 | console.warn('IpAccessCombined: fetch error: ' + error) 25 | ); 26 | }, []); 27 | 28 | const currentIpLoc = ipLocCounts.find( 29 | ({ source_ip }) => source_ip === currentIp 30 | ); 31 | 32 | return ( 33 | <> 34 |
35 | 40 | {currentIp && ( 41 | <> 42 |

43 | Location: 44 | {currentIpLoc?.city}, {currentIpLoc?.region},{' '} 45 | {currentIpLoc?.country} 46 |

47 | {/* Make sure the chart renders only when IP is selected */} 48 | 49 | 50 | )} 51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ModalProps } from '../types'; 3 | 4 | const Modal: React.FC = ({ 5 | isOpen, 6 | onClose, 7 | event, 8 | isDarkMode, 9 | }) => { 10 | if (!isOpen || !event) return null; 11 | 12 | return ( 13 |
17 |
e.stopPropagation()} 20 | > 21 |
22 |

Event Details

23 | 29 |
30 |
31 |

32 | Event Type: {event.type ?? 'N/A'} 33 |

34 |

35 | Event: {event.name ?? 'N/A'} 36 |

37 |

38 | Timestamp:{' '} 39 | {event.time.toLocaleString() ?? 'Invalid Date'} 40 |

41 |

42 | Source IP: {event.source_ip ?? 'N/A'} 43 |

44 |

45 | User Type: {event.user_identity_type ?? 'N/A'} 46 |

47 |

48 | Raw JSON Data: 49 |

50 |
53 |
{JSON.stringify(event, null, 2)}
54 |
55 |
56 |
57 | 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Modal; 70 | -------------------------------------------------------------------------------- /client/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { NavbarProps } from '../types'; 4 | import LOGO from '../assets/RAILGUIDE.png'; 5 | //import '../index.scss'; 6 | 7 | const Navbar: React.FC = ({ 8 | toggleDarkMode, 9 | isDarkMode, 10 | username, 11 | setUser, 12 | }) => { 13 | const [dropdownOpen, setDropdownOpen] = useState(false); 14 | const dropdownRef = useRef(null); 15 | const navigate = useNavigate(); 16 | 17 | const toggleDropdown = () => { 18 | setDropdownOpen((prev) => !prev); 19 | }; 20 | 21 | const handleLogout = () => { 22 | setUser(null); 23 | window.localStorage.removeItem('user'); 24 | navigate('/login'); 25 | }; 26 | 27 | useEffect(() => { 28 | const handleClickOutside = (event: MouseEvent) => { 29 | if ( 30 | dropdownRef.current && 31 | !dropdownRef.current.contains(event.target as Node) 32 | ) { 33 | setDropdownOpen(false); 34 | } 35 | }; 36 | 37 | document.addEventListener('mousedown', handleClickOutside); 38 | return () => { 39 | document.removeEventListener('mousedown', handleClickOutside); 40 | }; 41 | }, []); 42 | 43 | return ( 44 | 85 | ); 86 | }; 87 | 88 | export default Navbar; 89 | -------------------------------------------------------------------------------- /client/src/components/charts/AccessPerIp.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Cell, Legend, Pie, PieChart } from 'recharts'; 3 | import { CountedEvent, IPLocation } from '../../types'; 4 | 5 | const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; 6 | 7 | export default function AccessPerIpChart({ 8 | currentIp, 9 | setCurrentIp, 10 | ipLocCounts, 11 | }: { 12 | currentIp?: string; 13 | setCurrentIp: React.Dispatch>; 14 | ipLocCounts: (IPLocation & CountedEvent)[]; 15 | }): JSX.Element { 16 | const [loading, setLoading] = useState(true); // Add loading state 17 | 18 | useEffect(() => { 19 | // Simulate loading delay for data 20 | setLoading(true); 21 | if (ipLocCounts && ipLocCounts.length > 0) { 22 | setLoading(false); // Once data is available, set loading to false 23 | } 24 | }, [ipLocCounts]); 25 | 26 | const RADIAN = Math.PI / 180; 27 | const renderCustomizedLabel = ({ 28 | cx, 29 | cy, 30 | midAngle, 31 | innerRadius, 32 | outerRadius, 33 | percent, 34 | }: { 35 | cx: number; 36 | cy: number; 37 | midAngle: number; 38 | innerRadius: number; 39 | outerRadius: number; 40 | percent: number; 41 | }) => { 42 | const radius = innerRadius + (outerRadius - innerRadius) * 0.5; 43 | const x = cx + radius * Math.cos(-midAngle * RADIAN); 44 | const y = cy + radius * Math.sin(-midAngle * RADIAN); 45 | 46 | return ( 47 | cx ? 'start' : 'end'} 53 | dominantBaseline="central" 54 | > 55 | {`${(percent * 100).toFixed(0)}%`} 56 | 57 | ); 58 | }; 59 | 60 | // Show loading message while data is being fetched 61 | if (loading) return

Loading chart...

; 62 | 63 | return ( 64 | 65 | 71 | {ipLocCounts.map((_, index) => ( 72 | 73 | ))} 74 | 75 | { 80 | return value === currentIp ? ( 81 | {value} 82 | ) : ( 83 | value 84 | ); 85 | }} 86 | onClick={(payload) => { 87 | const payloadData = payload.payload as 88 | | (IPLocation & CountedEvent) 89 | | undefined; 90 | if (payloadData) { 91 | setCurrentIp((current: string | undefined) => 92 | payloadData.source_ip === current 93 | ? undefined 94 | : payloadData.source_ip 95 | ); 96 | } 97 | }} 98 | /> 99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /client/src/components/charts/AnomalyChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ScatterChart, 4 | Scatter, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | ResponsiveContainer, 10 | Legend, 11 | } from 'recharts'; 12 | 13 | interface DataPoint { 14 | timestamp: string; 15 | count: number; 16 | } 17 | 18 | const dummyData: DataPoint[] = [ 19 | { timestamp: '2024-10-29T09:00:00Z', count: 30 }, 20 | { timestamp: '2024-10-29T09:10:00Z', count: 25 }, 21 | { timestamp: '2024-10-29T09:20:00Z', count: 80 }, 22 | { timestamp: '2024-10-29T09:30:00Z', count: 40 }, 23 | { timestamp: '2024-10-29T09:40:00Z', count: 50 }, 24 | { timestamp: '2024-10-29T09:50:00Z', count: 90 }, 25 | { timestamp: '2024-10-29T10:00:00Z', count: 45 }, 26 | ]; 27 | 28 | const isAnomaly = (count: number): boolean => count > 70; // Define a threshold for anomalies 29 | 30 | const AnomalyChart: React.FC = () => { 31 | return ( 32 | 33 | 34 | 35 | new Date(time).toLocaleTimeString()} 39 | /> 40 | 41 | 42 | 43 | 49 | {dummyData.map((entry, index) => { 50 | const x = new Date(entry.timestamp).getTime(); 51 | const y = entry.count; 52 | return ( 53 | 60 | ); 61 | })} 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default AnomalyChart; 69 | -------------------------------------------------------------------------------- /client/src/components/charts/EventSource.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Bar, BarChart, LabelList, XAxis, Cell } from 'recharts'; 3 | import { CountedEvent } from '../../types'; 4 | 5 | const COLORS = [ 6 | '#0088FE', 7 | '#00C49F', 8 | '#FFBB28', 9 | '#FF8042', 10 | '#FF6666', 11 | '#FF99CC', 12 | '#FFCC99', 13 | ]; 14 | 15 | export default function EventSourceChart() { 16 | const [events, setEvents] = useState([]); 17 | const [loading, setLoading] = useState(true); 18 | const [selectedEventSource, setSelectedEventSource] = useState( 19 | null 20 | ); // State for clicked event source 21 | 22 | useEffect(() => { 23 | setLoading(true); 24 | fetch('/events?countOn=source') 25 | .then((response) => { 26 | if (response.ok) return response.json(); 27 | throw new Error(response.status + ': ' + response.statusText); 28 | }) 29 | .then((data: CountedEvent[] | { err: string }) => { 30 | setEvents(data as CountedEvent[]); 31 | setLoading(false); 32 | }) 33 | .catch((error) => 34 | console.warn('Could not fetch event name counts: ' + error) 35 | ); 36 | }, []); 37 | 38 | if (loading) return

Loading chart...

; 39 | 40 | const handleClick = (source: string) => { 41 | setSelectedEventSource((prevSelected) => 42 | prevSelected === source ? null : source 43 | ); 44 | }; 45 | 46 | return ( 47 |
48 | 56 | 63 | 64 | {events.map((entry, index) => ( 65 | handleClick(entry.source)} // Attach the click handler to each Cell 69 | style={{ cursor: 'pointer' }} // Add a pointer cursor to indicate clickability 70 | /> 71 | ))} 72 | 81 | 82 | 83 | 84 | {/* Display selected event source under the chart title */} 85 | {selectedEventSource && ( 86 |

Event Source: {selectedEventSource}

87 | )} 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /client/src/components/charts/EventType.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Bar, BarChart, LabelList, XAxis, Cell } from 'recharts'; 3 | import { CountedEvent } from '../../types'; 4 | 5 | const COLORS = [ 6 | '#0088FE', 7 | '#00C49F', 8 | '#FFBB28', 9 | '#FF8042', 10 | '#FF6666', 11 | '#FF99CC', 12 | '#FFCC99', 13 | ]; 14 | 15 | export default function EventTypeChart() { 16 | const [events, setEvents] = useState([]); 17 | const [loading, setLoading] = useState(true); 18 | const [selectedEventName, setSelectedEventName] = useState( 19 | null 20 | ); // State for clicked event name 21 | 22 | useEffect(() => { 23 | setLoading(true); 24 | fetch('/events?countOn=name') 25 | .then((response) => { 26 | if (response.ok) return response.json(); 27 | throw new Error(response.status + ': ' + response.statusText); 28 | }) 29 | .then((data: CountedEvent[] | { err: string }) => { 30 | setEvents( 31 | (data as CountedEvent[]).map((event) => ({ 32 | ...event, 33 | name: event.name.replace(/([A-Z])/g, ' $1'), 34 | })) 35 | ); 36 | setLoading(false); 37 | }) 38 | .catch((error) => 39 | console.warn('Could not fetch event name counts: ', error) 40 | ); 41 | }, []); 42 | 43 | if (loading) return

Loading chart...

; 44 | 45 | const handleClick = (data: { name: string }) => { 46 | // Toggle selection: if already selected, deselect; otherwise, select 47 | setSelectedEventName((prevSelected) => 48 | prevSelected === data.name ? null : data.name 49 | ); 50 | }; 51 | 52 | return ( 53 |
54 | 62 | 69 | 75 | {events.map((data, index) => ( 76 | handleClick(data)} 79 | fill={COLORS[index % COLORS.length]} 80 | /> 81 | ))} 82 | 91 | 92 | 93 | 94 | {/* Display selected event name under the chart title */} 95 | {selectedEventName && ( 96 |

Selected Event: {selectedEventName}

97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /client/src/components/charts/HeatMap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | ComposableMap, 4 | Geographies, 5 | Geography, 6 | Marker, 7 | ZoomableGroup, 8 | } from 'react-simple-maps'; 9 | import { 10 | CountedEvent, 11 | GeoJSONFeatureCollection, 12 | IPLocation, 13 | } from '../../types'; 14 | 15 | const HeatMap: React.FC = () => { 16 | const [geoJSON, setGeoJSON] = useState(null); 17 | const [ipData, setIpData] = useState<(IPLocation & CountedEvent)[]>([]); 18 | 19 | useEffect(() => { 20 | fetch( 21 | 'https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson' 22 | ) 23 | .then((response) => response.json()) 24 | .then((data: GeoJSONFeatureCollection) => setGeoJSON(data)) 25 | .catch((error) => console.error('Error fetching geoJSON:', error)); 26 | 27 | fetch('/events?countOn=source_ip&includeLocation=true') 28 | .then((response) => { 29 | if (response.ok) return response.json(); 30 | throw new Error(response.status + ': ' + response.statusText); 31 | }) 32 | .then((data: (IPLocation & CountedEvent)[] | { err: string }) => 33 | setIpData(() => data as (IPLocation & CountedEvent)[]) 34 | ) 35 | .catch((error) => 36 | console.warn('Could not fetch event ip counts and locations: ', error) 37 | ); 38 | }, []); 39 | 40 | return ( 41 |
42 | {geoJSON && ( 43 | 48 | 49 | 50 | {({ geographies }) => 51 | geographies.map((geo) => ( 52 | 62 | )) 63 | } 64 | 65 | {ipData.map(({ source_ip, lat, long, count }) => ( 66 | 67 | 72 | {`Count: ${count}`} 73 | 74 | ))} 75 | 76 | 77 | )} 78 |
79 | ); 80 | }; 81 | 82 | export default HeatMap; 83 | -------------------------------------------------------------------------------- /client/src/components/charts/IpAccessOverTime.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { AreaChart, XAxis, YAxis, Area } from 'recharts'; 3 | import { CountedEvent } from '../../types'; 4 | 5 | export default function IpAccessOverTimeChart({ 6 | currentIp, 7 | }: { 8 | currentIp?: string; 9 | }): JSX.Element | null { 10 | const [ipTimes, setIpTimes] = useState([]); 11 | const [loading, setLoading] = useState(true); // Add loading state 12 | 13 | useEffect(() => { 14 | setLoading(true); // Set loading to true before fetching data 15 | fetch('/events?countOn=time&groupTimeBy=minute') 16 | .then((response) => { 17 | if (response.ok) return response.json(); 18 | throw new Error(response.status + ': ' + response.statusText); 19 | }) 20 | .then((data: CountedEvent[] | { err: string }) => { 21 | setIpTimes(() => data as CountedEvent[]); 22 | setLoading(false); // Set loading to true before fetching data 23 | }) 24 | .catch((error) => 25 | console.warn('IpAccessOverTime: fetch error: ' + error) 26 | ); 27 | }, [currentIp]); 28 | 29 | if (!currentIp) return null; // Return null instead of undefined 30 | if (loading) return

Loading chart...

; 31 | //reversed the times to show the most recent first 32 | return ( 33 | 34 | 35 | 36 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/src/components/charts/PieChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts'; 3 | 4 | interface DataPoint { 5 | name: string; 6 | value: number; 7 | } 8 | 9 | const initialData: DataPoint[] = [ 10 | { name: 'Sadness', value: 400 }, 11 | { name: 'Anger', value: 300 }, 12 | { name: 'Frustration', value: 300 }, 13 | { name: 'Depression', value: 200 }, 14 | ]; 15 | 16 | const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; 17 | 18 | const TestPieChart: React.FC = () => { 19 | const [data, setData] = useState(initialData); 20 | 21 | useEffect(() => { 22 | const intervalId = setInterval(() => { 23 | setData(prevData => 24 | prevData.map(point => ({ 25 | ...point, 26 | value: Math.floor(Math.random() * 500), 27 | })) 28 | ); 29 | }, 5000); 30 | 31 | return () => clearInterval(intervalId); 32 | }, []); 33 | 34 | return ( 35 | 36 | name} // Using name directly 42 | outerRadius={80} 43 | fill="#8884d8" 44 | dataKey="value" 45 | > 46 | {data.map((_, index) => ( 47 | 48 | ))} 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default TestPieChart; 57 | 58 | -------------------------------------------------------------------------------- /client/src/components/charts/UserActivity.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | CartesianGrid, 4 | XAxis, 5 | YAxis, 6 | AreaChart, 7 | Area, 8 | Tooltip, 9 | } from 'recharts'; 10 | import { SimplifiedEvent } from '../../types'; 11 | 12 | const UserActivityChart: React.FC = () => { 13 | const [data, setData] = useState([]); 14 | 15 | useEffect(() => { 16 | fetch('/events?countOn=time&groupTimeBy=minute') 17 | .then((response) => { 18 | if (response.ok) return response.json(); 19 | throw new Error(response.status + ': ' + response.statusText); 20 | }) 21 | .then((data: { time: string; count: number }[]) => 22 | setData( 23 | (data as { time: string; count: number }[]).map((event) => ({ 24 | localTime: new Date(event.time).toLocaleString(), 25 | count: event.count, 26 | })) 27 | ) 28 | ) 29 | .catch((error) => 30 | console.warn('Could not fetch event time counts: ', error) 31 | ); 32 | }, []); 33 | 34 | // Format for the X-axis to display Mon, Tue, etc. 35 | const formatXAxis = (tickItem: string) => { 36 | const date = new Date(tickItem); 37 | const day = date.toLocaleDateString('en-US', { weekday: 'short' }); 38 | const dayMonth = `${date.getMonth() + 1}/${date.getDate()}`; 39 | return `${day} ${dayMonth}`; 40 | }; 41 | 42 | return ( 43 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default UserActivityChart; 65 | -------------------------------------------------------------------------------- /client/src/index.scss: -------------------------------------------------------------------------------- 1 | // Variables for colors and fonts 2 | $background-light: #f5e9c4; 3 | $background-dark: #2c2c2c; 4 | $primary-color: #eed074; 5 | $primary-hover: #bd9a33; 6 | $error-color: #ff0000; // red 7 | $border-radius: 15px; 8 | $font-family: 'Fredoka', sans-serif; 9 | $success-color: #28a745; 10 | $success-hover: #218838; 11 | $dark-button-color: #44619a; 12 | $dark-button-color-hover: #172e55; 13 | $dark-background: #2c2c2c; 14 | $dark-border: #444444; 15 | $dark-input-background: #3a3a3a; 16 | $dark-input-border: #555555; 17 | $light-text: #d3d3d3; // lightgray 18 | $event-button-color: #193d01; // adjusted from rgba(25, 61, 1, 0.92) 19 | $event-button-hover: #4b603e; 20 | $light-card: #e9d9a8; 21 | $light-mode-font-color: #193d01; // adjusted from rgba(25, 61, 1, 0.92) 22 | $draggable-border-color: #cccccc; // Border color for draggable cards 23 | $draggable-hover-bg: #f5f5f5; // Background color when dragging 24 | 25 | // General Styles 26 | html, body { 27 | height:100%; 28 | margin: 0; 29 | font-family: Arial, sans-serif; 30 | background: $background-light; 31 | 32 | &.dark-mode { 33 | background-color: $background-dark; 34 | } 35 | } 36 | 37 | h2 { 38 | text-align: center; 39 | margin-bottom: 20px; 40 | font-family: $font-family; 41 | } 42 | 43 | .error-message { 44 | color: $error-color; 45 | margin-bottom: 15px; 46 | text-align: center; 47 | } 48 | 49 | // Navigation Styles 50 | nav { 51 | position: relative; 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | padding: 1rem; 56 | width: 100%; 57 | background: linear-gradient(to right,#245901, #24361a); // adjusted from rgba(25, 61, 1, 0.92) 58 | color: #ffffff; // adjusted from rgba(255, 255, 255, 0.879) 59 | 60 | tspan { 61 | color: #f5f5f5; // whitesmoke 62 | } 63 | 64 | .logo { 65 | display: flex; 66 | align-items: center; 67 | 68 | .logo-image { 69 | width: auto; 70 | height: 250px; 71 | margin-top: -50px; 72 | margin-bottom: -140px; 73 | 74 | &:hover { 75 | transform: scale(1.05); 76 | } 77 | } 78 | } 79 | 80 | .nav-buttons { 81 | display: flex; 82 | align-items: center; 83 | margin-left: auto; 84 | margin-right: 2em; 85 | 86 | .nav-button { 87 | background-color: $primary-color; 88 | border: none; 89 | color: #475a2e; // adjusted from rgba(71, 90, 46, 1) 90 | cursor: pointer; 91 | padding: 0.5rem 1rem; 92 | margin-left: 5px; 93 | font-size: 1rem; 94 | font-family: $font-family; 95 | font-weight: 950; 96 | text-align: center; 97 | transition: background-color 0.3s, transform 0.2s, box-shadow 0.3s; 98 | border-radius: $border-radius; 99 | text-decoration: none; 100 | 101 | &:hover { 102 | background-color: $primary-hover; 103 | transform: scale(1.05); 104 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 105 | } 106 | } 107 | } 108 | 109 | .dropdown { 110 | position: absolute; 111 | top: 100%; 112 | right: 3.5%; 113 | background-color: $light-card; 114 | color: black; 115 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 116 | border-radius: 5px; 117 | z-index: 1000; 118 | 119 | .dropdown-link { 120 | display: block; 121 | padding: 0.5rem 1rem; 122 | text-decoration: none; 123 | color: black; 124 | transition: background-color 0.3s ease; 125 | 126 | &:hover { 127 | background-color: $primary-hover; 128 | } 129 | } 130 | } 131 | } 132 | 133 | //Homepage Styles 134 | .home-container { 135 | padding-left: 1em; 136 | padding-right: 3em; 137 | } 138 | 139 | .draggable-grid { 140 | display: grid; 141 | grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); // Base columns setup 142 | gap: 40px; 143 | width: 100%; 144 | padding-top: 10px; 145 | background-color: $background-light; 146 | } 147 | .draggable-card { 148 | width: 100%; 149 | box-sizing: border-box; 150 | transition: all 0.3s ease; 151 | height: auto; 152 | 153 | &.expanded { 154 | grid-column: span 2; 155 | transition: all 0.3s ease; 156 | } 157 | 158 | &:hover { 159 | transform: scale(1.01); 160 | } 161 | 162 | .card { 163 | display: flex; 164 | flex-direction: column; 165 | justify-content: flex-start; 166 | align-items: center; 167 | width: 100%; 168 | min-height: 400px; // Adjusted for better spacing with charts 169 | max-height: 450px; // To limit card expansion on larger screens 170 | border-radius: $border-radius; 171 | box-shadow: 0.2rem 0.2rem 1rem 0.01rem #282c34; 172 | background-color: $light-card; 173 | color: black; 174 | padding: 1rem; 175 | text-align: center; 176 | overflow: auto; // Ensure that overflowing content doesn’t break the layout 177 | 178 | .card-header { 179 | display: flex; 180 | margin-top: -20px; // Adjusted for better spacing 181 | } 182 | 183 | .card-content { 184 | display: flex; 185 | justify-content: center; 186 | align-items: center; 187 | width: 100%; 188 | height: 100%; 189 | padding-top: 0; 190 | } 191 | } 192 | } 193 | 194 | 195 | .underchart { 196 | font-size: 1.2rem; 197 | font-family: 'Fredoka'; 198 | font-weight: bold; 199 | text-align: center; 200 | margin-top: 10px; 201 | } 202 | 203 | .pie-label { 204 | font-size: 1em; 205 | font-weight: bold; 206 | color: #475a2e; // adjusted from rgba(71, 90, 46, 1) 207 | } 208 | 209 | .heatmap-container { 210 | display: flex; 211 | align-items: center; 212 | justify-content: center; 213 | width: 100%; 214 | height: auto; 215 | } 216 | 217 | .map { 218 | width: 100%; 219 | height: 100%; 220 | } 221 | 222 | //Event Dashboard Styles 223 | .event-dashboard { 224 | padding: 20px; 225 | 226 | .grid-container { 227 | display: grid; 228 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 229 | gap: 20px; 230 | } 231 | 232 | .event-card { 233 | background-color: $light-card; 234 | border: 1px solid #cccccc; // adjusted from #ccc 235 | border-radius: 8px; 236 | padding: 16px; 237 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 238 | transition: transform 0.2s, box-shadow 0.2s; 239 | font-size: .8em; 240 | 241 | &:hover { 242 | transform: translateY(-2px); 243 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 244 | } 245 | 246 | h3{ 247 | font-size: 1em; 248 | font-weight: bold; 249 | } 250 | 251 | button { 252 | background-color: $event-button-color; 253 | color: #ffffff; // white 254 | border: none; 255 | margin-left: 20%; 256 | margin-right: 20%; 257 | border-radius: 5px; 258 | padding: 10px 15px; 259 | cursor: pointer; 260 | transition: background-color 0.3s; 261 | font-family: $font-family; 262 | 263 | &:hover { 264 | background-color: $event-button-hover; 265 | } 266 | } 267 | } 268 | } 269 | 270 | // Modal Styles//Dashboard 271 | .modal-overlay { 272 | position: fixed; 273 | top: 0; 274 | left: 0; 275 | right: 0; 276 | bottom: 0; 277 | background: #0000059d; // adjusted from rgba(0, 0, 0, 0.5) 278 | display: flex; 279 | justify-content: center; 280 | align-items: center; 281 | z-index: 999; 282 | } 283 | 284 | .modal { 285 | background: $light-card; 286 | border-radius: 8px; 287 | box-shadow: 0 4px 20px #000000; // adjusted from rgb(0, 0, 0) 288 | width: 90%; 289 | max-width: 600px; 290 | overflow: hidden; 291 | 292 | .modal-header { 293 | padding: 16px; 294 | border-bottom: 1px solid #e0e0e0; 295 | display: flex; 296 | justify-content: space-between; 297 | align-items: center; 298 | } 299 | 300 | .modal-content { 301 | padding: 16px; 302 | max-height: 400px; 303 | overflow-y: auto; 304 | } 305 | 306 | .raw-json-container { 307 | max-height: 300px; 308 | overflow-y: auto; 309 | background-color: #f9e9ba; 310 | border: 1px solid #cccccc; // adjusted from #ccc 311 | border-radius: 4px; 312 | padding: 10px; 313 | font-family: monospace; 314 | } 315 | 316 | .modal-footer { 317 | padding: 16px; 318 | border-top: 1px solid #e0e0e0; 319 | text-align: right; 320 | } 321 | 322 | .close-button { 323 | background: #be302e; 324 | color: #ffffff; // adjusted from rgba(255, 255, 255, 0.917) 325 | border: none; 326 | border-radius: 8px; 327 | padding: 8px 16px; 328 | cursor: pointer; 329 | 330 | &:hover { 331 | background: #810404; 332 | } 333 | } 334 | } 335 | 336 | // Login and Signup Containers 337 | .login-container, 338 | .signup-container { 339 | width: 300px; 340 | margin: 50px auto; 341 | padding: 20px; 342 | border: 1px solid #cccccc; // adjusted from #ccc 343 | border-radius: 10px; 344 | background-color: #efe3c6; 345 | box-shadow: 0 0 10px #000000; // adjusted from rgba(0, 0, 0, 0.1) 346 | 347 | h2 { 348 | text-align: center; 349 | margin-bottom: 20px; 350 | } 351 | 352 | .form-group { 353 | margin-bottom: 15px; 354 | 355 | label { 356 | display: block; 357 | margin-bottom: 5px; 358 | } 359 | 360 | input { 361 | width: calc(100% - 16px); 362 | padding: 8px; 363 | box-sizing: border-box; 364 | background-color: #ffffff; // white 365 | color: #000000; // black 366 | border: 1px solid #cccccc; // adjusted from #ccc 367 | 368 | &::placeholder { 369 | color: #bbbbbb; // adjusted from #bbb 370 | } 371 | } 372 | } 373 | 374 | .error-message { 375 | color: $error-color; 376 | margin-bottom: 15px; 377 | text-align: center; 378 | } 379 | 380 | button { 381 | width: 100%; 382 | padding: 10px; 383 | background-color: $primary-color; 384 | color: $light-mode-font-color; 385 | font-size: medium; 386 | font-weight: bold; 387 | border: none; 388 | border-radius: 5px; 389 | cursor: pointer; 390 | 391 | &:hover { 392 | background-color: $primary-hover; 393 | } 394 | } 395 | } 396 | 397 | .signup-link { 398 | margin-top: 20px; 399 | text-align: center; 400 | 401 | p { 402 | margin: 0; 403 | } 404 | 405 | a { 406 | color: orange; 407 | text-decoration: none; 408 | 409 | &:hover { 410 | text-decoration: underline; 411 | } 412 | } 413 | } 414 | 415 | .profile-container { 416 | display: flex; 417 | flex-wrap: wrap; 418 | position: relative; 419 | min-height: 100vh; 420 | padding: 40px; 421 | background-color: $background-light; 422 | gap: 20px; 423 | justify-content: center; 424 | } 425 | 426 | .left-container, 427 | .right-container { 428 | flex: 1 1 45%; 429 | min-width: 300px; 430 | border: none; 431 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 432 | padding: 30px; 433 | background-color: $light-card; 434 | border-radius: 8px; 435 | transition: transform 0.2s, box-shadow 0.3s; 436 | display: flex; 437 | flex-direction: column; 438 | align-items: center; 439 | text-align: center; 440 | } 441 | 442 | .right-container { 443 | align-items: flex-start; 444 | } 445 | 446 | .left-container:hover, 447 | .right-container:hover { 448 | transform: scale(1.02); 449 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); 450 | } 451 | 452 | .aws-login-button, 453 | .logout-button, 454 | .submit-button { 455 | width: 100%; 456 | font-size: 1rem; 457 | padding: 0.75rem 1.5rem; 458 | margin-top: 20px; 459 | border-radius: 8px; 460 | font-family: 'Fredoka', sans-serif; 461 | font-weight: 700; 462 | cursor: pointer; 463 | transition: background-color 0.3s ease, transform 0.2s, box-shadow 0.3s; 464 | } 465 | 466 | .submit-button { 467 | background: #ff9900; 468 | color: white; 469 | border: none; 470 | } 471 | 472 | .submit-button:hover { 473 | background-color: #e68a00; 474 | transform: scale(1.05); 475 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 476 | } 477 | 478 | .logout-button { 479 | background: #d13212; 480 | color: rgba(255, 255, 255, 0.917); 481 | border: none; 482 | } 483 | 484 | .logout-button:hover { 485 | background: #a4260b; 486 | transform: scale(1.05); 487 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 488 | } 489 | 490 | .aws-login-button { 491 | background: #ff9900; 492 | color: white; 493 | border: none; 494 | text-align: center; 495 | } 496 | 497 | .aws-login-button:hover { 498 | background-color: #e68a00; 499 | transform: scale(1.05); 500 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 501 | } 502 | 503 | .aws-login-button a { 504 | color: white; 505 | text-decoration: none; 506 | } 507 | 508 | .profile-picture img { 509 | width: 100px; 510 | height: 100px; 511 | border-radius: 50%; 512 | object-fit: cover; 513 | transition: transform 0.2s; 514 | } 515 | 516 | .profile-picture img:hover { 517 | transform: scale(1.05); 518 | } 519 | 520 | .aws-logo { 521 | width: 120px; 522 | height: auto; 523 | margin-bottom: 20px; 524 | } 525 | 526 | .profile-info { 527 | width: 100%; 528 | } 529 | 530 | .profile-info p { 531 | margin: 10px 0; 532 | color: #333; 533 | font-size: 1rem; 534 | } 535 | 536 | .profile-info .info-container { 537 | padding: 15px; 538 | margin-bottom: 15px; 539 | border: 1px solid #e1e4e8; 540 | border-radius: 8px; 541 | background-color: #fafbfc; 542 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 543 | width: 100%; 544 | text-align: left; 545 | } 546 | 547 | .input-container { 548 | width: 100%; 549 | margin-bottom: 20px; 550 | display: flex; 551 | align-content: center; 552 | flex-direction: column; 553 | align-items: flex-start; 554 | } 555 | 556 | .input-container label { 557 | margin-bottom: 8px; 558 | font-weight: bold; 559 | color: #333; 560 | font-size: 1rem; 561 | } 562 | 563 | .input-container input, 564 | .input-container select { 565 | width: 100%; 566 | padding: 12px; 567 | border: 1px solid #e1e4e8; 568 | border-radius: 8px; 569 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 570 | } 571 | 572 | .bordered { 573 | border: 2px solid #0073bb; 574 | padding: 15px; 575 | border-radius: 8px; 576 | margin-bottom: 15px; 577 | } 578 | 579 | .settings-section { 580 | width: 100%; 581 | } 582 | 583 | .settings-section h3 { 584 | margin-top: 20px; 585 | font-weight: bold; 586 | text-align: left; 587 | width: 100%; 588 | color: #232f3e; 589 | } 590 | 591 | .settings-section .info-container { 592 | padding: 15px; 593 | margin-bottom: 15px; 594 | border: 1px solid #e1e4e8; 595 | border-radius: 8px; 596 | background-color: #fafbfc; 597 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 598 | width: 100%; 599 | text-align: left; 600 | } 601 | 602 | // Dark mode styles for profile page 603 | .dark-mode .profile-container, 604 | .dark-mode .left-container, 605 | .dark-mode .right-container { 606 | background-color: #232f3e; 607 | border: 1px solid #444; 608 | color: lightgray; 609 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7); 610 | } 611 | 612 | .dark-mode .left-container:hover, 613 | .dark-mode .right-container:hover { 614 | transform: scale(1.02); 615 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 616 | } 617 | 618 | .dark-mode .aws-login-button { 619 | background: #ff9900; 620 | } 621 | 622 | .dark-mode .aws-login-button:hover { 623 | background-color: #e68a00; 624 | } 625 | 626 | .dark-mode .logout-button { 627 | background: #a4260b; 628 | color: white; 629 | } 630 | 631 | .dark-mode .logout-button:hover { 632 | background: #7b1e06; 633 | } 634 | 635 | .dark-mode .profile-info p { 636 | color: lightgray; 637 | } 638 | 639 | .dark-mode .profile-info .info-container { 640 | background-color: #3a3a3a; 641 | border: 1px solid #555; 642 | color: lightgray; 643 | } 644 | 645 | .dark-mode .settings-section h3 { 646 | color: lightgray; 647 | } 648 | 649 | .dark-mode .settings-section .info-container { 650 | background-color: #3a3a3a; 651 | border: 1px solid #555; 652 | color: lightgray; 653 | } 654 | 655 | .dark-mode .input-container label { 656 | color: lightgray; 657 | } 658 | 659 | .dark-mode .input-container input, 660 | .dark-mode .input-container select { 661 | background-color: #3a3a3a; 662 | border: 1px solid #555; 663 | color: lightgray; 664 | } 665 | 666 | .dark-mode .bordered { 667 | border: 2px solid #ff9900; 668 | } 669 | 670 | 671 | // Dark mode styles 672 | .dark-mode { 673 | background-color: $background-dark; 674 | 675 | // Login and Signup Containers 676 | .login-container, 677 | .signup-container { 678 | background-color: $dark-background; 679 | border: 1px solid $dark-border; 680 | color: $light-text; 681 | 682 | .form-group { 683 | input { 684 | background-color: $dark-input-background; 685 | color: $light-text; 686 | border: 1px solid $dark-input-border; 687 | 688 | &::placeholder { 689 | color: #bbbbbb; // adjusted from #bbb 690 | } 691 | } 692 | } 693 | 694 | button { 695 | background-color: $dark-button-color; 696 | color: #ffffff; // white 697 | 698 | &:hover { 699 | background-color: $dark-button-color-hover; 700 | } 701 | } 702 | 703 | .error-message { 704 | color: $light-text; 705 | } 706 | } 707 | 708 | // Navigation 709 | nav { 710 | background:linear-gradient(to right,$dark-button-color,#0f1a2c); 711 | 712 | .logo { 713 | .logo-image { 714 | filter: invert(100%) saturate(10%) brightness(85%); 715 | } 716 | } 717 | 718 | .nav-button { 719 | background-color: $dark-button-color; 720 | color: #ffffff; // white 721 | 722 | &:hover { 723 | background-color: $dark-button-color-hover; 724 | } 725 | } 726 | } 727 | 728 | // Dropdown 729 | .dropdown { 730 | background-color: $dark-input-background; 731 | color: #ffffff; // white 732 | 733 | .dropdown-link { 734 | color: #ffffff; // white 735 | 736 | &:hover { 737 | background-color: $dark-button-color-hover; 738 | } 739 | } 740 | } 741 | 742 | .draggable-grid { 743 | background-color: $dark-background; 744 | } 745 | 746 | .home-container { 747 | background-color: $dark-background; 748 | } 749 | 750 | // Card Styles 751 | .card { 752 | background-color: #474747; 753 | color: #d3d3d3; // lightgray 754 | box-shadow: 0.2rem 0.2rem 1rem 0.01rem #000000; // adjusted from #000 755 | 756 | .card-title { 757 | color: #e4e1e1; // adjusted from rgb(228, 225, 225) 758 | background: none; 759 | } 760 | } 761 | 762 | .event-dashboard h1 { 763 | color: #e4e1e1; // adjusted from rgb(228, 225, 225) 764 | } 765 | 766 | // Event Card Styles 767 | .event-card { 768 | background-color: #4a4a4a; 769 | border: 1px solid #444444; // adjusted from #444 770 | color: #e5e3e3; // adjusted from rgb(229, 227, 227) 771 | 772 | button { 773 | background-color: $dark-button-color; 774 | 775 | &:hover { 776 | background-color: $dark-button-color-hover; 777 | } 778 | } 779 | } 780 | 781 | // Modal Styles 782 | .modal-overlay { 783 | background: #0000059d; // adjusted from rgba(0, 0, 0, 0.5) 784 | } 785 | 786 | .modal { 787 | background: #2c2c2c; 788 | color: #d3d3d3; // lightgray 789 | 790 | .modal-header { 791 | border-bottom: 1px solid #444444; // adjusted from #444 792 | } 793 | 794 | .raw-json-container { 795 | background-color: #3a3a3a; 796 | color: #d3d3d3; // lightgray 797 | } 798 | } 799 | 800 | // Signup Link 801 | .signup-link a { 802 | color: orange; 803 | } 804 | } 805 | 806 | // Media Queries 807 | @media (max-width: 1200px) { 808 | .draggable-grid { 809 | grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr)); /* Slightly smaller columns */ 810 | } 811 | } 812 | 813 | @media (max-width: 900px) { 814 | .draggable-grid { 815 | grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); /* Even smaller for tablet sizes */ 816 | } 817 | } 818 | 819 | @media (max-width: 600px) { 820 | .draggable-grid { 821 | grid-template-columns: 1fr; /* Single column for small screens */ 822 | } 823 | } 824 | 825 | 826 | // For tablets and larger devices 827 | @media (max-width: 768px) { 828 | nav { 829 | width: 100%; // Full width for navigation 830 | flex-direction: column; // Stack items vertically on smaller screens 831 | align-items: flex-start; // Align items to the start 832 | 833 | .nav-buttons { 834 | margin-left: 0; // Reset margin for buttons 835 | margin-top: 1rem; // Add space above buttons 836 | } 837 | } 838 | 839 | .profile-container { 840 | padding: 20px; 841 | flex-direction: column; // Stack containers vertically on tablets 842 | } 843 | 844 | .left-container, 845 | .right-container { 846 | flex: 1 1 100%; // Make containers take full width on tablets 847 | } 848 | 849 | 850 | .draggable-grid { 851 | grid-template-columns: 1fr; // Switch to single column 852 | padding: 40px; // Adjust padding as needed 853 | margin-left: 10px; 854 | } 855 | 856 | .card { 857 | min-width: 70%; // Make cards full width 858 | max-width: 225px; 859 | } 860 | 861 | .login-container, 862 | .signup-container { 863 | width: 90%; // Make forms more responsive 864 | } 865 | 866 | .event-dashboard { 867 | padding: 10px; // Reduce padding for smaller screens 868 | } 869 | } 870 | 871 | // For mobile devices 872 | @media (max-width: 480px) { 873 | body { 874 | font-size: 14px; // Adjust base font size for readability 875 | } 876 | 877 | h2 { 878 | font-size: 1.5rem; // Adjust header size 879 | } 880 | 881 | nav { 882 | width: 100%; // Full width for navigation 883 | } 884 | 885 | .nav-button { 886 | padding: 0.5rem; // Adjust button padding 887 | font-size: 0.9rem; // Smaller font size 888 | } 889 | 890 | .card { 891 | max-width: 250px; 892 | margin-left: -10px; 893 | margin-right: -10px;// Reduce margins for cards 894 | } 895 | 896 | .modal { 897 | width: 95%; // Make modals wider on smaller screens 898 | } 899 | 900 | .event-card { 901 | padding: 12px; // Adjust padding 902 | } 903 | 904 | .login-container, 905 | .signup-container { 906 | width: 100%; // Full width for mobile 907 | } 908 | 909 | .dropdown { 910 | right: auto; // Adjust dropdown position 911 | left: 0; // Align to the left 912 | } 913 | 914 | .profile-container { 915 | padding: 10px; 916 | } 917 | 918 | .left-container, 919 | .right-container { 920 | flex: 1 1 100%; // Stack containers vertically on mobile 921 | padding: 15px; 922 | } 923 | 924 | .profile-picture img { 925 | width: 80px; // Reduce image size for mobile 926 | height: 80px; 927 | } 928 | 929 | .aws-logo { 930 | width: 100px; // Reduce logo size for mobile 931 | } 932 | } 933 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.scss'; 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /client/src/pages/EventsDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Modal from '../components/Modal'; 3 | import { EventsDashboardProps, TGEvent } from '../types'; 4 | import EventCard from '../components/EventCard'; 5 | 6 | // const EventCard = lazy(() => import('../components/EventCard')); 7 | // const Modal = lazy(() => import('../components/Modal')); 8 | 9 | const EventsDashboard: React.FC = ({ isDarkMode }) => { 10 | const [modalOpen, setModalOpen] = useState(false); 11 | const [selectedEvent, setSelectedEvent] = useState(null); 12 | const [events, setEvents] = useState([]); 13 | const [loading, setLoading] = useState(true); 14 | const [error, setError] = useState(null); 15 | 16 | useEffect(() => { 17 | fetch('/events') 18 | .then((response) => { 19 | if (response.ok) return response.json(); 20 | throw new Error(response.status + ': ' + response.statusText); 21 | }) 22 | .then((data: TGEvent[]) => setEvents(() => data)) 23 | .catch((error) => { 24 | if (error === '403: Forbidden') 25 | setError('Please enter AWS Credentials to view events'); 26 | else console.warn('Could not fetch events: ', error); 27 | }); 28 | 29 | setLoading(false); 30 | }, []); 31 | 32 | const handleOpenModal = (event: TGEvent): void => { 33 | setSelectedEvent(event); 34 | setModalOpen(true); 35 | }; 36 | 37 | const handleCloseModal = (): void => { 38 | setModalOpen(false); 39 | setSelectedEvent(null); 40 | }; 41 | 42 | return ( 43 |
44 |

Recent Events

45 | {loading &&

Loading events...

} 46 | {error &&

{error}

} 47 | {!loading && !error && ( 48 |
49 | {events.map((event) => ( 50 | 56 | ))} 57 |
58 | )} 59 | 60 | {modalOpen && selectedEvent && ( 61 | 67 | )} 68 |
69 | ); 70 | }; 71 | 72 | export default EventsDashboard; 73 | -------------------------------------------------------------------------------- /client/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | DragDropContext, 4 | Droppable, 5 | Draggable, 6 | DropResult, 7 | } from '@hello-pangea/dnd'; 8 | import { CardState } from '../types'; 9 | import UserActivityChart from '../components/charts/UserActivity'; 10 | import EventTypeChart from '../components/charts/EventType'; 11 | import EventSourceChart from '../components/charts/EventSource'; 12 | import HeatMap from '../components/charts/HeatMap'; 13 | import IpAccessCombined from '../components/IpAccessCombined'; 14 | import AnomalyChart from '../components/charts/AnomalyChart'; 15 | import Card from '../components/Card'; 16 | 17 | const Home: React.FC<{ isDarkMode: boolean }> = ({ isDarkMode }) => { 18 | // State to track the current IP (null means no IP selected) 19 | const [currentIp, setCurrentIp] = useState(); 20 | 21 | const [cards, setCards] = useState([ 22 | { 23 | id: 'userActivity', 24 | title: 'User Activity', 25 | component: , 26 | }, 27 | { id: 'eventTypes', title: 'Event Names', component: }, 28 | { 29 | id: 'eventSources', 30 | title: 'Event Sources', 31 | component: , 32 | }, 33 | { id: 'heatMap', title: 'IP Address Heat Map', component: }, 34 | { 35 | id: 'ipAccess', 36 | title: 'Access by IP Address', 37 | component: ( 38 | 42 | ), 43 | }, 44 | { 45 | id: 'anomalyDetection', 46 | title: 'Anomaly Detection', 47 | component: , 48 | }, 49 | ]); 50 | 51 | const handleDragEnd = (result: DropResult) => { 52 | if (!result.destination) return; 53 | 54 | const updatedCards = Array.from(cards); 55 | const [movedCard] = updatedCards.splice(result.source.index, 1); 56 | updatedCards.splice(result.destination.index, 0, movedCard); 57 | setCards(updatedCards); 58 | }; 59 | 60 | return ( 61 |
62 | 63 | 64 | {(provided) => ( 65 |
70 | <> 71 | {cards.map((card, index) => ( 72 | 73 | {(provided, snapshot) => ( 74 |
82 | 83 | {card.component} 84 | 85 |
86 | )} 87 |
88 | ))} 89 | 90 | {provided.placeholder} 91 |
92 | )} 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Home; 100 | -------------------------------------------------------------------------------- /client/src/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate, Link } from 'react-router-dom'; 3 | import { UserDetails } from '../types'; 4 | 5 | const Login: React.FC<{ 6 | setUser: React.Dispatch>; 7 | }> = ({ setUser }) => { 8 | const [localUsername, setLocalUsername] = useState(''); 9 | const [email, setEmail] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | const [error, setError] = useState(null); 12 | const navigate = useNavigate(); 13 | 14 | const handleLogin = async (event: React.FormEvent) => { 15 | event.preventDefault(); 16 | setError(null); 17 | 18 | // Basic form validation 19 | if ((!localUsername && !email) || (localUsername && email)) { 20 | setError('Please provide either a username or an email, but not both'); 21 | return; 22 | } 23 | 24 | // Email format validation if email is provided 25 | if (email) { 26 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 27 | if (!emailRegex.test(email)) { 28 | setError('Please enter a valid email address'); 29 | return; 30 | } 31 | } 32 | 33 | try { 34 | //Send resgiter request to the backend 35 | const response = await fetch('/api/login', { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | body: JSON.stringify({ 41 | username: localUsername || null, 42 | work_email: email || null, 43 | password, 44 | }), 45 | }); 46 | const user = (await response.json()) as UserDetails; 47 | 48 | if (response.ok) { 49 | setUser(user); 50 | window.localStorage.setItem('user', JSON.stringify(user)); 51 | navigate('/profile'); 52 | } else { 53 | setError('Could Not Log In. Please Try again'); 54 | } 55 | } catch (err) { 56 | setError('Error logging in. Please try again.'); 57 | console.error(err, 'Error in login at Login.tsx;'); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 |

Login

64 | {error && ( 65 |
66 | {error} 67 |
68 | )} 69 |
void handleLogin(event)}> 70 |
71 | 72 | setLocalUsername(e.target.value)} 77 | autoComplete="username" 78 | /> 79 |
80 |
81 | 82 | setEmail(e.target.value)} 87 | autoComplete="email" 88 | /> 89 |
90 |
91 | 92 | setPassword(e.target.value)} 97 | required 98 | autoComplete="current-password" 99 | /> 100 |
101 | 104 |
105 |
106 |

107 | Don't have an account? Sign up here 108 |

109 |
110 |
111 | ); 112 | }; 113 | 114 | export default Login; 115 | -------------------------------------------------------------------------------- /client/src/pages/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AWSCredentials, ProfileProps, UserDetails } from '../types'; 3 | 4 | const Profile: React.FC = ({ 5 | isDarkMode, 6 | user, 7 | updateCredentials, 8 | }) => { 9 | function handleCredentialSubmit() { 10 | const locallyStoredUser: UserDetails = JSON.parse( 11 | window.localStorage.getItem('user')! 12 | ) as UserDetails; 13 | const domCollectedCreds: AWSCredentials = { 14 | aws_access_key: 15 | (document.getElementById('accessKey') as HTMLInputElement | null) 16 | ?.value ?? 'Could not find accessKey element', 17 | aws_secret_access_key: 18 | (document.getElementById('secretAccessKey') as HTMLInputElement | null) 19 | ?.value ?? 'Could not find secretAccessKey element', 20 | aws_region: 21 | (document.getElementById('region') as HTMLInputElement | null)?.value ?? 22 | 'Could not find region element', 23 | }; 24 | console.log(locallyStoredUser); 25 | console.log(domCollectedCreds); 26 | updateCredentials({ 27 | aws_access_key: 28 | domCollectedCreds.aws_access_key !== '' 29 | ? domCollectedCreds.aws_access_key 30 | : locallyStoredUser.aws_access_key ?? 'No locally stored access key', 31 | aws_secret_access_key: 32 | domCollectedCreds.aws_secret_access_key !== '' 33 | ? domCollectedCreds.aws_secret_access_key 34 | : locallyStoredUser.aws_secret_access_key ?? 35 | 'No locally stored secret access key', 36 | aws_region: 37 | domCollectedCreds.aws_region !== '' 38 | ? domCollectedCreds.aws_region 39 | : locallyStoredUser.aws_region ?? 'No locally stored region', 40 | }); 41 | } 42 | 43 | return ( 44 |
45 |
46 |
47 |
48 | Profile 52 |
53 |
54 |
55 |

Username: {user?.username ?? 'Not Logged In'}

56 |
57 |
58 |

Display Name: {user?.display_name ?? 'Not Logged In'}

59 |
60 |
61 |

Work Email: {user?.work_email ?? 'Not Logged In'}

62 |
63 |
64 |

Work Phone: {user?.work_phone ?? 'Not Logged In'}

65 |
66 | AWS Logo 71 |
72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 | 88 | 94 | AWS Log-in Information 95 | 96 |
97 | 98 | {/*} 99 |
100 |
101 |

Alert Settings

102 |
103 |

Settings related to alerts go here...

104 |
105 | 106 |

AI Settings

107 |
108 |

Settings related to AI features go here...

109 |
110 | 111 |

Homepage Settings

112 |
113 |
114 | 115 | 120 |
121 |
122 | 123 | 124 |
125 |
126 | 127 |
128 | 134 | 135 |
136 |
137 | 143 | 144 |
145 |
146 |
147 | 148 |
149 | 155 | 156 |
157 |
158 | 164 | 165 |
166 |
167 |
168 | 169 | 170 |
171 |
172 | 173 | 179 |
180 |
181 | 182 | 183 |
184 |
185 |
186 |
187 | */} 188 |
189 | ); 190 | }; 191 | 192 | export default Profile; 193 | -------------------------------------------------------------------------------- /client/src/pages/SignUp.tsx: -------------------------------------------------------------------------------- 1 | // import { deepStrictEqual } from 'assert'; 2 | import React, { useState } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const SignUp: React.FC = () => { 6 | const [username, setUsername] = useState(''); 7 | const [displayName, setDisplayName] = useState(''); 8 | const [work_email, setWorkEmail] = useState(''); 9 | const [workPhone, setWorkPhone] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | const [confirmPassword, setConfirmPassword] = useState(''); 12 | const [error, setError] = useState(null); 13 | const navigate = useNavigate(); 14 | 15 | const handleSignUp = async (event: React.FormEvent) => { 16 | event.preventDefault(); 17 | setError(null); 18 | 19 | // Basic form validation 20 | if (!username || !work_email || !password || !confirmPassword) { 21 | setError('Please fill in fields that is mandatory'); 22 | return; 23 | } 24 | 25 | // Email format validation 26 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 27 | if (!emailRegex.test(work_email)) { 28 | setError('Please enter a valid email address'); 29 | return; 30 | } 31 | 32 | // Password match validation 33 | if (password !== confirmPassword) { 34 | setError('Passwords do not match'); 35 | return; 36 | } 37 | 38 | try { 39 | //Send resgiter request to the backend 40 | const response = await fetch('/api/signup', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify({ 46 | username, 47 | password, 48 | displayName, 49 | work_email, 50 | workPhone, 51 | }), 52 | }); 53 | 54 | if (response.ok) { 55 | navigate('/login'); 56 | } 57 | } catch (err) { 58 | setError('Error signing up. Please try again.'); 59 | console.error(err, 'Error in signup at SignUp.tsx;'); 60 | } 61 | }; 62 | 63 | return ( 64 |
65 |

Sign Up

66 | {error &&
{error}
} 67 |
void handleSignUp(event)}> 68 |
69 | 70 | setUsername(e.target.value)} 75 | required 76 | /> 77 |
78 |
79 | 80 | setDisplayName(e.target.value)} 85 | required 86 | /> 87 |
88 |
89 | 90 | setWorkEmail(e.target.value)} 95 | required 96 | /> 97 |
98 |
99 | 100 | setWorkPhone(e.target.value)} 105 | required 106 | /> 107 |
108 |
109 | 110 | setPassword(e.target.value)} 115 | required 116 | /> 117 |
118 |
119 | 120 | setConfirmPassword(e.target.value)} 125 | required 126 | /> 127 |
128 | 131 |
132 |
133 | ); 134 | }; 135 | 136 | export default SignUp; 137 | -------------------------------------------------------------------------------- /client/src/profile.css: -------------------------------------------------------------------------------- 1 | .profile-container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | position: relative; 5 | min-height: 100vh; 6 | padding: 40px; 7 | background-color: #f3f4f6; 8 | gap: 20px; 9 | justify-content: center; 10 | } 11 | 12 | .left-container, 13 | .right-container { 14 | flex: 1 1 45%; 15 | min-width: 300px; 16 | border: none; 17 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 18 | padding: 30px; 19 | background-color: #ffffff; 20 | border-radius: 8px; 21 | transition: transform 0.2s, box-shadow 0.3s; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | text-align: center; 26 | } 27 | 28 | .right-container { 29 | align-items: flex-start; 30 | } 31 | 32 | .left-container:hover, 33 | .right-container:hover { 34 | transform: scale(1.02); 35 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); 36 | } 37 | 38 | .aws-login-button, 39 | .logout-button, 40 | .submit-button { 41 | width: 100%; 42 | font-size: 1rem; 43 | padding: 0.75rem 1.5rem; 44 | margin-top: 20px; 45 | border-radius: 8px; 46 | font-family: 'Fredoka', sans-serif; 47 | font-weight: 700; 48 | cursor: pointer; 49 | transition: background-color 0.3s ease, transform 0.2s, box-shadow 0.3s; 50 | } 51 | 52 | .submit-button { 53 | background: #ff9900; 54 | color: white; 55 | border: none; 56 | } 57 | 58 | .submit-button:hover { 59 | background-color: #e68a00; 60 | transform: scale(1.05); 61 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 62 | } 63 | 64 | .logout-button { 65 | background: #d13212; 66 | color: rgba(255, 255, 255, 0.917); 67 | border: none; 68 | } 69 | 70 | .logout-button:hover { 71 | background: #a4260b; 72 | transform: scale(1.05); 73 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 74 | } 75 | 76 | .aws-login-button { 77 | background: #ff9900; 78 | color: white; 79 | border: none; 80 | text-align: center; 81 | } 82 | 83 | .aws-login-button:hover { 84 | background-color: #e68a00; 85 | transform: scale(1.05); 86 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 87 | } 88 | 89 | .aws-login-button a { 90 | color: white; 91 | text-decoration: none; 92 | } 93 | 94 | .profile-picture img { 95 | width: 100px; 96 | height: 100px; 97 | border-radius: 50%; 98 | object-fit: cover; 99 | transition: transform 0.2s; 100 | } 101 | 102 | .profile-picture img:hover { 103 | transform: scale(1.05); 104 | } 105 | 106 | .aws-logo { 107 | width: 120px; 108 | height: auto; 109 | margin-bottom: 20px; 110 | } 111 | 112 | .profile-info { 113 | width: 100%; 114 | } 115 | 116 | .profile-info p { 117 | margin: 10px 0; 118 | color: #333; 119 | font-size: 1rem; 120 | } 121 | 122 | .profile-info .info-container { 123 | padding: 15px; 124 | margin-bottom: 15px; 125 | border: 1px solid #e1e4e8; 126 | border-radius: 8px; 127 | background-color: #fafbfc; 128 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 129 | width: 100%; 130 | text-align: left; 131 | } 132 | 133 | .input-container { 134 | width: 100%; 135 | margin-bottom: 20px; 136 | display: flex; 137 | flex-direction: column; 138 | align-items: flex-start; 139 | } 140 | 141 | .input-container label { 142 | margin-bottom: 8px; 143 | font-weight: bold; 144 | color: #333; 145 | font-size: 1rem; 146 | } 147 | 148 | .input-container input, 149 | .input-container select { 150 | width: 100%; 151 | padding: 12px; 152 | border: 1px solid #e1e4e8; 153 | border-radius: 8px; 154 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 155 | } 156 | 157 | .bordered { 158 | border: 2px solid #0073bb; 159 | padding: 15px; 160 | border-radius: 8px; 161 | margin-bottom: 15px; 162 | } 163 | 164 | .settings-section { 165 | width: 100%; 166 | } 167 | 168 | .settings-section h3 { 169 | margin-top: 20px; 170 | font-weight: bold; 171 | text-align: left; 172 | width: 100%; 173 | color: #232f3e; 174 | } 175 | 176 | .settings-section .info-container { 177 | padding: 15px; 178 | margin-bottom: 15px; 179 | border: 1px solid #e1e4e8; 180 | border-radius: 8px; 181 | background-color: #fafbfc; 182 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 183 | width: 100%; 184 | text-align: left; 185 | } 186 | 187 | .dark-mode .profile-container, 188 | .dark-mode .left-container, 189 | .dark-mode .right-container { 190 | background-color: #232f3e; 191 | border: 1px solid #444; 192 | color: lightgray; 193 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7); 194 | } 195 | 196 | .dark-mode .left-container:hover, 197 | .dark-mode .right-container:hover { 198 | transform: scale(1.02); 199 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 200 | } 201 | 202 | .dark-mode .aws-login-button { 203 | background: #ff9900; 204 | } 205 | 206 | .dark-mode .aws-login-button:hover { 207 | background-color: #e68a00; 208 | } 209 | 210 | .dark-mode .logout-button { 211 | background: #a4260b; 212 | color: white; 213 | } 214 | 215 | .dark-mode .logout-button:hover { 216 | background: #7b1e06; 217 | } 218 | 219 | .dark-mode .profile-info p { 220 | color: lightgray; 221 | } 222 | 223 | .dark-mode .profile-info .info-container { 224 | background-color: #3a3a3a; 225 | border: 1px solid #555; 226 | color: lightgray; 227 | } 228 | 229 | .dark-mode .settings-section h3 { 230 | color: lightgray; 231 | } 232 | 233 | .dark-mode .settings-section .info-container { 234 | background-color: #3a3a3a; 235 | border: 1px solid #555; 236 | color: lightgray; 237 | } 238 | 239 | .dark-mode .input-container label { 240 | color: lightgray; 241 | } 242 | 243 | .dark-mode .input-container input, 244 | .dark-mode .input-container select { 245 | background-color: #3a3a3a; 246 | border: 1px solid #555; 247 | color: lightgray; 248 | } 249 | 250 | .dark-mode .bordered { 251 | border: 2px solid #ff9900; 252 | } 253 | -------------------------------------------------------------------------------- /client/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ================================= 3 | * CLIENT TYPES 4 | * ================================= 5 | */ 6 | 7 | export interface UserDetails extends AWSCredentials { 8 | username: string; 9 | display_name: string; 10 | work_email: string; 11 | work_phone: string; 12 | } 13 | 14 | export interface AWSCredentials { 15 | aws_access_key: string; 16 | aws_secret_access_key: string; 17 | aws_region: string; 18 | } 19 | 20 | /** 21 | * REACT PROPS TYPES 22 | */ 23 | 24 | export interface ProfileProps { 25 | isDarkMode: boolean; 26 | user: UserDetails | null; 27 | updateCredentials: (credentials: AWSCredentials) => void; 28 | } 29 | 30 | export interface CardProps { 31 | title: string; 32 | children: React.ReactNode; 33 | isDarkMode: boolean; 34 | } 35 | 36 | export interface CardState { 37 | id: string; 38 | title: string; 39 | component: React.ReactNode; 40 | } 41 | 42 | export interface IpAccessCombinedProps { 43 | currentIp?: string; 44 | setCurrentIp: React.Dispatch>; 45 | } 46 | 47 | export interface EventsDashboardProps { 48 | isDarkMode: boolean; 49 | } 50 | 51 | export interface NavbarProps { 52 | toggleDarkMode: () => void; 53 | isDarkMode: boolean; 54 | username: string | null; 55 | setUser: React.Dispatch>; 56 | } 57 | 58 | export interface EventCardProps { 59 | event: LocationTGEvent | TGEvent | CountedEvent; 60 | onViewDetails: (event: LocationTGEvent | TGEvent | CountedEvent) => void; 61 | isDarkMode: boolean; 62 | } 63 | 64 | export interface ModalProps { 65 | isOpen: boolean; 66 | onClose: () => void; 67 | event: TGEvent | null; 68 | isDarkMode: boolean; 69 | } 70 | 71 | export interface IPAPIResponse { 72 | ip: string; 73 | version: string; 74 | city: string; 75 | region: string; 76 | region_code: string; 77 | country_code: string; 78 | country_code_iso3: string; 79 | country_name: string; 80 | country_capital: string; 81 | country_tld: string; 82 | continent_code: string; 83 | in_eu: boolean; 84 | postal: string; 85 | latitude: number; 86 | longitude: number; 87 | timezone: string; 88 | utc_offset: string; 89 | country_calling_code: string; 90 | currency: string; 91 | currency_name: string; 92 | languages: string; 93 | country_area: number; 94 | country_population: number; 95 | asn: string; 96 | org: string; 97 | hostname: string; 98 | } 99 | 100 | export interface TGEvent { 101 | _id: string; 102 | name: string; 103 | source: string; 104 | read_only: boolean; 105 | username: string; 106 | accesskey_id: string; 107 | account_id: string; 108 | arn: string; 109 | aws_region: string; 110 | cipher_suite: string; 111 | client_provided_host_header: string; 112 | category: string; 113 | time: Date; 114 | type: string; 115 | version: string; 116 | is_management: boolean; 117 | pricipal_id: string; 118 | recipient_account_id: string; 119 | request_id: string; 120 | source_ip: string; 121 | tls_version: string; 122 | user_identity_type: string; 123 | user_agent: string; 124 | } 125 | 126 | export interface IPLocation { 127 | country: string; 128 | region: string; 129 | city: string; 130 | lat: number; 131 | long: number; 132 | } 133 | 134 | export interface SimplifiedEvent { 135 | localTime: string; 136 | count: number; 137 | } 138 | 139 | export interface CountedEvent extends TGEvent { 140 | count: number; 141 | } 142 | 143 | export type LocationTGEvent = IPLocation & (TGEvent | CountedEvent); 144 | 145 | export interface LoginFormData { 146 | username: string; 147 | password: string; 148 | } 149 | 150 | /** 151 | * GeoJSON Types 152 | */ 153 | export interface GeoJSONFeatureCollection { 154 | type: string; 155 | features: { 156 | type: string; 157 | properties: Record; 158 | geometry: { 159 | type: string; 160 | coordinates: number[][] | number[][][]; 161 | }; 162 | }[]; 163 | } 164 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "isolatedModules": true, 11 | "moduleDetection": "force", 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "removeComments": true 20 | }, 21 | "include": ["**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] // Optional, but good practice 23 | } 24 | -------------------------------------------------------------------------------- /client/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /compose-dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dev-db: 3 | image: trailguide/trailguide-db-dev 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-postgres 7 | container_name: trailguide-db-dev 8 | restart: always 9 | healthcheck: 10 | test: ['CMD-SHELL', 'pg_isready -U tgadmin -d tgdb-dev'] 11 | interval: 5s 12 | timeout: 5s 13 | retries: 5 14 | environment: 15 | - POSTGRES_PASSWORD=secret 16 | - POSTGRES_USER=tgadmin 17 | - POSTGRES_DB=tgdb-dev 18 | volumes: 19 | - trailguide-db-dev:/var/lib/postgresql/data 20 | ports: 21 | - 5432:5432 22 | 23 | app-dev: 24 | image: trailguide/trailguide-dev 25 | build: 26 | context: . 27 | dockerfile: Dockerfile 28 | target: dev-deps 29 | container_name: trailguide-server-dev 30 | # env_file: .env 31 | environment: 32 | - POSTGRES_PASSWORD=secret 33 | - POSTGRES_USER=tgadmin 34 | - POSTGRES_DB=tgdb-dev 35 | - NODE_ENV=development 36 | ports: 37 | - 8080:8080 38 | volumes: 39 | - ./:/usr/src/app 40 | - node_modules:/usr/src/app/node_modules 41 | command: ['npm', 'run', 'dev'] 42 | depends_on: 43 | dev-db: 44 | condition: service_healthy 45 | 46 | volumes: 47 | trailguide-db-dev: 48 | node_modules: 49 | -------------------------------------------------------------------------------- /compose-node_modules.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | node_modules: 3 | 4 | services: 5 | bash: 6 | image: trailguide/trailguide-dev 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | target: dev-deps 11 | ports: 12 | - 8080:8080 13 | volumes: 14 | - ./:/usr/src/app 15 | - node_modules:/usr/src/app/node_modules 16 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: trailguide/trailguide-db-prod 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-postgres 7 | container_name: trailguide-db-prod 8 | restart: always 9 | healthcheck: 10 | test: ['CMD-SHELL', 'pg_isready -U tgadmin -d tgdb'] 11 | interval: 5s 12 | timeout: 5s 13 | retries: 5 14 | environment: 15 | - POSTGRES_PASSWORD=secret 16 | - POSTGRES_USER=tgadmin 17 | - POSTGRES_DB=tgdb 18 | volumes: 19 | - trailguide-db:/var/lib/postgresql/data 20 | ports: 21 | - 5432:5432 22 | 23 | server: 24 | image: trailguide/trailguide-prod 25 | build: 26 | context: . 27 | container_name: trailguide-server 28 | environment: 29 | - POSTGRES_PASSWORD=secret 30 | - POSTGRES_USER=tgadmin 31 | - POSTGRES_DB=tgdb 32 | - NODE_ENV=production 33 | ports: 34 | - 8080:8080 35 | depends_on: 36 | db: 37 | condition: service_healthy 38 | 39 | volumes: 40 | trailguide-db: 41 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import react from 'eslint-plugin-react'; 4 | import reactHooks from 'eslint-plugin-react-hooks'; 5 | import reactRefresh from 'eslint-plugin-react-refresh'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | export default tseslint.config( 9 | { ignores: ['dist'] }, 10 | { 11 | extends: [ 12 | js.configs.recommended, 13 | ...tseslint.configs.recommendedTypeChecked, 14 | ...tseslint.configs.stylisticTypeChecked, 15 | ], 16 | files: ['**/*.{ts,tsx}'], 17 | languageOptions: { 18 | ecmaVersion: 2020, 19 | globals: globals.browser, 20 | }, 21 | settings: { react: { version: '18.3' } }, 22 | plugins: { 23 | react: react, 24 | 'react-hooks': reactHooks, 25 | 'react-refresh': reactRefresh, 26 | }, 27 | rules: { 28 | ...reactHooks.configs.recommended.rules, 29 | 'react-refresh/only-export-components': [ 30 | 'warn', 31 | { allowConstantExport: true }, 32 | ], 33 | ...react.configs.recommended.rules, 34 | ...react.configs['jsx-runtime'].rules, 35 | }, 36 | }, 37 | { 38 | languageOptions: { 39 | // other options... 40 | parserOptions: { 41 | project: ['./tsconfig.node.json', './client/tsconfig.app.json'], 42 | tsconfigRootDir: import.meta.dirname, 43 | }, 44 | }, 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trailguide", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "nodemon server/server.js", 8 | "build": "tsc -b && vite build", 9 | "start": "docker compose up", 10 | "lint": "eslint .", 11 | "preview": "vite preview", 12 | "docker:install": "docker compose -f compose-node_modules.yml run --rm --service-ports bash npm i --", 13 | "docker:dev": "docker compose -f compose-dev.yml up --remove-orphans --build", 14 | "docker-remove-all": "docker rm $(docker ps -q -a -f 'name=trailguide-') --force && docker image rm $(docker images trailguide/trailguide-* -q) --force && docker volume rm $(docker volume ls -q -f 'name=trailguide*') --force" 15 | }, 16 | "dependencies": { 17 | "@aws-sdk/client-cloudtrail": "^3.583.0", 18 | "@hello-pangea/dnd": "^17.0.0", 19 | "bcrypt": "^5.1.1", 20 | "bcryptjs": "^2.4.3", 21 | "dotenv": "^16.4.5", 22 | "express": "^4.21.1", 23 | "pg": "^8.13.0", 24 | "react": "^18.3.1", 25 | "react-dom": "^18.3.1", 26 | "react-router-dom": "^6.26.2", 27 | "react-simple-maps": "^3.0.0", 28 | "recharts": "^2.13.0", 29 | "vite-express": "^0.19.0" 30 | }, 31 | "devDependencies": { 32 | "@eslint/js": "^9.11.1", 33 | "@types/express": "^5.0.0", 34 | "@types/node": "^22.7.5", 35 | "@types/pg": "^8.11.10", 36 | "@types/react": "^18.3.11", 37 | "@types/react-dom": "^18.3.1", 38 | "@types/react-simple-maps": "^3.0.6", 39 | "@types/recharts": "^1.8.29", 40 | "@vitejs/plugin-react": "^4.3.2", 41 | "eslint": "^9.11.1", 42 | "eslint-plugin-react": "^7.37.1", 43 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 44 | "eslint-plugin-react-refresh": "^0.4.12", 45 | "globals": "^15.9.0", 46 | "nodemon": "^3.1.7", 47 | "sass": "^1.80.3", 48 | "ts-node": "^10.9.2", 49 | "typescript": "^5.5.3", 50 | "typescript-eslint": "^8.7.0", 51 | "vite": "^5.4.8" 52 | }, 53 | "nodemonConfig": { 54 | "watch": [ 55 | "server/server.js", 56 | "server/**/**/*" 57 | ], 58 | "ignore": [ 59 | "client/*", 60 | "dist/*", 61 | "vite.config.ts" 62 | ], 63 | "ext": "js,json" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /readmeAssets/aws-credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/readmeAssets/aws-credential.png -------------------------------------------------------------------------------- /readmeAssets/log-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/readmeAssets/log-in.png -------------------------------------------------------------------------------- /readmeAssets/sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/readmeAssets/sign-up.png -------------------------------------------------------------------------------- /readmeAssets/trailguide-readme-main.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/TrailGuide/2582e28da0495b4535794652a7332deea81613eb/readmeAssets/trailguide-readme-main.webp -------------------------------------------------------------------------------- /scripts/db_init.sql: -------------------------------------------------------------------------------- 1 | -- CREATE DATABASE IF NOT EXISTS tgdb; 2 | -- CREATE DATABASE IF NOT EXISTS tgbd-dev; 3 | 4 | CREATE TABLE IF NOT EXISTS events ( 5 | _id VARCHAR PRIMARY KEY, 6 | name VARCHAR, 7 | source VARCHAR, 8 | read_only BOOLEAN, 9 | username VARCHAR, 10 | accesskey_id VARCHAR, 11 | account_id VARCHAR, 12 | arn VARCHAR, 13 | aws_region VARCHAR, 14 | cipher_suite VARCHAR, 15 | client_provided_host_header VARCHAR, 16 | category VARCHAR, 17 | time TIMESTAMPTZ, 18 | type VARCHAR, 19 | version VARCHAR, 20 | is_management BOOLEAN, 21 | principal_id VARCHAR, 22 | recipient_account_id VARCHAR, 23 | request_id VARCHAR, 24 | source_ip VARCHAR, 25 | tls_version VARCHAR, 26 | user_identity_type VARCHAR, 27 | user_agent VARCHAR 28 | ); 29 | 30 | CREATE TABLE IF NOT EXISTS ips ( 31 | ip VARCHAR(30) PRIMARY KEY, 32 | country VARCHAR(50), 33 | region VARCHAR(50), 34 | city VARCHAR(50), 35 | lat NUMERIC(9, 6), 36 | long NUMERIC(9, 6) 37 | ); 38 | 39 | CREATE TABLE IF NOT EXISTS users( 40 | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, 41 | username VARCHAR(255) UNIQUE NOT NULL, 42 | password VARCHAR(255) NOT NULL, 43 | display_name VARCHAR(100), 44 | work_email VARCHAR(255) UNIQUE NOT NULL, 45 | work_phone VARCHAR(25), 46 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, 47 | aws_access_key VARCHAR, 48 | aws_secret_access_key VARCHAR, 49 | aws_region VARCHAR 50 | ); -------------------------------------------------------------------------------- /server/controllers/awsController.js: -------------------------------------------------------------------------------- 1 | import * as timeBuckets from '../utils/timeBuckets.js'; 2 | import { configureCloudtrailClient, query } from '../models/eventsModel.js'; 3 | 4 | export default { 5 | setCredentials: (req, res, next) => { 6 | try { 7 | const { aws_access_key, aws_secret_access_key, aws_region } = req.body; 8 | if (!aws_access_key || !aws_secret_access_key || !aws_region) { 9 | return next({ 10 | log: `awsController.setCredentials: Malformed Request: aws_access_key= ${aws_access_key} typeof aws_secret_access_key= ${typeof aws_secret_access_key} aws_region= ${aws_region}`, 11 | status: 400, 12 | message: { err: 'Malformed Request' }, 13 | }); 14 | } 15 | process.env.AWS_ACCESS_KEY_ID = aws_access_key; 16 | process.env.AWS_SECRET_ACCESS_KEY = aws_secret_access_key; 17 | process.env.AWS_REGION = aws_region; 18 | configureCloudtrailClient(); 19 | res.locals.awsCredentials = { 20 | aws_access_key, 21 | aws_secret_access_key, 22 | aws_region, 23 | }; 24 | return next(); 25 | } catch (error) { 26 | return next({ 27 | log: 'awsController.setCredentials: ' + error, 28 | status: 500, 29 | message: { 30 | err: 'A server error occured', 31 | }, 32 | }); 33 | } 34 | }, 35 | 36 | getEvents: async (req, res, next) => { 37 | if ( 38 | !process.env.AWS_ACCESS_KEY_ID || 39 | process.env.AWS_ACCESS_KEY_ID === '' || 40 | !process.env.AWS_SECRET_ACCESS_KEY || 41 | process.env.AWS_SECRET_ACCESS_KEY_ID === '' || 42 | !process.env.AWS_REGION || 43 | process.env.AWS_REGION === '' 44 | ) { 45 | return next({ 46 | log: 'awsController.getEvents: trying to get events without an accesskey', 47 | status: 403, 48 | message: { 49 | err: 'AWS Credentials not Authorized', 50 | }, 51 | }); 52 | } 53 | try { 54 | const result = await query( 55 | ` 56 | SELECT * FROM events 57 | WHERE name != 'LookupEvents' 58 | ORDER BY time DESC 59 | LIMIT $1 60 | `, 61 | [req.query.amount || 100] 62 | ); 63 | res.locals.events = result.rows; 64 | return next(); 65 | } catch (err) { 66 | return next({ 67 | log: 'awsController.getEvents: ' + err, 68 | status: 500, 69 | message: { 70 | err: 'A server error occured', 71 | }, 72 | }); 73 | } 74 | }, 75 | 76 | /** 77 | * Middleware to convert the array of events (res.locals.events) down to distinct groups, with a count of 78 | * number of events for each. 79 | * the '?countOn=' query string parameter specifies the event property to get distinct events by 80 | * if no countOn query string parameter given, the function will not mutate the events array 81 | * the '?groupTimeBy=' optional query string parameter specifies how to bucket time values. 82 | * ( groupTimeBy=hour (minute is default) would count events with the same hour as the same event, giving a count per hour) 83 | * @param {*} req express middleware request object 84 | * @param {*} res express middleware response object 85 | * @param {*} next express middleware next function 86 | * @returns (all changes are made on the res.locals.events array) events array will be an array of objects of general type {countOn key : distinct value, count: number} 87 | */ 88 | countOn: (req, res, next) => { 89 | // error checking for early exit if needed data doesn't exist 90 | if ( 91 | !req.query.countOn || 92 | !res.locals.events || 93 | !Array.isArray(res.locals.events) || 94 | res.locals.events.length === 0 95 | ) 96 | return next(); 97 | try { 98 | // bucket events (stored in res.locals.events) by user specified 99 | // 'groupByTime' function, or by minute as default 100 | if (req.query.countOn === 'time' && req.query.groupTimeBy) { 101 | const groupTimeBy = 102 | timeBuckets[req.query.groupTimeBy] || timeBuckets.minute; 103 | res.locals.events.forEach( 104 | (event) => (event.time = groupTimeBy(event.time)) 105 | ); 106 | } 107 | 108 | // reduce the events array into a single object where each key is a distint 109 | // value of the event propertywe want to 'countOn', 110 | // each value of this 'countsPerField' object is the number of events with that distinct key 111 | const countsPerField = res.locals.events.reduce( 112 | (counts, event) => ({ 113 | ...counts, 114 | [event[req.query.countOn]]: 115 | (counts[event[req.query.countOn]] || 0) + 1, 116 | }), 117 | {} // start with an empty counts object 118 | ); 119 | 120 | // convert the object back into an array of objects useable by our charts 121 | res.locals.events = Object.entries(countsPerField).map( 122 | ([group, count]) => ({ 123 | count, 124 | name: group, 125 | [req.query.countOn]: group, 126 | }) 127 | ); 128 | 129 | return next(); 130 | } catch (error) { 131 | return next({ 132 | log: 'awsController.groupOn: ' + err, 133 | status: 500, 134 | message: { 135 | err: 'A server error occured', 136 | }, 137 | }); 138 | } 139 | }, 140 | }; 141 | -------------------------------------------------------------------------------- /server/controllers/ipLocController.js: -------------------------------------------------------------------------------- 1 | import { query } from '../models/ipsModel.js'; 2 | 3 | export default { 4 | injectLocs: async (req, res, next) => { 5 | if (!req.query.includeLocation) return next(); 6 | try { 7 | for (let event of res.locals.events) { 8 | // try to get the data from the database's ips table 9 | let result = await query( 10 | ` 11 | SELECT country, region, city, lat, long FROM ips 12 | WHERE ip = $1; 13 | `, 14 | [event.source_ip] 15 | ); 16 | 17 | // if we aren't storing a location for this ip, query the ip api 18 | if (!result.rows || result.rows.length === 0) { 19 | const response = await fetch( 20 | 'https://ipapi.co/' + event.source_ip + '/json' 21 | ); 22 | const location = await response.json(); 23 | event = { ...event, ...location }; 24 | 25 | //overwrite the result with the returned row from the insert 26 | result = await query( 27 | ` 28 | INSERT INTO ips 29 | (ip, country, region, city, lat, long) 30 | VALUES( 31 | $1, 32 | $2, 33 | $3, 34 | $4, 35 | $5, 36 | $6 37 | ) 38 | ON CONFLICT (ip) DO NOTHING 39 | RETURNING country, region, city, lat, long; 40 | `, 41 | [ 42 | event.source_ip, 43 | location.country, 44 | location.region, 45 | location.city, 46 | location.latitude, 47 | location.longitude, 48 | ] 49 | ); 50 | } 51 | const { country, region, city, lat, long } = result.rows[0]; 52 | 53 | // update the event, then continue the loop 54 | event.country = country; 55 | event.region = region; 56 | event.city = city; 57 | event.lat = Number(lat); 58 | event.long = Number(long); 59 | } 60 | return next(); 61 | } catch (error) { 62 | return next({ 63 | log: 'ipLocController.injectLocs: Error: ' + error, 64 | status: 500, 65 | message: { 66 | err: 'A server error occured', 67 | }, 68 | }); 69 | } 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | //bcrypt can both verify and hash the user passwords 3 | import { query } from '../models/usersModel.js'; 4 | 5 | // signup user 6 | export default { 7 | createUser: async (req, res, next) => { 8 | const { username, password, displayName, work_email, workPhone } = req.body; 9 | 10 | //format validation 11 | if (!username || !password || !displayName || !work_email || !workPhone) { 12 | return next({ 13 | log: 'userController.createUser: malformat request', 14 | status: 400, 15 | message: { 16 | err: 'Malformat request', 17 | }, 18 | }); 19 | } 20 | 21 | try { 22 | const hashedPassword = await bcrypt.hash(password, 10); //salt = 10 23 | 24 | const queryText = ` 25 | INSERT INTO users (username, password, display_name, work_email, work_phone) 26 | VALUES ($1, $2, $3, $4, $5) RETURNING *; 27 | `; 28 | //values will replaced the placeholder above 29 | const values = [ 30 | username, 31 | hashedPassword, 32 | displayName, 33 | work_email, 34 | workPhone, 35 | ]; 36 | const result = await query(queryText, values); 37 | res.locals.createdUser = result.rows[0]; 38 | return next(); 39 | } catch (err) { 40 | next({ 41 | log: 'userController.createUser: ' + err, 42 | status: 500, 43 | message: { 44 | err: 'Error during user creation', 45 | }, 46 | }); 47 | } 48 | }, 49 | 50 | //login user 51 | loginUser: async (req, res, next) => { 52 | const { username, work_email, password } = req.body; 53 | try { 54 | const queryText = 55 | 'select * from users where work_email = $1 OR username = $2'; 56 | const result = await query(queryText, [work_email, username]); 57 | 58 | //edge case 1: when the user does not exist 59 | if (result.rows.length === 0) { 60 | return next({ 61 | log: 'userController.loginUser: User Does Not Exist in the database', 62 | status: 400, 63 | message: { 64 | err: 'Login Unseccessful', 65 | }, 66 | }); 67 | } 68 | 69 | const user = result.rows[0]; 70 | 71 | //edge case 2: when the password is wrong 72 | const isMatch = await bcrypt.compare(password, user.password); 73 | if (!isMatch) { 74 | return next({ 75 | log: 'userController.loginUser: user does not exist', 76 | status: 400, 77 | message: { 78 | err: 'Error during Login', 79 | }, 80 | }); 81 | } 82 | //return a response when login successfully 83 | res.locals.loggedinuser = user; 84 | return next(); 85 | } catch (err) { 86 | return next({ 87 | log: 'userController.loginUser: ' + err, 88 | status: 500, 89 | message: { 90 | err: 'Error during Login', 91 | }, 92 | }); 93 | } 94 | }, 95 | 96 | saveUserAwsCredentials: async (req, res, next) => { 97 | if (!req.body.username) 98 | return next({ 99 | log: 'userController.saveUserAWsCredentials: No username provided in request body', 100 | status: 400, 101 | message: { 102 | err: 'Malformed Request: include a username', 103 | }, 104 | }); 105 | try { 106 | const result = await query( 107 | ` 108 | UPDATE users 109 | SET aws_access_key = $1, 110 | aws_secret_access_key = $2, 111 | aws_region = $3 112 | WHERE username = $4 113 | RETURNING *; 114 | `, 115 | [ 116 | res.locals.awsCredentials.aws_access_key, 117 | res.locals.awsCredentials.aws_secret_access_key, 118 | res.locals.awsCredentials.aws_region, 119 | req.body.username, 120 | ] 121 | ); 122 | res.locals.updatedUser = result.rows[0]; 123 | return next(); 124 | } catch (error) { 125 | return next({ 126 | log: 'userController.saveUserAwsCredentials: ' + error, 127 | status: 500, 128 | message: { 129 | err: 'Error when saving credentials', 130 | }, 131 | }); 132 | } 133 | }, 134 | }; 135 | 136 | // getAllUsers: async (req, res, next) => { 137 | // try { 138 | // const queryText = 'SELECT * FROM users;'; 139 | // const result = await pool.query(queryText); 140 | // if (result.rows.length === 0) { 141 | // return res.status(404).json({ error: `No User found` }); 142 | // } 143 | // res.status(200).json(result.rows); 144 | // } catch (err) { 145 | // next(err); 146 | // } 147 | // }, 148 | 149 | // getUserByField: async (req, res, next) => { 150 | // const { field, value } = req.query; //used to be req.params 151 | // try { 152 | // const queryText = `SELECT * FROM users WHERE ${field} = $1;`; 153 | // const result = await pool.query(queryText, [value]); 154 | // return res.status(200).json(result.rows[0]); 155 | // } catch (err) { 156 | // next(err); 157 | // } 158 | // }, 159 | // }; 160 | 161 | //example 162 | //getUserByField('username', 'someUsername'); 163 | //getUserByField('work_email', 'someWorkEmail@example.com'); 164 | -------------------------------------------------------------------------------- /server/models/eventsModel.js: -------------------------------------------------------------------------------- 1 | import { 2 | CloudTrailClient, 3 | LookupEventsCommand, 4 | } from '@aws-sdk/client-cloudtrail'; 5 | import pg from 'pg'; 6 | // import 'dotenv/config'; 7 | 8 | // TODO: USE ENVIRONMENT VARIABLES 9 | const pool = new pg.Pool({ 10 | user: 'tgadmin', 11 | password: 'secret', 12 | host: 13 | process.env.NODE_ENV === 'production' 14 | ? 'trailguide-db-prod' 15 | : 'trailguide-db-dev', 16 | port: 5432, 17 | database: process.env.POSTGRES_DB || 'tgdb-dev', 18 | }); 19 | 20 | // if an error is encountered by a client while it sits idle in the pool 21 | // the pool itself will emit an error event with both the error and 22 | // the client which emitted the original error 23 | // this is a rare occurrence but can happen if there is a network partition 24 | // between your application and the database, the database restarts, etc. 25 | // and so you might want to handle it and at least log it out 26 | pool.on('error', function (err, client) { 27 | console.error('idle client error', err.message, err.stack); 28 | }); 29 | 30 | //export the query method for passing queries to the pool 31 | export async function query(text, values) { 32 | // console.log( 33 | // 'eventsModel.query: ', 34 | // text.split('\n')[1], 35 | // ' with ', 36 | // values?.length || 0, 37 | // 'values' 38 | // ); 39 | return pool.query(text, values); 40 | } 41 | 42 | // the pool also supports checking out a client for 43 | // multiple operations, such as a transaction 44 | export async function connect() { 45 | return pool.connect(); 46 | } 47 | 48 | let cloudtrailClient; 49 | 50 | export function configureCloudtrailClient() { 51 | try { 52 | cloudtrailClient = new CloudTrailClient({ 53 | region: process.env.AWS_REGION, 54 | credentials: { 55 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 56 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 57 | }, 58 | }); 59 | } catch (error) { 60 | console.log( 61 | `Cannot create cloudtrail client with following credentials: Access Key: ${ 62 | process.env.AWS_ACCESS_KEY_ID 63 | }, Region: ${ 64 | process.env.AWS_REGION 65 | } Secret Access Key type: ${typeof process.env.AWS_SECRET_ACCESS_KEY}` 66 | ); 67 | } 68 | } 69 | 70 | configureCloudtrailClient(); 71 | 72 | async function getLastEvent() { 73 | try { 74 | const result = await query( 75 | ` 76 | SELECT time 77 | FROM events 78 | ORDER BY time DESC 79 | LIMIT 1; 80 | ` 81 | ); 82 | if (result.rows.length === 0) return; 83 | return new Date(result.rows[0].time); 84 | } catch (error) { 85 | console.warn('Could not get last event!: ' + error); 86 | } 87 | } 88 | 89 | async function updateEvents(next, config = {}) { 90 | // if we haven't received all events from our last call 91 | // continue receiving them 92 | // otherwise, find the most recent event in the database, 93 | // and get any events more recent than that 94 | 95 | if ( 96 | !cloudtrailClient || 97 | !process.env.AWS_ACCESS_KEY_ID || 98 | process.env.AWS_ACCESS_KEY_ID === '' || 99 | !process.env.AWS_SECRET_ACCESS_KEY || 100 | process.env.AWS_SECRET_ACCESS_KEY === '' || 101 | !process.env.AWS_REGION || 102 | process.env.AWS_REGION === '' 103 | ) { 104 | console.log('skipping event fetching because the keys are not set'); 105 | return; 106 | } 107 | 108 | if (!next) { 109 | const startTime = await getLastEvent(); 110 | if (startTime) config.StartTime = startTime; 111 | } 112 | let data; 113 | try { 114 | const command = new LookupEventsCommand(config); 115 | data = await cloudtrailClient.send(command); 116 | } catch (error) { 117 | console.error( 118 | 'eventsModel.updateEvents: LookupEvents error:' + error.message 119 | ); 120 | return; 121 | } 122 | if (!data) return; 123 | for (const event of data.Events) { 124 | const cloudtrailevent = JSON.parse(event.CloudTrailEvent); 125 | try { 126 | await query( 127 | ` 128 | INSERT INTO events ( 129 | _id, 130 | name, 131 | source, 132 | read_only, 133 | username, 134 | accesskey_id, 135 | account_id, 136 | arn, 137 | aws_region, 138 | cipher_suite, 139 | client_provided_host_header, 140 | category, 141 | time, 142 | type, 143 | version, 144 | is_management, 145 | principal_id, 146 | recipient_account_id, 147 | request_id, 148 | source_ip, 149 | tls_version, 150 | user_identity_type, 151 | user_agent 152 | ) 153 | VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 154 | $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) 155 | ON CONFLICT (_id) 156 | DO NOTHING; 157 | `, 158 | [ 159 | event.EventId, 160 | event.EventName, 161 | event.EventSource, 162 | cloudtrailevent.readOnly, 163 | event.Username, 164 | event.AccessKeyId, 165 | cloudtrailevent.userIdentity.accountId, 166 | cloudtrailevent.userIdentity.arn, 167 | cloudtrailevent.awsRegion, 168 | cloudtrailevent.tlsDetails?.cipherSuite || 'NULL', 169 | cloudtrailevent.tlsDetails?.clientProvidedHostHeader || 'NULL', 170 | cloudtrailevent.eventCategory, 171 | event.EventTime.toUTCString(), 172 | cloudtrailevent.eventType, 173 | cloudtrailevent.eventVersion, 174 | cloudtrailevent.managementEvent, 175 | cloudtrailevent.userIdentity.principalId, 176 | cloudtrailevent.recipientAccountId, 177 | cloudtrailevent.requestID, 178 | cloudtrailevent.sourceIPAddress, 179 | cloudtrailevent.tlsDetails?.tlsVersion || 'NULL', 180 | cloudtrailevent.userIdentity.type, 181 | cloudtrailevent.userAgent, 182 | ] 183 | ); 184 | } catch (error) { 185 | console.warn('Could not insert cloudtrailevent: ', event.EventId); 186 | } 187 | } 188 | return { next: data.NextToken, config }; 189 | } 190 | 191 | function repeatUpdate(next, config) { 192 | setTimeout(async () => { 193 | const { new_next, new_config } = updateEvents(next, config); 194 | repeatUpdate(new_next, new_config); 195 | }, 1000 * 10); 196 | } 197 | repeatUpdate(); 198 | -------------------------------------------------------------------------------- /server/models/ipsModel.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | import 'dotenv/config'; 3 | 4 | const pool = new pg.Pool({ 5 | user: 'tgadmin', 6 | password: 'secret', 7 | host: 8 | process.env.NODE_ENV === 'production' 9 | ? 'trailguide-db-prod' 10 | : 'trailguide-db-dev', 11 | port: 5432, 12 | database: process.env.POSTGRES_DB || 'tgdb-dev', 13 | }); 14 | 15 | // if an error is encountered by a client while it sits idle in the pool 16 | // the pool itself will emit an error event with both the error and 17 | // the client which emitted the original error 18 | // this is a rare occurrence but can happen if there is a network partition 19 | // between your application and the database, the database restarts, etc. 20 | // and so you might want to handle it and at least log it out 21 | pool.on('error', function (err, client) { 22 | console.error('idle client error', err.message, err.stack); 23 | }); 24 | 25 | //export the query method for passing queries to the pool 26 | export async function query(text, values) { 27 | // console.log('ipsModel.query:', text.split('\n')[1], values); 28 | return pool.query(text, values); 29 | } 30 | 31 | // the pool also supports checking out a client for 32 | // multiple operations, such as a transaction 33 | export async function connect() { 34 | return pool.connect(); 35 | } 36 | -------------------------------------------------------------------------------- /server/models/usersModel.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | import 'dotenv/config'; 3 | 4 | const pool = new pg.Pool({ 5 | user: 'tgadmin', 6 | password: 'secret', 7 | host: 8 | process.env.NODE_ENV === 'production' 9 | ? 'trailguide-db-prod' 10 | : 'trailguide-db-dev', 11 | port: 5432, 12 | database: process.env.POSTGRES_DB || 'tgdb-dev', 13 | }); 14 | 15 | // if an error is encountered by a client while it sits idle in the pool 16 | // the pool itself will emit an error event with both the error and 17 | // the client which emitted the original error 18 | // this is a rare occurrence but can happen if there is a network partition 19 | // between your application and the database, the database restarts, etc. 20 | // and so you might want to handle it and at least log it out 21 | pool.on('error', function (err, client) { 22 | console.error('idle client error', err.message, err.stack); 23 | }); 24 | 25 | //export the query method for passing queries to the pool 26 | export async function query(text, values) { 27 | // console.log('ipsModel.query:', text.split('\n')[1], values); 28 | return pool.query(text, values); 29 | } 30 | 31 | // the pool also supports checking out a client for 32 | // multiple operations, such as a transaction 33 | export async function connect() { 34 | return pool.connect(); 35 | } 36 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ViteExpress from 'vite-express'; 3 | import userController from './controllers/userController.js'; 4 | import awsController from './controllers/awsController.js'; 5 | import ipLocController from './controllers/ipLocController.js'; 6 | 7 | const PORT = 8080; 8 | 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | app.use(express.urlencoded({ extended: true })); 13 | 14 | //signup router 15 | app.post('/api/signup', userController.createUser, (req, res) => { 16 | res.status(201).json(res.locals.createdUser); 17 | }); 18 | 19 | //login router 20 | app.post('/api/login', userController.loginUser, (req, res) => { 21 | res.status(200).json(res.locals.loggedinuser); 22 | }); 23 | 24 | // route to get all users 25 | // app.get('/api/users', userController.getAllUsers); 26 | 27 | // app.get('/api/user', userController.getUserByField); 28 | 29 | app.get( 30 | '/events', 31 | awsController.getEvents, 32 | awsController.countOn, 33 | ipLocController.injectLocs, 34 | (_req, res) => { 35 | return res.status(200).json(res.locals.events); 36 | } 37 | ); 38 | 39 | app.post( 40 | '/credentials', 41 | awsController.setCredentials, 42 | userController.saveUserAwsCredentials, 43 | (_req, res) => { 44 | return res.status(201).json(res.locals.updatedUser); 45 | } 46 | ); 47 | 48 | app.use((error, _req, res, _next) => { 49 | const DEFAULT_ERROR = { 50 | log: 'An Unkown middleware error occurred', 51 | status: 500, 52 | message: { 53 | err: 'A server error has occurred', 54 | }, 55 | }; 56 | const specificError = { ...DEFAULT_ERROR, ...error }; 57 | console.error(specificError.log); 58 | return res.status(specificError.status).json(specificError.message); 59 | }); 60 | 61 | ViteExpress.listen(app, PORT, async () => { 62 | const { root, base } = await ViteExpress.getViteConfig(); 63 | console.log(`Serving app from root ${root}`); 64 | console.log(`Server is listening at http://localhost:${PORT}${base}`); 65 | console.log(); 66 | console.log( 67 | '>>======================================================================<<' 68 | ); 69 | console.log(); 70 | }); 71 | -------------------------------------------------------------------------------- /server/utils/timeBuckets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bucket means defining how close two times have to be to be considered the 'same' 3 | * The below function would 'bucket' times by hour by 'flooring' it to the nearest hour 4 | */ 5 | 6 | //return a new date object rather than mutating the original time argument. This will prevent accidental side effects 7 | //where other parts of your code may still need the unmodified date. 8 | 9 | /** 10 | * Buckets time by the hour, flooring it to the nearest hour 11 | */ 12 | export function hour(time) { 13 | const newTime = new Date(time); 14 | newTime.setMilliseconds(0); 15 | newTime.setSeconds(0); 16 | newTime.setMinutes(0); 17 | return newTime; 18 | } 19 | 20 | /** 21 | * Buckets time by the day, flooring it to the start of the day (00:00) 22 | */ 23 | export function day(time) { 24 | const newTime = new Date(time); 25 | newTime.setMilliseconds(0); 26 | newTime.setSeconds(0); 27 | newTime.setMinutes(0); 28 | newTime.setHours(0); 29 | return newTime; 30 | } 31 | 32 | /** 33 | * Buckets time by the minute, flooring it to the nearest minute 34 | */ 35 | export function minute(time) { 36 | const newTime = new Date(time); 37 | newTime.setMilliseconds(0); 38 | newTime.setSeconds(0); 39 | return newTime; 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./client/tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["./vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | /** @type {import('vite').UserConfig} */ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | root: './client', 9 | build: { 10 | outDir: '../dist', 11 | emptyOutDir: true, 12 | rollupOptions: { 13 | output: { 14 | manualChunks(id) { 15 | if (id.includes('node_modules')) { 16 | return id 17 | .toString() 18 | .split('node_modules/')[1] 19 | .split('/')[0] 20 | .toString(); 21 | } 22 | }, 23 | }, 24 | }, 25 | }, 26 | }); 27 | --------------------------------------------------------------------------------