updated build/instructions

This commit is contained in:
nvrl committed 2026-02-16 17:02:07 +01:00
1 parent d9aeeaaab8
commit e8ed7bf7ae
12 files changed
+176 -61

No files matched your search

+5 -5
View File
@@ -59,10 +59,10 @@ class Flavor(models.Model):
image_url = models.URLField(max_length=1000, blank=True, null=True)
image = models.ImageField(upload_to='flavors/', blank=True, null=True)
shop_url = models.URLField(max_length=500, blank=True, null=True)
is_available = models.BooleanField(default=True)
is_legacy = models.BooleanField(default=False)
is_available = models.BooleanField(default=True, db_index=True)
is_legacy = models.BooleanField(default=False, db_index=True)
external_id = models.BigIntegerField(unique=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
def __str__(self):
return self.name
@@ -83,9 +83,9 @@ class Flavor(models.Model):
class Rating(models.Model):
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
flavor = models.ForeignKey(Flavor, related_name='ratings', on_delete=models.CASCADE)
score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)], db_index=True)
comment = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
unique_together = ('user', 'flavor')
+7 -3
View File
@@ -4,6 +4,8 @@ from rest_framework.response import Response
from rest_framework.decorators import action
from django.db.models import Avg
from django_filters.rest_framework import DjangoFilterBackend
from django_ratelimit.decorators import ratelimit
from django.utils.decorators import method_decorator
from .models import User, Flavor, Category, Rating, Reply
from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer, ReplySerializer
@@ -32,6 +34,7 @@ class RatingViewSet(viewsets.ModelViewSet):
serializer_class = RatingSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
@method_decorator(ratelimit(key='user', rate='10/m', method='POST', block=True))
def perform_create(self, serializer):
# Ensure user can only have one rating per flavor
flavor = serializer.validated_data['flavor']
@@ -80,6 +83,7 @@ class UserViewSet(viewsets.ModelViewSet):
return super().get_permissions()
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
@method_decorator(ratelimit(key='ip', rate='5/h', method='POST', block=True))
def signup(self, request):
username = request.data.get('username')
email = request.data.get('email')
@@ -146,9 +150,7 @@ class UserViewSet(viewsets.ModelViewSet):
except User.DoesNotExist:
return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
ratings = user.ratings.select_related('flavor', 'flavor__category').exclude(flavor__category__slug='packs-and-other').all()
# Sort by score descending to highlight favorites
ratings = sorted(ratings, key=lambda x: x.score, reverse=True)
ratings = user.ratings.select_related('flavor', 'flavor__category').exclude(flavor__category__slug='packs-and-other').order_by('-score')
return Response({
'username': user.username,
@@ -247,6 +249,7 @@ class UserViewSet(viewsets.ModelViewSet):
return Response({'status': 'Email confirmed', 'email': user.email})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
@method_decorator(ratelimit(key='ip', rate='3/h', method='POST', block=True))
def request_password_reset(self, request):
email = request.data.get('email')
try:
@@ -270,6 +273,7 @@ class UserViewSet(viewsets.ModelViewSet):
return Response({'status': 'If an account exists with this email, a reset code has been sent.'})
@action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny])
@method_decorator(ratelimit(key='ip', rate='5/h', method='POST', block=True))
def complete_password_reset(self, request):
email = request.data.get('email')
code = request.data.get('code')
+1 -1
View File
@@ -20,4 +20,4 @@ python manage.py sync_flavors
# Start the server using Gunicorn
echo "Starting Gunicorn..."
exec gunicorn holy_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3
exec gunicorn holy_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120
+20 -5
View File
@@ -21,12 +21,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-zos99ue2a89!4jrv4g@_b4nn4z!5o*@8k!5cr9!u$j%_@xb=l8'
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-zos99ue2a89!4jrv4g@_b4nn4z!5o*@8k!5cr9!u$j%_@xb=l8')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'true').lower() == 'true'
DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
ALLOWED_HOSTS_STRING = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1,backend')
ALLOWED_HOSTS_STRING = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1,backend,holy.narl.io')
ALLOWED_HOSTS = [host.strip() for host in ALLOWED_HOSTS_STRING.split(',')]
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
'corsheaders',
'django_filters',
'drf_spectacular',
'django_ratelimit',
# Local
'api',
]
@@ -53,6 +54,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'compression_middleware.middleware.CompressionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -141,8 +143,19 @@ USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Production Security
if not DEBUG:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Read CSRF trusted origins from environment variable
CSRF_TRUSTED_ORIGINS_STRING = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:5173')
CSRF_TRUSTED_ORIGINS_STRING = os.environ.get('CSRF_TRUSTED_ORIGINS', 'https://holy.narl.io,http://localhost:5173')
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in CSRF_TRUSTED_ORIGINS_STRING.split(',')]
# Custom User Model
@@ -158,11 +171,13 @@ REST_FRAMEWORK = {
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50,
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
# CORS
CORS_ALLOWED_ORIGINS_STRING = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:5173,http://127.0.0.1:5173')
CORS_ALLOWED_ORIGINS_STRING = os.environ.get('CORS_ALLOWED_ORIGINS', 'https://holy.narl.io,http://localhost:5173,http://127.0.0.1:5173')
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS_STRING.split(',')]
import os
+4 -1
View File
@@ -7,9 +7,12 @@ from django.conf.urls.static import static
from django.views.static import serve
import re
import os
admin_path = os.environ.get('ADMIN_URL', 'admin/')
urlpatterns = [
path('admin/', admin.site.urls),
path(admin_path, admin.site.urls),
path('api/', include('api.urls')),
path('api/token/', obtain_auth_token, name='api_token'),
path('api-auth/', include('rest_framework.urls')),
+2
View File
@@ -7,3 +7,5 @@ Pillow>=10.2.0
drf-spectacular>=0.27.0
whitenoise>=6.6.0
gunicorn>=21.2.0
django-ratelimit>=4.1.0
django-compression-middleware>=0.5.0