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:
22
.env.example
Normal file
22
.env.example
Normal 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
29
.gitignore
vendored
Normal 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
120
INIT.md
Normal 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
262
PROJECT_DOCS.md
Normal 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
187
README.md
Normal 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
163
TESTING.md
Normal 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
45
backend/.gitignore
vendored
Normal 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
31
backend/Dockerfile
Normal 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
1
backend/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Apps package
|
||||||
1
backend/apps/api/__init__.py
Normal file
1
backend/apps/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# API app
|
||||||
6
backend/apps/api/apps.py
Normal file
6
backend/apps/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.api'
|
||||||
7
backend/apps/api/graphql_urls.py
Normal file
7
backend/apps/api/graphql_urls.py
Normal 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)),
|
||||||
|
]
|
||||||
31
backend/apps/api/schema.py
Normal file
31
backend/apps/api/schema.py
Normal 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)
|
||||||
29
backend/apps/api/serializers.py
Normal file
29
backend/apps/api/serializers.py
Normal 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
12
backend/apps/api/urls.py
Normal 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
11
backend/apps/api/views.py
Normal 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
|
||||||
1
backend/apps/core/__init__.py
Normal file
1
backend/apps/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Core app
|
||||||
6
backend/apps/core/apps.py
Normal file
6
backend/apps/core/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.core'
|
||||||
1
backend/apps/users/__init__.py
Normal file
1
backend/apps/users/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Users app
|
||||||
6
backend/apps/users/apps.py
Normal file
6
backend/apps/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.users'
|
||||||
22
backend/apps/users/models.py
Normal file
22
backend/apps/users/models.py
Normal 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
|
||||||
20
backend/apps/users/serializers.py
Normal file
20
backend/apps/users/serializers.py
Normal 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')
|
||||||
10
backend/apps/users/urls.py
Normal file
10
backend/apps/users/urls.py
Normal 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)),
|
||||||
|
]
|
||||||
23
backend/apps/users/views.py
Normal file
23
backend/apps/users/views.py
Normal 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)
|
||||||
1
backend/config/__init__.py
Normal file
1
backend/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Config package
|
||||||
9
backend/config/asgi.py
Normal file
9
backend/config/asgi.py
Normal 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()
|
||||||
165
backend/config/settings/base.py
Normal file
165
backend/config/settings/base.py
Normal 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',
|
||||||
|
],
|
||||||
|
}
|
||||||
19
backend/config/settings/dev.py
Normal file
19
backend/config/settings/dev.py
Normal 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',
|
||||||
|
]
|
||||||
31
backend/config/settings/prod.py
Normal file
31
backend/config/settings/prod.py
Normal 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
18
backend/config/urls.py
Normal 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
9
backend/config/wsgi.py
Normal 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
22
backend/manage.py
Normal 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
11
backend/requirements.txt
Normal 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
29
backend/start.sh
Executable 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
54
docker-compose.yml
Normal 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
2
frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
REACT_APP_API_URL=http://localhost:8000
|
||||||
|
REACT_APP_ENV=development
|
||||||
23
frontend/.gitignore
vendored
Normal file
23
frontend/.gitignore
vendored
Normal 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
31
frontend/Dockerfile
Normal 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
37
frontend/nginx.conf
Normal 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
48
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
15
frontend/public/index.html
Normal file
15
frontend/public/index.html
Normal 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
35
frontend/src/App.js
Normal 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
27
frontend/src/index.js
Normal 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>
|
||||||
|
);
|
||||||
54
frontend/src/services/api.js
Normal file
54
frontend/src/services/api.js
Normal 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;
|
||||||
5
frontend/src/setupTests.js
Normal file
5
frontend/src/setupTests.js
Normal 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';
|
||||||
44
frontend/src/stores/AuthStore.js
Normal file
44
frontend/src/stores/AuthStore.js
Normal 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;
|
||||||
32
frontend/src/stores/UserStore.js
Normal file
32
frontend/src/stores/UserStore.js
Normal 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;
|
||||||
24
frontend/src/styles/global.js
Normal file
24
frontend/src/styles/global.js
Normal 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
13
frontend/start.sh
Executable 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
|
||||||
Reference in New Issue
Block a user