mirror of
https://github.com/thecyberlearn/modern-django-starter.git
synced 2026-08-18 07:52:54 +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