├── .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 |
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 |
27 |
28 | 2. Login
29 |
30 |
31 |
32 | 3. Copy paste the aws credentials in the fields in the profile
33 |
34 |
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 |
onViewDetails(event)}>View Details
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 |
27 | X
28 |
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 |
61 | Close
62 |
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 |
45 |
46 |
47 |
48 |
49 |
50 | RECENT EVENTS
51 |
52 |
53 | {isDarkMode ? 'LIGHT MODE' : 'DARK MODE'}
54 |
55 |
56 |
62 | {username && typeof username === 'string'
63 | ? username.toUpperCase()
64 | : 'USER'}
65 |
66 |
67 | {dropdownOpen && (
68 |
69 |
70 | Profile
71 |
72 | {!username && (
73 |
74 | Login
75 |
76 | )}
77 | {username && (
78 |
79 | Logout
80 |
81 | )}
82 |
83 | )}
84 |
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 |
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 |
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 |
71 |
72 |
73 |
74 | Enter Access Key
75 |
76 |
77 |
78 | Enter Secret Access Key
79 |
80 |
81 |
82 | Enter Region
83 |
84 |
85 |
86 | Submit
87 |
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 | Select an Option
115 |
120 |
121 |
122 | Toggle Feature
123 |
124 |
125 |
126 |
Radio Options
127 |
128 |
134 | Radio 1
135 |
136 |
137 |
143 | Radio 2
144 |
145 |
146 |
147 |
Checkbox Options
148 |
149 |
155 | Checkbox 1
156 |
157 |
158 |
164 | Checkbox 2
165 |
166 |
167 |
168 | Pick a Date
169 |
170 |
171 |
172 | Tag Selector
173 |
179 |
180 |
181 | Enter a Number
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 |
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 |
--------------------------------------------------------------------------------