Initial commit: React + Django full-stack project setup

- Backend: Django 4.2 + DRF + JWT + GraphQL
- Frontend: React 18 + MobX + styled-components
- Deployment: Docker + Docker Compose + Nginx
- Database: PostgreSQL support
- Documentation: README, INIT, PROJECT_DOCS, TESTING
This commit is contained in:
mashen
2026-04-09 12:06:14 +00:00
commit cb491e8b87
49 changed files with 1804 additions and 0 deletions

22
.env.example Normal file
View File

@@ -0,0 +1,22 @@
# Django Settings
DJANGO_SECRET_KEY=your-secret-key-here-change-in-production
DJANGO_DEBUG=False
# Database
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASSWORD=your_database_password
DB_HOST=postgres
DB_PORT=5432
# Allowed Hosts
ALLOWED_HOSTS=localhost,yourdomain.com
# CORS
CORS_ALLOWED_ORIGINS=http://localhost,https://yourdomain.com
# Email (optional)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@example.com
EMAIL_HOST_PASSWORD=your-email-password

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Django
*.log
.env
/local_settings.py
# Node
node_modules/
npm-debug.log*
# Build
build/
dist/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

120
INIT.md Normal file
View File

@@ -0,0 +1,120 @@
# 项目初始化指南
## 快速开始
### 1. 配置环境变量
```bash
# 复制环境变量模板
cp .env.example .env
# 编辑 .env 文件,配置以下内容:
# - DJANGO_SECRET_KEY (生成一个安全的密钥)
# - DB_NAME, DB_USER, DB_PASSWORD, DB_HOST (数据库配置)
# - ALLOWED_HOSTS (允许的域名)
```
### 2. 后端初始化
```bash
cd backend
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
# 配置自定义用户模型 (首次运行需要)
# 在运行迁移前,确保 apps.users.models.User 已创建
# 运行迁移
python manage.py migrate
# 创建超级用户
python manage.py createsuperuser
# 运行开发服务器
python manage.py runserver
```
后端将在 http://localhost:8000 启动
### 3. 前端初始化
```bash
cd frontend
# 安装依赖
npm install
# 配置环境变量
cp .env.example .env
# 编辑 .env设置 REACT_APP_API_URL
# 启动开发服务器
npm start
```
前端将在 http://localhost:3000 启动
## Docker 部署
```bash
# 构建并启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
# 停止并删除卷
docker-compose down -v
```
## 数据库配置
### 使用外部 PostgreSQL
`.env` 中配置:
```env
DB_HOST=your-db-host
DB_PORT=5432
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASSWORD=your_database_password
```
### 使用 Docker PostgreSQL
默认配置会启动一个 PostgreSQL 容器,无需额外配置。
## 验证安装
1. 访问 http://localhost:8000/admin - Django 管理后台
2. 访问 http://localhost:8000/api/ - REST API
3. 访问 http://localhost:8000/graphql/?graphiql - GraphQL playground
4. 访问 http://localhost:3000 - React 前端
## 常见问题
### 迁移错误
如果遇到迁移错误,删除 migrations 文件并重新生成:
```bash
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
python manage.py makemigrations
python manage.py migrate
```
### 静态文件收集错误
```bash
python manage.py collectstatic --noinput
```
### 数据库连接错误
检查 `.env` 文件中的数据库配置是否正确。

262
PROJECT_DOCS.md Normal file
View File

@@ -0,0 +1,262 @@
# 项目架构文档
## 技术栈
### 后端 (Django)
- **框架**: Django 4.2
- **API 框架**: Django REST Framework
- **认证**: JWT (djangorestframework-simplejwt)
- **GraphQL**: graphene-django
- **数据库**: PostgreSQL
- **静态文件**: Whitenoise
- **生产服务器**: Gunicorn
### 前端 (React)
- **框架**: React 18
- **构建工具**: Create React App
- **状态管理**: MobX + mobx-react-lite
- **样式**: styled-components
- **路由**: React Router 6
- **HTTP 客户端**: axios
### 部署
- **容器化**: Docker + Docker Compose
- **反向代理**: Nginx
- **进程管理**: Gunicorn
## 项目结构
### 后端结构
```
backend/
├── config/
│ ├── settings/
│ │ ├── base.py # 基础配置
│ │ ├── dev.py # 开发环境
│ │ └── prod.py # 生产环境
│ ├── urls.py # 主路由
│ ├── wsgi.py # WSGI 配置
│ └── asgi.py # ASGI 配置
├── apps/
│ ├── users/ # 用户管理
│ │ ├── models.py # User 模型
│ │ ├── serializers.py
│ │ ├── views.py # ViewSets
│ │ └── urls.py
│ ├── core/ # 核心业务
│ └── api/ # API 配置
│ ├── serializers.py
│ ├── views.py
│ ├── urls.py
│ ├── schema.py # GraphQL schema
│ └── graphql_urls.py
├── static/ # 静态文件
├── media/ # 媒体文件
├── requirements.txt
├── manage.py
└── start.sh # 快速启动脚本
```
### 前端结构
```
frontend/
├── src/
│ ├── components/ # React 组件
│ ├── stores/ # MobX stores
│ │ ├── AuthStore.js
│ │ └── UserStore.js
│ ├── services/ # API 服务
│ │ └── api.js # axios 配置
│ ├── styles/ # 全局样式
│ │ └── global.js
│ ├── App.js # 主应用
│ └── index.js # 入口文件
├── public/ # 公共资源
├── package.json
├── .env.example
├── nginx.conf # Nginx 配置
├── Dockerfile
└── start.sh # 快速启动脚本
```
## 核心功能
### 1. 用户认证
**JWT 认证流程:**
1. 用户发送 email + password 到 `/api/auth/login/`
2. 后端验证并返回 access_token 和 refresh_token
3. 前端保存 tokens 到 localStorage
4. 后续请求在 Header 中携带 Bearer token
5. Token 过期时自动使用 refresh_token 刷新
**自定义用户模型:**
```python
# apps/users/models.py
class User(AbstractUser):
email = models.EmailField(unique=True)
avatar = models.ImageField(upload_to='avatars/')
USERNAME_FIELD = 'email'
```
### 2. REST API
**端点:**
- `POST /api/auth/login/` - 登录获取 token
- `POST /api/auth/token/refresh/` - 刷新 token
- `GET /api/users/` - 获取用户列表
- `GET /api/users/me/` - 获取当前用户
- `GET /api/users/:id/` - 获取特定用户
### 3. GraphQL
**Schema**
```graphql
type User {
id: ID!
email: String!
username: String!
}
type Query {
allUsers: [User!]!
me: User
}
```
访问 `/graphql/?graphiql` 查看调试界面。
### 4. 状态管理 (MobX)
**AuthStore**
- 管理认证状态
- 处理登录/登出
- 保存和读取 tokens
**UserStore**
- 获取当前用户信息
- 管理用户数据状态
## 开发流程
### 1. 添加新的 API 端点
```python
# apps/yourapp/views.py
class YourViewSet(viewsets.ModelViewSet):
queryset = YourModel.objects.all()
serializer_class = YourSerializer
# apps/yourapp/urls.py
router = DefaultRouter()
router.register(r'endpoint', YourViewSet)
```
### 2. 添加新的 MobX Store
```javascript
// src/stores/YourStore.js
import { makeAutoObservable } from 'mobx';
class YourStore {
data = null;
loading = false;
constructor() {
makeAutoObservable(this);
}
async fetchData() {
this.loading = true;
// API 调用
this.loading = false;
}
}
export default YourStore;
```
### 3. 添加新的 React 组件
```javascript
// src/components/YourComponent.js
import React from 'react';
import styled from 'styled-components';
const Container = styled.div`
padding: 20px;
`;
function YourComponent() {
return <Container>Your content</Container>;
}
export default YourComponent;
```
## 部署配置
### Docker Compose 服务
**backend**
- 基于 Python 3.11
- 运行 Gunicorn
- 暴露 8000 端口
- 挂载 static 和 media 卷
**frontend**
- 基于 Node 18 构建
- 使用 nginx 作为服务器
- 暴露 80 端口
- 代理 API 请求到 backend
**db**
- PostgreSQL 15
- 持久化数据卷
### Nginx 配置
- `/` → React 应用
- `/api/*` → Django 后端
- `/graphql/*` → Django GraphQL
- `/media/*` → Django media
- `/static/*` → Django static
## 安全注意事项
1. **生产环境必须:**
- 设置 `DJANGO_SECRET_KEY` 为强随机值
- `DEBUG=False`
- 配置 `ALLOWED_HOSTS`
- 启用 HTTPS
- 设置 `CORS_ALLOWED_ORIGINS`
2. **数据库:**
- 使用强密码
- 不暴露到公网
- 定期备份
3. **JWT**
- Token 过期时间合理配置
- 刷新 token 轮换
- 生产环境使用 HTTPS
## 下一步开发建议
1. **后端:**
- 添加更多业务 apps
- 实现权限控制
- 添加 API 文档 (Swagger)
- 实现文件上传功能
2. **前端:**
- 创建登录/注册页面
- 添加路由保护
- 实现加载状态处理
- 添加错误处理
3. **部署:**
- 配置 HTTPS (Let's Encrypt)
- 设置 CI/CD
- 配置日志收集
- 实现自动备份

187
README.md Normal file
View File

@@ -0,0 +1,187 @@
# React + Django Full-Stack Project
## Tech Stack
### Backend
- Django 4.2
- Django REST Framework
- JWT Authentication (djangorestframework-simplejwt)
- GraphQL (graphene-django)
- PostgreSQL
### Frontend
- React 18 (Create React App)
- MobX (state management)
- styled-components (CSS-in-JS)
- React Router
### Deployment
- Docker & Docker Compose
- Nginx (reverse proxy)
## Project Structure
```
.
├── backend/ # Django project
│ ├── config/ # Settings and configuration
│ ├── apps/ # Django apps
│ ├── static/ # Static files
│ └── media/ # Media files
├── frontend/ # React project
│ ├── src/
│ │ ├── components/
│ │ ├── stores/ # MobX stores
│ │ ├── services/ # API calls
│ │ └── styles/
│ └── public/
├── docker-compose.yml
└── .env.example
```
## Setup Instructions
### 1. Environment Variables
Copy `.env.example` to `.env` and configure:
```bash
cp .env.example .env
```
Update the following variables:
- `DJANGO_SECRET_KEY` - Generate a secure secret key
- Database credentials (DB_NAME, DB_USER, DB_PASSWORD)
- `ALLOWED_HOSTS` - Add your domain(s)
### 2. PostgreSQL Configuration
If using external PostgreSQL (already deployed):
Update `.env` with your database credentials:
```env
DB_HOST=your-db-host
DB_PORT=5432
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASSWORD=your_database_password
```
If using Docker PostgreSQL:
The default values in `docker-compose.yml` will work.
### 3. Backend Setup
```bash
cd backend
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Run migrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Run development server
python manage.py runserver
```
### 4. Frontend Setup
```bash
cd frontend
# Install dependencies
npm install
# Start development server
npm start
```
### 5. Docker Deployment
```bash
# Build and start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
```
## API Endpoints
### REST API
- `/api/users/` - User management
- `/api/users/me/` - Current user
- `/api/auth/token/` - Login (POST)
- `/api/auth/token/refresh/` - Refresh token (POST)
### GraphQL
- `/graphql/` - GraphQL endpoint
- `/graphql/?graphiql` - GraphQL playground
## Development
### Running Backend
```bash
cd backend
python manage.py runserver
```
### Running Frontend
```bash
cd frontend
npm start
```
### Running Tests
Backend:
```bash
cd backend
python manage.py test
```
Frontend:
```bash
cd frontend
npm test
```
## Production Deployment
### Using Docker Compose
1. Update `.env` with production values
2. Build images: `docker-compose build`
3. Start services: `docker-compose up -d`
### Nginx Configuration
The frontend Dockerfile includes nginx configuration that:
- Serves React app
- Proxies `/api` requests to Django backend
- Proxies `/graphql` requests to Django backend
- Handles static and media files
## Security Notes
- **Never commit `.env` files**
- Change `DJANGO_SECRET_KEY` in production
- Use strong passwords for database
- Enable HTTPS in production
- Configure `ALLOWED_HOSTS` properly
- Set `DEBUG=False` in production
## License
MIT

163
TESTING.md Normal file
View File

@@ -0,0 +1,163 @@
# API 测试指南
## 测试后端 API
### 1. 获取 Token (登录)
```bash
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{
"email": "your@email.com",
"password": "your_password"
}'
```
响应示例:
```json
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"user": {
"id": 1,
"email": "your@email.com",
"username": "your_username",
"first_name": "First",
"last_name": "Last"
}
}
```
### 2. 刷新 Token
```bash
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d '{
"refresh": "your_refresh_token"
}'
```
### 3. 获取用户列表 (需要认证)
```bash
curl -X GET http://localhost:8000/api/users/ \
-H "Authorization: Bearer your_access_token"
```
### 4. 获取当前用户
```bash
curl -X GET http://localhost:8000/api/users/me/ \
-H "Authorization: Bearer your_access_token"
```
## GraphQL 测试
访问 http://localhost:8000/graphql/?graphiql
### 查询所有用户
```graphql
query {
allUsers {
id
email
username
firstName
lastName
}
}
```
### 查询当前用户
```graphql
query {
me {
id
email
username
firstName
lastName
}
}
```
## Postman 集合
你可以导入以下 Postman 集合来测试 API
### 环境变量
- `base_url`: http://localhost:8000
- `access_token`: (登录后自动填充)
### 请求示例
**1. 登录**
- Method: POST
- URL: `{{base_url}}/api/auth/login/`
- Body:
```json
{
"email": "test@example.com",
"password": "testpass123"
}
```
- Tests (自动提取 token):
```javascript
var jsonData = pm.response.json();
pm.environment.set("access_token", jsonData.access);
```
**2. 获取用户**
- Method: GET
- URL: `{{base_url}}/api/users/`
- Headers:
```
Authorization: Bearer {{access_token}}
```
**3. 获取当前用户**
- Method: GET
- URL: `{{base_url}}/api/users/me/`
- Headers:
```
Authorization: Bearer {{access_token}}
```
## 自动化测试
### 后端测试
```bash
cd backend
python manage.py test
```
### 前端测试
```bash
cd frontend
npm test
```
## 常见错误
### 401 Unauthorized
- Token 过期,使用 refresh_token 刷新
- Token 格式错误,确保 `Bearer` 前缀存在
- Token 被吊销
### 403 Forbidden
- 权限不足,检查用户是否有相应权限
- CSRF token 问题(开发环境可能遇到)
### 404 Not Found
- 端点不存在,检查 URL 路径
- 检查 Django 是否正确启动
### 500 Server Error
- 检查 Django 日志
- 检查数据库连接
- 检查代码语法错误

45
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Django
*.log
/local_settings.py
/staticfiles/
/media/
# Environment
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

31
backend/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install \
gcc \
postgresql-client \
-y --no-install-recommends && \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput --settings=config.settings.prod
# Expose port
EXPOSE 8000
# Run gunicorn
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

1
backend/apps/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Apps package

View File

@@ -0,0 +1 @@
# API app

6
backend/apps/api/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.api'

View File

@@ -0,0 +1,7 @@
from django.urls import path
from graphene_django.views import GraphQLView
from apps.api.schema import schema
urlpatterns = [
path('', GraphQLView.as_view(graphiql=True, schema=schema)),
]

View File

@@ -0,0 +1,31 @@
import graphene
from graphene_django import DjangoObjectType
from apps.users.models import User
class UserType(DjangoObjectType):
"""GraphQL type for User."""
class Meta:
model = User
fields = '__all__'
class Query(graphene.ObjectType):
"""Root GraphQL Query."""
all_users = graphene.List(UserType)
me = graphene.Field(UserType)
def resolve_all_users(root, info):
"""Resolve all users query."""
return User.objects.all()
def resolve_me(root, info):
"""Resolve current user query."""
if info.context.user.is_authenticated:
return info.context.user
return None
schema = graphene.Schema(query=Query)

View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
User = get_user_model()
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""Custom JWT token serializer that includes user data."""
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token['email'] = user.email
token['username'] = user.username
return token
def validate(self, attrs):
data = super().validate(attrs)
# Add user data to response
data['user'] = {
'id': self.user.id,
'email': self.user.email,
'username': self.user.username,
'first_name': self.user.first_name,
'last_name': self.user.last_name,
}
return data

12
backend/apps/api/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenRefreshView,
)
from apps.users.urls import urlpatterns as users_urls
from apps.api.views import CustomTokenObtainPairView
urlpatterns = [
path('auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('', include(users_urls)),
]

11
backend/apps/api/views.py Normal file
View File

@@ -0,0 +1,11 @@
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer
class CustomTokenObtainPairView(TokenObtainPairView):
"""Custom token view that returns user data with tokens."""
serializer_class = CustomTokenObtainPairSerializer

View File

@@ -0,0 +1 @@
# Core app

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.core'

View File

@@ -0,0 +1 @@
# Users app

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'

View File

@@ -0,0 +1,22 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""Custom user model extending AbstractUser."""
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=150)
last_name = models.CharField(max_length=150)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name']
class Meta:
db_table = 'users'
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.email

View File

@@ -0,0 +1,20 @@
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
"""Serializer for User model."""
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar')
read_only_fields = ('id',)
class UserDetailSerializer(serializers.ModelSerializer):
"""Detailed serializer for User model."""
class Meta:
model = User
fields = '__all__'
read_only_fields = ('id', 'date_joined', 'last_login')

View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,23 @@
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer, UserDetailSerializer
class UserViewSet(viewsets.ModelViewSet):
"""ViewSet for User model."""
queryset = User.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
if self.action in ['retrieve', 'update', 'partial_update']:
return UserDetailSerializer
return UserSerializer
@action(detail=False, methods=['get'])
def me(self, request):
"""Get current user."""
serializer = self.get_serializer(request.user)
return Response(serializer.data)

View File

@@ -0,0 +1 @@
# Config package

9
backend/config/asgi.py Normal file
View File

@@ -0,0 +1,9 @@
"""
ASGI config for the project.
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
application = get_asgi_application()

View File

@@ -0,0 +1,165 @@
"""
Base Django settings for the project.
"""
from pathlib import Path
import os
# Build paths inside the project
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-this-in-production')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework',
'rest_framework_simplejwt',
'corsheaders',
'django_filters',
'graphene_django',
# Local apps
'apps.users',
'apps.core',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'postgres'),
'USER': os.environ.get('DB_USER', 'postgres'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'postgres'),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
AUTH_USER_MODEL = 'users.User'
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Media files
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# JWT Settings
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
}
# CORS Settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
]
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
]
# GraphQL
GRAPHENE = {
'SCHEMA': 'apps.api.schema.schema',
'MIDDLEWARE': [
'graphql_jwt.middleware.JSONWebTokenMiddleware',
],
}

View File

@@ -0,0 +1,19 @@
"""
Development settings for the project.
"""
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
CORS_ALLOW_ALL_ORIGINS = True
# Email backend (console for development)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Additional apps for development
INSTALLED_APPS += [
'django_extensions',
]

View File

@@ -0,0 +1,31 @@
"""
Production settings for the project.
"""
from .base import *
DEBUG = False
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# CORS - use environment variable
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',')
CSORS_ALLOW_CREDENTIALS = True
# Email backend (use your email service in production)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')

18
backend/config/urls.py Normal file
View File

@@ -0,0 +1,18 @@
"""
URL configuration for the project.
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('apps.api.urls')),
path('graphql/', include('apps.api.graphql_urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

9
backend/config/wsgi.py Normal file
View File

@@ -0,0 +1,9 @@
"""
WSGI config for the project.
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
application = get_wsgi_application()

22
backend/manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

11
backend/requirements.txt Normal file
View File

@@ -0,0 +1,11 @@
Django>=4.2,<5.0
djangorestframework>=3.14
djangorestframework-simplejwt>=5.2
django-cors-headers>=4.0
psycopg2-binary>=2.9
python-dotenv>=1.0
Pillow>=10.0
graphene-django>=3.1
django-filter>=23.0
gunicorn>=21.0
whitenoise>=6.5

29
backend/start.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
echo "🚀 Starting Django Backend..."
# 激活虚拟环境
if [ -d "venv" ]; then
source venv/bin/activate
echo "✅ Virtual environment activated"
else
echo "❌ Virtual environment not found. Creating one..."
python -m venv venv
source venv/bin/activate
fi
# 安装依赖
echo "📦 Installing dependencies..."
pip install -r requirements.txt
# 运行迁移
echo "🔄 Running migrations..."
python manage.py migrate
# 收集静态文件
echo "📁 Collecting static files..."
python manage.py collectstatic --noinput
# 启动服务器
echo "🎉 Starting development server on http://localhost:8000"
python manage.py runserver

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: django_backend
restart: unless-stopped
env_file:
- .env
volumes:
- ./backend:/app
- backend_static:/app/staticfiles
- backend_media:/app/media
ports:
- "8000:8000"
depends_on:
- db
networks:
- app_network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: react_frontend
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
networks:
- app_network
db:
image: postgres:15-alpine
container_name: postgres_db
restart: unless-stopped
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app_network
volumes:
postgres_data:
backend_static:
backend_media:
networks:
app_network:
driver: bridge

2
frontend/.env.example Normal file
View File

@@ -0,0 +1,2 @@
REACT_APP_API_URL=http://localhost:8000
REACT_APP_ENV=development

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

31
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM node:18-alpine as build
# Set work directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy project
COPY . .
# Build
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files
COPY --from=build /app/build /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

37
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /graphql {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /media {
proxy_pass http://backend:8000;
}
location /static {
proxy_pass http://backend:8000;
}
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
}

48
frontend/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "react-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.6.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-scripts": "5.0.1",
"styled-components": "^6.1.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"eslint": "^8.55.0",
"prettier": "^3.1.0"
},
"proxy": "http://localhost:8000"
}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="React + Django App" />
<title>React + Django App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

35
frontend/src/App.js Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import styled from 'styled-components';
const Container = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 20px;
`;
const Header = styled.header`
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #eee;
`;
const Title = styled.h1`
margin: 0;
font-size: 28px;
`;
function App() {
return (
<Container>
<Header>
<Title>React + Django App</Title>
</Header>
<Routes>
<Route path="/" element={<div>Welcome to the app!</div>} />
</Routes>
</Container>
);
}
export default App;

27
frontend/src/index.js Normal file
View File

@@ -0,0 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'mobx-react-lite';
import App from './App';
import './styles/global';
// Import stores
import UserStore from './stores/UserStore';
import AuthStore from './stores/AuthStore';
const root = ReactDOM.createRoot(document.getElementById('root'));
const stores = {
userStore: new UserStore(),
authStore: new AuthStore(),
};
root.render(
<React.StrictMode>
<Provider {...stores}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);

View File

@@ -0,0 +1,54 @@
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8000',
headers: {
'Content-Type': 'application/json',
},
});
// Add token to requests
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Handle token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refresh');
const response = await axios.post('/api/token/refresh/', {
refresh: refreshToken,
});
const newToken = response.data.access;
localStorage.setItem('token', newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (refreshError) {
localStorage.removeItem('token');
localStorage.removeItem('refresh');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,44 @@
import { makeAutoObservable } from 'mobx';
import axios from 'axios';
class AuthStore {
token = localStorage.getItem('token') || null;
isAuthenticated = !!localStorage.getItem('token');
constructor() {
makeAutoObservable(this);
}
async login(email, password) {
try {
const response = await axios.post('/api/auth/login/', {
email,
password,
});
this.token = response.data.access;
this.isAuthenticated = true;
localStorage.setItem('token', this.token);
localStorage.setItem('refresh', response.data.refresh);
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`;
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.detail || 'Login failed',
};
}
}
logout() {
this.token = null;
this.isAuthenticated = false;
localStorage.removeItem('token');
localStorage.removeItem('refresh');
delete axios.defaults.headers.common['Authorization'];
}
}
export default AuthStore;

View File

@@ -0,0 +1,32 @@
import { makeAutoObservable } from 'mobx';
import axios from 'axios';
class UserStore {
user = null;
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
async fetchCurrentUser() {
this.loading = true;
this.error = null;
try {
const response = await axios.get('/api/users/me/');
this.user = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch user';
} finally {
this.loading = false;
}
}
clearUser() {
this.user = null;
}
}
export default UserStore;

View File

@@ -0,0 +1,24 @@
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
`;
export default GlobalStyle;

13
frontend/start.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
echo "🚀 Starting React Frontend..."
# 检查 node_modules
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# 启动开发服务器
echo "🎉 Starting development server on http://localhost:3000"
npm start