Compare commits

...

10 commits

Author SHA1 Message Date
8ed93ebc12 Add some details on cert renewal. 2024-04-08 08:56:33 +01:00
jmsgrogan
06f53e8304 VIew view 2024-02-22 19:00:27 +00:00
jmsgrogan
c4ed1e0792 Fix password check 2024-02-18 22:11:28 +00:00
jmsgrogan
7de5cd52d2 Fix up content 2024-02-18 21:27:08 +00:00
jmsgrogan
7ba077608c Get site running on staging domain. 2024-02-18 17:20:38 +00:00
jmsgrogan
4be5e93c9c Start adding virtual sites with nginx. 2024-02-18 15:04:22 +00:00
jmsgrogan
03778e9834 Fix missing images. 2024-02-18 14:33:44 +00:00
jmsgrogan
2b9dbcbb2d Handle media files. 2024-02-18 14:25:20 +00:00
jmsgrogan
9576202fa1 Start adding deployment infra. 2024-02-18 14:09:12 +00:00
jmsgrogan
288759146c Clean images and case insensitive nav. 2024-02-18 10:51:57 +00:00
22 changed files with 306 additions and 20 deletions

4
.gitignore vendored
View file

@ -1,7 +1,9 @@
/build /build
*/uploads */mediafiles
*/staticfiles
__pycache__ __pycache__
*.pyc *.pyc
.venv .venv
.env*
*.sqlite3 *.sqlite3

View file

@ -2,7 +2,7 @@
Web backend for hosting a wedding website. Web backend for hosting a wedding website.
## Run development server: # Run Local server:
```sh ```sh
source .venv/bin/activate source .venv/bin/activate
@ -10,10 +10,49 @@ cd wedding_site
python manage.py runserver python manage.py runserver
``` ```
## Do Migrations # Do Migrations
```sh ```sh
cd wedding_site cd wedding_site
python manage.py makemigrations primary python manage.py makemigrations primary
python manage.py migrate python manage.py migrate
``` ```
# 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
```

52
compose.prod.yaml Normal file
View file

@ -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:

52
compose.staging.yaml Normal file
View file

@ -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:

12
compose.yaml Normal file
View file

@ -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

View file

@ -0,0 +1 @@
pacman -Sy podman aardvark-dns

4
nginx/Dockerfile Normal file
View file

@ -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

1
nginx/custom.conf Normal file
View file

@ -0,0 +1 @@
client_max_body_size 100M;

9
nginx/vhost.d/default Normal file
View file

@ -0,0 +1,9 @@
location /static/ {
alias /staticfiles/;
add_header Access-Control-Allow-Origin *;
}
location /media/ {
alias /mediafiles/;
add_header Access-Control-Allow-Origin *;
}

View file

@ -1,4 +1 @@
django podman-compose
markdown
pillow
beautifulsoup4

19
wedding_site/Dockerfile Normal file
View file

@ -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 . .

View file

@ -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),
),
]

View file

@ -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',
),
]

View file

@ -18,6 +18,7 @@ class Page(models.Model):
pretty_name = models.CharField(max_length=200, default="") pretty_name = models.CharField(max_length=200, default="")
published = models.BooleanField(default=False) published = models.BooleanField(default=False)
navigable = models.BooleanField(default=False) navigable = models.BooleanField(default=False)
priority = models.IntegerField(default=0)
def __str__(self): def __str__(self):
return self.name return self.name

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -33,9 +33,9 @@ body {
img { img {
margin:0 auto; margin:0 auto;
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
max-width:100%;
max-height:300px; max-height:300px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 8px 0; box-shadow: 0 4px 8px 0;
@ -107,6 +107,10 @@ ul {
list-style-position: inside; list-style-position: inside;
} }
ul li {
padding: 5px 0px;
}
.login-input{ .login-input{
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
@ -143,7 +147,7 @@ input[type=submit] {
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
width: 50%; width: 75%;
margin:0 auto; margin:0 auto;
font-family: GlacialIndifference; font-family: GlacialIndifference;
} }

View file

@ -3,4 +3,5 @@
content="width=device-width, initial-scale=1"> content="width=device-width, initial-scale=1">
{% load static %} {% load static %}
<link rel="shortcut icon" type="image/png" href="{% static 'primary/favicon.ico' %}"/>
<link rel="stylesheet" href="{% static 'primary/style.css' %}"> <link rel="stylesheet" href="{% static 'primary/style.css' %}">

View file

@ -7,6 +7,7 @@ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path('login', views.login_view, name="login"), path('login', views.login_view, name="login"),
path("home", views.home, name="home"), path("home", views.home, name="home"),
path("afters", views.afters, name="afters"),
path("schedule", views.schedule, name="schedule"), path("schedule", views.schedule, name="schedule"),
path("thingstodo", views.things_to_do, name="thingstodo"), path("thingstodo", views.things_to_do, name="thingstodo"),
path("travel", views.travel, name="travel"), path("travel", views.travel, name="travel"),

View file

@ -6,6 +6,8 @@ from django.template import Context
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.defaults import page_not_found
import markdown import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@ -24,6 +26,17 @@ _TEMPLATE = """
</html> </html>
""" """
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): def get_site_header(site):
template = Template(site.header) template = Template(site.header)
context = Context({"site": site}) context = Context({"site": site})
@ -46,8 +59,11 @@ def index(request):
return HttpResponse(soup.prettify()) return HttpResponse(soup.prettify())
def login_view(request): def login_view(request):
token = request.POST["token"] if request.method == "GET":
user = authenticate(request, username="guest", password=token) token = request.GET["token"]
else:
token = request.POST["token"]
user = authenticate(request, username="guest", password=token.lower())
if user is not None: if user is not None:
login(request, user) login(request, user)
return redirect("home") return redirect("home")
@ -58,9 +74,13 @@ def login_view(request):
def home(request): def home(request):
return get_page("Home") return get_page("Home")
@login_required(login_url="/")
def afters(request):
return get_page("Afters")
def get_page_header(site: Site): 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) template = Template(site.page_header)
context = Context({"pages" : pages}) context = Context({"pages" : pages})
return template.render(context) return template.render(context)
@ -87,7 +107,8 @@ def get_page(name:str):
img_name = img["src"] img_name = img["src"]
db_images = Image.objects.filter(name=img_name) db_images = Image.objects.filter(name=img_name)
if db_images: 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()) return HttpResponse(soup.prettify())

View file

@ -0,0 +1,5 @@
django
markdown
pillow
beautifulsoup4
gunicorn

View file

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
""" """
from pathlib import Path from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent 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 # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get("SECRET_KEY")
SECRET_KEY = 'django-insecure-bj9fez3qztt5e2lrzpgh%%nat@w^kn!k@l92l=+#%wm)4)p^5m'
# SECURITY WARNING: don't run with debug turned on in production! if "DJANGO_DEBUG" in os.environ:
DEBUG = True 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 # Application definition
@ -112,12 +115,29 @@ USE_I18N = True
USE_TZ = 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) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/ # https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/' 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 # Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

View file

@ -19,7 +19,16 @@ from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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 = [ urlpatterns = [
path("", include('primary.urls')), path("", include('primary.urls')),
path('admin/', admin.site.urls), path(ADMIN_URL, admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
handler404 = 'primary.views.handler404'