feat(web)!: SPA (#5069)

* feat(web): SPA

* chore: remove unnecessary prune

* feat(web): merge with immich-server

* Correct method name

* fix: bugs, docs, workflows, etc.

* chore: keep dockerignore for dev

* chore: remove license

* fix: expose 2283

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-11-17 23:13:36 -05:00 committed by GitHub
parent 5118d261ab
commit adae5dd758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 730 additions and 1446 deletions

20
.dockerignore Normal file
View File

@ -0,0 +1,20 @@
.vscode/
cli/
design/
docker/
docs/
fastlane/
machine-learning/
misc/
mobile/
server/node_modules
server/coverage/
server/.reverse-geocoding-dump/
server/upload/
server/dist/
web/node_modules/
web/coverage/
web/.svelte-kit
web/build/

View File

@ -29,14 +29,11 @@ jobs:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
- primary-name: "immich-web"
- primary-name: "immich-proxy"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
-
name: Clean temporary images
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
with:
@ -60,15 +57,12 @@ jobs:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
- primary-name: "immich-web"
- primary-name: "immich-proxy"
- primary-name: "immich-build-cache"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
-
name: Clean untagged images
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.4.0
with:

View File

@ -24,16 +24,12 @@ jobs:
fail-fast: false
matrix:
include:
- context: "web"
image: "immich-web"
platforms: "linux/amd64,linux/arm64"
- context: "machine-learning"
file: "machine-learning/Dockerfile"
image: "immich-machine-learning"
platforms: "linux/amd64,linux/arm64"
- context: "nginx"
image: "immich-proxy"
platforms: "linux/amd64,linux/arm64"
- context: "server"
- context: "."
file: "server/Dockerfile"
image: "immich-server"
platforms: "linux/arm64,linux/amd64"
@ -103,6 +99,7 @@ jobs:
uses: docker/build-push-action@v5.0.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}

View File

@ -6,31 +6,34 @@ version: "3.8"
name: immich-dev
x-server-build: &server-common
image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile
target: dev
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
environment:
- NODE_ENV=development
ulimits:
nofile:
soft: 1048576
hard: 1048576
services:
immich-server:
container_name: immich_server
image: immich-server-dev:latest
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run start:debug immich
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
<<: *server-common
ports:
- 3001:3001
- 9230:9230
env_file:
- .env
environment:
- NODE_ENV=development
ulimits:
nofile:
soft: 1048576
hard: 1048576
depends_on:
- redis
- database
@ -38,30 +41,13 @@ services:
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
command: npm run start:debug microservices
<<: *server-common
# extends:
# file: hwaccel.yml
# service: hwaccel
build:
context: ../server
dockerfile: Dockerfile
target: builder
command: npm run start:debug microservices
volumes:
- ../server:/usr/src/app
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 9231:9230
environment:
- NODE_ENV=development
ulimits:
nofile:
soft: 1048576
hard: 1048576
depends_on:
- database
- immich-server
@ -73,12 +59,11 @@ services:
build:
context: ../web
dockerfile: Dockerfile
target: dev
command: npm run dev --host
env_file:
- .env
ports:
- 3000:3000
- 2283:3000
- 24678:24678
volumes:
- ../web:/usr/src/app
@ -139,22 +124,5 @@ services:
ports:
- 5432:5432
immich-proxy:
container_name: immich_proxy
image: immich-proxy-dev:latest
environment:
# Make sure these values get passed through from the env file
- IMMICH_SERVER_URL
- IMMICH_WEB_URL
build:
context: ../nginx
dockerfile: Dockerfile
ports:
- 2283:8080
depends_on:
- immich-server
- immich-web
restart: unless-stopped
volumes:
model-cache:

View File

@ -2,19 +2,25 @@ version: "3.8"
name: immich-prod
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
restart: always
services:
immich-server:
container_name: immich_server
image: immich-server:latest
build:
context: ../server
dockerfile: Dockerfile
command: [ "./start-server.sh" ]
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
<<: *server-common
ports:
- 2283:3001
depends_on:
- redis
- database
@ -22,35 +28,15 @@ services:
immich-microservices:
container_name: immich_microservices
image: immich-microservices:latest
command: [ "./start-microservices.sh" ]
<<: *server-common
# extends:
# file: hwaccel.yml
# service: hwaccel
build:
context: ../server
dockerfile: Dockerfile
command: [ "./start-microservices.sh" ]
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
- redis
- database
- immich-server
- typesense
restart: always
immich-web:
container_name: immich_web
image: immich-web:latest
build:
context: ../web
dockerfile: Dockerfile
env_file:
- .env
restart: always
depends_on:
- immich-server
immich-machine-learning:
@ -95,23 +81,5 @@ services:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
restart: always
immich-proxy:
container_name: immich_proxy
image: immich-proxy:latest
environment:
# Make sure these values get passed through from the env file
- IMMICH_SERVER_URL
- IMMICH_WEB_URL
build:
context: ../nginx
dockerfile: Dockerfile
ports:
- 2283:8080
logging:
driver: none
depends_on:
- immich-server
restart: always
volumes:
model-cache:

View File

@ -6,9 +6,9 @@ services:
immich-server:
image: immich-server-dev:latest
build:
context: ../server
dockerfile: Dockerfile
target: builder
context: ../
dockerfile: server/Dockerfile
target: dev
command: npm run test:e2e
volumes:
- ../server:/usr/src/app

View File

@ -12,6 +12,8 @@ services:
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 2283:3001
depends_on:
- redis
- database
@ -45,13 +47,6 @@ services:
- .env
restart: always
immich-web:
container_name: immich_web
image: ghcr.io/immich-app/immich-web:${IMMICH_VERSION:-release}
env_file:
- .env
restart: always
typesense:
container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@ -82,16 +77,6 @@ services:
- pgdata:/var/lib/postgresql/data
restart: always
immich-proxy:
container_name: immich_proxy
image: ghcr.io/immich-app/immich-proxy:${IMMICH_VERSION:-release}
ports:
- 2283:8080
depends_on:
- immich-server
- immich-web
restart: always
volumes:
pgdata:
model-cache:

View File

@ -1,21 +1,6 @@
# Reverse Proxy
When deploying Immich it is important to understand that a reverse proxy is required in front of the server and web container. The reverse proxy acts as an intermediary between the user and container, forwarding requests to the correct container based on the URL path.
## Default Reverse Proxy
Immich provides a default nginx reverse proxy preconfigured to perform the correct routing and set the necessary headers for the server and web container to use. These headers are crucial to redirect to the correct URL and determine the client's IP address.
## Using a Different Reverse Proxy
While the reverse proxy provided by Immich works well for basic deployments, some users may want to use a different reverse proxy. Fortunately, Immich is flexible enough to accommodate different reverse proxies. Users can either:
1. Add another reverse proxy on top of Immich's reverse proxy
2. Completely replace the default reverse proxy
## Adding a Custom Reverse Proxy
Users can deploy a custom reverse proxy that forwards requests to Immich's reverse proxy. This way, the new reverse proxy can handle TLS termination, load balancing, or other advanced features, while still delegating routing decisions to Immich's reverse proxy. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
### Nginx example config
@ -43,7 +28,3 @@ server {
}
}
```
## Replacing the Default Reverse Proxy
Replacing Immich's default reverse proxy is an advanced deployment and support may be limited. When replacing Immich's default proxy it is important to ensure that requests to `/api/*` are routed to the server container and all other requests to the web container. Additionally, the previously mentioned headers should be configured accordingly. You may find our [nginx configuration file](https://github.com/immich-app/immich/blob/main/nginx/templates/default.conf.template) a helpful reference.

View File

@ -17,6 +17,5 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
| `machine-learning/` | Source code for the `immich-machine-learning` docker image |
| `misc/release/` | Scripts for version pumps and draft releases |
| `mobile/` | Source code for the mobile app, both Android and iOS |
| `nginx/` | Source code for the `immich-proxy` docker image |
| `server/` | Source code for the `immich-server` docker image |
| `web/` | Source code for the `immich-web` docker image |
| `web/` | Source code for the `web` |

View File

@ -52,7 +52,7 @@ If you only want to do web development connected to an existing, remote backend,
3. Start the web development server
```
PUBLIC_IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev
```
## IDE setup

View File

@ -13,7 +13,3 @@ Running Immich on Windows can be frustrating and there are lots of ways it can g
### NTFS Mounted Volumes
The docker-compose.dev.yml and docker-compose.prod.yml use volume mounts for the postgres database. On start-up, postgres will try to `chown` the data directory, but fail. See [this post](https://forums.docker.com/t/data-directory-var-lib-postgresql-data-pgdata-has-wrong-ownership/17963/24) for more information about this issue and possible solutions.
### `Cannot read properties of null (reading 'split')`
This error occurs when trying to access the app via port `3000` instead of `2283`. During development `immich-proxy` runs on port 2283, while `immich-web` runs on `3000`.

View File

@ -122,28 +122,6 @@ TYPESENSE_API_KEY=some-random-text
PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
####################################################################################
# Alternative Service Addresses - Optional
#
# This is an advanced feature for users who may be running their immich services on different hosts.
# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers.
# Note: immich-microservices is bound to 3002, but no references are made
####################################################################################
IMMICH_WEB_URL=http://immich-web:3000
IMMICH_SERVER_URL=http://immich-server:3001
####################################################################################
# Alternative API's External Address - Optional
#
# This is an advanced feature used to control the public server endpoint returned to clients during Well-known discovery.
# You should only use this if you want mobile apps to access the immich API over a custom URL. Do not include trailing slash.
# NOTE: At this time, the web app will not be affected by this setting and will continue to use the relative path: /api
# Examples: http://localhost:3001, http://immich-api.example.com, etc
####################################################################################
#IMMICH_API_URL_EXTERNAL=http://localhost:3001
###################################################################################
# Immich Version - Optional
#

View File

@ -63,21 +63,6 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## URLs
| Variable | Description | Default | Services |
| :------------------------- | :---------------------- | :-------------------------: | :--------- |
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |
:::info
The above paths are modifying the internal paths of the containers.
:::
## Database
| Variable | Description | Default | Services |

View File

@ -98,12 +98,12 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_proxy` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_web` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
<img
src={require('./img/unraid06.webp').default}
width="80%"
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
alt="Go to Docker Tab and visit the address listed next to immich-web"
/>
<details >
@ -112,12 +112,12 @@ alt="Go to Docker Tab and visit the address listed next to immich-proxy"
<img
src={require('./img/unraid07.webp').default}
width="80%"
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
alt="Go to Docker Tab and visit the address listed next to immich-web"
/>
<img
src={require('./img/unraid08.webp').default}
width="90%"
alt="Go to Docker Tab and visit the address listed next to immich-proxy"
alt="Go to Docker Tab and visit the address listed next to immich-web"
/>
</details>

View File

@ -1,44 +0,0 @@
#!/usr/bin/env sh
# vim:sw=4:ts=4:et
set -e
entrypoint_log() {
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
echo "$@"
fi
}
ME=$(basename $0)
DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf"
# check if we have ipv6 available
if [ ! -f "/proc/net/if_inet6" ]; then
entrypoint_log "$ME: info: ipv6 not available"
exit 0
fi
if [ ! -f "/$DEFAULT_CONF_FILE" ]; then
entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE is not a file or does not exist"
exit 0
fi
# check if the file can be modified, e.g. not on a r/o filesystem
touch /$DEFAULT_CONF_FILE 2>/dev/null || { entrypoint_log "$ME: info: can not modify /$DEFAULT_CONF_FILE (read-only file system?)"; exit 0; }
# check if the file is already modified, e.g. on a container restart
grep -q "listen \[::]\:8080;" /$DEFAULT_CONF_FILE && { entrypoint_log "$ME: info: IPv6 listen already enabled"; exit 0; }
if [ -f "/etc/os-release" ]; then
. /etc/os-release
else
entrypoint_log "$ME: info: can not guess the operating system"
exit 0
fi
# enable ipv6 on default.conf listen sockets
sed -i -E 's,listen 8080;,listen 8080;\n listen [::]:8080;,' /$DEFAULT_CONF_FILE
entrypoint_log "$ME: info: Enabled listen on IPv6 in /$DEFAULT_CONF_FILE"
exit 0

View File

@ -1,13 +0,0 @@
#!/usr/bin/env sh
set -e
export IMMICH_WEB_URL="${IMMICH_WEB_URL:-http://immich-web:3000}"
IMMICH_WEB_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "http://")
export IMMICH_WEB_SCHEME
IMMICH_WEB_HOST=$(echo "$IMMICH_WEB_URL" | cut -d '/' -f 3)
export IMMICH_WEB_HOST
export IMMICH_SERVER_URL="${IMMICH_SERVER_URL:-http://immich-server:3001}"
IMMICH_SERVER_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "http://")
export IMMICH_SERVER_SCHEME
IMMICH_SERVER_HOST=$(echo "$IMMICH_SERVER_URL" | cut -d '/' -f 3)
export IMMICH_SERVER_HOST

View File

@ -1,9 +0,0 @@
FROM ghcr.io/nginxinc/nginx-unprivileged:1.25.1-alpine3.17@sha256:c38e27fdba47f725f49177b88fdd1fd2feef11b13dc11dea3695c3feb2c6d96d
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d
COPY 15-set-env-variables.envsh /docker-entrypoint.d
COPY templates/ /etc/nginx/templates

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,72 +0,0 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map $http_x_forwarded_proto $forwarded_protocol {
default $scheme;
# Only allow the values 'http' and 'https' for the X-Forwarded-Proto header.
http http;
https https;
}
upstream server {
server ${IMMICH_SERVER_HOST};
keepalive 2;
}
upstream web {
server ${IMMICH_WEB_HOST};
keepalive 2;
}
server {
listen 8080;
access_log off;
client_max_body_size 50000M;
# Compression
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied any;
gzip_vary on;
gunzip on;
# text/html is included by default
gzip_types
application/javascript
application/json
font/ttf
image/svg+xml
text/css;
proxy_buffering off;
proxy_request_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k;
proxy_buffers 64 4k;
proxy_force_ranges on;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_protocol;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
location /api {
rewrite /api/(.*) /$1 break;
proxy_pass ${IMMICH_SERVER_SCHEME}server;
}
location / {
proxy_pass ${IMMICH_WEB_SCHEME}web;
}
}

View File

@ -1,5 +0,0 @@
node_modules/
upload/
dist/
coverage/
.reverse-geocoding-dump

View File

@ -1,33 +1,42 @@
FROM ghcr.io/immich-app/base-server-dev:20231109 as builder
# dev build
FROM ghcr.io/immich-app/base-server-dev:20231109 as dev
COPY package.json package-lock.json ./
WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./
RUN npm ci
COPY . .
COPY server .
FROM builder as prod
FROM dev AS prod
RUN npm run build
RUN npm prune --omit=dev --omit=optional
# web build
FROM node:20.8-alpine3.18 as web
WORKDIR /usr/src/app
COPY web/package.json web/package-lock.json ./
RUN npm ci
COPY web .
RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20231109
WORKDIR /usr/src/app
ENV NODE_ENV=production
COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin
COPY ./assets ./assets
COPY --from=web /usr/src/app/build ./www
COPY server/assets assets
COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./
RUN npm link && npm cache clean --force
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
COPY package.json package-lock.json ./
COPY start*.sh ./
RUN npm link && npm cache clean --force
VOLUME /usr/src/app/upload
EXPOSE 3001
ENTRYPOINT ["tini", "--", "/bin/sh"]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -15,7 +15,7 @@ export class EnablePasswordLoginCommand extends CommandRunner {
const config = await this.configService.getConfig();
config.passwordLogin.enabled = true;
await this.configService.updateConfig(config);
await axios.post('http://localhost:3001/refresh-config');
await axios.post('http://localhost:3001/api/refresh-config');
console.log('Password login has been enabled.');
}
}
@ -33,7 +33,7 @@ export class DisablePasswordLoginCommand extends CommandRunner {
const config = await this.configService.getConfig();
config.passwordLogin.enabled = false;
await this.configService.updateConfig(config);
await axios.post('http://localhost:3001/refresh-config');
await axios.post('http://localhost:3001/api/refresh-config');
console.log('Password login has been disabled.');
}
}

View File

@ -306,9 +306,9 @@ describe(SystemConfigService.name, () => {
});
});
describe('getTheme', () => {
describe('getCustomCss', () => {
it('should return the default theme', async () => {
await expect(sut.getTheme()).resolves.toEqual(defaults.theme);
await expect(sut.getCustomCss()).resolves.toEqual(defaults.theme.customCss);
});
});
});

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { JobName } from '../job';
import { CommunicationEvent, ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories';
import { SystemConfigThemeDto } from './dto/system-config-theme.dto';
import { SystemConfigDto, mapConfig } from './dto/system-config.dto';
import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto';
import {
@ -31,11 +30,6 @@ export class SystemConfigService {
return this.core.config$;
}
async getTheme(): Promise<SystemConfigThemeDto> {
const { theme } = await this.core.getConfig();
return theme;
}
async getConfig(): Promise<SystemConfigDto> {
const config = await this.core.getConfig();
return mapConfig(config);
@ -87,4 +81,9 @@ export class SystemConfigService {
return JSON.parse(await this.repository.readFile(`./assets/style-${theme}.json`));
}
async getCustomCss(): Promise<string> {
const { theme } = await this.core.getConfig();
return theme.customCss;
}
}

View File

@ -13,6 +13,7 @@ import {
SwaggerDocumentOptions,
SwaggerModule,
} from '@nestjs/swagger';
import { NextFunction, Request, Response } from 'express';
import { writeFileSync } from 'fs';
import path from 'path';
@ -56,6 +57,12 @@ const patchOpenAPI = (document: OpenAPIObject) => {
document.components.schemas = sortKeys(document.components.schemas);
}
for (const [key, value] of Object.entries(document.paths)) {
const newKey = key.replace('/api/', '/');
delete document.paths[key];
document.paths[newKey] = value;
}
for (const path of Object.values(document.paths)) {
const operations = {
get: path.get,
@ -94,6 +101,14 @@ const patchOpenAPI = (document: OpenAPIObject) => {
return document;
};
export const indexFallback = (excludePaths: string[]) => (req: Request, res: Response, next: NextFunction) => {
if (req.url.startsWith('/api') || req.method.toLowerCase() !== 'get' || excludePaths.indexOf(req.url) !== -1) {
next();
} else {
res.sendFile('/www/index.html', { root: process.cwd() });
}
};
export const useSwagger = (app: INestApplication, isDev: boolean) => {
const config = new DocumentBuilder()
.setTitle('Immich')

View File

@ -1,15 +1,34 @@
import { SystemConfigService } from '@app/domain';
import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { Controller, Get, Header, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
import { PublicRoute } from '../app.guard';
@Controller()
export class AppController {
constructor(private configService: SystemConfigService) {}
constructor(private service: SystemConfigService) {}
@ApiExcludeEndpoint()
@Get('.well-known/immich')
getImmichWellKnown() {
return {
api: {
endpoint: '/api',
},
};
}
@ApiExcludeEndpoint()
@PublicRoute()
@Get('custom.css')
@Header('Content-Type', 'text/css')
getCustomCss() {
return this.service.getCustomCss();
}
@ApiExcludeEndpoint()
@Post('refresh-config')
@HttpCode(HttpStatus.OK)
public reloadConfig() {
return this.configService.refreshConfig();
return this.service.refreshConfig();
}
}

View File

@ -6,7 +6,7 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import { json } from 'body-parser';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { useSwagger } from './app.utils';
import { indexFallback, useSwagger } from './app.utils';
const logger = new Logger('ImmichServer');
const port = Number(process.env.SERVER_PORT) || 3001;
@ -24,6 +24,11 @@ export async function bootstrap() {
app.useWebSocketAdapter(new RedisIoAdapter(app));
useSwagger(app, isDev);
const excludePaths = ['/.well-known/immich', '/custom.css'];
app.setGlobalPrefix('api', { exclude: excludePaths });
app.useStaticAssets('www');
app.use(indexFallback(excludePaths));
const server = await app.listen(port);
server.requestTimeout = 30 * 60 * 1000;

View File

@ -3,7 +3,7 @@ import { Logger } from '@nestjs/common';
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: true })
@WebSocketGateway({ cors: true, path: '/api/socket.io' })
export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository {
private logger = new Logger(CommunicationRepository.name);
private onConnectCallbacks: Callback[] = [];

View File

@ -1,4 +1,4 @@
node_modules/
upload/
dist/
coverage/
.svelte-kit
build/

View File

@ -1,44 +1,10 @@
# Our Node base image
FROM node:20.8-alpine3.18 as base
FROM node:20.8-alpine3.18
WORKDIR /usr/src/app
EXPOSE 3000
RUN apk add --no-cache setpriv tini
FROM base as builder
RUN chown node:node /usr/src/app
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .
EXPOSE 3000
FROM builder AS dev
ENV CHOKIDAR_USEPOLLING=true
EXPOSE 24678
EXPOSE 3000
CMD ["npm", "run", "dev"]
FROM builder AS prod
RUN npm run build
RUN npm prune --omit=dev
FROM base
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/build ./build
COPY package.json package-lock.json ./
COPY entrypoint.sh ./
ENTRYPOINT ["tini", "--", "/bin/sh"]
CMD ["entrypoint.sh"]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,11 +0,0 @@
#! /bin/sh
# Rebind env vars to PUBLIC_ for svelte
export PUBLIC_IMMICH_SERVER_URL=$IMMICH_SERVER_URL
export PUBLIC_IMMICH_API_URL_EXTERNAL=$IMMICH_API_URL_EXTERNAL
if [ "$(id -u)" -eq 0 ] && [ -n "$PUID" ] && [ -n "$PGID" ]; then
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups node /usr/src/app/build/index.js
else
exec node /usr/src/app/build/index.js
fi

210
web/package-lock.json generated
View File

@ -31,7 +31,7 @@
"@babel/preset-typescript": "^7.22.5",
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@sveltejs/adapter-node": "^1.2.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.20.4",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/svelte": "^4.0.3",
@ -2989,98 +2989,6 @@
"integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==",
"dev": true
},
"node_modules/@rollup/plugin-commonjs": {
"version": "25.0.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.4.tgz",
"integrity": "sha512-L92Vz9WUZXDnlQQl3EwbypJR4+DM2EbsO+/KOcEkP4Mc6Ct453EeDB2uH9lgRwj4w5yflgNpq9pHOiY8aoUXBQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"glob": "^8.0.3",
"is-reference": "1.2.1",
"magic-string": "^0.27.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz",
"integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz",
"integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-builtin-module": "^3.2.1",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz",
"integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -3110,19 +3018,13 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@sveltejs/adapter-node": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz",
"integrity": "sha512-A0VgRQDCDPzdLNoiAbcOxGw4zT1Mc+n1LwT1OmO350R7WxrEqdMUChPPOd1iMfIDWlP4ie6E2d/WQf5es2d4Zw==",
"node_modules/@sveltejs/adapter-static": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz",
"integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==",
"dev": true,
"dependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "^3.7.0"
},
"peerDependencies": {
"@sveltejs/kit": "^1.0.0"
"@sveltejs/kit": "^1.5.0"
}
},
"node_modules/@sveltejs/kit": {
@ -3678,12 +3580,6 @@
"integrity": "sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==",
"dev": true
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
@ -4639,18 +4535,6 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bytewise": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
@ -4910,12 +4794,6 @@
"node": ">= 6"
}
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -6013,12 +5891,6 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@ -6386,25 +6258,6 @@
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
},
"node_modules/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -6417,27 +6270,6 @@
"node": ">=10.13.0"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/global-prefix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
@ -6921,21 +6753,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@ -7031,12 +6848,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -7087,15 +6898,6 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",

View File

@ -24,7 +24,7 @@
"@babel/preset-typescript": "^7.22.5",
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@sveltejs/adapter-node": "^1.2.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.20.4",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/svelte": "^4.0.3",

View File

@ -26,7 +26,7 @@ import { BASE_PATH } from './open-api/base';
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
import type { ApiParams } from './types';
export class ImmichApi {
class ImmichApi {
public activityApi: ActivityApi;
public albumApi: AlbumApi;
public libraryApi: LibraryApi;

5
web/src/app.d.ts vendored
View File

@ -3,11 +3,6 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface Locals {
user?: import('@api').UserResponseDto;
api: import('@api').ImmichApi;
}
interface PageData {
meta: {
title: string;

View File

@ -13,6 +13,7 @@
document.documentElement.classList.remove('dark');
}
</script>
<link rel="stylesheet" href="/custom.css" />
</head>
<body class="bg-immich-bg dark:bg-immich-dark-bg">

40
web/src/hooks.client.ts Normal file
View File

@ -0,0 +1,40 @@
import type { HandleClientError } from '@sveltejs/kit';
import type { AxiosError, AxiosResponse } from 'axios';
const LOG_PREFIX = '[hooks.client.ts]';
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => {
const httpError = error as AxiosError;
const request = httpError?.request as Request & { path: string };
const response = httpError?.response as AxiosResponse<{
message: string;
statusCode: number;
error: string;
}>;
let code = response?.data?.statusCode || response?.status || httpError.code || '500';
if (response) {
code += ` - ${response.data?.error || response.statusText}`;
}
if (request && response) {
console.log({
status: response.status,
url: `${request.method} ${request.path}`,
response: response.data || 'No data',
});
}
return {
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
code,
stack: httpError?.stack,
};
};
export const handleError: HandleClientError = ({ error }) => {
const result = parseError(error);
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
return result;
};

View File

@ -1,77 +0,0 @@
import { env } from '$env/dynamic/public';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import type { AxiosError, AxiosResponse } from 'axios';
import { ImmichApi } from './api/api';
const LOG_PREFIX = '[hooks.server.ts]';
export const handle = (async ({ event, resolve }) => {
const basePath = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001';
const accessToken = event.cookies.get('immich_access_token');
const api = new ImmichApi({ basePath, accessToken });
// API instance that should be used for all server-side requests.
event.locals.api = api;
if (accessToken) {
try {
const { data: user } = await api.userApi.getMyUserInfo();
event.locals.user = user;
} catch (err) {
console.log(`${LOG_PREFIX} Unable to get my user`, parseError(err));
const apiError = err as AxiosError;
// Ignore 401 unauthorized errors and log all others.
if (apiError.response?.status && apiError.response?.status !== 401) {
console.error(`${LOG_PREFIX}:handle`, err);
} else if (!apiError.response?.status) {
console.error(`${LOG_PREFIX}:handle`, apiError?.message);
}
}
}
const res = await resolve(event);
// The link header can grow quite big and has caused issues with our nginx
// proxy returning a 502 Bad Gateway error. Therefore the header gets deleted.
res.headers.delete('Link');
return res;
}) satisfies Handle;
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => {
const httpError = error as AxiosError;
const request = httpError?.request as Request & { path: string };
const response = httpError?.response as AxiosResponse<{
message: string;
statusCode: number;
error: string;
}>;
let code = response?.data?.statusCode || response?.status || httpError.code || '500';
if (response) {
code += ` - ${response.data?.error || response.statusText}`;
}
if (request && response) {
console.log({
status: response.status,
url: `${request.method} ${request.path}`,
response: response.data || 'No data',
});
}
return {
message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
code,
stack: httpError?.stack,
};
};
export const handleError: HandleServerError = ({ error }) => {
const result = parseError(error);
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
return result;
};

View File

@ -26,9 +26,6 @@
const logOut = async () => {
const { data } = await api.authenticationApi.logout();
await fetch('/auth/logout', { method: 'POST' });
goto(data.redirectUri || '/auth/login?autoLaunch=0');
};
</script>

34
web/src/lib/utils/auth.ts Normal file
View File

@ -0,0 +1,34 @@
import { api } from '@api';
import { redirect } from '@sveltejs/kit';
import { AppRoute } from '../constants';
export interface AuthOptions {
admin?: true;
}
export const getAuthUser = async () => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
return user;
} catch {
return null;
}
};
// TODO: re-use already loaded user (once) instead of fetching on each page navigation
export const authenticate = async (options?: AuthOptions) => {
options = options || {};
const user = await getAuthUser();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
if (options.admin && !user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
return user;
};
export const isLoggedIn = async () => getAuthUser().then((user) => !!user);

View File

@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: albums } = await api.albumApi.getAllAlbums();
return {
user: user,
albums: albums,
meta: {
title: 'Albums',
},
};
} catch (e) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
}) satisfies PageServerLoad;

View File

@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: albums } = await api.albumApi.getAllAlbums();
return {
user,
albums,
meta: {
title: 'Albums',
},
};
}) satisfies PageLoad;

View File

@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ params, locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: album } = await api.albumApi.getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
user,
meta: {
title: album.albumName,
},
};
} catch (e) {
throw redirect(302, AppRoute.ALBUMS);
}
}) satisfies PageServerLoad;

View File

@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: album } = await api.albumApi.getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
user,
meta: {
title: album.albumName,
},
};
}) satisfies PageLoad;

View File

@ -1,15 +1,9 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ params, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const albumId = params['albumId'];
export const load: PageLoad = async ({ params }) => {
const albumId = params.albumId;
if (albumId) {
throw redirect(302, `${AppRoute.ALBUMS}/${albumId}`);

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Archive',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Archive',
},
};
}) satisfies PageLoad;

View File

@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load: PageLoad = async () => {
throw redirect(302, AppRoute.ARCHIVE);
};

View File

@ -1,21 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: items } = await locals.api.searchApi.getExploreData();
const { data: response } = await locals.api.personApi.getAllPeople({ withHidden: false });
return {
user,
items,
response,
meta: {
title: 'Explore',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: items } = await api.searchApi.getExploreData();
const { data: response } = await api.personApi.getAllPeople({ withHidden: false });
return {
user,
items,
response,
meta: {
title: 'Explore',
},
};
}) satisfies PageLoad;

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Favorites',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Favorites',
},
};
}) satisfies PageLoad;

View File

@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.FAVORITES);
}
};

View File

@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
throw redirect(302, AppRoute.FAVORITES);
};

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Map',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Map',
},
};
}) satisfies PageLoad;

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Memory',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Memory',
},
};
}) satisfies PageLoad;

View File

@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.MEMORY);
}
};

View File

@ -0,0 +1,9 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
throw redirect(302, AppRoute.MEMORY);
}) satisfies PageLoad;

View File

@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.MEMORY);
}
};

View File

@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.PHOTOS);
}) satisfies PageLoad;

View File

@ -1,21 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: partner } = await api.userApi.getUserById({ id: params['userId'] });
return {
user,
partner,
meta: {
title: 'Partner',
},
};
};

View File

@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: partner } = await api.userApi.getUserById({ id: params.userId });
return {
user,
partner,
meta: {
title: 'Partner',
},
};
}) satisfies PageLoad;

View File

@ -1,19 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: true });
return {
user,
people,
meta: {
title: 'People',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,16 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: people } = await api.personApi.getAllPeople({ withHidden: true });
return {
user,
people,
meta: {
title: 'People',
},
};
}) satisfies PageLoad;

View File

@ -1,22 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent, params }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
const { data: statistics } = await locals.api.personApi.getPersonStatistics({ id: params.personId });
return {
user,
person,
statistics,
meta: {
title: person.name || 'Person',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const user = await authenticate();
const { data: person } = await api.personApi.getPerson({ id: params.personId });
const { data: statistics } = await api.personApi.getPersonStatistics({ id: params.personId });
return {
user,
person,
statistics,
meta: {
title: person.name || 'Person',
},
};
}) satisfies PageLoad;

View File

@ -1,14 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ params, parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const personId = params['personId'];
throw redirect(302, `${AppRoute.PEOPLE}/${personId}`);
};
export const load = (async ({ params }) => {
throw redirect(302, `${AppRoute.PEOPLE}/${params.personId}`);
}) satisfies PageLoad;

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Photos',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Photos',
},
};
}) satisfies PageLoad;

View File

@ -1,15 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import { AppRoute } from '$lib/constants';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else {
throw redirect(302, AppRoute.PHOTOS);
}
};

View File

@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.PHOTOS);
}) satisfies PageLoad;

View File

@ -1,23 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals, parent, url }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
const { data: results } = await locals.api.searchApi.search({}, { params: url.searchParams });
return {
user,
term,
results,
meta: {
title: 'Search',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,20 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const url = new URL(location.href);
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
const { data: results } = await api.searchApi.search({}, { params: url.searchParams });
return {
user,
term,
results,
meta: {
title: 'Search',
},
};
}) satisfies PageLoad;

View File

@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load = (async () => {
throw redirect(302, AppRoute.SEARCH);
};
}) satisfies PageLoad;

View File

@ -1,31 +1,32 @@
import featurePanelUrl from '$lib/assets/feature-panel.png';
import { api as clientApi, ThumbnailFormat } from '@api';
import { getAuthUser } from '$lib/utils/auth';
import { api, ThumbnailFormat } from '@api';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import type { AxiosError } from 'axios';
import type { PageLoad } from './$types';
export const load = (async ({ params, locals: { api }, cookies }) => {
export const load = (async ({ params }) => {
const { key } = params;
const token = cookies.get('immich_shared_link_token');
const user = await getAuthUser();
try {
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key, token });
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
const assetCount = sharedLink.assets.length;
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
return {
user,
sharedLink,
meta: {
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
description: sharedLink.description || `${assetCount} shared photos & videos.`,
imageUrl: assetId
? clientApi.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
: featurePanelUrl,
imageUrl: assetId ? api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key) : featurePanelUrl,
},
};
} catch (e) {
// handle unauthorized error
// TODO this doesn't allow for 404 shared links anymore
if ((e as AxiosError).response?.status === 401) {
return {
passwordRequired: true,
@ -40,4 +41,4 @@ export const load = (async ({ params, locals: { api }, cookies }) => {
message: 'Invalid shared link',
});
}
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@ -1,19 +0,0 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ params, locals: { api } }) => {
const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
if (!asset) {
throw error(404, 'Asset not found');
}
return {
asset,
key,
meta: {
title: 'Public Share',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,15 @@
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById({ id: assetId, key });
return {
asset,
key,
meta: {
title: 'Public Share',
},
};
}) satisfies PageLoad;

View File

@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { api, user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
try {
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
return {
user,
sharedAlbums,
partners,
meta: {
title: 'Sharing',
},
};
} catch (e) {
console.log(e);
throw redirect(302, AppRoute.AUTH_LOGIN);
}
}) satisfies PageServerLoad;

View File

@ -0,0 +1,18 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
return {
user,
sharedAlbums,
partners,
meta: {
title: 'Sharing',
},
};
}) satisfies PageLoad;

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Shared Links',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Shared Links',
},
};
}) satisfies PageLoad;

View File

@ -1,16 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Trash',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
return {
user,
meta: {
title: 'Trash',
},
};
}) satisfies PageLoad;

View File

@ -1,13 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
export const load = (async () => {
throw redirect(302, AppRoute.TRASH);
};
}) satisfies PageLoad;

View File

@ -1,22 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
const { data: keys } = await locals.api.keyApi.getApiKeys();
const { data: devices } = await locals.api.authenticationApi.getAuthDevices();
return {
user,
keys,
devices,
meta: {
title: 'Settings',
},
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,19 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate();
const { data: keys } = await api.keyApi.getApiKeys();
const { data: devices } = await api.authenticationApi.getAuthDevices();
return {
user,
keys,
devices,
meta: {
title: 'Settings',
},
};
}) satisfies PageLoad;

View File

@ -1,5 +0,0 @@
import type { LayoutServerLoad } from './$types';
export const load = (async ({ locals: { user } }) => {
return { user };
}) satisfies LayoutServerLoad;

View File

@ -24,7 +24,10 @@
export let data: LayoutData;
let albumId: string | undefined;
if ($page.route.id?.startsWith('/(user)/share/[key]')) {
const isSharedLinkRoute = (route: string | null) => route?.startsWith('/(user)/share/[key]');
const isAuthRoute = (route?: string) => route?.startsWith('/auth');
if (isSharedLinkRoute($page.route?.id)) {
api.setKey($page.params.key);
}
@ -32,11 +35,11 @@
const fromRoute = from?.route?.id || '';
const toRoute = to?.route?.id || '';
if (fromRoute.startsWith('/auth') && !toRoute.startsWith('/auth')) {
if (isAuthRoute(fromRoute) && !isAuthRoute(toRoute)) {
openWebsocketConnection();
}
if (!fromRoute.startsWith('/auth') && toRoute.startsWith('/auth')) {
if (!isAuthRoute(fromRoute) && isAuthRoute(toRoute)) {
closeWebsocketConnection();
}
@ -80,7 +83,6 @@
<svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/custom.css" />
<meta name="theme-color" content="currentColor" />
<FaviconHeader />
<AppleHeader />

25
web/src/routes/+layout.ts Normal file
View File

@ -0,0 +1,25 @@
import { api } from '../api';
import type { LayoutLoad } from './$types';
const getUser = async () => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
return user;
} catch {
return null;
}
};
export const ssr = false;
export const csr = true;
export const load = (async () => {
const user = await getUser();
return {
user,
meta: {
title: 'Immich',
},
};
}) satisfies LayoutLoad;

View File

@ -1,17 +1,19 @@
export const prerender = false;
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { api } from '../api';
import { isLoggedIn } from '../lib/utils/auth';
import type { PageLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (user) {
export const ssr = false;
export const csr = true;
export const load = (async () => {
const authenticated = await isLoggedIn();
if (authenticated) {
throw redirect(302, AppRoute.PHOTOS);
}
const { data } = await api.serverInfoApi.getServerConfig();
if (data.isInitialized) {
// Redirect to login page if there exists an admin account (i.e. server is initialized)
throw redirect(302, AppRoute.AUTH_LOGIN);
@ -23,4 +25,4 @@ export const load = (async ({ parent, locals: { api } }) => {
description: 'Immich Web Interface',
},
};
}) satisfies PageServerLoad;
}) satisfies PageLoad;

View File

@ -1,11 +0,0 @@
import { json } from '@sveltejs/kit';
const endpoint = process.env.IMMICH_API_URL_EXTERNAL || '/api';
export const GET = async () => {
return json({
api: {
endpoint,
},
});
};

View File

@ -1,15 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
};

View File

@ -0,0 +1,7 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
throw redirect(302, AppRoute.ADMIN_USER_MANAGEMENT);
}) satisfies PageLoad;

View File

@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ locals: { user, api } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
try {
const { data: jobs } = await api.jobApi.getAllJobsStatus();
return {
user,
jobs,
meta: {
title: 'Job Status',
},
};
} catch (err) {
console.error('[jobs] > getAllJobsStatus', err);
throw err;
}
}) satisfies PageServerLoad;

View File

@ -0,0 +1,17 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import type { PageLoad } from './$types';
export const load = (async () => {
const user = await authenticate({ admin: true });
const { data: jobs } = await api.jobApi.getAllJobsStatus();
return {
user,
jobs,
meta: {
title: 'Job Status',
},
};
}) satisfies PageLoad;

View File

@ -1,26 +0,0 @@
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load = (async ({ parent, locals: { api } }) => {
const { user } = await parent();
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
} else if (!user.isAdmin) {
throw redirect(302, AppRoute.PHOTOS);
}
const {
data: { orphans, extras },
} = await api.auditApi.getAuditFiles();
return {
user,
orphans,
extras,
meta: {
title: 'Repair',
},
};
}) satisfies PageServerLoad;

Some files were not shown because too many files have changed in this diff Show More