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

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