1
0
Fork 0
mirror of https://github.com/alangrainger/immich-public-proxy.git synced 2024-12-26 19:01: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.
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" ]

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

View file

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

View file

@ -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)
}
/**

View file

@ -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')
})

View file

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

View file

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

View file

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

View file

@ -8,6 +8,6 @@ services:
environment:
- IMMICH_URL=http://your-internal-immich-server:2283
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
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
}
}
```