diff --git a/.env.example b/.env.example deleted file mode 100644 index 930af5b..0000000 --- a/.env.example +++ /dev/null @@ -1,55 +0,0 @@ -# Copy this file to .env and fill in the values -# .env is gitignored and must never be committed - -SECRET_KEY=change-me-to-a-long-random-string - -# Set to True only for development -DEBUG=False - -# Comma-separated list of additional allowed hosts (beyond 127.0.0.1 and localhost) -EXTRA_HOSTS=photos.crans.org,photos-dev.crans.org - -# Comma-separated list of admins in "Name:email" format -ADMINS=admin:photos-admin@lists.crans.org - -# Email address used as sender for server emails -SERVER_EMAIL=photos@crans.org - -# Email verification: 'mandatory', 'optional', or 'none' -EMAIL_VERIFICATION=mandatory - -# Mail server settings -SMTP_HOST=localhost -SMTP_PORT=25 -#SMTP_USER= -#SMTP_PASSWORD= -SMTP_USE_TLS=False - -# OAuth2 settings -# Enable OAuth2 login -OAUTH_ENABLED=False -# Disable normal username/password login (requires OAUTH_ENABLED=True) -OAUTH_ONLY=False -# OAuth2 server base URL (e.g. auth.example.com) -#OAUTH_SERVER_URL= -# OAuth2 app credentials -#OAUTH_CLIENT_ID= -#OAUTH_CLIENT_SECRET= -# Button appearance on the login page -#OAUTH_BUTTON_TEXT=Login with OAuth -#OAUTH_BUTTON_IMAGE= -# Space-separated OAuth2 scopes -#OAUTH_SCOPE=openid profile email - -# Database engine: 'sqlite' or 'postgres' -DB_ENGINE=sqlite - -# PostgreSQL settings (only used when DB_ENGINE=postgres) -#DB_NAME=photo21 -#DB_USER=photo21 -#DB_PASSWORD= -#DB_HOST=localhost -#DB_PORT=5432 - -# SQLite settings (only used when DB_ENGINE=sqlite) -#DB_PATH=/app/data/db.sqlite3 diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml deleted file mode 100644 index b5e40dd..0000000 --- a/.forgejo/workflows/docker.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Docker - -on: - push: - branches: - - master - tags: - - 'v*' - -jobs: - build: - runs-on: docker - steps: - - uses: actions/checkout@v3 - - - name: Log in to Codeberg registry - if: startsWith(github.ref, 'refs/tags/') - uses: docker/login-action@v3 - with: - registry: codeberg.org - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Extract version tag - if: startsWith(github.ref, 'refs/tags/') - id: meta - run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Build (verify only, no push) - if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v5 - with: - context: . - push: false - - - name: Build and push (tagged release) - if: startsWith(github.ref, 'refs/tags/') - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - codeberg.org/${{ github.repository }}:${{ steps.meta.outputs.tag }} - codeberg.org/${{ github.repository }}:latest diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 850dd33..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM python:3.11-slim - -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -# Create volume mount points -RUN mkdir -p /app/media /app/static /app/data - -# Collect static files at build time (uses a dummy key, no DB needed) -RUN SECRET_KEY=build-time-placeholder DB_ENGINE=sqlite python manage.py collectstatic --noinput - -EXPOSE 8000 - -RUN chmod +x entrypoint.sh -ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 5878775..d8e6581 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Photo server 2021-2023 -This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay. - [](https://www.gnu.org/licenses/gpl-3.0.txt) This is the source code for the webserver hosting pictures from the @@ -17,6 +15,9 @@ run and to maintain. ```bash sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil + + # Only for production + sudo apt install nginx uwsgi uwsgi-plugin-python3 python3-certbot-nginx ``` 2. **Cloning.** @@ -24,14 +25,25 @@ run and to maintain. In production, we usually use `/var/www/photos/` as the `root` user. ```bash - git clone https://codeberg.org/krek0/photo21.git && cd photo21 + git clone https://gitlab.crans.org/bde/photo21.git && cd photo21 ``` 3. **Configuration (production only).** ```bash + # Only for production sudo mkdir static media + sudo cp docs/maintenance.html static/maintenance.html + sudo chown www-data:www-data -R static media + sudo chmod g+rwx -R static media sudo chmod +x maintenance_tool.sh + sudo cp docs/uwsgi_photos.ini /etc/uwsgi/apps-available/uwsgi_photos.ini + sudo ln -s /etc/uwsgi/apps-available/uwsgi_photos.ini /etc/uwsgi/apps-enabled/ + sudo cp docs/nginx_photos_maintenance /etc/nginx/sites-available/photos.crans.org + sudo ln -s /etc/nginx/sites-available/photos.crans.org /etc/nginx/sites-enabled/ + sudo cp docs/letsencrypt_photos.crans.org /etc/letsencrypt/conf.d/photos.crans.org + sudo cp docs/renewal-hooks_post_nginx /etc/letsencrypt/renewal-hooks/post/nginx + sudo certbot --config /etc/letsencrypt/conf.d/photos.crans.org.ini certonly ``` 4. **Database (production only).** @@ -68,6 +80,7 @@ run and to maintain. 6. *Enjoy \o/* + In production, the NGINX site should now work. In development, you can launch the development server using: ```bash diff --git a/allauth_oauth/__init__.py b/allauth_note_kfet/__init__.py similarity index 69% rename from allauth_oauth/__init__.py rename to allauth_note_kfet/__init__.py index eae4948..6c9e378 100644 --- a/allauth_oauth/__init__.py +++ b/allauth_note_kfet/__init__.py @@ -1,5 +1,3 @@ # This file is part of photo21 # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - -default_app_config = "allauth_oauth.apps.AllauthOAuthConfig" diff --git a/allauth_oauth/provider.py b/allauth_note_kfet/provider.py similarity index 83% rename from allauth_oauth/provider.py rename to allauth_note_kfet/provider.py index 6a6430c..d2ce6a5 100644 --- a/allauth_oauth/provider.py +++ b/allauth_note_kfet/provider.py @@ -7,15 +7,15 @@ from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider -class OAuthAccount(ProviderAccount): +class NoteKfetAccount(ProviderAccount): def to_str(self): return self.account.extra_data.get("username") -class OAuthProvider(OAuth2Provider): - id = "oauth" - name = "OAuth" - account_class = OAuthAccount +class NoteKfetProvider(OAuth2Provider): + id = "notekfet" + name = "Note Kfet" + account_class = NoteKfetAccount def extract_uid(self, data): return str(data["username"]) @@ -39,4 +39,4 @@ class OAuthProvider(OAuth2Provider): return ret -provider_classes = [OAuthProvider] +provider_classes = [NoteKfetProvider] diff --git a/allauth_oauth/urls.py b/allauth_note_kfet/urls.py similarity index 70% rename from allauth_oauth/urls.py rename to allauth_note_kfet/urls.py index 190eac3..e1bf561 100644 --- a/allauth_oauth/urls.py +++ b/allauth_note_kfet/urls.py @@ -4,6 +4,6 @@ from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns -from .provider import OAuthProvider +from .provider import NoteKfetProvider -urlpatterns = default_urlpatterns(OAuthProvider) +urlpatterns = default_urlpatterns(NoteKfetProvider) diff --git a/allauth_oauth/views.py b/allauth_note_kfet/views.py similarity index 77% rename from allauth_oauth/views.py rename to allauth_note_kfet/views.py index 7a568a5..86a16ee 100644 --- a/allauth_oauth/views.py +++ b/allauth_note_kfet/views.py @@ -10,11 +10,11 @@ from allauth.socialaccount.providers.oauth2.views import ( OAuth2LoginView, ) -from .provider import OAuthProvider +from .provider import NoteKfetProvider -class OAuthAdapter(OAuth2Adapter): - provider_id = OAuthProvider.id +class NoteKfetOAuth2Adapter(OAuth2Adapter): + provider_id = NoteKfetProvider.id def complete_login(self, request, app, token, **kwargs): headers = { @@ -31,7 +31,7 @@ class OAuthAdapter(OAuth2Adapter): @property def domain(self): - return self.settings.get("DOMAIN", "") + return self.settings.get("DOMAIN", "note.crans.org") @property def access_token_url(self): @@ -46,5 +46,5 @@ class OAuthAdapter(OAuth2Adapter): return f"https://{self.domain}/api/me/" -oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter) -oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter) +oauth2_login = OAuth2LoginView.adapter_view(NoteKfetOAuth2Adapter) +oauth2_callback = OAuth2CallbackView.adapter_view(NoteKfetOAuth2Adapter) diff --git a/allauth_oauth/apps.py b/allauth_oauth/apps.py deleted file mode 100644 index 69b727a..0000000 --- a/allauth_oauth/apps.py +++ /dev/null @@ -1,12 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.apps import AppConfig - - -class AllauthOAuthConfig(AppConfig): - name = "allauth_oauth" - - def ready(self): - import allauth_oauth.signals # noqa: F401 diff --git a/allauth_oauth/signals.py b/allauth_oauth/signals.py deleted file mode 100644 index d7f1cf6..0000000 --- a/allauth_oauth/signals.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from allauth.socialaccount.signals import pre_social_login -from django.dispatch import receiver - - -@receiver(pre_social_login) -def sync_user_fields(sender, request, sociallogin, **kwargs): - if not sociallogin.is_existing: - return - - user = sociallogin.user - data = sociallogin.account.extra_data - changed = False - - email = data.get("email") - if email and user.email != email: - user.email = email - changed = True - - username = data.get("username") - if username and user.username != username: - user.username = username - changed = True - - if changed: - user.save() diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 37554d0..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -e - -python manage.py migrate --noinput -exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/photo21/forms.py b/photo21/forms.py index 253fab3..7c15e1e 100644 --- a/photo21/forms.py +++ b/photo21/forms.py @@ -13,7 +13,8 @@ class CustomSignupForm(SignupForm): # Add description on email field self.fields["email"].help_text = _( - "Please enter a valid email address ending with `@ens-rennes.fr`" + "Please enter a valid email address ending with `@crans.org` or " + "`@ens-paris-saclay.fr`." ) def clean_email(self): @@ -21,8 +22,10 @@ class CustomSignupForm(SignupForm): Check that the email address ends with a trusted domain. """ email = super().clean_email() - if not email.endswith("@ens-rennes.fr"): + if not email.endswith("@crans.org") and not email.endswith( + "@ens-paris-saclay.fr" + ): raise forms.ValidationError( - _("Must end with `@ens-rennes.fr`.") + _("Must end with `@crans.org` or `@ens-paris-saclay.fr`.") ) return email diff --git a/photo21/settings.py b/photo21/settings.py index ec542ea..7f86536 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -14,7 +14,6 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os -from decouple import Csv, config from django.contrib.messages import constants as messages from django.utils.translation import gettext_lazy as _ @@ -26,12 +25,17 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = config("SECRET_KEY") +SECRET_KEY = "CHANGE ME" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config("DEBUG", default=False, cast=bool) +DEBUG = False -ALLOWED_HOSTS = ["127.0.0.1", "localhost"] + config("EXTRA_HOSTS", default="", cast=Csv()) +ALLOWED_HOSTS = [ + "127.0.0.1", + "localhost", + "photos.crans.org", + "photos-dev.crans.org", +] INTERNAL_IPS = [ "127.0.0.1", @@ -39,16 +43,14 @@ INTERNAL_IPS = [ ] # Admins receive server errors, this is useful to be notified of potential bugs -# Format: "Name:email,Name2:email2" -ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) if a] +ADMINS = [ + ("admin", "photos-admin@lists.crans.org"), +] # Use secure cookies in production SESSION_COOKIE_SECURE = not DEBUG CSRF_COOKIE_SECURE = not DEBUG -# Trust Caddy's forwarded proto header -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - # Remember HTTPS for 1 year SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True @@ -56,15 +58,6 @@ SECURE_HSTS_PRELOAD = True # Application definition -OAUTH_ENABLED = config("OAUTH_ENABLED", default=False, cast=bool) -OAUTH_ONLY = config("OAUTH_ONLY", default=False, cast=bool) -OAUTH_CLIENT_ID = config("OAUTH_CLIENT_ID", default="") -OAUTH_CLIENT_SECRET = config("OAUTH_CLIENT_SECRET", default="") -OAUTH_SERVER_URL = config("OAUTH_SERVER_URL", default="") -OAUTH_BUTTON_TEXT = config("OAUTH_BUTTON_TEXT", default="Login with OAuth") -OAUTH_BUTTON_IMAGE = config("OAUTH_BUTTON_IMAGE", default="") -OAUTH_SCOPE = config("OAUTH_SCOPE", default="openid profile email", cast=Csv(delimiter=" ")) - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.admindocs", @@ -78,20 +71,17 @@ INSTALLED_APPS = [ "allauth", "allauth.account", "allauth.socialaccount", + "allauth_note_kfet", "crispy_forms", "photologue", "photo21", ] -if OAUTH_ENABLED: - INSTALLED_APPS += ["allauth_oauth"] - if DEBUG: INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -136,31 +126,12 @@ WSGI_APPLICATION = "photo21.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases -_db_engine = config("DB_ENGINE", default="sqlite").strip().lower() - -if _db_engine == "postgres": - DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": config("DB_NAME", default="photo21"), - "USER": config("DB_USER", default="photo21"), - "PASSWORD": config("DB_PASSWORD", default=""), - "HOST": config("DB_HOST", default="localhost"), - "PORT": config("DB_PORT", default="5432"), - } +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } -elif _db_engine == "sqlite": - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": config("DB_PATH", default=os.path.join(BASE_DIR, "db.sqlite3")), - "OPTIONS": { - "timeout": 10, - }, - } - } -else: - raise ValueError(f"Unknown DB_ENGINE '{_db_engine}'. Must be 'sqlite' or 'postgres'.") +} CACHES = { "default": { @@ -223,18 +194,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/") MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/" -STORAGES = { - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - "staticfiles": { - "BACKEND": "photo21.storage.CompressedManifestStorage", - }, -} - - -WHITENOISE_MANIFEST_STRICT = False - LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")] FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")] @@ -245,14 +204,9 @@ if DEBUG: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Mail will be sent from this address -SERVER_EMAIL = config("SERVER_EMAIL", default="photos@crans.org") +SERVER_EMAIL = "photos@crans.org" DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>" EMAIL_SUBJECT_PREFIX = "[Serveur photos] " -EMAIL_HOST = config("SMTP_HOST", default="localhost") -EMAIL_PORT = config("SMTP_PORT", default=25, cast=int) -EMAIL_HOST_USER = config("SMTP_USER", default="") -EMAIL_HOST_PASSWORD = config("SMTP_PASSWORD", default="") -EMAIL_USE_TLS = config("SMTP_USE_TLS", default=False, cast=bool) # After login redirect user to transfer page LOGIN_REDIRECT_URL = "/" @@ -272,23 +226,16 @@ MESSAGE_TAGS = { # Allauth configuration ## For the django =< 5.0 ACCOUNT_EMAIL_REQUIRED = True # ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*'] ## For the django =< 5.0 -ACCOUNT_EMAIL_VERIFICATION = config("EMAIL_VERIFICATION", default="mandatory") +ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_AUTHENTICATION_METHOD = "username_email" # ACCOUNT_LOGIN_METHODS = {'username', 'email'} ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} - -if OAUTH_ENABLED: - SOCIALACCOUNT_ONLY = OAUTH_ONLY - SOCIALACCOUNT_PROVIDERS = { - "oauth": { - "SCOPE": OAUTH_SCOPE, - "DOMAIN": OAUTH_SERVER_URL, - "APP": { - "client_id": OAUTH_CLIENT_ID, - "secret": OAUTH_CLIENT_SECRET, - }, - }, - } +SOCIALACCOUNT_PROVIDERS = { + "notekfet": { + # Fetch user profile + "SCOPE": ["1_1"], + }, +} # Use Bootstrap forms CRISPY_TEMPLATE_PACK = "bootstrap4" diff --git a/photo21/static/layout.css b/photo21/static/layout.css index 8e5568e..a0026d1 100644 --- a/photo21/static/layout.css +++ b/photo21/static/layout.css @@ -45,29 +45,6 @@ SPDX-License-Identifier: GPL-3.0-or-later background-color: rgba(163, 163, 163, 0.274); } -/* Gallery - Google Photos style justified grid */ -#lightgallery { - position: relative; -} - -.photo-item { - position: absolute; - overflow: hidden; - visibility: hidden; -} - -.photo-item img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -.photo-item img.photo-private { - outline: 5px solid var(--bs-danger); - outline-offset: -5px; -} - /* Language selector */ .lang-select { border: none; diff --git a/photo21/storage.py b/photo21/storage.py deleted file mode 100644 index fa1b779..0000000 --- a/photo21/storage.py +++ /dev/null @@ -1,12 +0,0 @@ -from whitenoise.storage import CompressedManifestStaticFilesStorage - - -class CompressedManifestStorage(CompressedManifestStaticFilesStorage): - """Like CompressedManifestStaticFilesStorage but silently skips missing - referenced files (e.g. source maps not included in the package).""" - - def hashed_name(self, name, content=None, filename=None): - try: - return super().hashed_name(name, content, filename) - except ValueError: - return name diff --git a/photo21/templates/account/login.html b/photo21/templates/account/login.html index a2bc666..1e7723f 100644 --- a/photo21/templates/account/login.html +++ b/photo21/templates/account/login.html @@ -39,5 +39,5 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Forgot Password?" %} - +
{% trans "If any problem, please contact the server owners at" %} photos[at]crans.org.
{{ request.user.username }} ·
{% endif %}
- {% trans "Source code" %} ·
-
-
-
-
-
-
-
-
-
+ {% trans "Source code" %} ·
+
diff --git a/photo21/templates/index.html b/photo21/templates/index.html
index d4db262..b7fa2e9 100644
--- a/photo21/templates/index.html
+++ b/photo21/templates/index.html
@@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% blocktrans trimmed %} This website aims to collect the pictures and movies taken in the student - life of ENS Rennes or involving its students. + life of ENS Paris-Saclay or involving its students. {% endblocktrans %}
@@ -25,16 +25,21 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}
- {% if not perms.photologue.add_photo %} ++ {% blocktrans trimmed %} + If you want a photo to be deleted, please let us know: + Abuse request + {% endblocktrans %} +
+ {% if not perms.photologue.add_photo %}{% blocktrans trimmed %} If you want to obtain the right to upload pictures, please let us know: - Become a photograph + Become a photograph {% endblocktrans %}
{% endif %} -
{% blocktrans trimmed %}
- This project if a fork of Photo21.
Because we value your privacy, we do not sell the data on this site,
unlike many free online platforms.
-
-
-
-
-
-
-
+ The dedicated server running this website is kindly hosted by the
+ Crans at the ENS Paris-Saclay
+ basement.
+ It is not managed by the Crans. Current active administrators are:
+ {% endblocktrans %}
+ {% for user in superusers %} {{ user.username }}{% endfor %}.
+ {% trans "They should be contacted at" %}
+ photos@crans.org.