Skip to content

Commit 9cbfc29

Browse files
committed
Initial project setup with backend, frontend, and CI/CD configuration
- Added FastAPI backend with authentication, user management, and database services - Created React frontend with authentication, dashboard, and admin pages - Configured Docker and docker-compose for development and testing - Set up GitHub Actions for automated testing - Added comprehensive test suites for backend and frontend - Implemented CI/CD workflow with pytest and Jest - Created configuration files for various environments
0 parents  commit 9cbfc29

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+21733
-0
lines changed

.github/workflows/tests.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
frontend-tests:
11+
runs-on: ubuntu-latest
12+
defaults:
13+
run:
14+
working-directory: ./frontend
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '18'
23+
cache: 'npm'
24+
cache-dependency-path: './frontend/package-lock.json'
25+
26+
- name: Install frontend dependencies
27+
run: npm ci
28+
29+
- name: Run frontend tests
30+
run: npm test -- --watchAll=false
31+
32+
backend-tests:
33+
runs-on: ubuntu-latest
34+
defaults:
35+
run:
36+
working-directory: .
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- name: Set up Python
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: '3.11'
45+
cache: 'pip'
46+
47+
- name: Install backend dependencies
48+
run: |
49+
python -m pip install --upgrade pip
50+
pip install -r requirements.txt
51+
pip install -r requirements-test.txt
52+
53+
- name: Run backend tests
54+
run: pytest tests/

.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Node.js dependencies
2+
**/node_modules/
3+
4+
# Logs
5+
npm-debug.log*
6+
yarn-debug.log*
7+
yarn-error.log*
8+
9+
# Environment variables
10+
.env
11+
.env.local
12+
.env.*.local
13+
14+
# Build outputs
15+
**/dist/
16+
**/build/
17+
18+
# Python
19+
__pycache__/
20+
*.py[cod]
21+
*$py.class
22+
.pytest_cache/
23+
.coverage
24+
htmlcov/
25+
.tox/
26+
.venv/
27+
venv/
28+
ENV/
29+
30+
# IDE and editor files
31+
.idea/
32+
.vscode/
33+
*.swp
34+
*.swo
35+
36+
# OS generated files
37+
.DS_Store
38+
.DS_Store?
39+
._*
40+
.Spotlight-V100
41+
.Trashes
42+
ehthumbs.db
43+
Thumbs.db

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM python:3.9-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY app app/
9+
10+
# Create a script to initialize the database and start the server
11+
COPY scripts/start.sh /start.sh
12+
RUN chmod +x /start.sh
13+
14+
CMD ["/start.sh"]

Dockerfile.test

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM python:3.9-slim
2+
3+
WORKDIR /app
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
gcc \
8+
&& rm -rf /var/lib/apt/lists/*
9+
10+
# Copy requirements files
11+
COPY requirements.txt .
12+
COPY requirements-test.txt .
13+
14+
# Install Python dependencies
15+
RUN pip install --no-cache-dir -r requirements.txt -r requirements-test.txt
16+
17+
# Copy application code and tests
18+
COPY app/ app/
19+
COPY tests/ tests/
20+
21+
# Set environment variables for testing
22+
ENV PYTHONPATH=/app
23+
ENV TESTING=1
24+
25+
# Command to run tests
26+
CMD ["pytest", "-v", "tests/"]

Makefile

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
.PHONY: test test-build test-clean test-network test-postgres frontend-test frontend-test-build
2+
3+
# Default target
4+
all: test
5+
6+
# Create Docker network if it doesn't exist
7+
test-network:
8+
docker network create amazeeai_default 2>/dev/null || true
9+
10+
# Build the test container
11+
test-build:
12+
docker build -t amazee-backend-test -f Dockerfile.test .
13+
14+
# Start PostgreSQL container for testing
15+
test-postgres: test-network
16+
docker run -d \
17+
--name amazee-test-postgres \
18+
--network amazeeai_default \
19+
-e POSTGRES_USER=postgres \
20+
-e POSTGRES_PASSWORD=postgres \
21+
-e POSTGRES_DB=postgres_service \
22+
postgres:14 && \
23+
sleep 5
24+
25+
# Run tests in a new container
26+
test: test-build test-postgres
27+
docker run --rm \
28+
--network amazeeai_default \
29+
-e DATABASE_URL="postgresql://postgres:postgres@amazee-test-postgres/postgres_service" \
30+
-e SECRET_KEY="test-secret-key" \
31+
-e POSTGRES_HOST="amazee-test-postgres" \
32+
-e POSTGRES_USER="postgres" \
33+
-e POSTGRES_PASSWORD="postgres" \
34+
-e POSTGRES_DB="postgres_service" \
35+
-e LITELLM_API_URL="https://test-litellm.ai" \
36+
-e LITELLM_MASTER_KEY="test-master-key" \
37+
-e TESTING="1" \
38+
-v $(PWD)/app:/app/app \
39+
-v $(PWD)/tests:/app/tests \
40+
amazee-backend-test
41+
42+
# Run tests with coverage report
43+
test-cov: test-build test-postgres
44+
docker run --rm \
45+
--network amazeeai_default \
46+
-e DATABASE_URL="postgresql://postgres:postgres@amazee-test-postgres/postgres_service" \
47+
-e SECRET_KEY="test-secret-key" \
48+
-e POSTGRES_HOST="amazee-test-postgres" \
49+
-e POSTGRES_USER="postgres" \
50+
-e POSTGRES_PASSWORD="postgres" \
51+
-e POSTGRES_DB="postgres_service" \
52+
-e LITELLM_API_URL="https://test-litellm.ai" \
53+
-e LITELLM_MASTER_KEY="test-master-key" \
54+
-e TESTING="1" \
55+
-v $(PWD)/app:/app/app \
56+
-v $(PWD)/tests:/app/tests \
57+
amazee-backend-test pytest -v --cov=app tests/
58+
59+
# Build the frontend test container
60+
frontend-test-build:
61+
cd frontend && docker build -t amazeeai-frontend-test -f Dockerfile .
62+
63+
# Run frontend tests
64+
frontend-test: frontend-test-build
65+
docker run --rm \
66+
-e CI=true \
67+
amazeeai-frontend-test npm test -- --ci
68+
69+
# Run all tests (backend and frontend)
70+
test-all: test frontend-test
71+
72+
# Clean up test containers and images
73+
test-clean:
74+
docker stop amazee-test-postgres 2>/dev/null || true
75+
docker rm amazee-test-postgres 2>/dev/null || true
76+
docker network rm amazeeai_default 2>/dev/null || true
77+
docker rmi amazee-backend-test 2>/dev/null || true
78+
docker rmi amazeeai-frontend-test 2>/dev/null || true

app/api/auth.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from datetime import datetime, timedelta
2+
from typing import Optional
3+
from fastapi import APIRouter, Depends, HTTPException, status, Response, Cookie, Header
4+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
5+
from sqlalchemy.orm import Session
6+
7+
from app.db.database import get_db
8+
from app.schemas.models import Token, User, UserCreate
9+
from app.db.models import DBUser, DBAPIToken
10+
from app.core.security import (
11+
verify_password,
12+
get_password_hash,
13+
create_access_token,
14+
get_current_user,
15+
)
16+
from app.core.config import settings
17+
18+
router = APIRouter()
19+
20+
def authenticate_user(db: Session, email: str, password: str) -> Optional[DBUser]:
21+
user = db.query(DBUser).filter(DBUser.email == email).first()
22+
if not user:
23+
return None
24+
if not verify_password(password, user.hashed_password):
25+
return None
26+
return user
27+
28+
@router.post("/login", response_model=Token)
29+
async def login(
30+
response: Response,
31+
form_data: OAuth2PasswordRequestForm = Depends(),
32+
db: Session = Depends(get_db)
33+
):
34+
user = authenticate_user(db, form_data.username, form_data.password)
35+
if not user:
36+
raise HTTPException(
37+
status_code=status.HTTP_401_UNAUTHORIZED,
38+
detail="Incorrect email or password"
39+
)
40+
41+
access_token = create_access_token(
42+
data={"sub": user.email}
43+
)
44+
45+
# Set cookie
46+
response.set_cookie(
47+
key="access_token",
48+
value=access_token,
49+
httponly=True,
50+
max_age=1800,
51+
expires=1800,
52+
)
53+
54+
return {"access_token": access_token, "token_type": "bearer"}
55+
56+
@router.post("/logout")
57+
async def logout(response: Response):
58+
response.delete_cookie(
59+
key="access_token",
60+
path="/"
61+
)
62+
return {"message": "Successfully logged out"}
63+
64+
async def get_current_user_from_token(
65+
api_token: str = Header(None, alias="X-API-Token"),
66+
db: Session = Depends(get_db)
67+
) -> DBUser:
68+
if not api_token:
69+
raise HTTPException(
70+
status_code=status.HTTP_401_UNAUTHORIZED,
71+
detail="API token is missing",
72+
)
73+
74+
db_token = db.query(DBAPIToken).filter(DBAPIToken.token == api_token).first()
75+
if not db_token:
76+
raise HTTPException(
77+
status_code=status.HTTP_401_UNAUTHORIZED,
78+
detail="Invalid API token",
79+
)
80+
81+
# Update last used timestamp
82+
db_token.last_used_at = datetime.utcnow()
83+
db.commit()
84+
85+
return db_token.user
86+
87+
async def get_current_user_from_auth(
88+
access_token: Optional[str] = Cookie(None, alias="access_token"),
89+
api_token: Optional[str] = Header(None, alias="X-API-Token"),
90+
db: Session = Depends(get_db)
91+
) -> DBUser:
92+
if api_token:
93+
return await get_current_user_from_token(api_token, db)
94+
95+
if not access_token:
96+
raise HTTPException(
97+
status_code=status.HTTP_401_UNAUTHORIZED,
98+
detail="Could not validate credentials",
99+
headers={"WWW-Authenticate": "Bearer"},
100+
)
101+
102+
# Remove "Bearer " prefix if present
103+
if access_token.startswith("Bearer "):
104+
access_token = access_token[7:]
105+
106+
return await get_current_user(token=access_token, db=db)
107+
108+
@router.get("/me", response_model=User)
109+
async def read_users_me(current_user: DBUser = Depends(get_current_user_from_auth)):
110+
return current_user
111+
112+
@router.post("/register", response_model=User)
113+
async def register(user: UserCreate, db: Session = Depends(get_db)):
114+
# Check if user with this email exists
115+
db_user = db.query(DBUser).filter(DBUser.email == user.email).first()
116+
if db_user:
117+
raise HTTPException(
118+
status_code=status.HTTP_400_BAD_REQUEST,
119+
detail="Email already registered"
120+
)
121+
122+
# Create new user
123+
hashed_password = get_password_hash(user.password)
124+
db_user = DBUser(
125+
email=user.email,
126+
hashed_password=hashed_password,
127+
is_admin=user.is_admin
128+
)
129+
db.add(db_user)
130+
db.commit()
131+
db.refresh(db_user)
132+
return db_user

0 commit comments

Comments
 (0)