Archived
- Backend: Django 5 + DRF with accounts, documents, collections, and reading apps - Custom User model with email-based auth, JWT via SimpleJWT - Full CRUD viewsets with ModelSerializer + DRF routers - pytest, Ruff, drf-spectacular (OpenAPI), whitenoise - Dockerfile for production deployment - Frontend: React 18 + TypeScript + Vite - Lazy-loaded routes with ProtectedRoute/PublicRoute guards - Auth context with useReducer, token refresh interceptor - Pages: Login, Register, Library, Document Detail, Reader, Collections, Settings - Dark theme, responsive grid layout, Vite proxy to Django backend - Mobile: Expo SDK 51 + React Native + Expo Router - File-based routing with login, register, and library screens - AsyncStorage for token persistence, token refresh interceptor - Shared API types via @cloud-reader/shared workspace package - Shared: TypeScript types (API responses, auth, documents, etc.) - CI/CD: 3 independent GitHub Actions pipelines (backend, frontend, mobile)
170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
import decouple
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Environment
|
|
# ---------------------------------------------------------------------------
|
|
config = decouple.AutoConfig(search_path=BASE_DIR / ".env")
|
|
|
|
SECRET_KEY = config("SECRET_KEY", default="django-insecure-change-me-in-production")
|
|
DEBUG = config("DEBUG", default=False, cast=bool)
|
|
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=decouple.Csv())
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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",
|
|
"drf_spectacular",
|
|
# Local
|
|
"apps.accounts",
|
|
"apps.documents",
|
|
"apps.collections",
|
|
"apps.reading",
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
"corsheaders.middleware.CorsMiddleware",
|
|
"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": [BASE_DIR / "templates"],
|
|
"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": config("DB_ENGINE", default="django.db.backends.postgresql"),
|
|
"NAME": config("DB_NAME", default="cloud_reader"),
|
|
"USER": config("DB_USER", default="cloud_reader"),
|
|
"PASSWORD": config("DB_PASSWORD", default="cloud_reader"),
|
|
"HOST": config("DB_HOST", default="localhost"),
|
|
"PORT": config("DB_PORT", default="5432", cast=int),
|
|
}
|
|
}
|
|
|
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auth
|
|
# ---------------------------------------------------------------------------
|
|
AUTH_USER_MODEL = "accounts.User"
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
]
|
|
|
|
LOGIN_URL = "rest_framework:login"
|
|
LOGOUT_URL = "rest_framework:logout"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internationalization
|
|
# ---------------------------------------------------------------------------
|
|
LANGUAGE_CODE = "en-us"
|
|
TIME_ZONE = "UTC"
|
|
USE_I18N = True
|
|
USE_TZ = True
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Static & Media files
|
|
# ---------------------------------------------------------------------------
|
|
STATIC_URL = "static/"
|
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
|
|
MEDIA_URL = "media/"
|
|
MEDIA_ROOT = BASE_DIR / "media"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CORS
|
|
# ---------------------------------------------------------------------------
|
|
CORS_ALLOWED_ORIGINS = config(
|
|
"CORS_ALLOWED_ORIGINS",
|
|
default="http://localhost:5173,http://localhost:3000",
|
|
cast=decouple.Csv(),
|
|
)
|
|
CORS_ALLOW_CREDENTIALS = True
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# REST Framework
|
|
# ---------------------------------------------------------------------------
|
|
REST_FRAMEWORK = {
|
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
|
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
|
),
|
|
"DEFAULT_PERMISSION_CLASSES": (
|
|
"rest_framework.permissions.IsAuthenticated",
|
|
),
|
|
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
"PAGE_SIZE": 20,
|
|
"DEFAULT_FILTER_BACKENDS": [
|
|
"django_filters.rest_framework.DjangoFilterBackend",
|
|
"rest_framework.filters.SearchFilter",
|
|
"rest_framework.filters.OrderingFilter",
|
|
],
|
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
"EXCEPTION_HANDLER": "config.exceptions.custom_exception_handler",
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SimpleJWT
|
|
# ---------------------------------------------------------------------------
|
|
from datetime import timedelta # noqa: E402
|
|
|
|
SIMPLE_JWT = {
|
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
|
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
|
"ROTATE_REFRESH_TOKENS": True,
|
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# drf-spectacular (OpenAPI)
|
|
# ---------------------------------------------------------------------------
|
|
SPECTACULAR_SETTINGS = {
|
|
"TITLE": "Cloud Reader API",
|
|
"VERSION": "0.1.0",
|
|
"SERVE_INCLUDE_SCHEMA": False,
|
|
} |