mirror of
https://github.com/thecyberlearn/modern-django-starter.git
synced 2026-08-18 08:52:55 +00:00
Initial commit: Django boilerplate with authentication, Docker, and Dokploy support
✨ Features: - Complete Django project with authentication - Email/password and social login (Google, Facebook) - Role-based access control (admin, staff, user) - Docker and Docker Compose setup - Production-ready configuration - Static file handling with WhiteNoise - PostgreSQL database integration - Comprehensive documentation - GitHub CI/CD workflows - Dokploy deployment configuration 🚀 Ready for deployment on Dokploy, Heroku, Railway, and other platforms
This commit is contained in:
commit
4bbc56a08f
34
.env.example
Normal file
34
.env.example
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Django Configuration
|
||||||
|
SECRET_KEY=your-secret-key-here-make-it-long-and-random
|
||||||
|
DEBUG=True
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_NAME=django_db
|
||||||
|
DB_USER=django_user
|
||||||
|
DB_PASSWORD=django_password
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Email Configuration (for production)
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
EMAIL_HOST_USER=your-email@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-password
|
||||||
|
DEFAULT_FROM_EMAIL=noreply@your-domain.com
|
||||||
|
|
||||||
|
# Social Authentication - Google
|
||||||
|
GOOGLE_OAUTH2_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_OAUTH2_CLIENT_SECRET=your-google-client-secret
|
||||||
|
|
||||||
|
# Social Authentication - Facebook
|
||||||
|
FACEBOOK_APP_ID=your-facebook-app-id
|
||||||
|
FACEBOOK_APP_SECRET=your-facebook-app-secret
|
||||||
|
|
||||||
|
# Production SSL Settings (set to True in production)
|
||||||
|
SECURE_SSL_REDIRECT=False
|
||||||
|
|
||||||
|
# Sentry (optional - for error tracking in production)
|
||||||
|
SENTRY_DSN=your-sentry-dsn-here
|
||||||
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Environment (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu 20.04]
|
||||||
|
- Python version: [e.g. 3.11]
|
||||||
|
- Django version: [e.g. 4.2.7]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Docker version [if using Docker]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
|
## How Has This Been Tested?
|
||||||
|
|
||||||
|
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
|
||||||
|
|
||||||
|
- [ ] Test A
|
||||||
|
- [ ] Test B
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have made corresponding changes to the documentation
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] New and existing unit tests pass locally with my changes
|
||||||
|
- [ ] Any dependent changes have been merged and published
|
||||||
90
.github/workflows/ci.yml
vendored
Normal file
90
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: test_db
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements/development.txt
|
||||||
|
|
||||||
|
- name: Create .env file
|
||||||
|
run: |
|
||||||
|
echo "SECRET_KEY=test-secret-key-for-ci" >> .env
|
||||||
|
echo "DEBUG=True" >> .env
|
||||||
|
echo "DB_NAME=test_db" >> .env
|
||||||
|
echo "DB_USER=postgres" >> .env
|
||||||
|
echo "DB_PASSWORD=postgres" >> .env
|
||||||
|
echo "DB_HOST=localhost" >> .env
|
||||||
|
echo "DB_PORT=5432" >> .env
|
||||||
|
|
||||||
|
- name: Run migrations
|
||||||
|
run: |
|
||||||
|
python manage.py migrate --settings=django_project.settings.development
|
||||||
|
|
||||||
|
- name: Create test groups
|
||||||
|
run: |
|
||||||
|
python manage.py create_groups --settings=django_project.settings.development
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
python manage.py test --settings=django_project.settings.development
|
||||||
|
|
||||||
|
- name: Check for missing migrations
|
||||||
|
run: |
|
||||||
|
python manage.py makemigrations --check --dry-run --settings=django_project.settings.development
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build --target development -t django-template:test .
|
||||||
|
|
||||||
|
- name: Test Docker image
|
||||||
|
run: |
|
||||||
|
docker run --rm django-template:test python manage.py check --settings=django_project.settings.development
|
||||||
149
.gitignore
vendored
Normal file
149
.gitignore
vendored
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# Django specific
|
||||||
|
media/
|
||||||
|
staticfiles/
|
||||||
|
static_cdn/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# SSL certificates
|
||||||
|
ssl/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
273
DEPLOYMENT.md
Normal file
273
DEPLOYMENT.md
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
This guide covers how to deploy the Django Template application using various platforms including Dokploy.
|
||||||
|
|
||||||
|
## 🚀 Dokploy Deployment
|
||||||
|
|
||||||
|
Dokploy is a modern deployment platform that makes it easy to deploy applications with Docker.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. A Dokploy account and server
|
||||||
|
2. A GitHub repository with your code
|
||||||
|
3. Domain name (optional but recommended)
|
||||||
|
|
||||||
|
### Quick Deploy
|
||||||
|
|
||||||
|
1. **Fork this repository** to your GitHub account
|
||||||
|
|
||||||
|
2. **Connect to Dokploy:**
|
||||||
|
- Log in to your Dokploy dashboard
|
||||||
|
- Click "New Application"
|
||||||
|
- Connect your GitHub repository
|
||||||
|
|
||||||
|
3. **Configure Environment Variables:**
|
||||||
|
```env
|
||||||
|
SECRET_KEY=your-very-long-random-secret-key
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=your-domain.com,www.your-domain.com
|
||||||
|
|
||||||
|
# Database (Dokploy will provide these)
|
||||||
|
DB_NAME=django_db
|
||||||
|
DB_USER=django_user
|
||||||
|
DB_PASSWORD=secure_password
|
||||||
|
DB_HOST=postgres
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
EMAIL_HOST_USER=your-email@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-password
|
||||||
|
DEFAULT_FROM_EMAIL=noreply@your-domain.com
|
||||||
|
|
||||||
|
# Social Authentication
|
||||||
|
GOOGLE_OAUTH2_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_OAUTH2_CLIENT_SECRET=your-google-client-secret
|
||||||
|
FACEBOOK_APP_ID=your-facebook-app-id
|
||||||
|
FACEBOOK_APP_SECRET=your-facebook-app-secret
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SECURE_SSL_REDIRECT=True
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Configure Database:**
|
||||||
|
- Add a PostgreSQL database service
|
||||||
|
- Use PostgreSQL 15
|
||||||
|
- Database name: `django_db`
|
||||||
|
- Username: `django_user`
|
||||||
|
|
||||||
|
5. **Deploy:**
|
||||||
|
- Click "Deploy"
|
||||||
|
- Dokploy will build and deploy your application automatically
|
||||||
|
|
||||||
|
### Post-Deployment Steps
|
||||||
|
|
||||||
|
1. **Run initial setup:**
|
||||||
|
```bash
|
||||||
|
# Access your application console in Dokploy
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py create_groups
|
||||||
|
python manage.py createsuperuser
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Domain:**
|
||||||
|
- Add your domain in Dokploy dashboard
|
||||||
|
- Configure SSL certificate (Let's Encrypt recommended)
|
||||||
|
|
||||||
|
3. **Set up Social Authentication:**
|
||||||
|
- Configure Google OAuth2 callback URL: `https://your-domain.com/accounts/google/login/callback/`
|
||||||
|
- Configure Facebook OAuth2 callback URL: `https://your-domain.com/accounts/facebook/login/callback/`
|
||||||
|
|
||||||
|
### Environment Variables Reference
|
||||||
|
|
||||||
|
| Variable | Description | Required | Default |
|
||||||
|
|----------|-------------|----------|---------|
|
||||||
|
| `SECRET_KEY` | Django secret key | Yes | - |
|
||||||
|
| `DEBUG` | Debug mode | Yes | False |
|
||||||
|
| `ALLOWED_HOSTS` | Allowed hostnames | Yes | - |
|
||||||
|
| `DB_NAME` | Database name | Yes | django_db |
|
||||||
|
| `DB_USER` | Database user | Yes | django_user |
|
||||||
|
| `DB_PASSWORD` | Database password | Yes | - |
|
||||||
|
| `DB_HOST` | Database host | Yes | postgres |
|
||||||
|
| `DB_PORT` | Database port | Yes | 5432 |
|
||||||
|
| `EMAIL_HOST` | SMTP host | No | localhost |
|
||||||
|
| `EMAIL_PORT` | SMTP port | No | 587 |
|
||||||
|
| `EMAIL_HOST_USER` | SMTP username | No | - |
|
||||||
|
| `EMAIL_HOST_PASSWORD` | SMTP password | No | - |
|
||||||
|
| `GOOGLE_OAUTH2_CLIENT_ID` | Google OAuth2 ID | No | - |
|
||||||
|
| `GOOGLE_OAUTH2_CLIENT_SECRET` | Google OAuth2 secret | No | - |
|
||||||
|
| `FACEBOOK_APP_ID` | Facebook app ID | No | - |
|
||||||
|
| `FACEBOOK_APP_SECRET` | Facebook app secret | No | - |
|
||||||
|
|
||||||
|
## 🐳 Docker Deployment
|
||||||
|
|
||||||
|
### Production Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create production environment file
|
||||||
|
cp .env.example .env.prod
|
||||||
|
|
||||||
|
# Edit .env.prod with production values
|
||||||
|
nano .env.prod
|
||||||
|
|
||||||
|
# Deploy with production compose
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build production image
|
||||||
|
docker build --target production -t django-template:latest .
|
||||||
|
|
||||||
|
# Run with environment variables
|
||||||
|
docker run -d \
|
||||||
|
--name django-app \
|
||||||
|
-p 80:8000 \
|
||||||
|
--env-file .env.prod \
|
||||||
|
django-template:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## ☁️ Cloud Platform Deployment
|
||||||
|
|
||||||
|
### Heroku
|
||||||
|
|
||||||
|
1. **Prepare for Heroku:**
|
||||||
|
```bash
|
||||||
|
# Create Procfile
|
||||||
|
echo "web: gunicorn django_project.wsgi --bind 0.0.0.0:\$PORT" > Procfile
|
||||||
|
|
||||||
|
# Create runtime.txt
|
||||||
|
echo "python-3.11.0" > runtime.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy to Heroku:**
|
||||||
|
```bash
|
||||||
|
heroku create your-app-name
|
||||||
|
heroku addons:create heroku-postgresql:mini
|
||||||
|
heroku config:set SECRET_KEY=your-secret-key
|
||||||
|
heroku config:set DEBUG=False
|
||||||
|
git push heroku main
|
||||||
|
heroku run python manage.py migrate
|
||||||
|
heroku run python manage.py create_groups
|
||||||
|
heroku run python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
|
||||||
|
1. **Connect GitHub repository to Railway**
|
||||||
|
2. **Add PostgreSQL database**
|
||||||
|
3. **Configure environment variables**
|
||||||
|
4. **Deploy automatically**
|
||||||
|
|
||||||
|
### DigitalOcean App Platform
|
||||||
|
|
||||||
|
1. **Create new app from GitHub**
|
||||||
|
2. **Add managed database (PostgreSQL)**
|
||||||
|
3. **Configure environment variables**
|
||||||
|
4. **Deploy**
|
||||||
|
|
||||||
|
## 🔒 Security Checklist
|
||||||
|
|
||||||
|
Before deploying to production:
|
||||||
|
|
||||||
|
- [ ] Set `DEBUG=False`
|
||||||
|
- [ ] Use a strong, unique `SECRET_KEY`
|
||||||
|
- [ ] Configure `ALLOWED_HOSTS` properly
|
||||||
|
- [ ] Set up SSL/TLS certificate
|
||||||
|
- [ ] Enable `SECURE_SSL_REDIRECT=True`
|
||||||
|
- [ ] Configure proper database credentials
|
||||||
|
- [ ] Set up email backend for notifications
|
||||||
|
- [ ] Configure social authentication with production URLs
|
||||||
|
- [ ] Set up monitoring and logging
|
||||||
|
- [ ] Configure backup strategy for database
|
||||||
|
- [ ] Review and update all default passwords
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
The application provides a health check endpoint:
|
||||||
|
- URL: `/` (returns 200 if healthy)
|
||||||
|
- Database connectivity check included
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Logs are configured for production in `settings/production.py`:
|
||||||
|
- Application logs: `/var/log/django/django.log`
|
||||||
|
- Console output for container logs
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
Consider adding:
|
||||||
|
- Application Performance Monitoring (APM)
|
||||||
|
- Database monitoring
|
||||||
|
- Error tracking (Sentry is pre-configured)
|
||||||
|
|
||||||
|
## 🔄 Updates and Maintenance
|
||||||
|
|
||||||
|
### Updating the Application
|
||||||
|
|
||||||
|
1. **Pull latest changes:**
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Rebuild and redeploy:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run migrations if needed:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml exec web python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
|
||||||
|
1. **Database backup:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml exec db pg_dump -U django_user django_db > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Media files backup:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml exec web tar -czf media_backup.tar.gz /app/media
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **500 Internal Server Error:**
|
||||||
|
- Check `DEBUG=False` and `ALLOWED_HOSTS`
|
||||||
|
- Verify database connection
|
||||||
|
- Check application logs
|
||||||
|
|
||||||
|
2. **Static files not loading:**
|
||||||
|
- Run `python manage.py collectstatic`
|
||||||
|
- Check `STATIC_URL` and `STATIC_ROOT` settings
|
||||||
|
|
||||||
|
3. **Social login not working:**
|
||||||
|
- Verify callback URLs in provider settings
|
||||||
|
- Check client ID and secret configuration
|
||||||
|
|
||||||
|
4. **Email not sending:**
|
||||||
|
- Verify SMTP settings
|
||||||
|
- Check firewall/security group settings
|
||||||
|
- Test email backend configuration
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check the application logs
|
||||||
|
- Review the GitHub Issues
|
||||||
|
- Consult the Django documentation
|
||||||
|
- Check provider-specific documentation (Dokploy, Heroku, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy deploying!** 🚀
|
||||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
FROM python:3.11-slim as base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
postgresql-client \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
gettext \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements/base.txt /app/requirements/
|
||||||
|
RUN pip install --no-cache-dir -r requirements/base.txt
|
||||||
|
|
||||||
|
FROM base as development
|
||||||
|
|
||||||
|
COPY requirements/development.txt /app/requirements/
|
||||||
|
RUN pip install --no-cache-dir -r requirements/development.txt
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
|
|
||||||
|
FROM base as production
|
||||||
|
|
||||||
|
COPY requirements/production.txt /app/requirements/
|
||||||
|
RUN pip install --no-cache-dir -r requirements/production.txt
|
||||||
|
|
||||||
|
RUN groupadd -r django && useradd -r -g django django
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
RUN chown -R django:django /app
|
||||||
|
USER django
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "django_project.wsgi:application"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Django Template
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
362
README.md
Normal file
362
README.md
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
# Django Boilerplate
|
||||||
|
|
||||||
|
A production-ready Django boilerplate with built-in authentication, social login, role-based permissions, and Docker support.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔐 **Complete Authentication System**
|
||||||
|
- Email/password registration and login
|
||||||
|
- Email verification required for account activation
|
||||||
|
- Password reset workflows
|
||||||
|
- Django Allauth integration
|
||||||
|
|
||||||
|
- 🌐 **Social Authentication**
|
||||||
|
- Google OAuth2 integration
|
||||||
|
- Facebook OAuth2 integration
|
||||||
|
- Easy to extend for other providers
|
||||||
|
|
||||||
|
- 👥 **Role-Based Access Control**
|
||||||
|
- User groups: `admin`, `staff`, `user`
|
||||||
|
- Permission-based access control
|
||||||
|
- Custom decorators and mixins for role checking
|
||||||
|
|
||||||
|
- 🐳 **Docker & Production Ready**
|
||||||
|
- Multi-stage Dockerfile
|
||||||
|
- Docker Compose for development and production
|
||||||
|
- PostgreSQL database
|
||||||
|
- Redis for caching
|
||||||
|
- Nginx reverse proxy
|
||||||
|
- Gunicorn WSGI server
|
||||||
|
|
||||||
|
- 🎨 **Modern Frontend**
|
||||||
|
- Bootstrap 5 integration
|
||||||
|
- Responsive design
|
||||||
|
- Clean, professional UI
|
||||||
|
|
||||||
|
- ⚙️ **Environment Management**
|
||||||
|
- Separate settings for development/production
|
||||||
|
- Environment variables for sensitive data
|
||||||
|
- Comprehensive configuration
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/django-template.git
|
||||||
|
cd django-template
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the `.env` file with your configuration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Django Configuration
|
||||||
|
SECRET_KEY=your-very-long-and-random-secret-key
|
||||||
|
DEBUG=True
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_NAME=django_db
|
||||||
|
DB_USER=django_user
|
||||||
|
DB_PASSWORD=secure_password
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Email Configuration (for production)
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
EMAIL_HOST_USER=your-email@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-password
|
||||||
|
DEFAULT_FROM_EMAIL=noreply@your-domain.com
|
||||||
|
|
||||||
|
# Social Authentication
|
||||||
|
GOOGLE_OAUTH2_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_OAUTH2_CLIENT_SECRET=your-google-client-secret
|
||||||
|
FACEBOOK_APP_ID=your-facebook-app-id
|
||||||
|
FACEBOOK_APP_SECRET=your-facebook-app-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build and Run with Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start the services
|
||||||
|
docker-compose up --build
|
||||||
|
|
||||||
|
# Or run in detached mode
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create Default User Groups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec web python manage.py create_groups
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Create a Superuser
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive creation
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
|
||||||
|
# Or use management command with defaults
|
||||||
|
docker-compose exec web python manage.py create_superuser --email admin@example.com --password admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Access the Application
|
||||||
|
|
||||||
|
- **Web Application**: http://localhost:8000
|
||||||
|
- **Django Admin**: http://localhost:8000/admin/
|
||||||
|
|
||||||
|
## 🔧 Development Setup
|
||||||
|
|
||||||
|
### Local Development (without Docker)
|
||||||
|
|
||||||
|
1. **Create a virtual environment:**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\\Scripts\\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements/development.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set up local database:**
|
||||||
|
```bash
|
||||||
|
# Install and start PostgreSQL
|
||||||
|
# Create database and user as configured in .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run migrations:**
|
||||||
|
```bash
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py create_groups
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start development server:**
|
||||||
|
```bash
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Social Authentication Setup
|
||||||
|
|
||||||
|
### Google OAuth2
|
||||||
|
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Create a new project or select existing
|
||||||
|
3. Enable Google+ API
|
||||||
|
4. Create OAuth2 credentials
|
||||||
|
5. Add authorized redirect URIs:
|
||||||
|
- `http://localhost:8000/accounts/google/login/callback/`
|
||||||
|
- `https://yourdomain.com/accounts/google/login/callback/`
|
||||||
|
6. Update `.env` with your client ID and secret
|
||||||
|
|
||||||
|
### Facebook OAuth2
|
||||||
|
|
||||||
|
1. Go to [Facebook Developers](https://developers.facebook.com/)
|
||||||
|
2. Create a new app
|
||||||
|
3. Add Facebook Login product
|
||||||
|
4. Configure Valid OAuth Redirect URIs:
|
||||||
|
- `http://localhost:8000/accounts/facebook/login/callback/`
|
||||||
|
- `https://yourdomain.com/accounts/facebook/login/callback/`
|
||||||
|
5. Update `.env` with your app ID and secret
|
||||||
|
|
||||||
|
## 📧 Email Configuration
|
||||||
|
|
||||||
|
For production email functionality:
|
||||||
|
|
||||||
|
1. **Gmail Setup:**
|
||||||
|
```env
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
EMAIL_HOST_USER=your-gmail@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-password
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate App Password:**
|
||||||
|
- Enable 2FA on your Gmail account
|
||||||
|
- Generate an app-specific password
|
||||||
|
- Use this password in `EMAIL_HOST_PASSWORD`
|
||||||
|
|
||||||
|
## 🐳 Production Deployment
|
||||||
|
|
||||||
|
### Using Docker Compose (Production)
|
||||||
|
|
||||||
|
1. **Update environment variables:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env.prod
|
||||||
|
# Edit .env.prod with production values
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy with production compose:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **SSL Configuration:**
|
||||||
|
- Place SSL certificates in `ssl/` directory
|
||||||
|
- Update `nginx.prod.conf` with your domain
|
||||||
|
- Certificates should be named `cert.pem` and `key.pem`
|
||||||
|
|
||||||
|
### Environment Variables for Production
|
||||||
|
|
||||||
|
```env
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
|
||||||
|
SECURE_SSL_REDIRECT=True
|
||||||
|
SECRET_KEY=generate-a-new-secure-secret-key
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_PASSWORD=use-a-strong-database-password
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||||
|
# Configure with your email provider
|
||||||
|
|
||||||
|
# Social Auth
|
||||||
|
# Configure with production callback URLs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
django-template/
|
||||||
|
├── django_project/ # Main Django project
|
||||||
|
│ ├── settings/ # Environment-specific settings
|
||||||
|
│ ├── urls.py # Main URL configuration
|
||||||
|
│ └── wsgi.py # WSGI application
|
||||||
|
├── apps/ # Django applications
|
||||||
|
│ ├── accounts/ # User authentication & profiles
|
||||||
|
│ └── core/ # Core application logic
|
||||||
|
├── templates/ # HTML templates
|
||||||
|
├── static/ # Static files (CSS, JS, images)
|
||||||
|
├── requirements/ # Python dependencies
|
||||||
|
├── Dockerfile # Docker configuration
|
||||||
|
├── docker-compose.yml # Development Docker Compose
|
||||||
|
├── docker-compose.prod.yml # Production Docker Compose
|
||||||
|
├── nginx.conf # Nginx configuration
|
||||||
|
└── entrypoint.sh # Docker entrypoint script
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 User Roles & Permissions
|
||||||
|
|
||||||
|
### Default User Groups
|
||||||
|
|
||||||
|
- **admin**: Full administrative access
|
||||||
|
- **staff**: Limited administrative access
|
||||||
|
- **user**: Basic user permissions
|
||||||
|
|
||||||
|
### Usage in Views
|
||||||
|
|
||||||
|
```python
|
||||||
|
from apps.core.views import admin_required, staff_required
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def admin_only_view(request):
|
||||||
|
return render(request, 'admin_only.html')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@staff_required
|
||||||
|
def staff_view(request):
|
||||||
|
return render(request, 'staff.html')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage in Templates
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% if user.has_role:'admin' %}
|
||||||
|
<a href="{% url 'admin_dashboard' %}">Admin Panel</a>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create default user groups
|
||||||
|
python manage.py create_groups
|
||||||
|
|
||||||
|
# Create superuser with admin role
|
||||||
|
python manage.py create_superuser --email admin@example.com --password admin123
|
||||||
|
|
||||||
|
# Standard Django commands
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py collectstatic
|
||||||
|
python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
docker-compose exec web python manage.py test
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
docker-compose exec web coverage run --source='.' manage.py test
|
||||||
|
docker-compose exec web coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 API Documentation
|
||||||
|
|
||||||
|
The project is ready for API development. Consider adding:
|
||||||
|
|
||||||
|
- Django REST Framework
|
||||||
|
- API documentation with drf-yasg
|
||||||
|
- Authentication tokens
|
||||||
|
- Throttling and permissions
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
- Create an issue for bug reports or feature requests
|
||||||
|
- Check the Django documentation: https://docs.djangoproject.com/
|
||||||
|
- Django Allauth documentation: https://django-allauth.readthedocs.io/
|
||||||
|
|
||||||
|
## 🎉 What's Next?
|
||||||
|
|
||||||
|
Consider adding these features:
|
||||||
|
|
||||||
|
- [ ] API with Django REST Framework
|
||||||
|
- [ ] Celery for background tasks
|
||||||
|
- [ ] Monitoring with Sentry
|
||||||
|
- [ ] CI/CD pipeline
|
||||||
|
- [ ] Advanced user profiles
|
||||||
|
- [ ] Multi-tenant support
|
||||||
|
- [ ] Internationalization (i18n)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy coding!** 🚀
|
||||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
48
apps/accounts/admin.py
Normal file
48
apps/accounts/admin.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from .models import User, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileInline(admin.StackedInline):
|
||||||
|
model = UserProfile
|
||||||
|
can_delete = False
|
||||||
|
verbose_name_plural = 'Profile'
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
inlines = (UserProfileInline,)
|
||||||
|
list_display = ('email', 'username', 'first_name', 'last_name', 'is_staff', 'is_verified', 'created_at')
|
||||||
|
list_filter = ('is_staff', 'is_superuser', 'is_active', 'is_verified', 'groups')
|
||||||
|
search_fields = ('email', 'username', 'first_name', 'last_name')
|
||||||
|
ordering = ('email',)
|
||||||
|
filter_horizontal = ('groups', 'user_permissions')
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email', 'password')}),
|
||||||
|
('Personal info', {'fields': ('username', 'first_name', 'last_name')}),
|
||||||
|
('Permissions', {
|
||||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'is_verified', 'groups', 'user_permissions'),
|
||||||
|
}),
|
||||||
|
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'username', 'first_name', 'last_name', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_inline_instances(self, request, obj=None):
|
||||||
|
if not obj:
|
||||||
|
return list()
|
||||||
|
return super().get_inline_instances(request, obj)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserProfile)
|
||||||
|
class UserProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'location', 'birth_date')
|
||||||
|
search_fields = ('user__email', 'user__first_name', 'user__last_name')
|
||||||
|
list_filter = ('location',)
|
||||||
9
apps/accounts/apps.py
Normal file
9
apps/accounts/apps.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.accounts'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import apps.accounts.signals
|
||||||
0
apps/accounts/management/__init__.py
Normal file
0
apps/accounts/management/__init__.py
Normal file
0
apps/accounts/management/commands/__init__.py
Normal file
0
apps/accounts/management/commands/__init__.py
Normal file
53
apps/accounts/management/commands/create_groups.py
Normal file
53
apps/accounts/management/commands/create_groups.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from apps.accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Create default user groups with appropriate permissions'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
groups_permissions = {
|
||||||
|
'admin': [
|
||||||
|
'add_user', 'change_user', 'delete_user', 'view_user',
|
||||||
|
'add_group', 'change_group', 'delete_group', 'view_group',
|
||||||
|
],
|
||||||
|
'staff': [
|
||||||
|
'view_user', 'change_user',
|
||||||
|
'view_group',
|
||||||
|
],
|
||||||
|
'user': [
|
||||||
|
'view_user',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for group_name, permissions in groups_permissions.items():
|
||||||
|
group, created = Group.objects.get_or_create(name=group_name)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Created group: {group_name}')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(f'Group already exists: {group_name}')
|
||||||
|
)
|
||||||
|
|
||||||
|
for permission_name in permissions:
|
||||||
|
try:
|
||||||
|
permission = Permission.objects.get(codename=permission_name)
|
||||||
|
group.permissions.add(permission)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f'Permission not found: {permission_name}')
|
||||||
|
)
|
||||||
|
|
||||||
|
group.save()
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'Configured permissions for {group_name}')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS('Successfully created all default groups')
|
||||||
|
)
|
||||||
43
apps/accounts/management/commands/create_superuser.py
Normal file
43
apps/accounts/management/commands/create_superuser.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from apps.accounts.models import User
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Create a superuser and assign admin role'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--email', type=str, help='Superuser email')
|
||||||
|
parser.add_argument('--password', type=str, help='Superuser password')
|
||||||
|
parser.add_argument('--first_name', type=str, help='First name', default='Admin')
|
||||||
|
parser.add_argument('--last_name', type=str, help='Last name', default='User')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
email = options.get('email') or config('SUPERUSER_EMAIL', default='admin@example.com')
|
||||||
|
password = options.get('password') or config('SUPERUSER_PASSWORD', default='admin123')
|
||||||
|
first_name = options.get('first_name', 'Admin')
|
||||||
|
last_name = options.get('last_name', 'User')
|
||||||
|
|
||||||
|
if User.objects.filter(email=email).exists():
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.WARNING(f'User with email {email} already exists')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
user = User.objects.create_superuser(
|
||||||
|
email=email,
|
||||||
|
username=email,
|
||||||
|
password=password,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name
|
||||||
|
)
|
||||||
|
|
||||||
|
admin_group, created = Group.objects.get_or_create(name='admin')
|
||||||
|
user.groups.add(admin_group)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f'Successfully created superuser: {email} with admin role'
|
||||||
|
)
|
||||||
|
)
|
||||||
59
apps/accounts/migrations/0001_initial.py
Normal file
59
apps/accounts/migrations/0001_initial.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-09-10 18:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('first_name', models.CharField(max_length=30)),
|
||||||
|
('last_name', models.CharField(max_length=30)),
|
||||||
|
('is_verified', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'auth_user',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('bio', models.TextField(blank=True, max_length=500)),
|
||||||
|
('location', models.CharField(blank=True, max_length=30)),
|
||||||
|
('birth_date', models.DateField(blank=True, null=True)),
|
||||||
|
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')),
|
||||||
|
('phone_number', models.CharField(blank=True, max_length=15)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/accounts/migrations/__init__.py
Normal file
0
apps/accounts/migrations/__init__.py
Normal file
50
apps/accounts/models.py
Normal file
50
apps/accounts/models.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser, Group
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
first_name = models.CharField(max_length=30)
|
||||||
|
last_name = models.CharField(max_length=30)
|
||||||
|
is_verified = models.BooleanField(default=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'auth_user'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}".strip()
|
||||||
|
|
||||||
|
def has_role(self, role_name):
|
||||||
|
return self.groups.filter(name=role_name).exists()
|
||||||
|
|
||||||
|
def assign_role(self, role_name):
|
||||||
|
group, created = Group.objects.get_or_create(name=role_name)
|
||||||
|
self.groups.add(group)
|
||||||
|
|
||||||
|
def remove_role(self, role_name):
|
||||||
|
try:
|
||||||
|
group = Group.objects.get(name=role_name)
|
||||||
|
self.groups.remove(group)
|
||||||
|
except Group.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||||
|
bio = models.TextField(max_length=500, blank=True)
|
||||||
|
location = models.CharField(max_length=30, blank=True)
|
||||||
|
birth_date = models.DateField(null=True, blank=True)
|
||||||
|
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||||
|
phone_number = models.CharField(max_length=15, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.email} Profile"
|
||||||
19
apps/accounts/signals.py
Normal file
19
apps/accounts/signals.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from .models import User, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def create_user_profile(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
UserProfile.objects.create(user=instance)
|
||||||
|
|
||||||
|
default_group, _ = Group.objects.get_or_create(name='user')
|
||||||
|
instance.groups.add(default_group)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
|
if hasattr(instance, 'profile'):
|
||||||
|
instance.profile.save()
|
||||||
10
apps/accounts/urls.py
Normal file
10
apps/accounts/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'accounts'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||||
|
path('profile/edit/', views.ProfileUpdateView.as_view(), name='profile_edit'),
|
||||||
|
path('dashboard/', views.dashboard, name='dashboard'),
|
||||||
|
]
|
||||||
40
apps/accounts/views.py
Normal file
40
apps/accounts/views.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.views.generic import TemplateView, UpdateView
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from .models import User, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'account/profile.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['user'] = self.request.user
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
|
model = UserProfile
|
||||||
|
template_name = 'account/profile_edit.html'
|
||||||
|
fields = ['bio', 'location', 'birth_date', 'avatar', 'phone_number']
|
||||||
|
success_url = reverse_lazy('accounts:profile')
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
profile, created = UserProfile.objects.get_or_create(user=self.request.user)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
messages.success(self.request, 'Your profile has been updated successfully!')
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
context = {
|
||||||
|
'user': request.user,
|
||||||
|
'user_groups': request.user.groups.all(),
|
||||||
|
}
|
||||||
|
return render(request, 'account/dashboard.html', context)
|
||||||
0
apps/core/__init__.py
Normal file
0
apps/core/__init__.py
Normal file
6
apps/core/apps.py
Normal file
6
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'
|
||||||
10
apps/core/urls.py
Normal file
10
apps/core/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'core'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.home, name='home'),
|
||||||
|
path('admin-dashboard/', views.admin_dashboard, name='admin_dashboard'),
|
||||||
|
path('staff-dashboard/', views.staff_dashboard, name='staff_dashboard'),
|
||||||
|
]
|
||||||
53
apps/core/views.py
Normal file
53
apps/core/views.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.http import HttpResponseForbidden
|
||||||
|
|
||||||
|
|
||||||
|
def home(request):
|
||||||
|
return render(request, 'home.html')
|
||||||
|
|
||||||
|
|
||||||
|
class HomeView(TemplateView):
|
||||||
|
template_name = 'home.html'
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRequiredMixin(UserPassesTestMixin):
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.has_role('admin')
|
||||||
|
|
||||||
|
|
||||||
|
class StaffRequiredMixin(UserPassesTestMixin):
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.has_role('staff') or self.request.user.has_role('admin')
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(function):
|
||||||
|
def wrap(request, *args, **kwargs):
|
||||||
|
if request.user.has_role('admin'):
|
||||||
|
return function(request, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return HttpResponseForbidden('Admin access required')
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def staff_required(function):
|
||||||
|
def wrap(request, *args, **kwargs):
|
||||||
|
if request.user.has_role('staff') or request.user.has_role('admin'):
|
||||||
|
return function(request, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return HttpResponseForbidden('Staff access required')
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def admin_dashboard(request):
|
||||||
|
return render(request, 'admin_dashboard.html')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@staff_required
|
||||||
|
def staff_dashboard(request):
|
||||||
|
return render(request, 'staff_dashboard.html')
|
||||||
0
django_project/__init__.py
Normal file
0
django_project/__init__.py
Normal file
0
django_project/settings/__init__.py
Normal file
0
django_project/settings/__init__.py
Normal file
180
django_project/settings/base.py
Normal file
180
django_project/settings/base.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
Base settings for Django project.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from decouple import config
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
|
SECRET_KEY = config('SECRET_KEY', default='django-insecure-change-this-in-production')
|
||||||
|
|
||||||
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||||
|
|
||||||
|
DJANGO_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
]
|
||||||
|
|
||||||
|
THIRD_PARTY_APPS = [
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
'allauth.socialaccount.providers.google',
|
||||||
|
'allauth.socialaccount.providers.facebook',
|
||||||
|
'whitenoise.runserver_nostatic',
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCAL_APPS = [
|
||||||
|
'apps.accounts',
|
||||||
|
'apps.core',
|
||||||
|
]
|
||||||
|
|
||||||
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'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',
|
||||||
|
'allauth.account.middleware.AccountMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'django_project.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 = 'django_project.wsgi.application'
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': config('DB_NAME', default='django_db'),
|
||||||
|
'USER': config('DB_USER', default='django_user'),
|
||||||
|
'PASSWORD': config('DB_PASSWORD', default='django_password'),
|
||||||
|
'HOST': config('DB_HOST', default='localhost'),
|
||||||
|
'PORT': config('DB_PORT', default='5432'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'accounts.User'
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
'allauth.account.auth_backends.AuthenticationBackend',
|
||||||
|
]
|
||||||
|
|
||||||
|
ACCOUNT_AUTHENTICATION_METHOD = 'email'
|
||||||
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
|
ACCOUNT_USERNAME_REQUIRED = False
|
||||||
|
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
|
||||||
|
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
|
||||||
|
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||||
|
ACCOUNT_LOGOUT_ON_GET = True
|
||||||
|
ACCOUNT_SESSION_REMEMBER = True
|
||||||
|
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True
|
||||||
|
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
|
||||||
|
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 300
|
||||||
|
|
||||||
|
LOGIN_URL = '/accounts/login/'
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
|
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
|
||||||
|
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||||
|
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
|
||||||
|
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
|
||||||
|
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||||
|
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||||
|
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='noreply@example.com')
|
||||||
|
|
||||||
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
|
'google': {
|
||||||
|
'SCOPE': [
|
||||||
|
'profile',
|
||||||
|
'email',
|
||||||
|
],
|
||||||
|
'AUTH_PARAMS': {
|
||||||
|
'access_type': 'online',
|
||||||
|
},
|
||||||
|
'OAUTH_PKCE_ENABLED': True,
|
||||||
|
},
|
||||||
|
'facebook': {
|
||||||
|
'METHOD': 'oauth2',
|
||||||
|
'SCOPE': ['email', 'public_profile'],
|
||||||
|
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
|
||||||
|
'INIT_PARAMS': {'cookie': True},
|
||||||
|
'FIELDS': [
|
||||||
|
'id',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'middle_name',
|
||||||
|
'name',
|
||||||
|
'name_format',
|
||||||
|
'picture',
|
||||||
|
'short_name'
|
||||||
|
],
|
||||||
|
'EXCHANGE_TOKEN': True,
|
||||||
|
'LOCALE_FUNC': 'path.to.callable',
|
||||||
|
'VERIFIED_EMAIL': False,
|
||||||
|
'VERSION': 'v7.0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCIALACCOUNT_LOGIN_ON_GET = True
|
||||||
41
django_project/settings/development.py
Normal file
41
django_project/settings/development.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Development settings for Django project.
|
||||||
|
"""
|
||||||
|
from .base import *
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': config('DB_NAME', default='django_db'),
|
||||||
|
'USER': config('DB_USER', default='django_user'),
|
||||||
|
'PASSWORD': config('DB_PASSWORD', default='django_password'),
|
||||||
|
'HOST': config('DB_HOST', default='db'),
|
||||||
|
'PORT': config('DB_PORT', default='5432'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
57
django_project/settings/production.py
Normal file
57
django_project/settings/production.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Production settings for Django project.
|
||||||
|
"""
|
||||||
|
from .base import *
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||||
|
|
||||||
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
SECURE_HSTS_SECONDS = 31536000
|
||||||
|
SECURE_REDIRECT_EXEMPT = []
|
||||||
|
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool)
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
|
||||||
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'file': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'filename': '/var/log/django/django.log',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
'console': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers': ['file', 'console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
16
django_project/urls.py
Normal file
16
django_project/urls.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""django_project URL Configuration"""
|
||||||
|
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('accounts/', include('allauth.urls')),
|
||||||
|
path('accounts/', include('apps.accounts.urls')),
|
||||||
|
path('', include('apps.core.urls')),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
16
django_project/wsgi.py
Normal file
16
django_project/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for django_project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings.production')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
51
docker-compose.prod.yml
Normal file
51
docker-compose.prod.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: production
|
||||||
|
command: >
|
||||||
|
sh -c "chmod +x /app/entrypoint.sh &&
|
||||||
|
/app/entrypoint.sh &&
|
||||||
|
gunicorn --bind 0.0.0.0:8000 --workers 3 django_project.wsgi:application"
|
||||||
|
volumes:
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=django_project.settings.production
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${DB_NAME:-django_db}
|
||||||
|
- POSTGRES_USER=${DB_USER:-django_user}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD:-django_password}
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.prod.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
- ./ssl:/etc/nginx/ssl
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static_volume:
|
||||||
|
media_volume:
|
||||||
53
docker-compose.yml
Normal file
53
docker-compose.yml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: development
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=django_project.settings.development
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${DB_NAME:-django_db}
|
||||||
|
- POSTGRES_USER=${DB_USER:-django_user}
|
||||||
|
- POSTGRES_PASSWORD=${DB_PASSWORD:-django_password}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
- static_volume:/app/staticfiles
|
||||||
|
- media_volume:/app/media
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
profiles:
|
||||||
|
- production
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static_volume:
|
||||||
|
media_volume:
|
||||||
143
dokploy.json
Normal file
143
dokploy.json
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"name": "django-template",
|
||||||
|
"description": "Django boilerplate with authentication and Docker support",
|
||||||
|
"type": "application",
|
||||||
|
"buildType": "dockerfile",
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"buildArgs": {
|
||||||
|
"TARGET": "production"
|
||||||
|
},
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"key": "SECRET_KEY",
|
||||||
|
"value": "your-production-secret-key-here",
|
||||||
|
"description": "Django secret key for production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DEBUG",
|
||||||
|
"value": "False",
|
||||||
|
"description": "Set to False in production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ALLOWED_HOSTS",
|
||||||
|
"value": "your-domain.com,www.your-domain.com",
|
||||||
|
"description": "Comma-separated list of allowed hosts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DB_NAME",
|
||||||
|
"value": "django_db",
|
||||||
|
"description": "PostgreSQL database name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DB_USER",
|
||||||
|
"value": "django_user",
|
||||||
|
"description": "PostgreSQL database user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DB_PASSWORD",
|
||||||
|
"value": "secure_password_here",
|
||||||
|
"description": "PostgreSQL database password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DB_HOST",
|
||||||
|
"value": "db",
|
||||||
|
"description": "PostgreSQL database host"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DB_PORT",
|
||||||
|
"value": "5432",
|
||||||
|
"description": "PostgreSQL database port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_BACKEND",
|
||||||
|
"value": "django.core.mail.backends.smtp.EmailBackend",
|
||||||
|
"description": "Email backend for production"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST",
|
||||||
|
"value": "smtp.gmail.com",
|
||||||
|
"description": "SMTP server host"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_PORT",
|
||||||
|
"value": "587",
|
||||||
|
"description": "SMTP server port"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_USE_TLS",
|
||||||
|
"value": "True",
|
||||||
|
"description": "Use TLS for email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_USER",
|
||||||
|
"value": "your-email@gmail.com",
|
||||||
|
"description": "Email account username"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_PASSWORD",
|
||||||
|
"value": "your-app-password",
|
||||||
|
"description": "Email account password or app password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DEFAULT_FROM_EMAIL",
|
||||||
|
"value": "noreply@your-domain.com",
|
||||||
|
"description": "Default from email address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_OAUTH2_CLIENT_ID",
|
||||||
|
"value": "your-google-client-id",
|
||||||
|
"description": "Google OAuth2 client ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_OAUTH2_CLIENT_SECRET",
|
||||||
|
"value": "your-google-client-secret",
|
||||||
|
"description": "Google OAuth2 client secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "FACEBOOK_APP_ID",
|
||||||
|
"value": "your-facebook-app-id",
|
||||||
|
"description": "Facebook app ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "FACEBOOK_APP_SECRET",
|
||||||
|
"value": "your-facebook-app-secret",
|
||||||
|
"description": "Facebook app secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SECURE_SSL_REDIRECT",
|
||||||
|
"value": "True",
|
||||||
|
"description": "Force HTTPS redirects in production"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"containerPort": 8000,
|
||||||
|
"hostPort": 80,
|
||||||
|
"protocol": "tcp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"name": "static_files",
|
||||||
|
"mountPath": "/app/staticfiles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "media_files",
|
||||||
|
"mountPath": "/app/media"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/",
|
||||||
|
"port": 8000,
|
||||||
|
"interval": 30,
|
||||||
|
"timeout": 10,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "postgresql",
|
||||||
|
"version": "15",
|
||||||
|
"name": "django_db",
|
||||||
|
"username": "django_user",
|
||||||
|
"password": "secure_password_here"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
entrypoint.sh
Executable file
17
entrypoint.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
echo "Running migrations..."
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
|
echo "Creating default groups..."
|
||||||
|
python manage.py create_groups
|
||||||
|
|
||||||
|
echo "Collecting static files..."
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
16
manage.py
Normal file
16
manage.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings.development')
|
||||||
|
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)
|
||||||
30
nginx.conf
Normal file
30
nginx.conf
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
upstream django {
|
||||||
|
server web:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://django;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /app/staticfiles/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
alias /app/media/;
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 20M;
|
||||||
|
}
|
||||||
51
nginx.prod.conf
Normal file
51
nginx.prod.conf
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
upstream django {
|
||||||
|
server web:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com www.your-domain.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name your-domain.com www.your-domain.com;
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://django;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /app/staticfiles/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
alias /app/media/;
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 20M;
|
||||||
|
}
|
||||||
7
requirements/base.txt
Normal file
7
requirements/base.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Django==4.2.7
|
||||||
|
django-allauth==0.57.0
|
||||||
|
python-decouple==3.8
|
||||||
|
psycopg2-binary==2.9.7
|
||||||
|
Pillow==10.0.1
|
||||||
|
whitenoise==6.6.0
|
||||||
|
gunicorn==21.2.0
|
||||||
4
requirements/development.txt
Normal file
4
requirements/development.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-r base.txt
|
||||||
|
|
||||||
|
django-debug-toolbar==4.2.0
|
||||||
|
django-extensions==3.2.3
|
||||||
3
requirements/production.txt
Normal file
3
requirements/production.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-r base.txt
|
||||||
|
|
||||||
|
sentry-sdk[django]==1.38.0
|
||||||
57
static/css/style.css
Normal file
57
static/css/style.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/* Custom styles for Django Boilerplate */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
transition: box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
22
static/js/main.js
Normal file
22
static/js/main.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Custom JavaScript for Django Boilerplate
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(function(alert) {
|
||||||
|
const bsAlert = new bootstrap.Alert(alert);
|
||||||
|
bsAlert.close();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Add smooth scrolling to anchor links
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
83
templates/account/dashboard.html
Normal file
83
templates/account/dashboard.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>Welcome to Your Dashboard</h1>
|
||||||
|
<p class="lead">Hello, <strong>{{ user.full_name|default:user.email }}</strong>!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Account Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Email:</strong> {{ user.email }}</p>
|
||||||
|
<p><strong>Name:</strong> {{ user.full_name|default:"Not set" }}</p>
|
||||||
|
<p><strong>Username:</strong> {{ user.username }}</p>
|
||||||
|
<p><strong>Member since:</strong> {{ user.date_joined|date:"M d, Y" }}</p>
|
||||||
|
<p><strong>Email verified:</strong>
|
||||||
|
{% if user.is_verified %}
|
||||||
|
<span class="badge bg-success">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'accounts:profile_edit' %}" class="btn btn-primary">Edit Profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Your Roles</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if user_groups %}
|
||||||
|
{% for group in user_groups %}
|
||||||
|
<span class="badge bg-secondary me-1">{{ group.name|title }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No roles assigned</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h6>Access Levels:</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>
|
||||||
|
<i class="text-success">✓</i> User Dashboard
|
||||||
|
</li>
|
||||||
|
{% if user.has_role:'staff' or user.has_role:'admin' %}
|
||||||
|
<li>
|
||||||
|
<i class="text-success">✓</i>
|
||||||
|
<a href="{% url 'core:staff_dashboard' %}">Staff Dashboard</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<i class="text-muted">✗</i> Staff Dashboard
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.has_role:'admin' %}
|
||||||
|
<li>
|
||||||
|
<i class="text-success">✓</i>
|
||||||
|
<a href="{% url 'core:admin_dashboard' %}">Admin Dashboard</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<i class="text-muted">✗</i> Admin Dashboard
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
49
templates/account/login.html
Normal file
49
templates/account/login.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load socialaccount %}
|
||||||
|
|
||||||
|
{% block title %}Login - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Login to Your Account</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h6>Or login with:</h6>
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||||
|
<a href="{% provider_login_url 'google' %}" class="btn btn-outline-danger">
|
||||||
|
Google
|
||||||
|
</a>
|
||||||
|
<a href="{% provider_login_url 'facebook' %}" class="btn btn-outline-primary">
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p>
|
||||||
|
Don't have an account?
|
||||||
|
<a href="{% url 'account_signup' %}">Sign up here</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'account_reset_password' %}">Forgot your password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
69
templates/account/profile.html
Normal file
69
templates/account/profile.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Profile - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5>Your Profile</h5>
|
||||||
|
<a href="{% url 'accounts:profile_edit' %}" class="btn btn-primary btn-sm">Edit Profile</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
{% if user.profile.avatar %}
|
||||||
|
<img src="{{ user.profile.avatar.url }}" class="img-thumbnail" width="150" height="150">
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-secondary d-flex align-items-center justify-content-center" style="width: 150px; height: 150px;">
|
||||||
|
<i class="text-white fs-1">👤</i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h4>{{ user.full_name|default:user.email }}</h4>
|
||||||
|
<p class="text-muted">{{ user.email }}</p>
|
||||||
|
|
||||||
|
{% if user.profile.bio %}
|
||||||
|
<p><strong>Bio:</strong> {{ user.profile.bio }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.profile.location %}
|
||||||
|
<p><strong>Location:</strong> {{ user.profile.location }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.profile.birth_date %}
|
||||||
|
<p><strong>Birth Date:</strong> {{ user.profile.birth_date }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.profile.phone_number %}
|
||||||
|
<p><strong>Phone:</strong> {{ user.profile.phone_number }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><strong>Member since:</strong> {{ user.date_joined|date:"M d, Y" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Account Status</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>Email verified:</strong>
|
||||||
|
{% if user.is_verified %}
|
||||||
|
<span class="badge bg-success">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p><strong>Account type:</strong> {{ user.groups.first.name|default:"User"|title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
25
templates/account/profile_edit.html
Normal file
25
templates/account/profile_edit.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Edit Profile - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Edit Your Profile</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'accounts:profile' %}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
46
templates/account/signup.html
Normal file
46
templates/account/signup.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load socialaccount %}
|
||||||
|
|
||||||
|
{% block title %}Sign Up - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">Create Your Account</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h6>Or sign up with:</h6>
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||||
|
<a href="{% provider_login_url 'google' %}" class="btn btn-outline-danger">
|
||||||
|
Google
|
||||||
|
</a>
|
||||||
|
<a href="{% provider_login_url 'facebook' %}" class="btn btn-outline-primary">
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p>
|
||||||
|
Already have an account?
|
||||||
|
<a href="{% url 'account_login' %}">Login here</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
70
templates/admin_dashboard.html
Normal file
70
templates/admin_dashboard.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Admin Dashboard - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>🔐 Admin Dashboard</h1>
|
||||||
|
<p class="lead">Administrative control panel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">User Management</h5>
|
||||||
|
<p class="card-text">Manage user accounts, roles, and permissions</p>
|
||||||
|
<a href="/admin/accounts/user/" class="btn btn-primary">Manage Users</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Group Management</h5>
|
||||||
|
<p class="card-text">Configure user groups and permissions</p>
|
||||||
|
<a href="/admin/auth/group/" class="btn btn-primary">Manage Groups</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Site Administration</h5>
|
||||||
|
<p class="card-text">Access full Django admin interface</p>
|
||||||
|
<a href="/admin/" class="btn btn-primary">Django Admin</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>System Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Current User</h6>
|
||||||
|
<p><strong>Name:</strong> {{ user.full_name|default:user.email }}</p>
|
||||||
|
<p><strong>Role:</strong> Administrator</p>
|
||||||
|
<p><strong>Last Login:</strong> {{ user.last_login|date:"M d, Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Quick Stats</h6>
|
||||||
|
<p><strong>Total Users:</strong> <span class="badge bg-info">{{ users_count|default:"N/A" }}</span></p>
|
||||||
|
<p><strong>Active Sessions:</strong> <span class="badge bg-success">{{ active_sessions|default:"N/A" }}</span></p>
|
||||||
|
<p><strong>System Status:</strong> <span class="badge bg-success">Online</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
93
templates/base.html
Normal file
93
templates/base.html
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Django Boilerplate{% endblock %}</title>
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'core:home' %}">Django App</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'core:home' %}">Home</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'accounts:dashboard' %}">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
{% if user.has_role %}
|
||||||
|
{% if user.has_role:'staff' or user.has_role:'admin' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'core:staff_dashboard' %}">Staff</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.has_role:'admin' %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'core:admin_dashboard' %}">Admin</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
|
{{ user.full_name|default:user.email }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'accounts:profile' %}">Profile</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'account_login' %}">Login</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'account_signup' %}">Sign Up</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container mt-4">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-dark text-light text-center py-3 mt-5">
|
||||||
|
<div class="container">
|
||||||
|
<p>© 2024 Django Boilerplate. Built with Django & Bootstrap.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{% static 'js/main.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
69
templates/home.html
Normal file
69
templates/home.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Home - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="jumbotron bg-light p-5 rounded">
|
||||||
|
<h1 class="display-4">Welcome to Django Boilerplate</h1>
|
||||||
|
<p class="lead">
|
||||||
|
A production-ready Django starter template with authentication, social login,
|
||||||
|
role-based permissions, and Docker support.
|
||||||
|
</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p>Hello, <strong>{{ user.full_name|default:user.email }}</strong>!</p>
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'accounts:dashboard' %}" role="button">
|
||||||
|
Go to Dashboard
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<p>Get started by creating an account or logging in.</p>
|
||||||
|
<a class="btn btn-primary btn-lg me-3" href="{% url 'account_signup' %}" role="button">
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-outline-primary btn-lg" href="{% url 'account_login' %}" role="button">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">🔐 Authentication</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Complete authentication system with email verification,
|
||||||
|
password reset, and social login (Google & Facebook).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">👥 Role-Based Access</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Built-in user group system with admin, staff, and user roles
|
||||||
|
for granular permission control.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">🐳 Docker Ready</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
Fully containerized with Docker Compose, PostgreSQL database,
|
||||||
|
and production-ready configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
62
templates/staff_dashboard.html
Normal file
62
templates/staff_dashboard.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Staff Dashboard - Django Boilerplate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>👨💼 Staff Dashboard</h1>
|
||||||
|
<p class="lead">Staff management panel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">User Support</h5>
|
||||||
|
<p class="card-text">Assist users with account and technical issues</p>
|
||||||
|
<a href="#" class="btn btn-primary">Support Center</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">Content Management</h5>
|
||||||
|
<p class="card-text">Manage and moderate platform content</p>
|
||||||
|
<a href="#" class="btn btn-primary">Content Panel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Staff Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Current User</h6>
|
||||||
|
<p><strong>Name:</strong> {{ user.full_name|default:user.email }}</p>
|
||||||
|
<p><strong>Role:</strong> Staff Member</p>
|
||||||
|
<p><strong>Department:</strong> {{ user.profile.department|default:"General" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Quick Actions</h6>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-outline-primary btn-sm">View Reports</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm">Export Data</button>
|
||||||
|
<button class="btn btn-outline-info btn-sm">Send Notifications</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in New Issue
Block a user