Skip to content

Commit 738fa33

Browse files
committed
Adding content
1 parent e3147a2 commit 738fa33

File tree

29 files changed

+1588
-0
lines changed

29 files changed

+1588
-0
lines changed

.github/workflows/deploy_api.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: Build, Push, and Deploy
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- 'api/**'
8+
- '.github/workflows/deploy_api.yml'
9+
10+
jobs:
11+
deploy-backend:
12+
runs-on: ubuntu-latest
13+
14+
env:
15+
STACK_NAME: ''
16+
AWS_REGION: ''
17+
AWS_ACCOUNT_ID: ''
18+
REPO_NAME: ''
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set ECR repository URI
25+
run: |
26+
echo "ECR_REPOSITORY=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$REPO_NAME" >> $GITHUB_ENV
27+
28+
- name: Configure AWS credentials
29+
uses: aws-actions/configure-aws-credentials@v1
30+
with:
31+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
32+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
33+
aws-region: ${{ env.AWS_REGION }}
34+
35+
- name: Login to Amazon ECR
36+
id: login-ecr
37+
uses: aws-actions/amazon-ecr-login@v2
38+
39+
- name: Navigate to the project directory
40+
run: cd backend
41+
42+
- name: Create ECR repository if it doesn't exist
43+
run: |
44+
if ! aws ecr describe-repositories --repository-names $REPO_NAME 2>/dev/null; then
45+
echo "Creating ECR repository $REPO_NAME"
46+
aws ecr create-repository --repository-name $REPO_NAME
47+
fi
48+
49+
- name: Build Docker image
50+
run: |
51+
cd backend
52+
docker build -t $STACK_NAME .
53+
docker tag $STACK_NAME $ECR_REPOSITORY:latest
54+
55+
- name: Push Docker image to ECR
56+
run: |
57+
cd backend
58+
# Push the image
59+
docker push $ECR_REPOSITORY:latest
60+
61+
# Get image digest reliably using AWS CLI
62+
echo "IMAGE_DIGEST=$(aws ecr describe-images --repository-name $REPO_NAME --image-ids imageTag=latest --query 'imageDetails[0].imageDigest' --output text)" >> $GITHUB_ENV
63+
64+
- name: Deploy to AWS CloudFormation
65+
run: |
66+
cd backend
67+
echo "Deploying stack $STACK_NAME to region $AWS_REGION with image digest $IMAGE_DIGEST"
68+
aws cloudformation deploy \
69+
--capabilities CAPABILITY_IAM \
70+
--stack-name $STACK_NAME \
71+
--template-file template.yaml \
72+
--region $AWS_REGION \
73+
--parameter-overrides \
74+
ImageUri="$ECR_REPOSITORY@$IMAGE_DIGEST" \
75+
MongoUsername="${{ secrets.MONGO_USERNAME }}" \
76+
MongoPassword="${{ secrets.MONGO_PASSWORD }}" \
77+
MongoHost="${{ secrets.MONGO_HOST }}" \
78+
MongoDbName="${{ secrets.MONGO_DB_NAME }}"
79+
80+
81+
- name: Get API Gateway URL
82+
run: |
83+
echo "API Gateway URL:"
84+
aws cloudformation describe-stacks --stack-name $STACK_NAME --query "Stacks[0].Outputs[?ExportName=='$STACK_NAME-ApiEndpoint'].OutputValue" --output text

api/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Cloudformation
2+
STACK_NAME=
3+
AWS_REGION=
4+
AWS_ACCOUNT_ID=
5+
REPO_NAME=
6+
7+
#MongoDB
8+
MONGO_HOST=cluster0.mongodb.net
9+
MONGO_USERNAME=
10+
MONGO_PASSWORD=
11+
MONGO_DB_NAME=

api/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
__pycache__
2+
app.egg-info
3+
*.pyc
4+
.mypy_cache
5+
.coverage
6+
htmlcov
7+
.cache
8+
.venv
9+
.env

api/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM public.ecr.aws/lambda/python:3.12
2+
3+
# Install Poetry using official installer
4+
RUN curl -sSL https://install.python-poetry.org | python3 -
5+
6+
# Add Poetry to PATH
7+
ENV PATH="/root/.local/bin:$PATH"
8+
9+
# Copy Poetry configuration files
10+
COPY pyproject.toml .
11+
COPY poetry.lock .
12+
13+
# Install dependencies
14+
RUN poetry config virtualenvs.create false && \
15+
poetry install --no-interaction --no-ansi --no-root
16+
17+
# Copy the application code
18+
COPY app/ ./app/
19+
20+
# Set entrypoint
21+
CMD [ "app.main.handler" ]

api/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Backend Developer Guide
2+
3+
This backend is built with **FastAPI**, **Pydantic**, **MongoDB** (via `pymongo`), and managed using **Poetry**. It serves as the API and data layer for the project.
4+
5+
## 🛠️ Tech Stack
6+
7+
- **FastAPI**: Web framework for building APIs
8+
- **Pydantic**: Data validation and settings management
9+
- **MongoDB**: NoSQL database
10+
- **pymongo**: MongoDB driver for Python
11+
- **Uvicorn**: ASGI server for running FastAPI
12+
- **Poetry**: Dependency and environment management
13+
14+
## 🚀 Getting Started
15+
16+
### 1. Install Dependencies
17+
18+
Make sure you have [Poetry](https://python-poetry.org/docs/#installation) installed.
19+
20+
```sh
21+
cd backend
22+
poetry install
23+
```
24+
25+
### 2. Environment Variables
26+
27+
Copy the example environment file and fill in your MongoDB credentials:
28+
29+
```sh
30+
cp .env.example .env
31+
# Edit .env with your MongoDB details
32+
```
33+
34+
### 3. Run the local Development Server
35+
36+
Use the provided script to start the server with hot-reload:
37+
38+
```sh
39+
sh scripts/start_server.sh
40+
```
41+
42+
### 4. Project Structure
43+
44+
```
45+
app/
46+
├── main.py # FastAPI app entrypoint
47+
├── api/ # API routers
48+
├── models/ # Pydantic models
49+
├── database/ # MongoDB drivers and connection logic
50+
├── core/ # Configuration and settings
51+
```
52+

api/app/api/deps.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Annotated
2+
3+
import jwt
4+
from fastapi import Depends, Request, status
5+
from fastapi.exceptions import HTTPException
6+
from fastapi.security import OAuth2PasswordBearer
7+
from jwt.exceptions import InvalidTokenError
8+
from pydantic import ValidationError
9+
10+
from app.core.config import settings
11+
from app.core.security import ALGORITHM
12+
from app.database import Database
13+
from app.models.security import TokenPayload
14+
from app.models.users import User
15+
16+
17+
async def get_db(request: Request) -> Database:
18+
return request.app.state.database
19+
20+
21+
get_auth = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/token")
22+
23+
DatabaseDep = Annotated[Database, Depends(get_db)]
24+
TokenDep = Annotated[str, Depends(get_auth)]
25+
26+
27+
async def get_current_user(db: DatabaseDep, token: TokenDep) -> User:
28+
"""
29+
By decoding the JWT token and extracting the user ID,
30+
we can retrieve the user from the database.
31+
If the token is invalid or the user does not exist, an HTTPException is raised.
32+
"""
33+
try:
34+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
35+
token_data = TokenPayload(**payload)
36+
except (InvalidTokenError, ValidationError):
37+
raise HTTPException(
38+
status_code=status.HTTP_403_FORBIDDEN,
39+
detail="Could not validate credentials",
40+
)
41+
user = await db.users.read(token_data.sub)
42+
if not user:
43+
raise HTTPException(status_code=404, detail="User not found")
44+
return user
45+
46+
47+
CurrentUserDep = Annotated[User, Depends(get_current_user)]

api/app/api/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fastapi import APIRouter
2+
3+
from app.api.routes import auth_router, root_router, users_router
4+
from app.core.config import settings
5+
6+
api_router = APIRouter()
7+
api_router.include_router(auth_router)
8+
api_router.include_router(users_router)
9+
api_router.include_router(root_router)
10+
11+
12+
if settings.ENVIRONMENT == "local":
13+
# Possilbe to add local only routers
14+
pass

api/app/api/routes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from app.api.routes.auth import router as auth_router
2+
from app.api.routes.root import router as root_router
3+
from app.api.routes.users import router as users_router

api/app/api/routes/auth.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
from fastapi.security import OAuth2PasswordRequestForm
5+
6+
from app.api.deps import DatabaseDep
7+
from app.core.config import settings
8+
from app.core.security import create_access_token, verify_password
9+
from app.models.security import Token
10+
11+
router = APIRouter(prefix="/auth", tags=["authentication"])
12+
13+
14+
@router.post("/token")
15+
async def login(
16+
db: DatabaseDep, data: Annotated[OAuth2PasswordRequestForm, Depends()]
17+
) -> Token:
18+
"""
19+
Authenticate a user and return an access token.
20+
"""
21+
found_users = await db.users.query({"username": data.username})
22+
23+
if len(found_users) == 0:
24+
raise HTTPException(status_code=400, detail="Incorrect username or password")
25+
else:
26+
user = found_users[0]
27+
28+
if not verify_password(data.password, user.password):
29+
raise HTTPException(status_code=400, detail="Incorrect username or password")
30+
31+
access_token = create_access_token(
32+
subject=user.id, expires_delta=settings.ACCESS_TOKEN_EXPIRE_DELTA
33+
)
34+
35+
return Token(access_token=access_token, token_type="bearer")

api/app/api/routes/root.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Any
2+
3+
from fastapi import APIRouter, HTTPException
4+
5+
from app.api.deps import DatabaseDep
6+
7+
router = APIRouter()
8+
9+
10+
@router.get("/")
11+
def ping() -> Any:
12+
"""
13+
Retrieve users.
14+
"""
15+
return "pong"
16+
17+
18+
@router.get("/mongodb")
19+
async def ping_mongodb(db: DatabaseDep) -> Any:
20+
"""
21+
Retrieve users.
22+
"""
23+
try:
24+
return await db.ping()
25+
except Exception as e:
26+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)