diff --git a/Dockerfile b/Dockerfile index e1b56e4..b5f4f49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,6 @@ ENV NODE_ENV=production # Type checking is done in the repo before building the image. RUN npx tsc --noCheck -HEALTHCHECK --interval=30s --start-period=10s --timeout=5s CMD wget -q --spider http://localhost:3000/healthcheck || exit 1 +HEALTHCHECK --interval=30s --start-period=10s --timeout=5s CMD wget -q --spider http://localhost:3000/share/healthcheck || exit 1 CMD ["pm2-runtime", "dist/index.js" ] diff --git a/README.md b/README.md index c0918e2..8effac0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Setup takes less than a minute, and you never need to touch it again as all of y ### Table of Contents - [About this project](#about-this-project) -- [Install with Docker](#how-to-install-with-docker) +- [Install with Docker](#installation) - [How to use it](#how-to-use-it) - [How it works](#how-it-works) - [Additional configuration](#additional-configuration) @@ -53,7 +53,7 @@ to make that path public. Any existing or future vulnerability has the potential For me, the ideal setup is to have Immich secured privately behind mTLS or VPN, and only allow public access to Immich Public Proxy. Here is an example setup for [securing Immich behind mTLS](./docs/securing-immich-with-mtls.md) using Caddy. -## How to install with Docker +## Installation 1. Download the [docker-compose.yml](https://github.com/alangrainger/immich-public-proxy/blob/main/docker-compose.yml) file. @@ -72,6 +72,12 @@ docker-compose up -d Now whenever you share an image or gallery through Immich, it will automatically create the correct public path for you. +### Running on a single domain + +Because all IPP paths are under `/share/...`, you can run Immich Public Proxy and Immich on the same domain. + +See the instructions here: [Running on a single domain](./docs/running-on-single-domain.md). + ## How to use it Other than the initial configuration above, everything else is managed through Immich. diff --git a/app/package.json b/app/package.json index e01ba47..d171ab0 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "immich-public-proxy", - "version": "1.4.6", + "version": "1.5.0", "scripts": { "dev": "ts-node src/index.ts", "build": "npx tsc", diff --git a/app/public/lg-fullscreen.min.js b/app/public/lg/lg-fullscreen.min.js similarity index 100% rename from app/public/lg-fullscreen.min.js rename to app/public/lg/lg-fullscreen.min.js diff --git a/app/public/lg-thumbnail.min.js b/app/public/lg/lg-thumbnail.min.js similarity index 100% rename from app/public/lg-thumbnail.min.js rename to app/public/lg/lg-thumbnail.min.js diff --git a/app/public/lg-video.min.js b/app/public/lg/lg-video.min.js similarity index 100% rename from app/public/lg-video.min.js rename to app/public/lg/lg-video.min.js diff --git a/app/public/lg-zoom.min.js b/app/public/lg/lg-zoom.min.js similarity index 100% rename from app/public/lg-zoom.min.js rename to app/public/lg/lg-zoom.min.js diff --git a/app/public/lightgallery-bundle.min.css b/app/public/lg/lightgallery-bundle.min.css similarity index 100% rename from app/public/lightgallery-bundle.min.css rename to app/public/lg/lightgallery-bundle.min.css diff --git a/app/public/lightgallery.min.js b/app/public/lg/lightgallery.min.js similarity index 100% rename from app/public/lightgallery.min.js rename to app/public/lg/lightgallery.min.js diff --git a/app/src/immich.ts b/app/src/immich.ts index f02ce36..f6818b6 100644 --- a/app/src/immich.ts +++ b/app/src/immich.ts @@ -158,18 +158,23 @@ class Immich { valid: true, passwordRequired: true } + } else if (jsonBody?.message === 'Invalid share key') { + log('Invalid share key ' + key) + } else { + console.log(JSON.stringify(jsonBody)) } } - } - // Otherwise return failure - log('Immich response ' + res.status + ' for key ' + key) - try { - console.log(res.headers.get('Content-Type')) - console.log((await res.text()).slice(0, 500)) - log('Unexpected response from Immich API at ' + this.apiUrl()) - log('Please make sure the IPP container is able to reach this path.') - } catch (e) { - console.log(e) + } else { + // Otherwise return failure + log('Immich response ' + res.status + ' for key ' + key) + try { + console.log(res.headers.get('Content-Type')) + console.log((await res.text()).slice(0, 500)) + log('Unexpected response from Immich API at ' + this.apiUrl()) + log('Please make sure the IPP container is able to reach this path.') + } catch (e) { + console.log(e) + } } return { valid: false @@ -210,7 +215,7 @@ class Immich { const path = ['photo', key, id] if (size) path.push(size) const params = password ? this.encryptPassword(password) : {} - return this.buildUrl('/' + path.join('/'), params) + return this.buildUrl('/share/' + path.join('/'), params) } /** @@ -218,7 +223,7 @@ class Immich { */ videoUrl (key: string, id: string, password?: string) { const params = password ? this.encryptPassword(password) : {} - return this.buildUrl(`/video/${key}/${id}`, params) + return this.buildUrl(`/share/video/${key}/${id}`, params) } /** diff --git a/app/src/index.ts b/app/src/index.ts index 29a6a7d..c59af30 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -13,9 +13,23 @@ const app = express() app.set('view engine', 'ejs') // For parsing the password unlock form app.use(express.json()) -// Serve static assets from the /public folder +// Serve static assets from the 'public' folder as /share/static +app.use('/share/static', express.static('public', { setHeaders: addResponseHeaders })) +// Serve the same assets on /, to allow for /robots.txt and /favicon.ico app.use(express.static('public', { setHeaders: addResponseHeaders })) +/* + * [ROUTE] Healthcheck + * The path matches for /share/healthcheck, and also the legacy /healthcheck + */ +app.get(/^(|\/share)\/healthcheck$/, async (_req, res) => { + if (await immich.accessible()) { + res.send('ok') + } else { + res.status(503).send() + } +}) + /* * [ROUTE] This is the main URL that someone would visit if they are opening a shared link */ @@ -29,7 +43,7 @@ app.get('/share/:key/:mode(download)?', async (req, res) => { /* * [ROUTE] Receive an unlock request from the password page */ -app.post('/unlock', async (req, res) => { +app.post('/share/unlock', async (req, res) => { await immich.handleShareRequest({ key: toString(req.body.key), password: toString(req.body.password) @@ -39,7 +53,7 @@ app.post('/unlock', async (req, res) => { /* * [ROUTE] This is the direct link to a photo or video asset */ -app.get('/:type(photo|video)/:key/:id/:size?', async (req, res) => { +app.get('/share/:type(photo|video)/:key/:id/:size?', async (req, res) => { // Add the headers configured in config.json (most likely `cache-control`) addResponseHeaders(res) @@ -96,17 +110,6 @@ app.get('/:type(photo|video)/:key/:id/:size?', async (req, res) => { } }) -/* - * [ROUTE] Healthcheck - */ -app.get('/healthcheck', async (_req, res) => { - if (await immich.accessible()) { - res.send('ok') - } else { - res.status(503).send() - } -}) - /* * [ROUTE] Home page * @@ -116,7 +119,7 @@ app.get('/healthcheck', async (_req, res) => { * If you don't want to see this, you can redirect to a URL of your choice by changing your * reverse proxy config, or even redirect to 404 if you like. */ -app.get('/', (_req, res) => { +app.get(/^\/(|share)\/*$/, (_req, res) => { addResponseHeaders(res) res.render('home') }) diff --git a/app/views/gallery.ejs b/app/views/gallery.ejs index 62752c9..de58d33 100644 --- a/app/views/gallery.ejs +++ b/app/views/gallery.ejs @@ -3,8 +3,9 @@