Cybersecurity-Projects/PROJECTS/advanced/bug-bounty-platform/learn/GETTING-STARTED.md

922 lines
21 KiB
Markdown

# Getting Started: Build Your Own Bug Bounty Platform
This guide walks you through building a similar platform from scratch, explaining each step and the reasoning behind design decisions.
---
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Project Setup](#project-setup)
3. [Backend Development](#backend-development)
4. [Frontend Development](#frontend-development)
5. [Database Design](#database-design)
6. [Authentication](#authentication)
7. [API Development](#api-development)
8. [Testing](#testing)
9. [Deployment](#deployment)
---
## Prerequisites
Before starting, you should understand:
**Python:**
- Async/await (`async def`, `await`)
- Type hints (`def func(x: int) -> str`)
- Context managers (`async with`)
- Decorators (`@router.get()`)
**TypeScript/JavaScript:**
- Promises and async/await
- React hooks (`useState`, `useEffect`)
- TypeScript types and interfaces
- ES6+ syntax (arrow functions, destructuring)
**Database:**
- SQL basics (SELECT, INSERT, UPDATE, DELETE)
- Relationships (one-to-many, many-to-many)
- Indexes and foreign keys
**Tools:**
- Git (version control)
- Docker (containerization)
- Command line basics
---
## Project Setup
### Step 1: Create Project Structure
```bash
mkdir bug-bounty-platform
cd bug-bounty-platform
mkdir -p backend/src/app
mkdir -p frontend/src
mkdir -p infra/nginx
mkdir -p infra/docker
```
### Step 2: Initialize Backend (Python)
```bash
cd backend
touch pyproject.toml
```
**pyproject.toml:**
```toml
[project]
name = "bug-bounty-backend"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.123.0",
"uvicorn[standard]>=0.40.0",
"sqlalchemy>=2.0.0",
"asyncpg>=0.30.0",
"alembic>=1.15.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.7.0",
"pyjwt>=2.10.0",
"pwdlib[argon2]>=0.2.0",
"uuid-utils>=0.10.0",
"python-multipart>=0.0.20",
]
[project.optional-dependencies]
dev = [
"ruff>=0.9.0",
"mypy>=1.15.0",
"pytest>=8.0.0",
"pytest-asyncio>=0.25.0",
]
[build-system]
requires = ["setuptools>=75.0.0"]
build-backend = "setuptools.build_meta"
```
**Why these dependencies?**
- `fastapi` - Modern async web framework
- `sqlalchemy` - ORM for database operations
- `asyncpg` - Fast async PostgreSQL driver
- `alembic` - Database migrations
- `pydantic` - Data validation
- `pyjwt` - JWT token handling
- `pwdlib[argon2]` - Secure password hashing
- `uuid-utils` - UUID v7 support
### Step 3: Initialize Frontend (React + TypeScript)
```bash
cd ../frontend
pnpm create vite@latest . --template react-ts
pnpm install
```
**Install additional dependencies:**
```bash
pnpm add react-router-dom @tanstack/react-query zustand axios zod
pnpm add -D sass stylelint @biomejs/biome
```
**Why these dependencies?**
- `react-router-dom` - File-based routing
- `@tanstack/react-query` - Server state management
- `zustand` - Client state management
- `axios` - HTTP client
- `zod` - Runtime validation
- `sass` - CSS preprocessing
- `biome` - Linting and formatting
---
## Backend Development
### Step 1: Create Base Configuration
**backend/src/config.py:**
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
)
APP_NAME: str = "Bug Bounty Platform"
ENVIRONMENT: str = "development"
DEBUG: bool = True
DATABASE_URL: str
SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
CORS_ORIGINS: list[str] = ["http://localhost:3000"]
settings = Settings()
```
**Why Pydantic Settings?**
- Type validation (DATABASE_URL must be a string)
- Environment variable loading (reads from .env)
- Default values
- IDE autocomplete
### Step 2: Set Up Database
**backend/src/app/core/database.py:**
```python
from sqlalchemy.ext.asyncio import (
create_async_engine,
AsyncSession,
async_sessionmaker,
)
from typing import AsyncGenerator
class DatabaseSessionManager:
def __init__(self):
self._engine = None
self._sessionmaker = None
def init(self, database_url: str):
self._engine = create_async_engine(
database_url,
echo=True, # Log SQL queries (disable in production)
pool_size=20,
max_overflow=10,
)
self._sessionmaker = async_sessionmaker(
self._engine,
class_=AsyncSession,
expire_on_commit=False,
)
async def session(self) -> AsyncGenerator[AsyncSession, None]:
if self._sessionmaker is None:
raise RuntimeError("Database not initialized")
async with self._sessionmaker() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()
db_manager = DatabaseSessionManager()
```
**Key concepts:**
- `AsyncSession` - Async database session
- `async_sessionmaker` - Factory for creating sessions
- Context manager (`async with`) - Automatically commits/rolls back
- Connection pooling - Reuse connections
### Step 3: Create Base Models
**backend/src/app/core/Base.py:**
```python
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import DateTime, func
from uuid import UUID
from datetime import datetime
import uuid_utils as uuid
class Base(DeclarativeBase):
pass
class UUIDMixin:
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid7,
)
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
)
```
**Why mixins?**
- DRY (Don't Repeat Yourself)
- Every model gets `id`, `created_at`, `updated_at` automatically
- Can add more mixins (SoftDeleteMixin, etc.)
### Step 4: Create User Model
**backend/src/app/user/models.py:**
```python
from sqlalchemy import String, Enum
from sqlalchemy.orm import Mapped, mapped_column
from app.core.Base import Base, UUIDMixin, TimestampMixin
from enum import StrEnum
class UserRole(StrEnum):
USER = "user"
COMPANY = "company"
ADMIN = "admin"
class User(Base, UUIDMixin, TimestampMixin):
__tablename__ = "users"
email: Mapped[str] = mapped_column(
String(255),
unique=True,
index=True,
)
password_hash: Mapped[str] = mapped_column(String(255))
full_name: Mapped[str] = mapped_column(String(255))
role: Mapped[UserRole] = mapped_column(
Enum(UserRole, values_callable=lambda obj: [e.value for e in obj]),
default=UserRole.USER,
)
```
**Key points:**
- `Mapped[str]` - Type hint for SQLAlchemy
- `unique=True, index=True` - Fast lookups, prevent duplicates
- `Enum` with values_callable - Store enum value, not name (SafeEnum pattern)
### Step 5: Create User Repository
**backend/src/app/user/repository.py:**
```python
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.user.models import User
from uuid import UUID
class UserRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, user_id: UUID) -> User | None:
stmt = select(User).where(User.id == user_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def get_by_email(self, email: str) -> User | None:
stmt = select(User).where(User.email == email)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(
self,
email: str,
password_hash: str,
full_name: str,
role: str = "user",
) -> User:
user = User(
email=email,
password_hash=password_hash,
full_name=full_name,
role=role,
)
self.session.add(user)
await self.session.flush()
await self.session.refresh(user)
return user
```
**Why repositories?**
- Separate data access from business logic
- Reusable queries
- Easy to test (mock repository)
### Step 6: Create User Schemas
**backend/src/app/user/schemas.py:**
```python
from pydantic import BaseModel, EmailStr, Field
from uuid import UUID
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
full_name: str = Field(..., min_length=1, max_length=255)
class UserResponse(BaseModel):
id: UUID
email: str
full_name: str
role: str
created_at: datetime
model_config = {"from_attributes": True}
```
**Why Pydantic schemas?**
- Input validation (email format, password length)
- Automatic OpenAPI docs
- Type safety
- Serialization (ORM model → JSON)
### Step 7: Create User Routes
**backend/src/app/user/routes.py:**
```python
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import db_manager
from app.user.schemas import UserResponse
from app.user.repository import UserRepository
from app.core.dependencies import get_current_user
from typing import Annotated
router = APIRouter()
DatabaseSession = Annotated[
AsyncSession,
Depends(db_manager.session)
]
CurrentUser = Annotated[User, Depends(get_current_user)]
@router.get("/me", response_model=UserResponse)
async def get_me(user: CurrentUser) -> UserResponse:
return UserResponse.from_orm(user)
```
**Key concepts:**
- `Annotated` - Type alias with dependency
- `Depends()` - Dependency injection
- `response_model` - Automatic serialization
- Route handler returns Python object, FastAPI converts to JSON
---
## Authentication
### Step 1: Password Hashing
**backend/src/app/core/security.py:**
```python
from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher
password_hash = PasswordHash((Argon2Hasher(),))
def hash_password(password: str) -> str:
return password_hash.hash(password)
def verify_password(password: str, hash: str) -> bool:
try:
return password_hash.verify(password, hash)
except Exception:
return False
```
**Why Argon2?**
- Winner of Password Hashing Competition (2015)
- Resistant to GPU cracking
- Memory-hard (expensive to parallelize)
### Step 2: JWT Token Creation
**backend/src/app/core/security.py (continued):**
```python
import jwt
from datetime import datetime, timedelta
from uuid import UUID
from config import settings
def create_access_token(user: User) -> str:
payload = {
"sub": str(user.id),
"role": user.role,
"token_version": user.token_version,
"exp": datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
),
"iat": datetime.utcnow(),
}
return jwt.encode(
payload,
settings.SECRET_KEY,
algorithm=settings.JWT_ALGORITHM,
)
def decode_access_token(token: str) -> dict:
try:
return jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM],
)
except jwt.ExpiredSignatureError:
raise UnauthorizedError("Token expired")
except jwt.InvalidTokenError:
raise UnauthorizedError("Invalid token")
```
**JWT payload:**
- `sub` (subject) - User ID
- `role` - User role (for authorization)
- `token_version` - For instant invalidation
- `exp` (expiration) - Token expires in 15 minutes
- `iat` (issued at) - Timestamp of creation
### Step 3: Authentication Dependency
**backend/src/app/core/dependencies.py:**
```python
from fastapi import Header, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import decode_access_token
from app.user.repository import UserRepository
from app.user.models import User
from uuid import UUID
async def get_current_user(
authorization: str = Header(...),
session: AsyncSession = Depends(db_manager.session),
) -> User:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token")
token = authorization.replace("Bearer ", "")
try:
payload = decode_access_token(token)
except Exception:
raise HTTPException(status_code=401, detail="Invalid token")
user_id = UUID(payload["sub"])
repo = UserRepository(session)
user = await repo.get_by_id(user_id)
if not user:
raise HTTPException(status_code=401, detail="User not found")
if user.token_version != payload["token_version"]:
raise HTTPException(status_code=401, detail="Token invalidated")
return user
```
**How it works:**
1. Extract `Authorization: Bearer <token>` header
2. Decode JWT token
3. Get user ID from token payload
4. Query database for user
5. Verify token version matches (for instant invalidation)
6. Return authenticated user
---
## Frontend Development
### Step 1: Create API Client
**frontend/src/api/index.ts:**
```typescript
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
try {
const { data } = await axios.post('/api/auth/refresh', {
refresh_token: refreshToken,
});
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
error.config.headers.Authorization = `Bearer ${data.access_token}`;
return axios(error.config);
} catch {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
export default api;
```
**What this does:**
- Automatically adds `Authorization` header to all requests
- Automatically refreshes expired access tokens
- Redirects to login if refresh fails
### Step 2: Create Auth Store (Zustand)
**frontend/src/stores/auth.store.ts:**
```typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthStore {
accessToken: string | null;
refreshToken: string | null;
user: User | null;
setTokens: (access: string, refresh: string) => void;
setUser: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
accessToken: null,
refreshToken: null,
user: null,
setTokens: (access, refresh) => set({
accessToken: access,
refreshToken: refresh,
}),
setUser: (user) => set({ user }),
logout: () => set({
accessToken: null,
refreshToken: null,
user: null,
}),
}),
{
name: 'auth-storage',
}
)
);
```
**Why Zustand?**
- Simple API (no boilerplate)
- Built-in persistence (saves to localStorage)
- TypeScript support
- No provider wrapper needed
### Step 3: Create React Query Hook
**frontend/src/api/hooks/useAuth.ts:**
```typescript
import { useMutation } from '@tanstack/react-query';
import api from '../index';
import { useAuthStore } from '../../stores/auth.store';
interface LoginRequest {
email: string;
password: string;
}
interface LoginResponse {
access_token: string;
refresh_token: string;
}
export const useLogin = () => {
const setTokens = useAuthStore((state) => state.setTokens);
return useMutation({
mutationFn: async (data: LoginRequest) => {
const response = await api.post<LoginResponse>('/auth/login', data);
return response.data;
},
onSuccess: (data) => {
setTokens(data.access_token, data.refresh_token);
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
},
});
};
```
**Usage in component:**
```typescript
function LoginForm() {
const login = useLogin();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
login.mutate({
email: 'user@example.com',
password: 'password123',
});
};
return (
<form onSubmit={handleSubmit}>
{/* ... */}
<button disabled={login.isPending}>
{login.isPending ? 'Logging in...' : 'Login'}
</button>
{login.isError && <p>Login failed</p>}
</form>
);
}
```
**Why TanStack Query?**
- Automatic loading/error states (`isPending`, `isError`)
- Optimistic updates
- Retry logic
- Cache management
---
## Testing
### Backend Testing
**backend/tests/test_user.py:**
```python
import pytest
from httpx import AsyncClient
from app.factory import create_app
@pytest.fixture
async def client():
app = create_app()
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.mark.asyncio
async def test_register_user(client: AsyncClient):
response = await client.post("/api/auth/register", json={
"email": "test@example.com",
"password": "password123",
"full_name": "Test User",
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "id" in data
```
**Run tests:**
```bash
pytest backend/tests/
```
### Frontend Testing
**frontend/src/components/LoginForm.test.tsx:**
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
test('renders login form', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
test('submits form with credentials', () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password123' },
});
fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
```
---
## Deployment
### Step 1: Create Docker Compose
**compose.yml:**
```yaml
version: '3.8'
services:
nginx:
build: ./infra/nginx
ports:
- "8420:80"
depends_on:
- backend
backend:
build: ./backend
environment:
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
db:
image: postgres:18-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
### Step 2: Deploy
```bash
cp .env.example .env
just up
```
---
## Next Steps
Now that you understand the basics:
1. **Read the architecture docs:** [ARCHITECTURE.md](./ARCHITECTURE.md)
2. **Learn design patterns:** [PATTERNS.md](./PATTERNS.md)
3. **Understand the database:** [DATABASE.md](./DATABASE.md)
4. **Study security features:** [SECURITY.md](./SECURITY.md)
---
## Common Pitfalls
**1. Not using async/await properly:**
```python
async def bad():
user_repo.get_by_id(user_id) # Missing await!
async def good():
user = await user_repo.get_by_id(user_id)
```
**2. Mixing sync and async code:**
```python
async def bad():
users = session.query(User).all() # Sync query in async function
async def good():
stmt = select(User)
result = await session.execute(stmt)
users = result.scalars().all()
```
**3. Not handling errors:**
```python
def bad():
user = await repo.get_by_id(user_id)
return user.email # What if user is None?
def good():
user = await repo.get_by_id(user_id)
if not user:
raise UserNotFoundError()
return user.email
```
**4. Storing passwords in plaintext:**
```python
def bad():
user = User(email=email, password=password) # Plaintext password!
def good():
user = User(email=email, password_hash=hash_password(password))
```
---
## Resources
**Official Documentation:**
- [FastAPI](https://fastapi.tiangolo.com/)
- [SQLAlchemy](https://docs.sqlalchemy.org/)
- [React](https://react.dev/)
- [TanStack Query](https://tanstack.com/query/latest)
**Books:**
- "Clean Architecture" by Robert C. Martin
- "Domain-Driven Design" by Eric Evans
- "Designing Data-Intensive Applications" by Martin Kleppmann
**This Codebase:**
- Explore the code in `/backend/src/app/`
- Read the other learning docs in `/learn/`
- Experiment by modifying the code
---
Good luck building your own platform!