diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index 6015694976..58581e669a 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -11,6 +11,7 @@ When contributing code through a pull request, please check the following: - [ ] `npm run lint` (linting via ESLint) - [ ] `npm run format` (formatting via Prettier) - [ ] `npm run check:svelte` (Type checking via SvelteKit) +- [ ] `npm run check:typescript` (check typescript) - [ ] `npm test` (unit tests) ## Documentation diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts index 4bff4b3dea..c89280f579 100644 --- a/e2e/src/api/specs/server.e2e-spec.ts +++ b/e2e/src/api/specs/server.e2e-spec.ts @@ -133,6 +133,7 @@ describe('/server', () => { userDeleteDelay: 7, isInitialized: true, externalDomain: '', + publicUsers: true, isOnboarded: false, mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', diff --git a/i18n/en.json b/i18n/en.json index 91bbb6cda2..9224597feb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -224,6 +224,8 @@ "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", + "server_public_users": "Public Users", + "server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.", "server_settings": "Server Settings", "server_settings_description": "Manage server settings", "server_welcome_message": "Welcome message", diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index bd5c2405e2..01c82af4d9 100644 Binary files a/mobile/openapi/lib/model/server_config_dto.dart and b/mobile/openapi/lib/model/server_config_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_server_dto.dart b/mobile/openapi/lib/model/system_config_server_dto.dart index b1b92c9515..8099292dd0 100644 Binary files a/mobile/openapi/lib/model/system_config_server_dto.dart and b/mobile/openapi/lib/model/system_config_server_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4674232139..20ebe607a4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10825,6 +10825,9 @@ "oauthButtonText": { "type": "string" }, + "publicUsers": { + "type": "boolean" + }, "trashDays": { "type": "integer" }, @@ -10840,6 +10843,7 @@ "mapDarkStyleUrl", "mapLightStyleUrl", "oauthButtonText", + "publicUsers", "trashDays", "userDeleteDelay" ], @@ -12014,11 +12018,15 @@ }, "loginPageMessage": { "type": "string" + }, + "publicUsers": { + "type": "boolean" } }, "required": [ "externalDomain", - "loginPageMessage" + "loginPageMessage", + "publicUsers" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ddec0f2421..9b79816091 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -928,6 +928,7 @@ export type ServerConfigDto = { mapDarkStyleUrl: string; mapLightStyleUrl: string; oauthButtonText: string; + publicUsers: boolean; trashDays: number; userDeleteDelay: number; }; @@ -1222,6 +1223,7 @@ export type SystemConfigReverseGeocodingDto = { export type SystemConfigServerDto = { externalDomain: string; loginPageMessage: string; + publicUsers: boolean; }; export type SystemConfigStorageTemplateDto = { enabled: boolean; diff --git a/server/src/config.ts b/server/src/config.ts index 2b74f00e7a..f5e78ab01b 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -149,6 +149,7 @@ export interface SystemConfig { server: { externalDomain: string; loginPageMessage: string; + publicUsers: boolean; }; user: { deleteDelay: number; @@ -296,6 +297,7 @@ export const defaults = Object.freeze({ server: { externalDomain: '', loginPageMessage: '', + publicUsers: true, }, notifications: { smtp: { diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 10076098d6..15bb1913db 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -39,8 +39,8 @@ export class UserController { @Get() @Authenticated() - searchUsers(): Promise { - return this.service.search(); + searchUsers(@Auth() auth: AuthDto): Promise { + return this.service.search(auth); } @Get('me') diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index cbabfa7aed..e1f94dbaa5 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -144,6 +144,7 @@ export class ServerConfigDto { isInitialized!: boolean; isOnboarded!: boolean; externalDomain!: string; + publicUsers!: boolean; mapDarkStyleUrl!: string; mapLightStyleUrl!: string; } diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 7a2e0632b4..8d79fecb22 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -404,6 +404,9 @@ class SystemConfigServerDto { @IsString() loginPageMessage!: string; + + @IsBoolean() + publicUsers!: boolean; } class SystemConfigSmtpTransportDto { diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 475d1d6193..3f7fafcebf 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -169,6 +169,7 @@ describe(ServerService.name, () => { isInitialized: undefined, isOnboarded: false, externalDomain: '', + publicUsers: true, mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 7df322a84e..e9dd908a7c 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -110,6 +110,7 @@ export class ServerService extends BaseService { isInitialized, isOnboarded: onboarding?.isOnboarded || false, externalDomain: config.server.externalDomain, + publicUsers: config.server.publicUsers, mapDarkStyleUrl: config.map.darkStyle, mapLightStyleUrl: config.map.lightStyle, }; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index f9ee42eb03..4d5a29e8a8 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -133,6 +133,7 @@ const updatedConfig = Object.freeze({ server: { externalDomain: '', loginPageMessage: '', + publicUsers: true, }, storageTemplate: { enabled: false, diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 767d8d8954..08b663046b 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -38,9 +38,9 @@ describe(UserService.name, () => { }); describe('getAll', () => { - it('should get all users', async () => { + it('admin should get all users', async () => { userMock.getList.mockResolvedValue([userStub.admin]); - await expect(sut.search()).resolves.toEqual([ + await expect(sut.search(authStub.admin)).resolves.toEqual([ expect.objectContaining({ id: authStub.admin.user.id, email: authStub.admin.user.email, @@ -48,6 +48,29 @@ describe(UserService.name, () => { ]); expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false }); }); + + it('non-admin should get all users when publicUsers enabled', async () => { + userMock.getList.mockResolvedValue([userStub.user1]); + await expect(sut.search(authStub.user1)).resolves.toEqual([ + expect.objectContaining({ + id: authStub.user1.user.id, + email: authStub.user1.user.email, + }), + ]); + expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false }); + }); + + it('non-admin user should only receive itself when publicUsers is disabled', async () => { + userMock.getList.mockResolvedValue([userStub.user1]); + systemMock.get.mockResolvedValue(systemConfigStub.publicUsersDisabled); + await expect(sut.search(authStub.user1)).resolves.toEqual([ + expect.objectContaining({ + id: authStub.user1.user.id, + email: authStub.user1.user.email, + }), + ]); + expect(userMock.getList).not.toHaveBeenCalledWith({ withDeleted: false }); + }); }); describe('get', () => { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 926482fb9c..f4ae42b5ed 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -19,8 +19,14 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti @Injectable() export class UserService extends BaseService { - async search(): Promise { - const users = await this.userRepository.getList({ withDeleted: false }); + async search(auth: AuthDto): Promise { + const config = await this.getConfig({ withCache: false }); + + let users: UserEntity[] = [auth.user]; + if (auth.user.isAdmin || config.server.publicUsers) { + users = await this.userRepository.getList({ withDeleted: false }); + } + return users.map((user) => mapUser(user)); } diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts index 10a0de77b0..ed8cc8694a 100644 --- a/server/test/fixtures/system-config.stub.ts +++ b/server/test/fixtures/system-config.stub.ts @@ -117,4 +117,9 @@ export const systemConfigStub = { }, }, }, + publicUsersDisabled: { + server: { + publicUsers: false, + }, + }, } satisfies Record>; diff --git a/web/src/lib/components/admin-page/settings/server/server-settings.svelte b/web/src/lib/components/admin-page/settings/server/server-settings.svelte index 14d5624c5f..b9134d1e50 100644 --- a/web/src/lib/components/admin-page/settings/server/server-settings.svelte +++ b/web/src/lib/components/admin-page/settings/server/server-settings.svelte @@ -5,6 +5,7 @@ import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings'; import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; + import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import { t } from 'svelte-i18n'; import { SettingInputFieldType } from '$lib/constants'; @@ -44,6 +45,13 @@ isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage} /> + +
onReset({ ...options, configKeys: ['server'] })} diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 358765fe0b..254db71946 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -34,6 +34,7 @@ export const serverConfig = writable({ externalDomain: '', mapDarkStyleUrl: '', mapLightStyleUrl: '', + publicUsers: true, }); export const retrieveServerConfig = async () => {