Skip to content

Commit a428a91

Browse files
authored
Merge pull request #30 from CS3219-AY2425S1/titus/setup-docker-compose
feat: setup docker compose
2 parents ab8dd37 + e679591 commit a428a91

File tree

8 files changed

+193
-15
lines changed

8 files changed

+193
-15
lines changed

apps/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# PeerPrep Docker Compose Guide
2+
3+
This project uses Docker Compose to manage multiple services such as a frontend, backend, and a database. The configuration is defined in the `docker-compose.yml` file, and environment variables can be stored in environment files for different environments (e.g., development, production).
4+
5+
## Prerequisites
6+
7+
Before you begin, ensure you have the following installed on your machine:
8+
9+
- [Docker](https://www.docker.com/get-started)
10+
- [Docker Compose](https://docs.docker.com/compose/install/)
11+
12+
## Project Structure
13+
14+
In the `./apps` directory:
15+
16+
```plaintext
17+
.
18+
├── docker-compose.yml # Docker Compose configuration
19+
├── .env # Global environment variables (optional)
20+
├── frontend
21+
│ ├── Dockerfile # Dockerfile for frontend
22+
│ └── ... (other frontend files)
23+
├── question-service
24+
│ ├── Dockerfile # Dockerfile for question-service
25+
│ └── ... (other question-service files)
26+
├── user-service
27+
│ ├── Dockerfile # Dockerfile for user-service
28+
│ └── ... (other user-service files)
29+
└── README.md # Project documentation (for docker compose)
30+
```
31+
32+
## Docker Compose Setup
33+
34+
By using multiple Dockerfiles in Docker Compose, we can manage complex multi-container applications where each service has its own environment and build process.
35+
36+
1. Build and Start the Application
37+
38+
To build and run both the frontend and backend services, you can change your directory to the `./apps` directory and run:
39+
40+
```bash
41+
docker-compose up --build
42+
```
43+
44+
This will:
45+
46+
- Build the Docker images for all services using the specified Dockerfiles
47+
- Start the containers and map the defined ports
48+
49+
2. Access the Application
50+
51+
Once running, you can access:
52+
53+
- The **frontend** at http://localhost:3000
54+
- The **user service** at http://localhost:3001
55+
- The **question service** at http://localhost:8080
56+
57+
3. Stopping Services
58+
59+
To stop the running services, run:
60+
61+
```bash
62+
docker-compose down
63+
```
64+
65+
This command will stop and remove the containers, networks, and volumes created by docker-compose up.
66+
67+
## Troubleshooting
68+
69+
**Common Issues**
70+
71+
- Port Conflicts: If you encounter port conflicts, ensure the host ports specified in docker-compose.yml (e.g., 3000:3000) are not in use by other applications.
72+
- Environment Variables Not Loaded: Ensure the `.env` files are in the correct directories as found in the `docker-compose.yml` file.
73+
74+
**Logs**
75+
76+
You can view the logs for each service using the following command:
77+
78+
```bash
79+
docker-compose logs
80+
```
81+
82+
**Useful Commands**
83+
84+
Rebuild a specific service:
85+
86+
```bash
87+
docker-compose build <service_name>
88+
```
89+
90+
Start services in detached mode (run in the background):
91+
92+
```bash
93+
docker-compose up -d
94+
```
95+
96+
Remove all containers, networks, and volumes created by Docker Compose:
97+
98+
```bash
99+
docker-compose down --volumes
100+
```

apps/docker-compose.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
services:
2+
frontend:
3+
build:
4+
context: ./frontend
5+
dockerfile: Dockerfile
6+
ports:
7+
- 3000:3000
8+
networks:
9+
- apps_network
10+
env_file:
11+
- ./frontend/.env
12+
volumes:
13+
- ./frontend:/frontend
14+
15+
user-service:
16+
build:
17+
context: ./user-service
18+
dockerfile: Dockerfile
19+
args:
20+
DB_CLOUD_URI: ${DB_CLOUD_URI}
21+
JWT_SECRET: ${JWT_SECRET}
22+
ports:
23+
- 3001:3001
24+
networks:
25+
- apps_network
26+
env_file:
27+
- ./user-service/.env
28+
volumes:
29+
- ./user-service:/user-service
30+
31+
question-service:
32+
build:
33+
context: ./question-service
34+
dockerfile: Dockerfile
35+
ports:
36+
- 8080:8080
37+
env_file:
38+
- ./question-service/.env
39+
networks:
40+
- apps_network
41+
volumes:
42+
- ./question-service:/question-service
43+
44+
networks:
45+
apps_network:

apps/frontend/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Replace with the corresponding url, credentials and endpoint
2-
NEXT_PUBLIC_API_URL="http://localhost:8080/"
2+
NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/"
33
NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/"

apps/frontend/Dockerfile

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
FROM node:18-alpine AS base
22

3-
# For now we can specify it here, else we can use --build-arg in our docker build command
4-
ARG NEXT_PUBLIC_API_URL
5-
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
6-
73
# Install dependencies only when needed
84
FROM base AS deps
95
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.

apps/frontend/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pnpm install
2323
Then, follow the `.env.example` file and create a `.env` file in the current directory. Replace the necessary values within.
2424

2525
```bash
26-
NEXT_PUBLIC_API_URL=http://localhost:8080
26+
NEXT_PUBLIC_QUESTION_SERVICE_URL=http://localhost:8080
2727
```
2828

2929
First, run the development server:
@@ -42,7 +42,7 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update
4242
# Navigate to the frontend app directory
4343
cd apps/frontend
4444
# Build dockerfile (Ensure that your docker daemon is running beforehand)
45-
docker build -t frontend --build-arg NEXT_PUBLIC_API_URL="http://localhost:8080/" -f Dockerfile .
45+
docker build -t frontend -e NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/" -f Dockerfile .
4646
```
4747

4848
Run the backend server locally and visit http://localhost:3000/ to see the frontend application working

apps/frontend/src/app/services/question.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { getToken } from "./login-store";
2+
3+
const QUESTION_SERVICE_URL = process.env.NEXT_PUBLIC_QUESTION_SERVICE_URL;
4+
15
export interface Question {
26
id: number;
37
docRefId: string;
@@ -37,7 +41,7 @@ export const GetQuestions = async (
3741
categories?: string[],
3842
title?: string
3943
): Promise<QuestionListResponse> => {
40-
let query_url = `${process.env.NEXT_PUBLIC_API_URL}questions`;
44+
let query_url = `${QUESTION_SERVICE_URL}questions`;
4145
let query_params = "";
4246

4347
if (currentPage) {
@@ -73,15 +77,26 @@ export const GetQuestions = async (
7377
}
7478

7579
query_url += query_params;
76-
const response = await fetch(query_url);
80+
const response = await fetch(query_url, {
81+
method: "GET",
82+
headers: {
83+
'Authorization': `Bearer ${getToken()}`,
84+
}
85+
});
7786
const data = await response.json();
7887
return data;
7988
};
8089

8190
// Get single question
8291
export const GetSingleQuestion = async (docRef: string): Promise<Question> => {
8392
const response = await fetch(
84-
`${process.env.NEXT_PUBLIC_API_URL}questions/${docRef}`
93+
`${QUESTION_SERVICE_URL}questions/${docRef}`,
94+
{
95+
method: "GET",
96+
headers: {
97+
'Authorization': `Bearer ${getToken()}`,
98+
}
99+
}
85100
);
86101
const data = await response.json();
87102
return data;
@@ -91,10 +106,11 @@ export const GetSingleQuestion = async (docRef: string): Promise<Question> => {
91106
export const CreateQuestion = async (
92107
question: NewQuestion
93108
): Promise<Question> => {
94-
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}questions`, {
109+
const response = await fetch(`${QUESTION_SERVICE_URL}questions`, {
95110
method: "POST",
96111
headers: {
97112
"Content-Type": "application/json",
113+
'Authorization': `Bearer ${getToken()}`,
98114
},
99115
body: JSON.stringify(question),
100116
});
@@ -113,11 +129,12 @@ export const EditQuestion = async (
113129
docRefId: string
114130
): Promise<Question> => {
115131
const response = await fetch(
116-
`${process.env.NEXT_PUBLIC_API_URL}questions/${docRefId}`,
132+
`${QUESTION_SERVICE_URL}questions/${docRefId}`,
117133
{
118134
method: "PUT",
119135
headers: {
120136
"Content-Type": "application/json",
137+
'Authorization': `Bearer ${getToken()}`,
121138
},
122139
body: JSON.stringify(question),
123140
}
@@ -135,9 +152,12 @@ export const EditQuestion = async (
135152
// Delete single question (TODO: Ryan)
136153
export async function DeleteQuestion(docRef: String): Promise<void> {
137154
const res = await fetch(
138-
`${process.env.NEXT_PUBLIC_API_URL}questions/${docRef}`,
155+
`${QUESTION_SERVICE_URL}questions/${docRef}`,
139156
{
140157
method: "DELETE",
158+
headers: {
159+
'Authorization': `Bearer ${getToken()}`,
160+
},
141161
}
142162
);
143163
// error handling later

apps/question-service/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
FIREBASE_CREDENTIAL_PATH=cs3219-g24-firebase-adminsdk-9cm7h-b1675603ab.json
2+
3+
# Secret for creating JWT signature
4+
JWT_SECRET=you-can-replace-this-with-your-own-secret

apps/question-service/middleware/basic-access-control.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package middleware
22

33
import (
4-
"github.com/golang-jwt/jwt/v4"
4+
"log"
55
"net/http"
66
"os"
77
"strings"
8+
9+
"github.com/golang-jwt/jwt/v4"
810
)
911

1012
func VerifyJWT(next http.Handler) http.Handler {
1113
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
// Skip JWT Verification for OPTIONS Requests
15+
if r.Method == "OPTIONS" {
16+
next.ServeHTTP(w, r)
17+
return
18+
}
19+
1220
// Get the token from the Authorization header
1321
authHeader := r.Header.Get("Authorization")
1422
if authHeader == "" {
@@ -17,7 +25,12 @@ func VerifyJWT(next http.Handler) http.Handler {
1725
}
1826

1927
// Split the header to get the token
20-
tokenString := strings.Split(authHeader, " ")[1]
28+
parts := strings.Split(authHeader, " ")
29+
if len(parts) != 2 || parts[0] != "Bearer" {
30+
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
31+
return
32+
}
33+
tokenString := parts[1]
2134

2235
// Retrieve the JWT secret from environment variables
2336
jwtSecret := []byte(os.Getenv("JWT_SECRET"))
@@ -36,6 +49,7 @@ func VerifyJWT(next http.Handler) http.Handler {
3649

3750
if err != nil || !token.Valid {
3851
http.Error(w, "Invalid token", http.StatusUnauthorized)
52+
log.Printf("Token parse error: %v", err)
3953
return
4054
}
4155

0 commit comments

Comments
 (0)