diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..d1f60fa477 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index ea7374bbf4..6de84423fe 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -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: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 823b73c254..4df3ade451 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index c2889ed8b9..646d6e9b6d 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index e2f8ebd529..ad764250c9 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -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: diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml index 11f40e8b20..442b0d4d53 100644 --- a/docker/docker-compose.test.yml +++ b/docker/docker-compose.test.yml @@ -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 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 04e6fcccbe..d4399201eb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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: diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 2debb44a11..53e1e0c61a 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -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. diff --git a/docs/docs/developer/directories.md b/docs/docs/developer/directories.md index 77f8e79606..3ec483294a 100644 --- a/docs/docs/developer/directories.md +++ b/docs/docs/developer/directories.md @@ -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` | diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index e712426ff0..2dcd3038a9 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -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 diff --git a/docs/docs/developer/troubleshooting.md b/docs/docs/developer/troubleshooting.md index 83e4f370df..8663f53563 100644 --- a/docs/docs/developer/troubleshooting.md +++ b/docs/docs/developer/troubleshooting.md @@ -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`. diff --git a/docs/docs/install/docker-compose.md b/docs/docs/install/docker-compose.md index d09ba531fc..6c1dc184be 100644 --- a/docs/docs/install/docker-compose.md +++ b/docs/docs/install/docker-compose.md @@ -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 # diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index d41fef0a75..025568a67f 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -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 | diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 8b2638e39c..cba15a12e2 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -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.
@@ -112,12 +112,12 @@ alt="Go to Docker Tab and visit the address listed next to immich-proxy" Go to Docker Tab and visit the address listed next to immich-proxy Go to Docker Tab and visit the address listed next to immich-proxy
diff --git a/nginx/10-listen-on-ipv6-by-default.sh b/nginx/10-listen-on-ipv6-by-default.sh deleted file mode 100755 index e4d85c7cfa..0000000000 --- a/nginx/10-listen-on-ipv6-by-default.sh +++ /dev/null @@ -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 diff --git a/nginx/15-set-env-variables.envsh b/nginx/15-set-env-variables.envsh deleted file mode 100755 index 5d4acb1ba7..0000000000 --- a/nginx/15-set-env-variables.envsh +++ /dev/null @@ -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 diff --git a/nginx/Dockerfile b/nginx/Dockerfile deleted file mode 100644 index 718e7f61cc..0000000000 --- a/nginx/Dockerfile +++ /dev/null @@ -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 diff --git a/nginx/LICENSE b/nginx/LICENSE deleted file mode 100644 index a72f398805..0000000000 --- a/nginx/LICENSE +++ /dev/null @@ -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. diff --git a/nginx/templates/default.conf.template b/nginx/templates/default.conf.template deleted file mode 100644 index 4eca1bd14a..0000000000 --- a/nginx/templates/default.conf.template +++ /dev/null @@ -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; - } -} diff --git a/server/.dockerignore b/server/.dockerignore deleted file mode 100644 index f4a26f12b1..0000000000 --- a/server/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -upload/ -dist/ -coverage/ -.reverse-geocoding-dump diff --git a/server/Dockerfile b/server/Dockerfile index 02aa18611d..784da7fcce 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -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"] diff --git a/server/LICENSE b/server/LICENSE deleted file mode 100644 index a72f398805..0000000000 --- a/server/LICENSE +++ /dev/null @@ -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. diff --git a/server/src/admin-cli/commands/password-login.ts b/server/src/admin-cli/commands/password-login.ts index 8cd1276bc0..c846ba2f40 100644 --- a/server/src/admin-cli/commands/password-login.ts +++ b/server/src/admin-cli/commands/password-login.ts @@ -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.'); } } diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 06e97e6460..cdeb552b09 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -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); }); }); }); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 1fdbca65ed..5e9743ba5a 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -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 { - const { theme } = await this.core.getConfig(); - return theme; - } - async getConfig(): Promise { 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 { + const { theme } = await this.core.getConfig(); + return theme.customCss; + } } diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts index a7584cb1ae..a667dce9f5 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/immich/app.utils.ts @@ -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') diff --git a/server/src/immich/controllers/app.controller.ts b/server/src/immich/controllers/app.controller.ts index 9764ceb141..bd7c5d4c75 100644 --- a/server/src/immich/controllers/app.controller.ts +++ b/server/src/immich/controllers/app.controller.ts @@ -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(); } } diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts index e3ac816317..832d988af1 100644 --- a/server/src/immich/main.ts +++ b/server/src/immich/main.ts @@ -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; diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index e1bbc77de9..3eabaa86af 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -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[] = []; diff --git a/web/.dockerignore b/web/.dockerignore index 834ab88b61..3b05277d16 100644 --- a/web/.dockerignore +++ b/web/.dockerignore @@ -1,4 +1,4 @@ node_modules/ -upload/ -dist/ - +coverage/ +.svelte-kit +build/ diff --git a/web/Dockerfile b/web/Dockerfile index d6d58217ed..48b1b3f070 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -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"] diff --git a/web/LICENSE b/web/LICENSE deleted file mode 100644 index a72f398805..0000000000 --- a/web/LICENSE +++ /dev/null @@ -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. diff --git a/web/entrypoint.sh b/web/entrypoint.sh deleted file mode 100755 index 5f52b7b5ff..0000000000 --- a/web/entrypoint.sh +++ /dev/null @@ -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 diff --git a/web/package-lock.json b/web/package-lock.json index dc0dc47785..d52584f1db 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index a964e97137..56a2e5d8ac 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 3a3584ed4d..fc6f49f021 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -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; diff --git a/web/src/app.d.ts b/web/src/app.d.ts index c29b346beb..241a579fc7 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -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; diff --git a/web/src/app.html b/web/src/app.html index 9da6868f7d..f35409429e 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -13,6 +13,7 @@ document.documentElement.classList.remove('dark'); } + diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts new file mode 100644 index 0000000000..1e29371fa9 --- /dev/null +++ b/web/src/hooks.client.ts @@ -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; +}; diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts deleted file mode 100644 index 809e3b0c3d..0000000000 --- a/web/src/hooks.server.ts +++ /dev/null @@ -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; -}; diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 2bb95495fa..3fa607e41d 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -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'); }; diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts new file mode 100644 index 0000000000..c5cd992105 --- /dev/null +++ b/web/src/lib/utils/auth.ts @@ -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); diff --git a/web/src/routes/(user)/albums/+page.server.ts b/web/src/routes/(user)/albums/+page.server.ts deleted file mode 100644 index a9373d09c5..0000000000 --- a/web/src/routes/(user)/albums/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/albums/+page.ts b/web/src/routes/(user)/albums/+page.ts new file mode 100644 index 0000000000..037fc1ef68 --- /dev/null +++ b/web/src/routes/(user)/albums/+page.ts @@ -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; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.server.ts b/web/src/routes/(user)/albums/[albumId]/+page.server.ts deleted file mode 100644 index bd19e7cb16..0000000000 --- a/web/src/routes/(user)/albums/[albumId]/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.ts b/web/src/routes/(user)/albums/[albumId]/+page.ts new file mode 100644 index 0000000000..8a6e4913f3 --- /dev/null +++ b/web/src/routes/(user)/albums/[albumId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/albums/[albumId]/photos/[assetId]/+page.ts b/web/src/routes/(user)/albums/[albumId]/photos/[assetId]/+page.ts index cb325776ee..42fa435c4b 100644 --- a/web/src/routes/(user)/albums/[albumId]/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId]/photos/[assetId]/+page.ts @@ -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}`); diff --git a/web/src/routes/(user)/archive/+page.server.ts b/web/src/routes/(user)/archive/+page.server.ts deleted file mode 100644 index 43e7c86524..0000000000 --- a/web/src/routes/(user)/archive/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/archive/+page.ts b/web/src/routes/(user)/archive/+page.ts new file mode 100644 index 0000000000..90c66d4d54 --- /dev/null +++ b/web/src/routes/(user)/archive/+page.ts @@ -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; diff --git a/web/src/routes/(user)/archive/photos/[assetId]/+page.ts b/web/src/routes/(user)/archive/photos/[assetId]/+page.ts index 88923a1af5..c8ee61048f 100644 --- a/web/src/routes/(user)/archive/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/archive/photos/[assetId]/+page.ts @@ -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); }; diff --git a/web/src/routes/(user)/explore/+page.server.ts b/web/src/routes/(user)/explore/+page.server.ts deleted file mode 100644 index 5d0491ddd7..0000000000 --- a/web/src/routes/(user)/explore/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/explore/+page.ts b/web/src/routes/(user)/explore/+page.ts new file mode 100644 index 0000000000..a89ac8c834 --- /dev/null +++ b/web/src/routes/(user)/explore/+page.ts @@ -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; diff --git a/web/src/routes/(user)/favorites/+page.server.ts b/web/src/routes/(user)/favorites/+page.server.ts deleted file mode 100644 index d65255b45f..0000000000 --- a/web/src/routes/(user)/favorites/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/favorites/+page.ts b/web/src/routes/(user)/favorites/+page.ts new file mode 100644 index 0000000000..bc2d3d201d --- /dev/null +++ b/web/src/routes/(user)/favorites/+page.ts @@ -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; diff --git a/web/src/routes/(user)/favorites/[assetId]/+page.server.ts b/web/src/routes/(user)/favorites/[assetId]/+page.server.ts deleted file mode 100644 index fd6aa42d4c..0000000000 --- a/web/src/routes/(user)/favorites/[assetId]/+page.server.ts +++ /dev/null @@ -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); - } -}; diff --git a/web/src/routes/(user)/favorites/[assetId]/+page.ts b/web/src/routes/(user)/favorites/[assetId]/+page.ts new file mode 100644 index 0000000000..6a46acf9de --- /dev/null +++ b/web/src/routes/(user)/favorites/[assetId]/+page.ts @@ -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); +}; diff --git a/web/src/routes/(user)/map/+page.server.ts b/web/src/routes/(user)/map/+page.server.ts deleted file mode 100644 index 50f450fa6a..0000000000 --- a/web/src/routes/(user)/map/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/map/+page.ts b/web/src/routes/(user)/map/+page.ts new file mode 100644 index 0000000000..2ee84c8767 --- /dev/null +++ b/web/src/routes/(user)/map/+page.ts @@ -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; diff --git a/web/src/routes/(user)/memory/+page.server.ts b/web/src/routes/(user)/memory/+page.server.ts deleted file mode 100644 index 3973762d7b..0000000000 --- a/web/src/routes/(user)/memory/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/memory/+page.ts b/web/src/routes/(user)/memory/+page.ts new file mode 100644 index 0000000000..e56dd392d6 --- /dev/null +++ b/web/src/routes/(user)/memory/+page.ts @@ -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; diff --git a/web/src/routes/(user)/memory/photos/+page.server.ts b/web/src/routes/(user)/memory/photos/+page.server.ts deleted file mode 100644 index bd32438aa0..0000000000 --- a/web/src/routes/(user)/memory/photos/+page.server.ts +++ /dev/null @@ -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); - } -}; diff --git a/web/src/routes/(user)/memory/photos/+page.ts b/web/src/routes/(user)/memory/photos/+page.ts new file mode 100644 index 0000000000..07263cff58 --- /dev/null +++ b/web/src/routes/(user)/memory/photos/+page.ts @@ -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; diff --git a/web/src/routes/(user)/memory/photos/[assetId]/+page.server.ts b/web/src/routes/(user)/memory/photos/[assetId]/+page.server.ts deleted file mode 100644 index bd32438aa0..0000000000 --- a/web/src/routes/(user)/memory/photos/[assetId]/+page.server.ts +++ /dev/null @@ -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); - } -}; diff --git a/web/src/routes/(user)/memory/photos/[assetId]/+page.ts b/web/src/routes/(user)/memory/photos/[assetId]/+page.ts new file mode 100644 index 0000000000..fb2fb42d12 --- /dev/null +++ b/web/src/routes/(user)/memory/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/partners/[userId]/+page.server.ts b/web/src/routes/(user)/partners/[userId]/+page.server.ts deleted file mode 100644 index 13fbb1c102..0000000000 --- a/web/src/routes/(user)/partners/[userId]/+page.server.ts +++ /dev/null @@ -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', - }, - }; -}; diff --git a/web/src/routes/(user)/partners/[userId]/+page.ts b/web/src/routes/(user)/partners/[userId]/+page.ts new file mode 100644 index 0000000000..6bbbdd42d1 --- /dev/null +++ b/web/src/routes/(user)/partners/[userId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/people/+page.server.ts b/web/src/routes/(user)/people/+page.server.ts deleted file mode 100644 index de5354d065..0000000000 --- a/web/src/routes/(user)/people/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/people/+page.ts b/web/src/routes/(user)/people/+page.ts new file mode 100644 index 0000000000..0d82a1fa48 --- /dev/null +++ b/web/src/routes/(user)/people/+page.ts @@ -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; diff --git a/web/src/routes/(user)/people/[personId]/+page.server.ts b/web/src/routes/(user)/people/[personId]/+page.server.ts deleted file mode 100644 index d81f893abf..0000000000 --- a/web/src/routes/(user)/people/[personId]/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/people/[personId]/+page.ts b/web/src/routes/(user)/people/[personId]/+page.ts new file mode 100644 index 0000000000..71f6f46b31 --- /dev/null +++ b/web/src/routes/(user)/people/[personId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts b/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts index ff1ae1cf8a..21ad37559e 100644 --- a/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/photos/+page.server.ts b/web/src/routes/(user)/photos/+page.server.ts deleted file mode 100644 index e8965f02fc..0000000000 --- a/web/src/routes/(user)/photos/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/photos/+page.ts b/web/src/routes/(user)/photos/+page.ts new file mode 100644 index 0000000000..26e6ffb2d6 --- /dev/null +++ b/web/src/routes/(user)/photos/+page.ts @@ -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; diff --git a/web/src/routes/(user)/photos/[assetId]/+page.server.ts b/web/src/routes/(user)/photos/[assetId]/+page.server.ts deleted file mode 100644 index 1c8a2a51cd..0000000000 --- a/web/src/routes/(user)/photos/[assetId]/+page.server.ts +++ /dev/null @@ -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); - } -}; diff --git a/web/src/routes/(user)/photos/[assetId]/+page.ts b/web/src/routes/(user)/photos/[assetId]/+page.ts new file mode 100644 index 0000000000..fb2fb42d12 --- /dev/null +++ b/web/src/routes/(user)/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/search/+page.server.ts b/web/src/routes/(user)/search/+page.server.ts deleted file mode 100644 index 17a09fefc0..0000000000 --- a/web/src/routes/(user)/search/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/search/+page.ts b/web/src/routes/(user)/search/+page.ts new file mode 100644 index 0000000000..ce150a37bc --- /dev/null +++ b/web/src/routes/(user)/search/+page.ts @@ -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; diff --git a/web/src/routes/(user)/search/photos/[assetId]/+page.ts b/web/src/routes/(user)/search/photos/[assetId]/+page.ts index 994670a69e..512030675a 100644 --- a/web/src/routes/(user)/search/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/search/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/share/[key]/+page.server.ts b/web/src/routes/(user)/share/[key]/+page.ts similarity index 67% rename from web/src/routes/(user)/share/[key]/+page.server.ts rename to web/src/routes/(user)/share/[key]/+page.ts index 5ba044df96..21604ed683 100644 --- a/web/src/routes/(user)/share/[key]/+page.server.ts +++ b/web/src/routes/(user)/share/[key]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.server.ts b/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.server.ts deleted file mode 100644 index b0e26e8426..0000000000 --- a/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.ts b/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.ts new file mode 100644 index 0000000000..b76a84be24 --- /dev/null +++ b/web/src/routes/(user)/share/[key]/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/sharing/+page.server.ts b/web/src/routes/(user)/sharing/+page.server.ts deleted file mode 100644 index 2b77b4677a..0000000000 --- a/web/src/routes/(user)/sharing/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/sharing/+page.ts b/web/src/routes/(user)/sharing/+page.ts new file mode 100644 index 0000000000..76932342c2 --- /dev/null +++ b/web/src/routes/(user)/sharing/+page.ts @@ -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; diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.server.ts b/web/src/routes/(user)/sharing/sharedlinks/+page.server.ts deleted file mode 100644 index c849ff997d..0000000000 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.ts b/web/src/routes/(user)/sharing/sharedlinks/+page.ts new file mode 100644 index 0000000000..61865785c4 --- /dev/null +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.ts @@ -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; diff --git a/web/src/routes/(user)/trash/+page.server.ts b/web/src/routes/(user)/trash/+page.server.ts deleted file mode 100644 index e9a726311a..0000000000 --- a/web/src/routes/(user)/trash/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/trash/+page.ts b/web/src/routes/(user)/trash/+page.ts new file mode 100644 index 0000000000..cc0e39f16b --- /dev/null +++ b/web/src/routes/(user)/trash/+page.ts @@ -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; diff --git a/web/src/routes/(user)/trash/photos/[assetId]/+page.ts b/web/src/routes/(user)/trash/photos/[assetId]/+page.ts index 6a840178cf..051024e5e4 100644 --- a/web/src/routes/(user)/trash/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/trash/photos/[assetId]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/user-settings/+page.server.ts b/web/src/routes/(user)/user-settings/+page.server.ts deleted file mode 100644 index 3465826383..0000000000 --- a/web/src/routes/(user)/user-settings/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/(user)/user-settings/+page.ts b/web/src/routes/(user)/user-settings/+page.ts new file mode 100644 index 0000000000..93aa5ddeeb --- /dev/null +++ b/web/src/routes/(user)/user-settings/+page.ts @@ -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; diff --git a/web/src/routes/+layout.server.ts b/web/src/routes/+layout.server.ts deleted file mode 100644 index 804071cd71..0000000000 --- a/web/src/routes/+layout.server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LayoutServerLoad } from './$types'; - -export const load = (async ({ locals: { user } }) => { - return { user }; -}) satisfies LayoutServerLoad; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 3c43b98ee6..928164bb67 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -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 @@ {$page.data.meta?.title || 'Web'} - Immich - diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 0000000000..d59082b9aa --- /dev/null +++ b/web/src/routes/+layout.ts @@ -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; diff --git a/web/src/routes/+page.server.ts b/web/src/routes/+page.ts similarity index 61% rename from web/src/routes/+page.server.ts rename to web/src/routes/+page.ts index b469170bed..438b289cea 100644 --- a/web/src/routes/+page.server.ts +++ b/web/src/routes/+page.ts @@ -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; diff --git a/web/src/routes/.well-known/immich/+server.ts b/web/src/routes/.well-known/immich/+server.ts deleted file mode 100644 index 37d7788062..0000000000 --- a/web/src/routes/.well-known/immich/+server.ts +++ /dev/null @@ -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, - }, - }); -}; diff --git a/web/src/routes/admin/+page.server.ts b/web/src/routes/admin/+page.server.ts deleted file mode 100644 index 777b940dcd..0000000000 --- a/web/src/routes/admin/+page.server.ts +++ /dev/null @@ -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); -}; diff --git a/web/src/routes/admin/+page.ts b/web/src/routes/admin/+page.ts new file mode 100644 index 0000000000..3eca8c08da --- /dev/null +++ b/web/src/routes/admin/+page.ts @@ -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; diff --git a/web/src/routes/admin/jobs-status/+page.server.ts b/web/src/routes/admin/jobs-status/+page.server.ts deleted file mode 100644 index 7e700ba338..0000000000 --- a/web/src/routes/admin/jobs-status/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/admin/jobs-status/+page.ts b/web/src/routes/admin/jobs-status/+page.ts new file mode 100644 index 0000000000..b6face823c --- /dev/null +++ b/web/src/routes/admin/jobs-status/+page.ts @@ -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; diff --git a/web/src/routes/admin/repair/+page.server.ts b/web/src/routes/admin/repair/+page.server.ts deleted file mode 100644 index 9f04e013ca..0000000000 --- a/web/src/routes/admin/repair/+page.server.ts +++ /dev/null @@ -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; diff --git a/web/src/routes/admin/repair/+page.ts b/web/src/routes/admin/repair/+page.ts new file mode 100644 index 0000000000..9ad200116d --- /dev/null +++ b/web/src/routes/admin/repair/+page.ts @@ -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({ admin: true }); + const { + data: { orphans, extras }, + } = await api.auditApi.getAuditFiles(); + + return { + user, + orphans, + extras, + meta: { + title: 'Repair', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/admin/server-status/+page.server.ts b/web/src/routes/admin/server-status/+page.server.ts deleted file mode 100644 index a9b08e2a16..0000000000 --- a/web/src/routes/admin/server-status/+page.server.ts +++ /dev/null @@ -1,23 +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: stats } = await api.serverInfoApi.getServerStatistics(); - - return { - user, - stats, - meta: { - title: 'Server Stats', - }, - }; -}) satisfies PageServerLoad; diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts new file mode 100644 index 0000000000..73d0ab2295 --- /dev/null +++ b/web/src/routes/admin/server-status/+page.ts @@ -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({ admin: true }); + const { data: stats } = await api.serverInfoApi.getServerStatistics(); + + return { + user, + stats, + meta: { + title: 'Server Stats', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/admin/system-settings/+page.server.ts b/web/src/routes/admin/system-settings/+page.server.ts deleted file mode 100644 index d1b6525184..0000000000 --- a/web/src/routes/admin/system-settings/+page.server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AppRoute } from '$lib/constants'; -import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = 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: configs } = await api.systemConfigApi.getConfig(); - - return { - user, - configs, - meta: { - title: 'System Settings', - }, - }; -}; diff --git a/web/src/routes/admin/system-settings/+page.ts b/web/src/routes/admin/system-settings/+page.ts new file mode 100644 index 0000000000..fa635ae82d --- /dev/null +++ b/web/src/routes/admin/system-settings/+page.ts @@ -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({ admin: true }); + const { data: configs } = await api.systemConfigApi.getConfig(); + + return { + user, + configs, + meta: { + title: 'System Settings', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/admin/user-management/+page.server.ts b/web/src/routes/admin/user-management/+page.server.ts deleted file mode 100644 index 54fdc4311c..0000000000 --- a/web/src/routes/admin/user-management/+page.server.ts +++ /dev/null @@ -1,23 +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: allUsers } = await api.userApi.getAllUsers({ isAll: false }); - - return { - user, - allUsers, - meta: { - title: 'User Management', - }, - }; -}) satisfies PageServerLoad; diff --git a/web/src/routes/admin/user-management/+page.ts b/web/src/routes/admin/user-management/+page.ts new file mode 100644 index 0000000000..566a307530 --- /dev/null +++ b/web/src/routes/admin/user-management/+page.ts @@ -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({ admin: true }); + const { data: allUsers } = await api.userApi.getAllUsers({ isAll: false }); + + return { + user, + allUsers, + meta: { + title: 'User Management', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/auth/change-password/+page.server.ts b/web/src/routes/auth/change-password/+page.server.ts deleted file mode 100644 index 774f2f0b09..0000000000 --- a/web/src/routes/auth/change-password/+page.server.ts +++ /dev/null @@ -1,18 +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); - } else if (!user.shouldChangePassword) { - throw redirect(302, AppRoute.PHOTOS); - } - - return { - user, - meta: { - title: 'Change Password', - }, - }; -}) satisfies PageServerLoad; diff --git a/web/src/routes/auth/change-password/+page.ts b/web/src/routes/auth/change-password/+page.ts new file mode 100644 index 0000000000..0f391a5ad6 --- /dev/null +++ b/web/src/routes/auth/change-password/+page.ts @@ -0,0 +1,18 @@ +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 () => { + const user = await authenticate(); + if (!user.shouldChangePassword) { + throw redirect(302, AppRoute.PHOTOS); + } + + return { + user, + meta: { + title: 'Change Password', + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/auth/login/+page.server.ts b/web/src/routes/auth/login/+page.ts similarity index 70% rename from web/src/routes/auth/login/+page.server.ts rename to web/src/routes/auth/login/+page.ts index a294173b72..270fff2d81 100644 --- a/web/src/routes/auth/login/+page.server.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,8 +1,9 @@ import { AppRoute } from '$lib/constants'; +import { api } from '@api'; import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; +import type { PageLoad } from './$types'; -export const load = (async ({ locals: { api } }) => { +export const load = (async () => { const { data } = await api.serverInfoApi.getServerConfig(); if (!data.isInitialized) { // Admin not registered @@ -14,4 +15,4 @@ export const load = (async ({ locals: { api } }) => { title: 'Login', }, }; -}) satisfies PageServerLoad; +}) satisfies PageLoad; diff --git a/web/src/routes/auth/logout/+server.ts b/web/src/routes/auth/logout/+server.ts deleted file mode 100644 index dff2627b8f..0000000000 --- a/web/src/routes/auth/logout/+server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { api } from '@api'; -import type { RequestHandler } from '@sveltejs/kit'; -import { json } from '@sveltejs/kit'; - -export const POST = (async ({ cookies }) => { - api.removeAccessToken(); - - cookies.delete('immich_auth_type', { path: '/' }); - cookies.delete('immich_access_token', { path: '/' }); - - return json({ ok: true }); -}) satisfies RequestHandler; diff --git a/web/src/routes/auth/register/+page.server.ts b/web/src/routes/auth/register/+page.ts similarity index 72% rename from web/src/routes/auth/register/+page.server.ts rename to web/src/routes/auth/register/+page.ts index 186ab2e3d8..d9709e3f09 100644 --- a/web/src/routes/auth/register/+page.server.ts +++ b/web/src/routes/auth/register/+page.ts @@ -1,8 +1,9 @@ import { AppRoute } from '$lib/constants'; +import { api } from '@api'; import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; +import type { PageLoad } from './$types'; -export const load = (async ({ locals: { api } }) => { +export const load = (async () => { const { data } = await api.serverInfoApi.getServerConfig(); if (data.isInitialized) { // Admin has been registered, redirect to login @@ -14,4 +15,4 @@ export const load = (async ({ locals: { api } }) => { title: 'Admin Registration', }, }; -}) satisfies PageServerLoad; +}) satisfies PageLoad; diff --git a/web/src/routes/custom.css/+server.ts b/web/src/routes/custom.css/+server.ts deleted file mode 100644 index e71728f2a6..0000000000 --- a/web/src/routes/custom.css/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RequestHandler, text } from '@sveltejs/kit'; -export const GET = (async ({ locals: { api } }) => { - const { - data: { customCss }, - } = await api.serverInfoApi.getTheme(); - return text(customCss, { - headers: { - 'Content-Type': 'text/css', - }, - }); -}) satisfies RequestHandler; diff --git a/web/svelte.config.js b/web/svelte.config.js index f9cef5166d..ebbb2beedc 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-node'; +import adapter from '@sveltejs/adapter-static'; import preprocess from 'svelte-preprocess'; /** @type {import('@sveltejs/kit').Config} */ @@ -11,7 +11,15 @@ const config = { handler(warning); }, kit: { - adapter: adapter({ out: 'build' }), + adapter: adapter({ + // default options are shown. On some platforms + // these options are set automatically — see below + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true, + }), }, }; diff --git a/web/vite.config.js b/web/vite.config.js index 4406c0ad00..6c550cd6f6 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1,6 +1,14 @@ import { sveltekit } from '@sveltejs/kit/vite'; import path from 'path'; +const upstream = { + target: process.env.IMMICH_SERVER_URL || 'http://immich-server:3001/', + secure: true, + changeOrigin: true, + logLevel: 'debug', + ws: true, +}; + /** @type {import('vite').UserConfig} */ const config = { resolve: { @@ -12,14 +20,9 @@ const config = { server: { // connect to a remote backend during web-only development proxy: { - '/api': { - target: process.env.PUBLIC_IMMICH_SERVER_URL, - secure: true, - changeOrigin: true, - logLevel: 'debug', - rewrite: (path) => path.replace(/^\/api/, ''), - ws: true, - }, + '/api': upstream, + '/.well-known/immich': upstream, + '/custom.css': upstream, }, }, plugins: [sveltekit()],