diff --git a/.gitignore b/.gitignore index e1dba2e..67588a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /build -*/uploads +*/mediafiles +*/staticfiles __pycache__ *.pyc .venv +.env* *.sqlite3 diff --git a/README.md b/README.md index d7112f5..cc8fb86 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Web backend for hosting a wedding website. -## Run development server: +# Run Local server: ```sh source .venv/bin/activate @@ -10,10 +10,49 @@ cd wedding_site python manage.py runserver ``` -## Do Migrations +# Do Migrations ```sh cd wedding_site python manage.py makemigrations primary python manage.py migrate -``` \ No newline at end of file +``` + +# Run Dev Server in container + +```sh +podman-compose -f compose.yaml up -d --build +``` + +Bring it down with: + +```sh +podman-compose down -v +``` + +# Run Prod Server + +```sh +podman-compose -f compose.prod.yaml up -d --build +``` + +Sync static files: + +```sh +podman-compose -f compose.prod.yaml exec web python manage.py collectstatic --no-input --clear +``` + +Check cert + +```sh +podman-compose -f compose.prod.yaml exec acme-companion /app/cert_status +``` + +Force renew cert: + +```sh +podman-compose -f compose.prod.yaml exec acme-companion /app/force_renew +``` + + + diff --git a/compose.prod.yaml b/compose.prod.yaml new file mode 100644 index 0000000..a60429c --- /dev/null +++ b/compose.prod.yaml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + web: + build: ./wedding_site + command: gunicorn wedding_site.wsgi:application --bind 0.0.0.0:8000 + volumes: + - ./wedding_site/:/usr/src/app/ + - static_volume:/staticfiles + - media_volume:/mediafiles + expose: + - 8000 + env_file: + - ./.env.prod + + nginx-proxy: + container_name: nginx-proxy + build: ./nginx + restart: always + ports: + - 443:443 + - 80:80 + volumes: + - static_volume:/staticfiles + - media_volume:/mediafiles + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + - vhost:/etc/nginx/vhost.d + - /var/run/docker.sock:/tmp/docker.sock:ro + depends_on: + - web + + acme-companion: + image: docker.io/nginxproxy/acme-companion + env_file: + - ./.env.prod.proxy-companion + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + - vhost:/etc/nginx/vhost.d + - acme:/etc/acme.sh + depends_on: + - nginx-proxy + +volumes: + static_volume: + media_volume: + certs: + html: + vhost: + acme: \ No newline at end of file diff --git a/compose.staging.yaml b/compose.staging.yaml new file mode 100644 index 0000000..5d151a9 --- /dev/null +++ b/compose.staging.yaml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + web: + build: ./wedding_site + command: gunicorn wedding_site.wsgi:application --bind 0.0.0.0:8000 + volumes: + - ./wedding_site/:/usr/src/app/ + - static_volume:/staticfiles + - media_volume:/mediafiles + expose: + - 8000 + env_file: + - ./.env.staging + + nginx-proxy: + container_name: nginx-proxy + build: ./nginx + restart: always + ports: + - 443:443 + - 80:80 + volumes: + - static_volume:/staticfiles + - media_volume:/mediafiles + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + - vhost:/etc/nginx/vhost.d + - /var/run/docker.sock:/tmp/docker.sock:ro + depends_on: + - web + + acme-companion: + image: docker.io/nginxproxy/acme-companion + env_file: + - ./.env.staging.proxy-companion + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + - vhost:/etc/nginx/vhost.d + - acme:/etc/acme.sh + depends_on: + - nginx-proxy + +volumes: + static_volume: + media_volume: + certs: + html: + vhost: + acme: \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..ddaa801 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + web: + build: ./wedding_site + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - ./wedding_site/:/usr/src/app/ + ports: + - 8000:8000 + env_file: + - ./.env.dev \ No newline at end of file diff --git a/infra/install_arch_deps.sh b/infra/install_arch_deps.sh new file mode 100644 index 0000000..8937870 --- /dev/null +++ b/infra/install_arch_deps.sh @@ -0,0 +1 @@ +pacman -Sy podman aardvark-dns \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..98aa030 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM docker.io/nginxproxy/nginx-proxy + +COPY vhost.d/default /etc/nginx/vhost.d/default +COPY custom.conf /etc/nginx/conf.d/custom.conf \ No newline at end of file diff --git a/nginx/custom.conf b/nginx/custom.conf new file mode 100644 index 0000000..e9a5e8e --- /dev/null +++ b/nginx/custom.conf @@ -0,0 +1 @@ +client_max_body_size 100M; \ No newline at end of file diff --git a/nginx/vhost.d/default b/nginx/vhost.d/default new file mode 100644 index 0000000..bd8fd4d --- /dev/null +++ b/nginx/vhost.d/default @@ -0,0 +1,9 @@ +location /static/ { + alias /staticfiles/; + add_header Access-Control-Allow-Origin *; +} + +location /media/ { + alias /mediafiles/; + add_header Access-Control-Allow-Origin *; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2e2e3ab..e7584bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1 @@ -django -markdown -pillow -beautifulsoup4 +podman-compose \ No newline at end of file diff --git a/wedding_site/Dockerfile b/wedding_site/Dockerfile new file mode 100644 index 0000000..0b84079 --- /dev/null +++ b/wedding_site/Dockerfile @@ -0,0 +1,19 @@ +# pull official base image +FROM python:3.11.4-slim-buster + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies +RUN pip install --upgrade pip +COPY ./requirements.txt . +RUN pip install -r requirements.txt + +RUN mkdir staticfiles +RUN mkdir mediafiles + +ADD ./mediafiles /mediafiles + +# copy project +COPY . . \ No newline at end of file diff --git a/wedding_site/primary/migrations/0010_page_prority.py b/wedding_site/primary/migrations/0010_page_prority.py new file mode 100644 index 0000000..01223ed --- /dev/null +++ b/wedding_site/primary/migrations/0010_page_prority.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-18 09:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0009_page_pretty_name'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='prority', + field=models.IntegerField(default=0), + ), + ] diff --git a/wedding_site/primary/migrations/0011_rename_prority_page_priority.py b/wedding_site/primary/migrations/0011_rename_prority_page_priority.py new file mode 100644 index 0000000..42ac5dd --- /dev/null +++ b/wedding_site/primary/migrations/0011_rename_prority_page_priority.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-18 10:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('primary', '0010_page_prority'), + ] + + operations = [ + migrations.RenameField( + model_name='page', + old_name='prority', + new_name='priority', + ), + ] diff --git a/wedding_site/primary/models.py b/wedding_site/primary/models.py index a73c009..ac7603e 100644 --- a/wedding_site/primary/models.py +++ b/wedding_site/primary/models.py @@ -18,6 +18,7 @@ class Page(models.Model): pretty_name = models.CharField(max_length=200, default="") published = models.BooleanField(default=False) navigable = models.BooleanField(default=False) + priority = models.IntegerField(default=0) def __str__(self): return self.name diff --git a/wedding_site/primary/static/primary/favicon.ico b/wedding_site/primary/static/primary/favicon.ico new file mode 100755 index 0000000..115ea84 Binary files /dev/null and b/wedding_site/primary/static/primary/favicon.ico differ diff --git a/wedding_site/primary/static/primary/style.css b/wedding_site/primary/static/primary/style.css index 8384158..b1383e3 100755 --- a/wedding_site/primary/static/primary/style.css +++ b/wedding_site/primary/static/primary/style.css @@ -33,9 +33,9 @@ body { img { margin:0 auto; - width: 100%; display: flex; justify-content: center; + max-width:100%; max-height:300px; border-radius: 8px; box-shadow: 0 4px 8px 0; @@ -107,6 +107,10 @@ ul { list-style-position: inside; } +ul li { + padding: 5px 0px; +} + .login-input{ margin-top: 30px; text-align: center; @@ -143,7 +147,7 @@ input[type=submit] { margin-top: 30px; text-align: center; font-size: 20px; - width: 50%; + width: 75%; margin:0 auto; font-family: GlacialIndifference; } diff --git a/wedding_site/primary/templates/header.html b/wedding_site/primary/templates/header.html index 6574036..32bb550 100644 --- a/wedding_site/primary/templates/header.html +++ b/wedding_site/primary/templates/header.html @@ -3,4 +3,5 @@ content="width=device-width, initial-scale=1"> {% load static %} + \ No newline at end of file diff --git a/wedding_site/primary/urls.py b/wedding_site/primary/urls.py index bb59882..2fe363b 100644 --- a/wedding_site/primary/urls.py +++ b/wedding_site/primary/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path("", views.index, name="index"), path('login', views.login_view, name="login"), path("home", views.home, name="home"), + path("afters", views.afters, name="afters"), path("schedule", views.schedule, name="schedule"), path("thingstodo", views.things_to_do, name="thingstodo"), path("travel", views.travel, name="travel"), diff --git a/wedding_site/primary/views.py b/wedding_site/primary/views.py index f9295ff..30ffb5d 100644 --- a/wedding_site/primary/views.py +++ b/wedding_site/primary/views.py @@ -6,6 +6,8 @@ from django.template import Context from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required +from django.views.defaults import page_not_found + import markdown from bs4 import BeautifulSoup @@ -24,6 +26,17 @@ _TEMPLATE = """ """ +def handler404(request, exception, template_name="404.html"): + page_names = ["home", "schedule", "thingstodo", "afters"] + working_path = request.path.lower() + if working_path and working_path[0] == "/": + working_path = working_path[1:] + if working_path and working_path[-1] == "/": + working_path = working_path[:-1] + if working_path in page_names: + return redirect(working_path) + return page_not_found(request, exception, template_name) + def get_site_header(site): template = Template(site.header) context = Context({"site": site}) @@ -46,8 +59,11 @@ def index(request): return HttpResponse(soup.prettify()) def login_view(request): - token = request.POST["token"] - user = authenticate(request, username="guest", password=token) + if request.method == "GET": + token = request.GET["token"] + else: + token = request.POST["token"] + user = authenticate(request, username="guest", password=token.lower()) if user is not None: login(request, user) return redirect("home") @@ -58,9 +74,13 @@ def login_view(request): def home(request): return get_page("Home") +@login_required(login_url="/") +def afters(request): + return get_page("Afters") + def get_page_header(site: Site): - pages = site.page_set.filter(navigable=True) + pages = site.page_set.order_by("priority").filter(navigable=True) template = Template(site.page_header) context = Context({"pages" : pages}) return template.render(context) @@ -87,7 +107,8 @@ def get_page(name:str): img_name = img["src"] db_images = Image.objects.filter(name=img_name) if db_images: - img["src"] = db_images[0].content.url + url = db_images[0].content.url.replace("uploads/","") + img["src"] = url return HttpResponse(soup.prettify()) diff --git a/wedding_site/requirements.txt b/wedding_site/requirements.txt new file mode 100644 index 0000000..2666a26 --- /dev/null +++ b/wedding_site/requirements.txt @@ -0,0 +1,5 @@ +django +markdown +pillow +beautifulsoup4 +gunicorn \ No newline at end of file diff --git a/wedding_site/wedding_site/settings.py b/wedding_site/wedding_site/settings.py index c43b4ba..b9b91cd 100644 --- a/wedding_site/wedding_site/settings.py +++ b/wedding_site/wedding_site/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/ """ from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -19,13 +20,15 @@ BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-bj9fez3qztt5e2lrzpgh%%nat@w^kn!k@l92l=+#%wm)4)p^5m' +SECRET_KEY = os.environ.get("SECRET_KEY") -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +if "DJANGO_DEBUG" in os.environ: + DEBUG = os.environ.get("DJANGO_DEBUG") == 1 +else: + DEBUG=False -ALLOWED_HOSTS = [] +if "DJANGO_ALLOWED_HOSTS" in os.environ: + ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") # Application definition @@ -112,12 +115,29 @@ USE_I18N = True USE_TZ = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +if "CSRF_TRUSTED_ORIGINS" in os.environ: + CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(" ") + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ STATIC_URL = 'static/' +if "DJANGO_STATIC_ROOT" in os.environ: + STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT") +else: + STATIC_ROOT = BASE_DIR / "staticfiles" + +MEDIA_URL = 'media/' + +if "DJANGO_MEDIA_ROOT" in os.environ: + MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT") +else: + MEDIA_ROOT = BASE_DIR / "mediafiles" + # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/wedding_site/wedding_site/urls.py b/wedding_site/wedding_site/urls.py index 6528297..0b74776 100644 --- a/wedding_site/wedding_site/urls.py +++ b/wedding_site/wedding_site/urls.py @@ -19,7 +19,16 @@ from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +import os + +if "DJANGO_ADMIN_PATH" in os.environ: + ADMIN_URL = os.environ.get("DJANGO_ADMIN_PATH") + "/" +else: + ADMIN_URL = "admin/" + urlpatterns = [ path("", include('primary.urls')), - path('admin/', admin.site.urls), + path(ADMIN_URL, admin.site.urls), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +handler404 = 'primary.views.handler404'