diff --git a/cli/package-lock.json b/cli/package-lock.json index dd368c2d6a..2937861ad9 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -54,7 +54,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.2", + "@types/node": "^20.11.0", "typescript": "^5.3.3" } }, diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 0000000000..5a1a8d5ba7 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,21 @@ + + +:443 { + tls internal { + on_demand + } + reverse_proxy immich-web:3000 + log { + output file /logs/caddy.log + } +} + +:444 { + tls internal { + on_demand + } + reverse_proxy immich-server:3001 + log { + output file /logs/caddy.log + } +} \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 09fa33c711..00c1f50bcb 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -5,6 +5,24 @@ name: immich-dev services: + nginx: + container_name: nginx_web + image: nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ../web/build:/usr/share/immich-web:ro + ports: + - 5000:80 + caddy: + container_name: immich_caddy + image: caddy + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + ports: + - 5001:443 + - 5002:444 + - 2019:2019 immich-server: container_name: immich_server command: ['/usr/src/app/bin/immich-dev'] @@ -19,6 +37,7 @@ services: restart: always volumes: - ../server:/usr/src/app + - ../server/node_modules:/usr/src/node_modules - ../open-api:/usr/src/open-api - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload @@ -107,7 +126,22 @@ services: interval: 5m start_interval: 30s start_period: 5m - command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] + command: + [ + 'postgres', + '-c', + 'shared_preload_libraries=vectors.so', + '-c', + 'search_path="$$user", public, vectors', + '-c', + 'logging_collector=on', + '-c', + 'max_wal_size=2GB', + '-c', + 'shared_buffers=512MB', + '-c', + 'wal_compression=on', + ] # set IMMICH_METRICS=true in .env to enable metrics # immich-prometheus: @@ -131,6 +165,7 @@ services: # - grafana-data:/var/lib/grafana volumes: + caddy_data: model-cache: prometheus-data: grafana-data: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 7503fa73dd..dd73b3316a 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,6 +1,14 @@ name: immich-prod services: + nginx: + container_name: nginx_web + image: nginx + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ../web/build:/usr/share/immich-web:ro + ports: + - 5000:80 immich-server: container_name: immich_server image: immich-server:latest @@ -15,6 +23,8 @@ services: - /etc/localtime:/etc/localtime:ro env_file: - .env + environment: + - IMMICH_ENV=production ports: - 2283:3001 depends_on: @@ -65,7 +75,22 @@ services: interval: 5m start_interval: 30s start_period: 5m - command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] + command: + [ + 'postgres', + '-c', + 'shared_preload_libraries=vectors.so', + '-c', + 'search_path="$$user", public, vectors', + '-c', + 'logging_collector=on', + '-c', + 'max_wal_size=2GB', + '-c', + 'shared_buffers=512MB', + '-c', + 'wal_compression=on', + ] restart: always # set IMMICH_METRICS=true in .env to enable metrics diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000000..84d5f12f09 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,40 @@ + +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on;\ + server { + listen 80; + location / { + root /usr/share/immich-web; + index index.html; + if (!-e $request_filename) { + rewrite ^(.*)$ /index.html break; + } + } + } +} + + diff --git a/docker/test.yml b/docker/test.yml new file mode 100644 index 0000000000..815789e51f --- /dev/null +++ b/docker/test.yml @@ -0,0 +1,23 @@ +# See: +# - https://immich.app/docs/developer/setup +# - https://immich.app/docs/developer/troubleshooting + +name: immich-dev + +services: + + caddy: + container_name: immich_caddy + image: caddy + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + ports: + - 5001:443 + - 2019:2019 + +volumes: + caddy_data: + model-cache: + prometheus-data: + grafana-data: diff --git a/e2e/package-lock.json b/e2e/package-lock.json index c9547b001f..217c07a496 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -88,7 +88,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.2", + "@types/node": "^20.11.0", "typescript": "^5.3.3" } }, diff --git a/server/src/utils/response.ts b/server/src/utils/response.ts index f318ca3300..112eb26427 100644 --- a/server/src/utils/response.ts +++ b/server/src/utils/response.ts @@ -5,7 +5,7 @@ import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto'; export const respondWithCookie = (res: Response, body: T, { isSecure, values }: CookieResponse) => { const defaults: CookieOptions = { path: '/', - sameSite: 'lax', + sameSite: 'none', httpOnly: true, secure: isSecure, maxAge: Duration.fromObject({ days: 400 }).toMillis(), diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 30d440240f..2f6862e80b 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -1,11 +1,13 @@ +import { CorsOptionsCallback, CustomOrigin } from '@nestjs/common/interfaces/external/cors-options.interface'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { json } from 'body-parser'; import cookieParser from 'cookie-parser'; +import { NextFunction, Request, Response } from 'express'; import { existsSync } from 'node:fs'; import sirv from 'sirv'; import { ApiModule } from 'src/app.module'; -import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants'; +import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ApiService } from 'src/services/api.service'; @@ -22,8 +24,15 @@ async function bootstrap() { const port = Number(process.env.IMMICH_PORT) || 3001; const app = await NestFactory.create(ApiModule, { bufferLogs: true }); + // app.use((req: Request, res: Response, next: NextFunction) => { + // console.log(req.url); + // console.log(req.get('origin')); + // console.log(req.get('host')); + // debugger; + // next(); + // }); const logger = await app.resolve(ILoggerRepository); - + //i logger.setAppName('Api'); logger.setContext('Bootstrap'); app.useLogger(logger); @@ -31,8 +40,32 @@ async function bootstrap() { app.set('etag', 'strong'); app.use(cookieParser()); app.use(json({ limit: '10mb' })); + debugger; // oddkjkjjh + const origins: CustomOrigin = (_, cb) => { + console.log('hi'); + debugger; + cb(null, [ + 'http://192.168.4.248:2283', + 'http://docker-dev:2283', + 'https://docker-dev:5002', + 'https://192.168.4.248:5001', + ]); + }; + if (isDev()) { - app.enableCors(); + debugger; + app.enableCors((req, cb) => { + if (req.get('origin')) { + cb(null, { + credentials: true, + origin: origins, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: ['content-type', '*'], + }); + } else { + cb(null, { origin: false }); + } + }); } app.useWebSocketAdapter(new WebSocketAdapter(app)); useSwagger(app); diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000000..5e076aadc8 --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,32 @@ + +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/web/package-lock.json b/web/package-lock.json index dc3fb46d37..0673db11df 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -74,7 +74,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.2", + "@types/node": "^20.11.0", "typescript": "^5.3.3" } }, diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index df5c9bc46a..c2a9a02a5b 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -15,7 +15,8 @@ export const loadUser = async () => { try { let user = get(user$); let preferences = get(preferences$); - if ((!user || !preferences) && hasAuthCookie()) { + debugger; + if (!user || !preferences) { [user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]); user$.set(user); preferences$.set(preferences); diff --git a/web/src/lib/utils/server.ts b/web/src/lib/utils/server.ts index ae28dd5176..09bb81c88b 100644 --- a/web/src/lib/utils/server.ts +++ b/web/src/lib/utils/server.ts @@ -5,20 +5,23 @@ const wait = (ms: number) => new Promise((_, reject) => setTimeout(reject, ms)); const tryServers = async (fetchFn: typeof fetch) => { const server_urls_env = env.PUBLIC_SERVER_URLS; - if (server_urls_env) { - const servers = server_urls_env.split(','); - // servers are in priority order, try in parallel, use first success - const fetchers = servers.map((url) => ({ url, fetcher: fetchFn(`${url}/server-info/config`) })); - for (const { url, fetcher } of fetchers) { - try { - const response = (await Promise.race([fetcher, wait(1000)])) as Response; - if (response?.ok) { - defaults.basePath = url; - return true; - } - } catch { - // ignore, handled upstream + + let servers = server_urls_env.split(','); + servers = ['https://docker-dev:5002/api']; + // servers = []; + // servers are in priority order, try in parallel, use first success + const fetchers = servers.map((url) => ({ url, fetcher: fetchFn(`${url}/server-info/config`) })); + for (const { url, fetcher } of fetchers) { + try { + const response = (await Promise.race([fetcher, wait(1000)])) as Response; + if (response?.ok) { + debugger; + defaults.baseUrl = url; + defaults.credentials = 'include'; + return true; } + } catch { + // ignore, handled upstream } } diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts index b7c62c142c..52c1c62a7b 100644 --- a/web/src/routes/+layout.ts +++ b/web/src/routes/+layout.ts @@ -7,8 +7,16 @@ import type { LayoutLoad } from './$types'; export const ssr = false; export const csr = true; +export const prerender = true; export const load = (async ({ fetch }) => { + for (const { code, loader } of langs) { + register(code, loader); + } + + const preferenceLang = get(lang); + + await init({ fallbackLocale: preferenceLang === 'dev' ? 'dev' : defaultLang.code, initialLocale: preferenceLang }); let hasError = false; try { await initSDK(fetch); @@ -17,14 +25,6 @@ export const load = (async ({ fetch }) => { hasError = true; } - for (const { code, loader } of langs) { - register(code, loader); - } - - const preferenceLang = get(lang); - - await init({ fallbackLocale: preferenceLang === 'dev' ? 'dev' : defaultLang.code, initialLocale: preferenceLang }); - return { hasError, meta: { diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index ed10f6020b..fd174a543e 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -1,4 +1,5 @@ import { AppRoute } from '$lib/constants'; +import { initSDK } from '$lib/utils/server'; import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import { t } from 'svelte-i18n'; @@ -10,6 +11,14 @@ export const ssr = false; export const csr = true; export const load = (async () => { + let hasError = false; + try { + await initSDK(fetch); + } catch { + // error pages use page layouts, so can't throw error - catch it and display error message in layout. + hasError = true; + } + debugger; const authenticated = await loadUser(); if (authenticated) { redirect(302, AppRoute.PHOTOS); @@ -24,6 +33,7 @@ export const load = (async () => { const $t = get(t); return { + hasError, meta: { title: $t('welcome') + ' 🎉', description: $t('immich_web_interface'), diff --git a/web/svelte.config.js b/web/svelte.config.js index 76a9c2e55b..1576e4bb32 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -5,8 +5,18 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; const config = { preprocess: vitePreprocess(), kit: { + prerender: { + handleHttpError: ({ path, referrer, message }) => { + if (path === '/custom.css') { + return; + } + + // otherwise fail the build + throw new Error(message); + }, + }, adapter: adapter({ - fallback: 'index.html', + // fallback: 'index.html', precompress: true, }), alias: { diff --git a/web/vite.config.js b/web/vite.config.js index 7d15832de4..2efbc7a958 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -13,6 +13,9 @@ const upstream = { }; export default defineConfig({ + build: { + minify: false, + }, resolve: { alias: { 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js', @@ -27,6 +30,9 @@ export default defineConfig({ '/.well-known/immich': upstream, '/custom.css': upstream, }, + cors: { + origin: false, + }, }, plugins: [ sveltekit(),