1
0
Fork 0
mirror of https://github.com/alangrainger/immich-public-proxy.git synced 2024-12-28 03:41:58 +00:00

Move all requests under the /share/ path

This commit is contained in:
Alan Grainger 2024-11-17 16:26:40 +01:00
parent 1e6c3c990f
commit 665942d267
16 changed files with 95 additions and 49 deletions

View file

@ -27,6 +27,6 @@ ENV NODE_ENV=production
# Type checking is done in the repo before building the image. # Type checking is done in the repo before building the image.
RUN npx tsc --noCheck 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" ] CMD ["pm2-runtime", "dist/index.js" ]

View file

@ -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 ### Table of Contents
- [About this project](#about-this-project) - [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 to use it](#how-to-use-it)
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Additional configuration](#additional-configuration) - [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. 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. 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. 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. 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 ## How to use it
Other than the initial configuration above, everything else is managed through Immich. Other than the initial configuration above, everything else is managed through Immich.

View file

@ -1,6 +1,6 @@
{ {
"name": "immich-public-proxy", "name": "immich-public-proxy",
"version": "1.4.6", "version": "1.5.0",
"scripts": { "scripts": {
"dev": "ts-node src/index.ts", "dev": "ts-node src/index.ts",
"build": "npx tsc", "build": "npx tsc",

View file

@ -158,9 +158,13 @@ class Immich {
valid: true, valid: true,
passwordRequired: true passwordRequired: true
} }
} else if (jsonBody?.message === 'Invalid share key') {
log('Invalid share key ' + key)
} else {
console.log(JSON.stringify(jsonBody))
} }
} }
} } else {
// Otherwise return failure // Otherwise return failure
log('Immich response ' + res.status + ' for key ' + key) log('Immich response ' + res.status + ' for key ' + key)
try { try {
@ -171,6 +175,7 @@ class Immich {
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
}
return { return {
valid: false valid: false
} }
@ -210,7 +215,7 @@ class Immich {
const path = ['photo', key, id] const path = ['photo', key, id]
if (size) path.push(size) if (size) path.push(size)
const params = password ? this.encryptPassword(password) : {} 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) { videoUrl (key: string, id: string, password?: string) {
const params = password ? this.encryptPassword(password) : {} const params = password ? this.encryptPassword(password) : {}
return this.buildUrl(`/video/${key}/${id}`, params) return this.buildUrl(`/share/video/${key}/${id}`, params)
} }
/** /**

View file

@ -13,9 +13,23 @@ const app = express()
app.set('view engine', 'ejs') app.set('view engine', 'ejs')
// For parsing the password unlock form // For parsing the password unlock form
app.use(express.json()) 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 })) 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 * [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 * [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({ await immich.handleShareRequest({
key: toString(req.body.key), key: toString(req.body.key),
password: toString(req.body.password) 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 * [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`) // Add the headers configured in config.json (most likely `cache-control`)
addResponseHeaders(res) 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 * [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 * 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. * reverse proxy config, or even redirect to 404 if you like.
*/ */
app.get('/', (_req, res) => { app.get(/^\/(|share)\/*$/, (_req, res) => {
addResponseHeaders(res) addResponseHeaders(res)
res.render('home') res.render('home')
}) })

View file

@ -3,8 +3,9 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title><%- title || 'Gallery' %></title> <title><%- title || 'Gallery' %></title>
<link type="text/css" rel="stylesheet" href="/style.css"/> <link rel="icon" href="/share/static/favicon.ico" type="image/x-icon">
<link type="text/css" rel="stylesheet" href="/lightgallery-bundle.min.css"/> <link type="text/css" rel="stylesheet" href="/share/static/style.css"/>
<link type="text/css" rel="stylesheet" href="/share/static/lg/lightgallery-bundle.min.css"/>
</head> </head>
<body> <body>
<div id="header"> <div id="header">
@ -35,12 +36,12 @@
<% } <% }
}) %> }) %>
</div> </div>
<script src="/web.js"></script> <script src="/share/static/web.js"></script>
<script src="/lightgallery.min.js"></script> <script src="/share/static/lg/lightgallery.min.js"></script>
<script src="/lg-fullscreen.min.js"></script> <script src="/share/static/lg/lg-fullscreen.min.js"></script>
<script src="/lg-thumbnail.min.js"></script> <script src="/share/static/lg/lg-thumbnail.min.js"></script>
<script src="/lg-video.min.js"></script> <script src="/share/static/lg/lg-video.min.js"></script>
<script src="/lg-zoom.min.js"></script> <script src="/share/static/lg/lg-zoom.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
initLightGallery(<%- JSON.stringify(lgConfig) %>) // initLightGallery imported from web.js initLightGallery(<%- JSON.stringify(lgConfig) %>) // initLightGallery imported from web.js
<% if (openItem) { %> <% if (openItem) { %>

View file

@ -3,6 +3,7 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title></title> <title></title>
<link rel="icon" href="/share/static/favicon.ico" type="image/x-icon">
<style> <style>
html, body { html, body {
margin: 0; margin: 0;

View file

@ -3,8 +3,9 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Password required</title> <title>Password required</title>
<link type="text/css" rel="stylesheet" href="/lightgallery-bundle.min.css"/> <link rel="icon" href="/share/static/favicon.ico" type="image/x-icon">
<link type="text/css" rel="stylesheet" href="/pico.min.css"/> <link type="text/css" rel="stylesheet" href="/share/static/lg/lightgallery-bundle.min.css"/>
<link type="text/css" rel="stylesheet" href="/share/static/pico.min.css"/>
</head> </head>
<body> <body>
<header></header> <header></header>
@ -39,12 +40,12 @@
<div></div> <div></div>
</div> </div>
</main> </main>
<script src="/web.js"></script> <script src="/share/static/web.js"></script>
<script> <script>
async function submitForm (formElement) { async function submitForm (formElement) {
const formData = new FormData(formElement) const formData = new FormData(formElement)
try { try {
const res = await fetch('/unlock', { const res = await fetch('/share/unlock', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(formData.entries())) body: JSON.stringify(Object.fromEntries(formData.entries()))
@ -62,10 +63,10 @@
submitForm(this) submitForm(this)
}) })
</script> </script>
<script src="/lightgallery.min.js"></script> <script src="/share/static/lg/lightgallery.min.js"></script>
<script src="/lg-fullscreen.min.js"></script> <script src="/share/static/lg/lg-fullscreen.min.js"></script>
<script src="/lg-thumbnail.min.js"></script> <script src="/share/static/lg/lg-thumbnail.min.js"></script>
<script src="/lg-video.min.js"></script> <script src="/share/static/lg/lg-video.min.js"></script>
<script src="/lg-zoom.min.js"></script> <script src="/share/static/lg/lg-zoom.min.js"></script>
</body> </body>
</html> </html>

View file

@ -8,6 +8,6 @@ services:
environment: environment:
- IMMICH_URL=http://your-internal-immich-server:2283 - IMMICH_URL=http://your-internal-immich-server:2283
healthcheck: healthcheck:
test: wget -q --spider http://localhost:3000/healthcheck || exit 1 test: wget -q --spider http://localhost:3000/share/healthcheck || exit 1
start_period: 10s start_period: 10s
timeout: 5s timeout: 5s

View file

@ -0,0 +1,29 @@
# Running IPP on a single domain with Immich
Because everything related to IPP happens within the `/share` path,
you can serve Immich and IPP on the same domain by configuring your reverse
proxy to send all `/share/*` requests to IPP.
## Caddy
Here's an example of how to do this with Caddy:
```
https://your-domain.com {
# Immich Public Proxy paths
@public path /share /share/*
handle @public {
# Your IPP server and port
reverse_proxy your_server:3000
}
# All other paths, require basic auth and send to Immich
handle {
basicauth {
user password_hash
}
# Your Immich server and port
reverse_proxy your_server:2283
}
}
```