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:
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.
|
||||
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" ]
|
||||
|
|
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
|
||||
|
||||
- [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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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) { %>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
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