Skip to content

Latest commit

 

History

719 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Conikuvat.fi / Larppikuvat.fi photo gallery v4 ("Edegal")

This repo serves Conikuvat.fi (pictures from anime and cosplay conventions in Finland) and Larppikuvat.fi (pictures from larps in Finland): a Next.js 16 application on PostgreSQL via Prisma 8, with Kompassi sign-in through Auth.js, uploads with a separate media worker, album and photographer management, on-demand album zips, a contact form and a Helm chart in chart/.

The previous Django backend (kept only for its /admin, until it was decommissioned), the React frontend of the generation before that, and the design notes of this rewrite have all been removed; they live in git history.

Development

Requirements: Node 24, PostgreSQL 17 or newer (production runs 17).

createuser edegal --pwprompt          # password "photos" in the defaults
createdb -O edegal edegal
cp .env.example .env                  # defaults match the above
npm install
npm run db:migrate:dev                # apply v4 migrations
npm run db:seed                       # example content and media
npm run db:reset                      # empty the v4 tables (users kept) and seed again
npm run dev                           # http://localhost:3160

To develop against real content locally, restore a production dump into the database before running the migrations and point MEDIA_BASE_URL at the production media host.

Media in Garage

Without S3_BUCKET, media files live under MEDIA_ROOT and are served by the app at /media. Production stores new uploads in an S3 bucket on the cluster's Garage instead: browsers upload straight to the bucket with presigned URLs and fetch every image through presigned URLs, so the bucket never needs public access. The same setup runs locally with Docker:

docker compose up -d                  # Garage v2.4.1 on localhost:3900 (S3) and :3903 (admin)
scripts/garage-init.sh                # layout, buckets `edegal` and `edegal-test`, keys

Copy the printed S3_* lines into .env, run npm run s3:setup once so the bucket allows browser uploads from AUTH_URL (CORS), and restart npm run dev and npm run worker. The TEST_S3_* lines drive the S3 integration suite:

eval "$(scripts/garage-init.sh | grep ^export)"
npm run test:integration:s3           # src/**/*.s3.test.ts against the edegal-test bucket

Rows record which backend holds their file, so a database with both kinds keeps working.

Photographers

Signed-in members of the photographer group (and admins) get album management links in the breadcrumb bar. The editor views are addressed by query parameters on the album URL: ?new=1 (create a subalbum), ?edit=1, ?upload=1, ?delete=1. Photos are uploaded one per request to POST /api/albums/<id>/photos (JPEG/PNG/WebP/AVIF, 100 MB max, stored byte for byte in its own format) and their previews are generated by the media worker, which must be running for uploads to become visible:

npm run worker

The worker also deletes finished job rows hourly (done after 7 days, failed after 30).

The edit form's parent field moves an album, with everything in it, under another album. The suggestions list every album the user may create subalbums in (own or open albums for photographers, everything for admins) except the album itself and its descendants; the server action accepts only those.

Photographers edit their name, links and reusable conditions of use at /profile.

Visibility inside hidden or private albums

An album's own visibility decides only whether it is listed inside its parent. Everywhere else (photographer pages, series, /random, search engine indexing, and access for private ancestors) the least visible album on the chain to the root counts. A public album under a hidden parent is therefore listed inside that parent for people with the link but stays off the rest of the site until the parent is public; a public album under a private parent is private. This is how a larp embargo works: one hidden parent, public children, released by one visibility change.

Redirects

Renaming or moving an album records every old path (the album, its subalbums and photos) in v4_redirect, so old links keep working with a redirect; a later move re-points the earlier records. The edit form's redirect field: a web address turns the album into an external link tile and forwards visitors, a gallery path forwards to that album, and either applies to everything below the album. Root slugs that routing claims (/admin, /api, /media, /photographers, ...) cannot be used for albums.

Importing Flickr albums

"Import album » Flickr album" in the album toolbar creates a subalbum that redirects to a Flickr album, for photographers who publish on Flickr but want to be listed here. The title, description and cover picture are read from the Flickr page's Open Graph tags; a (LARP) tag is dropped from the title and a date written in it becomes the event date. The cover is stored as the album's only photo so the worker gives it a thumbnail. Nothing else is fetched from Flickr, and the import goes through without a cover when Flickr does not hand one over.

Series

A series groups albums chronologically (the runs of a campaign, the years of an event) and has its own page at /<slug>. Admins create series from the front page (?newSeries=1) and edit or delete them on the series page; any photographer may put their album in a series through the album form. Members get the series in their breadcrumb and previous/next links ordered by event date.

The v4 root album is created automatically when the server starts and none exists yet (see src/instrumentation.ts).

Public photographer pages live on dedicated routes rather than the gallery catch-all: /photographers tiles every public photographer who has a cover picture, and /photographers/<slug> shows their introduction, links and albums grouped by year.

Contacting photographers

The picture view and the download dialog offer "Contact photographer" when a credited copyright holder has given an email address in their profile. The form posts to /api/contact, which resolves the album or photo path as the visitor would see it, mails every such address with the visitor's address as reply-to, and allows five messages per address in ten minutes. Mail goes out through SMTP_HOSTNAME (with SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD) from MAIL_SENDER as FORMATTED_MAIL_FROM; without a hostname, development prints the message to the terminal and production reports the form as unavailable.

Downloads

GET /api/zip/<album path> streams the album's originals as a zip after the same visibility checks as the album page. A single photo's download menu offers the original and every preview rendition with its dimensions and size, served from /media or, for files in S3, through a presigned URL that sets the download filename. The download dialog shows the album's terms (v4_terms, inherited from ancestors) and credit instructions before either.

Schema changes

  1. Edit src/prisma/contract.prisma.
  2. npm run db:plan -- <snake_slug> and review the generated migrations/app/<ts>_<slug>/migration.ts.
  3. npm run db:migrate:dev.
  4. Commit the contract, the emitted contract.json/contract.d.ts, the migration and migrations/app/refs/db.json.

Never use prisma db update here: it drops every table the contract does not describe, including the still-present legacy Django tables.

Deployment

Runs on Kubernetes behind Traefik with the Gateway API, deployed from chart/ by .github/workflows/v4.yaml on every push to main. See chart/README.md.

New media lives in a per-site S3 bucket on the cluster's Garage; legacy media stays on the shared NFS export until the migration in chart/README.md ("Migrating media to S3") has run. The pictures/ prefix (bucket or export) holds the originals: back them up. Previews and thumbnails can be regenerated.

Want to use it for your own picture gallery?

Have your pet clanker redo the authentication and authorization in src/auth.ts to support whatever OIDC backend you may be using. Auth.js v5 supports a wide variety of OIDC providers out of the box.

License

The MIT License (MIT)

Copyright © 2010-2026 Luka Pajukanta

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.

About

Fast web picture gallery – now with a Django/PostgreSQL backend

Topics

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages