mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor(server): external domain fallback (#13506)
This commit is contained in:
		
							parent
							
								
									51d4899cd1
								
							
						
					
					
						commit
						8ac40a933a
					
				@ -20,8 +20,6 @@ export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
 | 
				
			|||||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
 | 
					export const ONE_HOUR = Duration.fromObject({ hours: 1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 | 
					export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 | 
				
			||||||
const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
 | 
					 | 
				
			||||||
export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const citiesFile = 'cities500.txt';
 | 
					export const citiesFile = 'cities500.txt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
 | 
					import { BadRequestException, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
 | 
					 | 
				
			||||||
import { OnEvent } from 'src/decorators';
 | 
					import { OnEvent } from 'src/decorators';
 | 
				
			||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
 | 
					import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
 | 
				
			||||||
import { AlbumEntity } from 'src/entities/album.entity';
 | 
					import { AlbumEntity } from 'src/entities/album.entity';
 | 
				
			||||||
@ -16,6 +15,7 @@ import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification
 | 
				
			|||||||
import { BaseService } from 'src/services/base.service';
 | 
					import { BaseService } from 'src/services/base.service';
 | 
				
			||||||
import { getAssetFiles } from 'src/utils/asset.util';
 | 
					import { getAssetFiles } from 'src/utils/asset.util';
 | 
				
			||||||
import { getFilenameExtension } from 'src/utils/file';
 | 
					import { getFilenameExtension } from 'src/utils/file';
 | 
				
			||||||
 | 
					import { getExternalDomain } from 'src/utils/misc';
 | 
				
			||||||
import { isEqualObject } from 'src/utils/object';
 | 
					import { isEqualObject } from 'src/utils/object';
 | 
				
			||||||
import { getPreferences } from 'src/utils/preferences';
 | 
					import { getPreferences } from 'src/utils/preferences';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -128,10 +128,11 @@ export class NotificationService extends BaseService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { server } = await this.getConfig({ withCache: false });
 | 
					    const { server } = await this.getConfig({ withCache: false });
 | 
				
			||||||
 | 
					    const { port } = this.configRepository.getEnv();
 | 
				
			||||||
    const { html, text } = await this.notificationRepository.renderEmail({
 | 
					    const { html, text } = await this.notificationRepository.renderEmail({
 | 
				
			||||||
      template: EmailTemplate.TEST_EMAIL,
 | 
					      template: EmailTemplate.TEST_EMAIL,
 | 
				
			||||||
      data: {
 | 
					      data: {
 | 
				
			||||||
        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
 | 
					        baseUrl: getExternalDomain(server, port),
 | 
				
			||||||
        displayName: user.name,
 | 
					        displayName: user.name,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -156,10 +157,11 @@ export class NotificationService extends BaseService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { server } = await this.getConfig({ withCache: true });
 | 
					    const { server } = await this.getConfig({ withCache: true });
 | 
				
			||||||
 | 
					    const { port } = this.configRepository.getEnv();
 | 
				
			||||||
    const { html, text } = await this.notificationRepository.renderEmail({
 | 
					    const { html, text } = await this.notificationRepository.renderEmail({
 | 
				
			||||||
      template: EmailTemplate.WELCOME,
 | 
					      template: EmailTemplate.WELCOME,
 | 
				
			||||||
      data: {
 | 
					      data: {
 | 
				
			||||||
        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
 | 
					        baseUrl: getExternalDomain(server, port),
 | 
				
			||||||
        displayName: user.name,
 | 
					        displayName: user.name,
 | 
				
			||||||
        username: user.email,
 | 
					        username: user.email,
 | 
				
			||||||
        password: tempPassword,
 | 
					        password: tempPassword,
 | 
				
			||||||
@ -199,10 +201,11 @@ export class NotificationService extends BaseService {
 | 
				
			|||||||
    const attachment = await this.getAlbumThumbnailAttachment(album);
 | 
					    const attachment = await this.getAlbumThumbnailAttachment(album);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { server } = await this.getConfig({ withCache: false });
 | 
					    const { server } = await this.getConfig({ withCache: false });
 | 
				
			||||||
 | 
					    const { port } = this.configRepository.getEnv();
 | 
				
			||||||
    const { html, text } = await this.notificationRepository.renderEmail({
 | 
					    const { html, text } = await this.notificationRepository.renderEmail({
 | 
				
			||||||
      template: EmailTemplate.ALBUM_INVITE,
 | 
					      template: EmailTemplate.ALBUM_INVITE,
 | 
				
			||||||
      data: {
 | 
					      data: {
 | 
				
			||||||
        baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
 | 
					        baseUrl: getExternalDomain(server, port),
 | 
				
			||||||
        albumId: album.id,
 | 
					        albumId: album.id,
 | 
				
			||||||
        albumName: album.albumName,
 | 
					        albumName: album.albumName,
 | 
				
			||||||
        senderName: album.owner.name,
 | 
					        senderName: album.owner.name,
 | 
				
			||||||
@ -241,6 +244,7 @@ export class NotificationService extends BaseService {
 | 
				
			|||||||
    const attachment = await this.getAlbumThumbnailAttachment(album);
 | 
					    const attachment = await this.getAlbumThumbnailAttachment(album);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { server } = await this.getConfig({ withCache: false });
 | 
					    const { server } = await this.getConfig({ withCache: false });
 | 
				
			||||||
 | 
					    const { port } = this.configRepository.getEnv();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const recipient of recipients) {
 | 
					    for (const recipient of recipients) {
 | 
				
			||||||
      const user = await this.userRepository.get(recipient.id, { withDeleted: false });
 | 
					      const user = await this.userRepository.get(recipient.id, { withDeleted: false });
 | 
				
			||||||
@ -257,7 +261,7 @@ export class NotificationService extends BaseService {
 | 
				
			|||||||
      const { html, text } = await this.notificationRepository.renderEmail({
 | 
					      const { html, text } = await this.notificationRepository.renderEmail({
 | 
				
			||||||
        template: EmailTemplate.ALBUM_UPDATE,
 | 
					        template: EmailTemplate.ALBUM_UPDATE,
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN,
 | 
					          baseUrl: getExternalDomain(server, port),
 | 
				
			||||||
          albumId: album.id,
 | 
					          albumId: album.id,
 | 
				
			||||||
          albumName: album.albumName,
 | 
					          albumName: album.albumName,
 | 
				
			||||||
          recipientName: recipient.name,
 | 
					          recipientName: recipient.name,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
 | 
				
			||||||
import _ from 'lodash';
 | 
					import _ from 'lodash';
 | 
				
			||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
 | 
					 | 
				
			||||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 | 
					import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 | 
				
			||||||
import { SharedLinkType } from 'src/enum';
 | 
					import { SharedLinkType } from 'src/enum';
 | 
				
			||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
 | 
					import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
 | 
				
			||||||
@ -304,7 +303,7 @@ describe(SharedLinkService.name, () => {
 | 
				
			|||||||
      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual);
 | 
					      sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual);
 | 
				
			||||||
      await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
 | 
					      await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
 | 
				
			||||||
        description: '1 shared photos & videos',
 | 
					        description: '1 shared photos & videos',
 | 
				
			||||||
        imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
 | 
					        imageUrl: `http://localhost:2283/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
 | 
				
			||||||
        title: 'Public Share',
 | 
					        title: 'Public Share',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(sharedLinkMock.get).toHaveBeenCalled();
 | 
					      expect(sharedLinkMock.get).toHaveBeenCalled();
 | 
				
			||||||
@ -314,7 +313,7 @@ describe(SharedLinkService.name, () => {
 | 
				
			|||||||
      sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
 | 
					      sharedLinkMock.get.mockResolvedValue({ ...sharedLinkStub.individual, album: undefined, assets: [] });
 | 
				
			||||||
      await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
 | 
					      await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
 | 
				
			||||||
        description: '0 shared photos & videos',
 | 
					        description: '0 shared photos & videos',
 | 
				
			||||||
        imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/feature-panel.png`,
 | 
					        imageUrl: `http://localhost:2283/feature-panel.png`,
 | 
				
			||||||
        title: 'Public Share',
 | 
					        title: 'Public Share',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(sharedLinkMock.get).toHaveBeenCalled();
 | 
					      expect(sharedLinkMock.get).toHaveBeenCalled();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,20 @@
 | 
				
			|||||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
 | 
				
			||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
 | 
					 | 
				
			||||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
 | 
					import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
 | 
				
			||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
 | 
					import { AssetIdsDto } from 'src/dtos/asset.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  mapSharedLink,
 | 
				
			||||||
 | 
					  mapSharedLinkWithoutMetadata,
 | 
				
			||||||
  SharedLinkCreateDto,
 | 
					  SharedLinkCreateDto,
 | 
				
			||||||
  SharedLinkEditDto,
 | 
					  SharedLinkEditDto,
 | 
				
			||||||
  SharedLinkPasswordDto,
 | 
					  SharedLinkPasswordDto,
 | 
				
			||||||
  SharedLinkResponseDto,
 | 
					  SharedLinkResponseDto,
 | 
				
			||||||
  mapSharedLink,
 | 
					 | 
				
			||||||
  mapSharedLinkWithoutMetadata,
 | 
					 | 
				
			||||||
} from 'src/dtos/shared-link.dto';
 | 
					} from 'src/dtos/shared-link.dto';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
					import { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
				
			||||||
import { Permission, SharedLinkType } from 'src/enum';
 | 
					import { Permission, SharedLinkType } from 'src/enum';
 | 
				
			||||||
import { BaseService } from 'src/services/base.service';
 | 
					import { BaseService } from 'src/services/base.service';
 | 
				
			||||||
import { OpenGraphTags } from 'src/utils/misc';
 | 
					import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SharedLinkService extends BaseService {
 | 
					export class SharedLinkService extends BaseService {
 | 
				
			||||||
@ -177,6 +176,7 @@ export class SharedLinkService extends BaseService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const config = await this.getConfig({ withCache: true });
 | 
					    const config = await this.getConfig({ withCache: true });
 | 
				
			||||||
 | 
					    const { port } = this.configRepository.getEnv();
 | 
				
			||||||
    const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
 | 
					    const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
 | 
				
			||||||
    const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
 | 
					    const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
 | 
				
			||||||
    const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
 | 
					    const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;
 | 
				
			||||||
@ -187,7 +187,7 @@ export class SharedLinkService extends BaseService {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
 | 
					      title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
 | 
				
			||||||
      description: sharedLink.description || `${assetCount} shared photos & videos`,
 | 
					      description: sharedLink.description || `${assetCount} shared photos & videos`,
 | 
				
			||||||
      imageUrl: new URL(imagePath, config.server.externalDomain || DEFAULT_EXTERNAL_DOMAIN).href,
 | 
					      imageUrl: new URL(imagePath, getExternalDomain(config.server, port)).href,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,9 @@ import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
 | 
				
			|||||||
import { MetadataKey } from 'src/enum';
 | 
					import { MetadataKey } from 'src/enum';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getExternalDomain = (server: SystemConfig['server'], port: number) =>
 | 
				
			||||||
 | 
					  server.externalDomain || `http://localhost:${port}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @returns a list of strings representing the keys of the object in dot notation
 | 
					 * @returns a list of strings representing the keys of the object in dot notation
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
				
			|||||||
@ -49,10 +49,9 @@ const envData: EnvData = {
 | 
				
			|||||||
  noColor: false,
 | 
					  noColor: false,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const mockEnvData = (config: Partial<EnvData>) => ({ ...envData, ...config });
 | 
				
			||||||
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
 | 
					export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    getEnv: vitest.fn().mockReturnValue(envData),
 | 
					    getEnv: vitest.fn().mockReturnValue(mockEnvData({})),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const mockEnvData = (config: Partial<EnvData>) => ({ ...envData, ...config });
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user