mirror of
https://github.com/immich-app/immich.git
synced 2024-12-04 10:34:56 +02:00
feat(server): Add publicUsers toggle for user search (#14330)
* feat(server): Add publicUsers toggle for user search * tests * docs: add check:typescript for web PR checklist * return auth.user when publicUsers is false - app testing --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
b6ec79cbdd
commit
5417e34fb6
@ -11,6 +11,7 @@ When contributing code through a pull request, please check the following:
|
|||||||
- [ ] `npm run lint` (linting via ESLint)
|
- [ ] `npm run lint` (linting via ESLint)
|
||||||
- [ ] `npm run format` (formatting via Prettier)
|
- [ ] `npm run format` (formatting via Prettier)
|
||||||
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
||||||
|
- [ ] `npm run check:typescript` (check typescript)
|
||||||
- [ ] `npm test` (unit tests)
|
- [ ] `npm test` (unit tests)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
@ -133,6 +133,7 @@ describe('/server', () => {
|
|||||||
userDeleteDelay: 7,
|
userDeleteDelay: 7,
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
publicUsers: true,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
|
@ -224,6 +224,8 @@
|
|||||||
"send_welcome_email": "Send welcome email",
|
"send_welcome_email": "Send welcome email",
|
||||||
"server_external_domain_settings": "External domain",
|
"server_external_domain_settings": "External domain",
|
||||||
"server_external_domain_settings_description": "Domain for public shared links, including http(s)://",
|
"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": "Server Settings",
|
||||||
"server_settings_description": "Manage server settings",
|
"server_settings_description": "Manage server settings",
|
||||||
"server_welcome_message": "Welcome message",
|
"server_welcome_message": "Welcome message",
|
||||||
|
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
BIN
mobile/openapi/lib/model/server_config_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_server_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_server_dto.dart
generated
Binary file not shown.
@ -10825,6 +10825,9 @@
|
|||||||
"oauthButtonText": {
|
"oauthButtonText": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"publicUsers": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"trashDays": {
|
"trashDays": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -10840,6 +10843,7 @@
|
|||||||
"mapDarkStyleUrl",
|
"mapDarkStyleUrl",
|
||||||
"mapLightStyleUrl",
|
"mapLightStyleUrl",
|
||||||
"oauthButtonText",
|
"oauthButtonText",
|
||||||
|
"publicUsers",
|
||||||
"trashDays",
|
"trashDays",
|
||||||
"userDeleteDelay"
|
"userDeleteDelay"
|
||||||
],
|
],
|
||||||
@ -12014,11 +12018,15 @@
|
|||||||
},
|
},
|
||||||
"loginPageMessage": {
|
"loginPageMessage": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"publicUsers": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"externalDomain",
|
"externalDomain",
|
||||||
"loginPageMessage"
|
"loginPageMessage",
|
||||||
|
"publicUsers"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -928,6 +928,7 @@ export type ServerConfigDto = {
|
|||||||
mapDarkStyleUrl: string;
|
mapDarkStyleUrl: string;
|
||||||
mapLightStyleUrl: string;
|
mapLightStyleUrl: string;
|
||||||
oauthButtonText: string;
|
oauthButtonText: string;
|
||||||
|
publicUsers: boolean;
|
||||||
trashDays: number;
|
trashDays: number;
|
||||||
userDeleteDelay: number;
|
userDeleteDelay: number;
|
||||||
};
|
};
|
||||||
@ -1222,6 +1223,7 @@ export type SystemConfigReverseGeocodingDto = {
|
|||||||
export type SystemConfigServerDto = {
|
export type SystemConfigServerDto = {
|
||||||
externalDomain: string;
|
externalDomain: string;
|
||||||
loginPageMessage: string;
|
loginPageMessage: string;
|
||||||
|
publicUsers: boolean;
|
||||||
};
|
};
|
||||||
export type SystemConfigStorageTemplateDto = {
|
export type SystemConfigStorageTemplateDto = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -149,6 +149,7 @@ export interface SystemConfig {
|
|||||||
server: {
|
server: {
|
||||||
externalDomain: string;
|
externalDomain: string;
|
||||||
loginPageMessage: string;
|
loginPageMessage: string;
|
||||||
|
publicUsers: boolean;
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
deleteDelay: number;
|
deleteDelay: number;
|
||||||
@ -296,6 +297,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
server: {
|
server: {
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
|
publicUsers: true,
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
smtp: {
|
smtp: {
|
||||||
|
@ -39,8 +39,8 @@ export class UserController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
searchUsers(): Promise<UserResponseDto[]> {
|
searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
|
||||||
return this.service.search();
|
return this.service.search(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('me')
|
@Get('me')
|
||||||
|
@ -144,6 +144,7 @@ export class ServerConfigDto {
|
|||||||
isInitialized!: boolean;
|
isInitialized!: boolean;
|
||||||
isOnboarded!: boolean;
|
isOnboarded!: boolean;
|
||||||
externalDomain!: string;
|
externalDomain!: string;
|
||||||
|
publicUsers!: boolean;
|
||||||
mapDarkStyleUrl!: string;
|
mapDarkStyleUrl!: string;
|
||||||
mapLightStyleUrl!: string;
|
mapLightStyleUrl!: string;
|
||||||
}
|
}
|
||||||
|
@ -404,6 +404,9 @@ class SystemConfigServerDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
loginPageMessage!: string;
|
loginPageMessage!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
publicUsers!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigSmtpTransportDto {
|
class SystemConfigSmtpTransportDto {
|
||||||
|
@ -169,6 +169,7 @@ describe(ServerService.name, () => {
|
|||||||
isInitialized: undefined,
|
isInitialized: undefined,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
|
publicUsers: true,
|
||||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
});
|
});
|
||||||
|
@ -110,6 +110,7 @@ export class ServerService extends BaseService {
|
|||||||
isInitialized,
|
isInitialized,
|
||||||
isOnboarded: onboarding?.isOnboarded || false,
|
isOnboarded: onboarding?.isOnboarded || false,
|
||||||
externalDomain: config.server.externalDomain,
|
externalDomain: config.server.externalDomain,
|
||||||
|
publicUsers: config.server.publicUsers,
|
||||||
mapDarkStyleUrl: config.map.darkStyle,
|
mapDarkStyleUrl: config.map.darkStyle,
|
||||||
mapLightStyleUrl: config.map.lightStyle,
|
mapLightStyleUrl: config.map.lightStyle,
|
||||||
};
|
};
|
||||||
|
@ -133,6 +133,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
server: {
|
server: {
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
loginPageMessage: '',
|
loginPageMessage: '',
|
||||||
|
publicUsers: true,
|
||||||
},
|
},
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -38,9 +38,9 @@ describe(UserService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
it('should get all users', async () => {
|
it('admin should get all users', async () => {
|
||||||
userMock.getList.mockResolvedValue([userStub.admin]);
|
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||||
await expect(sut.search()).resolves.toEqual([
|
await expect(sut.search(authStub.admin)).resolves.toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: authStub.admin.user.id,
|
id: authStub.admin.user.id,
|
||||||
email: authStub.admin.user.email,
|
email: authStub.admin.user.email,
|
||||||
@ -48,6 +48,29 @@ describe(UserService.name, () => {
|
|||||||
]);
|
]);
|
||||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
|
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', () => {
|
describe('get', () => {
|
||||||
|
@ -19,8 +19,14 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService extends BaseService {
|
export class UserService extends BaseService {
|
||||||
async search(): Promise<UserResponseDto[]> {
|
async search(auth: AuthDto): Promise<UserResponseDto[]> {
|
||||||
const users = await this.userRepository.getList({ withDeleted: false });
|
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));
|
return users.map((user) => mapUser(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
server/test/fixtures/system-config.stub.ts
vendored
5
server/test/fixtures/system-config.stub.ts
vendored
@ -117,4 +117,9 @@ export const systemConfigStub = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
publicUsersDisabled: {
|
||||||
|
server: {
|
||||||
|
publicUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
} satisfies Record<string, DeepPartial<SystemConfig>>;
|
} satisfies Record<string, DeepPartial<SystemConfig>>;
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
|
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
|
||||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
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 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 { t } from 'svelte-i18n';
|
||||||
import { SettingInputFieldType } from '$lib/constants';
|
import { SettingInputFieldType } from '$lib/constants';
|
||||||
|
|
||||||
@ -44,6 +45,13 @@
|
|||||||
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('admin.server_public_users')}
|
||||||
|
subtitle={$t('admin.server_public_users_description')}
|
||||||
|
{disabled}
|
||||||
|
bind:checked={config.server.publicUsers}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
|
onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
|
||||||
|
@ -34,6 +34,7 @@ export const serverConfig = writable<ServerConfig>({
|
|||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
mapDarkStyleUrl: '',
|
mapDarkStyleUrl: '',
|
||||||
mapLightStyleUrl: '',
|
mapLightStyleUrl: '',
|
||||||
|
publicUsers: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const retrieveServerConfig = async () => {
|
export const retrieveServerConfig = async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user