Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions backend/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM rust:1.92.0

ENV PATH="/usr/local/cargo/bin:${PATH}"

RUN apt-get update && apt-get install -y \
libssl-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*

RUN cargo install cargo-watch
RUN cargo install sqlx-cli --no-default-features --features postgres

WORKDIR /app/backend/server

# Cache Rust dependencies at image build time for faster first run.
# The actual source code is bind-mounted by docker-compose for live reload.
COPY server/Cargo.toml server/Cargo.lock ./
RUN mkdir -p src && \
printf 'fn main() {}\n' > src/main.rs && \
cargo build && \
rm -rf src

EXPOSE 8080

94 changes: 94 additions & 0 deletions backend/README2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# CHAOS Backend

CHAOS' backend is implemented in Rust and for data persistence, we use PostgreSQL.

## Table of Contents

- [Dev Setup](#dev-setup)
- [Code Structure](#code-structure)
- [Tech Stack](#tech-stack)


## Dev Setup

### Backend Development
To run the backend in a dev/testing environment:
0. Contact your director before you do the set up.

1. Install `docker-compose` (see [official installation guide](https://docs.docker.com/compose/install/)).

2. Navigate to the directory this file is in (`backend`) in your terminal (not `backend/server`).

3. Possibly terminate any running instances of postgres, as the dockerized postgres we will spawn uses the same default port, so the two might interefere with each other.

4. If you are using WSL/Linux, install the OpenSSL development package with `sudo apt install libssl-dev`.

5. Run `./setup-dev-env.sh` (you might have to make it executable before with `chmod +x setup-dev-env.sh`), which should check if you have the correct docker setup and run the db-instance and do db setup and migrations. Then, it will also start your BE and FE container, so you could work on the project ASAP.

6 (DB Seeding IMPORTANT). To set up your first data for development, please get in `./setup-admin-db-dev.sh` and change GMAIL to your personal gmail. What it does is basically setup your personal email as a SUPERUSER ROLE, giving you access to all pages (no restriction).
After that, run `./setup-admin-db-dev.sh` (you might have to make it executable before with `chmod +x setup-admin-db-dev.sh`).

7. To run the FE and BE, you can either run it locally or using the docker:
- Local:
at `backend/server`: do `cargo run build` to run the backend server.
at `frontend-nextjs`: do `bun run dev` to run the frontend.
- Docker:
at the root `chaos/` folder,
do `docker compose -f docker-compose.local.yml up -d frontend backend`
do `docker compose -f docker-compose.local.yml up -d` to run all 3 (db, FE and BE)
or
If you want to run those seperately:
do `docker compose -f docker-compose.local.yml up -d backend`
do `docker compose -f docker-compose.local.yml up -d frontend`

7 (Optional). If you want to clear the database, an empty database to restart maybe, run `./reset-db-dev.sh` (you might have to make it executable before with `chmod +x ./reset-db-dev.sh`).


### Authentication
Some routes are only accessible by Users/Admins/SuperAdmins. To login your browser with a respective User/Admin/SuperAdmin cookie, seed your database as above (step 6), and then call one of the following routes in your browser:
- **Normal User:** `/api/v1/dev/user_login`
- **Organisation Admin User:** `/api/v1/dev/org_admin_login`
- **Super Admin User:** `/api/v1/dev/super_admin_login`

## Code Structure

### Handler
The handler module takes care of request handling. It implements the framework or library we are using, invokes the
service functions and responds via HTTP with their return values.

### Middleware
The middleware module contains middlewares, functions that run before or after the function handlers. A common use case
is authorization, where middleware is used to find the userId from the user's token.

### Models
Models are Rust structs that represent the data. There must be a struct for each table in the database, as well as a
struct to describe the fully joined data. E.g. A campaign struct with a array of questions, even though questions are
stored as rows in a separate table. These models implement all functions that conduct business logic on the respective
entity, and also interact with the database. This separation from request handling makes it easy to swap out any new
form of requests, but reuse the same logic functions.

### Service
The service module contains all helper functions. For example, functions for determining a user's authorization to
mutate an object are defined here.

#### Request Path
Request -> Middleware (optional) -> Handler -> Service -> Middleware (Optional) -> Response


## Tech Stack

### Web Server
- [Axum](https://github.com/tokio-rs/axum)

### Persistence
- [SQLx](https://github.com/launchbadge/sqlx) - Queries and Migrations
- PostgreSQL

### AuthN
- OAuth 2 (Google)

### AuthZ
- JWT

### Storage
- Object storage
13 changes: 13 additions & 0 deletions backend/reset-db-dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

# THIS SCRIPT ASSUMES THAT THE DB CONTAINER IS RUNNING.

# Drop the database, create it, and run the migrations.
echo "Dropping the database..."
sqlx db drop -f || exit 1
echo "Creating the database..."
sqlx db create || exit 1
echo "Running the migrations..."
sqlx migrate run || exit 1

echo "\nDatabase reset successfully!\n"
17 changes: 17 additions & 0 deletions backend/setup-admin-db-dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh

# THIS SCRIPT ASSUMES THAT THE DB CONTAINER IS RUNNING.

# Please run ./setup-dev-env.sh first. Before running this script.
this_script_dir="$(dirname "$(realpath "$0")")"
repo_root="$(realpath "$this_script_dir/..")"
working_dir="$repo_root/backend/database-seeding"
cd "$working_dir"
echo "Working directory: $working_dir"

# IMPORTANT: Replace YOUR_GMAIL with your account gmail.
# Run the database seeding script to set up the admin account.
# Like e.g. cargo run -- --email peter@gmail.com. Replace YOUR_GMAIL with your account gmail.
cargo run -- --email "vietthanh01202@gmail.com"

echo "\nAdmin account set up successfully!\n"
48 changes: 37 additions & 11 deletions backend/setup-dev-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
tmp_env_file="$(mktemp)"
trap 'rm -rf "$tmp_env_file"' EXIT INT TERM
cat << 'EOF' > "$tmp_env_file"
DATABASE_URL="postgres://user:password@localhost:5432/chaos"
DATABASE_URL="postgres://postgres:password@localhost:5432/chaos"
JWT_SECRET="test_secret"
GOOGLE_CLIENT_ID="test"
GOOGLE_CLIENT_SECRET="test"
Expand All @@ -26,13 +26,23 @@ SMTP_HOST="smtp.example.com"
EOF

# Check the user has all required tools installed.
for cmd in "which cargo" "which docker && docker info" "which docker-compose || docker compose"; do
for cmd in "which cargo" "which docker && docker info"; do
if ! eval "$cmd" 1>/dev/null 2>&1; then
echo "The command '$cmd' failed, indicating you might not have that tool installed." >&2
exit 1
fi
done

# Check if docker is available on the machine. Please install docker if don't have it.
if docker compose version >/dev/null 2>&1; then
compose_cmd="docker compose"
elif which docker-compose >/dev/null 2>&1; then
compose_cmd="docker-compose"
else
echo "Neither 'docker compose' nor 'docker-compose' is available. Please install Docker and try again." >&2
exit 1
fi

# Create .env file.
env_file=.env

Expand Down Expand Up @@ -64,22 +74,38 @@ fi

# Run postgres database in the background.
this_script_dir="$(dirname "$(realpath "$0")")"
docker_compose_file_name="setup-test-database.yml"
docker_compose_file_path="$this_script_dir/$docker_compose_file_name"
repo_root="$(realpath "$this_script_dir/..")"
docker_compose_file_path="$repo_root/docker-compose.local.yml"

echo 'Starting up postgres db container in docker-compose.local.yml'
# Run the db container only using docker compose.
$compose_cmd -f "$docker_compose_file_path" up --detach db || exit 1

echo 'Starting up postgres database in docker'
docker-compose -f "$docker_compose_file_path" up --detach || exit 1
echo "Waiting for db container to be ready"
# Wait for 30 seconds for the db container to be ready.
max_attempts=30
attempt=1

# Ensure the docker container gets killed once this script exits.
trap 'echo "shutting down $docker_compose_file_path" && docker-compose -f "$docker_compose_file_path" down' EXIT INT TERM
while [ "$attempt" -le "$max_attempts" ]; do
if PGPASSWORD=password psql -h localhost -p 5432 -U postgres -d postgres -c "SELECT 1" >/dev/null 2>&1; then
break
fi
echo "Database not ready yet (attempt $attempt/$max_attempts), retrying..."
sleep 2
attempt=$((attempt + 1))
done

# Wait for the database to be ready.
echo "Waiting for database to be ready"
sleep 3
if [ "$attempt" -gt "$max_attempts" ]; then
echo "Database did not become ready in time." >&2
exit 1
fi

# Setup sqlx.
echo "Setting up sqlx"
sqlx database create || exit 1
sqlx migrate run || exit 1

echo "\nDev environment setup complete! Only the db container is running. You can start the BE and FE containers now.\n"
echo "Don't forget to run ./setup-admin-db-dev.sh to set up the admin account. \n"

"$SHELL"
17 changes: 0 additions & 17 deletions backend/setup-test-database.yml

This file was deleted.

79 changes: 79 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
services:
# db
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_DB:-chaos}
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- chaos_postgres-data:/var/lib/postgresql/data
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-chaos}",
]
interval: 10s
timeout: 5s
retries: 10

# server
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
depends_on:
db:
condition: service_healthy
env_file:
- ./backend/.env
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-chaos}
ports:
- "${BACKEND_PORT:-8080}:8080"
volumes:
- ./backend:/app/backend:cached
- cargo-registry:/usr/local/cargo/registry
- cargo-git:/usr/local/cargo/git
- backend-target:/app/backend/target
working_dir: /app/backend/server
command: sh -c "cd /app/backend && /usr/local/cargo/bin/sqlx migrate run && cd server && cargo watch -x run"

# next.js-frontend
frontend:
build:
context: ./frontend-nextjs
dockerfile: Dockerfile.dev
depends_on:
- backend
env_file:
- ./frontend-nextjs/.env
environment:
NEXT_API_BASE_URL: http://backend:8080
NEXT_PUBLIC_API_BASE_URL: http://localhost:${BACKEND_PORT:-8080}
NEXT_PUBLIC_APP_URL: http://localhost:${FRONTEND_PORT:-3000}/
ports:
- "${FRONTEND_PORT:-3000}:3000"
volumes:
- ./frontend-nextjs:/app:cached
- frontend-node-modules:/app/node_modules
- frontend-next-cache:/app/.next
# Save time rebuilding the frontend if the node_modules directory is already present.
command:
sh -lc "if [ ! -d node_modules ] || [ -z \"$(ls -A node_modules 2>/dev/null)\" ];
then bun install; fi;
bun run dev -- -H 0.0.0.0 -p 3000"

volumes:
chaos_postgres-data:
external: true
cargo-registry:
cargo-git:
backend-target:
frontend-node-modules:
frontend-next-cache:

10 changes: 10 additions & 0 deletions frontend-nextjs/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM oven/bun:latest

WORKDIR /app

# Pre-install deps for faster first run; source will be bind-mounted in compose.
COPY package.json bun.lock ./
RUN bun install

EXPOSE 3000

9 changes: 8 additions & 1 deletion frontend-nextjs/src/app/[lang]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ export default async function Login({
}) {
const to = (await searchParams).to ?? "/dashboard";

redirect(`${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/google?to=${encodeURIComponent(to)}`)
const raw = process.env.NEXT_PUBLIC_API_BASE_URL;
let apiBase =
raw && String(raw).startsWith("http") ? raw : "http://localhost:8080/";

apiBase = apiBase.endsWith("/") ? apiBase : `${apiBase}/`; // add a trailing slash if not present

redirect(`${apiBase}auth/google?to=${encodeURIComponent(to)}`);

}
7 changes: 5 additions & 2 deletions frontend-nextjs/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://chaos-api.devsoc.app";

const isServer = typeof window === "undefined";
const API_BASE_URL = isServer
? process.env.NEXT_API_BASE_URL ||
process.env.NEXT_PUBLIC_API_BASE_URL ||
"http://backend:8080"
: process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080";

export class ApiError extends Error {
status: number;
Expand Down
Loading