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:
parent
1e6c3c990f
commit
665942d267
16 changed files with 95 additions and 49 deletions
|
@ -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" ]
|
||||||
|
|
10
README.md
10
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
|
### 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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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')
|
||||||
})
|
})
|
||||||
|
|
|
@ -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) { %>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
29
docs/running-on-single-domain.md
Normal file
29
docs/running-on-single-domain.md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
Loading…
Reference in a new issue