mirror of
https://github.com/immich-app/immich.git
synced 2025-01-21 00:52:43 -05: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
19 changed files with 93 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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",
|
||||
|
|
10
mobile/openapi/lib/model/server_config_dto.dart
generated
10
mobile/openapi/lib/model/server_config_dto.dart
generated
|
@ -20,6 +20,7 @@ class ServerConfigDto {
|
|||
required this.mapDarkStyleUrl,
|
||||
required this.mapLightStyleUrl,
|
||||
required this.oauthButtonText,
|
||||
required this.publicUsers,
|
||||
required this.trashDays,
|
||||
required this.userDeleteDelay,
|
||||
});
|
||||
|
@ -38,6 +39,8 @@ class ServerConfigDto {
|
|||
|
||||
String oauthButtonText;
|
||||
|
||||
bool publicUsers;
|
||||
|
||||
int trashDays;
|
||||
|
||||
int userDeleteDelay;
|
||||
|
@ -51,6 +54,7 @@ class ServerConfigDto {
|
|||
other.mapDarkStyleUrl == mapDarkStyleUrl &&
|
||||
other.mapLightStyleUrl == mapLightStyleUrl &&
|
||||
other.oauthButtonText == oauthButtonText &&
|
||||
other.publicUsers == publicUsers &&
|
||||
other.trashDays == trashDays &&
|
||||
other.userDeleteDelay == userDeleteDelay;
|
||||
|
||||
|
@ -64,11 +68,12 @@ class ServerConfigDto {
|
|||
(mapDarkStyleUrl.hashCode) +
|
||||
(mapLightStyleUrl.hashCode) +
|
||||
(oauthButtonText.hashCode) +
|
||||
(publicUsers.hashCode) +
|
||||
(trashDays.hashCode) +
|
||||
(userDeleteDelay.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]';
|
||||
String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, publicUsers=$publicUsers, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
@ -79,6 +84,7 @@ class ServerConfigDto {
|
|||
json[r'mapDarkStyleUrl'] = this.mapDarkStyleUrl;
|
||||
json[r'mapLightStyleUrl'] = this.mapLightStyleUrl;
|
||||
json[r'oauthButtonText'] = this.oauthButtonText;
|
||||
json[r'publicUsers'] = this.publicUsers;
|
||||
json[r'trashDays'] = this.trashDays;
|
||||
json[r'userDeleteDelay'] = this.userDeleteDelay;
|
||||
return json;
|
||||
|
@ -100,6 +106,7 @@ class ServerConfigDto {
|
|||
mapDarkStyleUrl: mapValueOfType<String>(json, r'mapDarkStyleUrl')!,
|
||||
mapLightStyleUrl: mapValueOfType<String>(json, r'mapLightStyleUrl')!,
|
||||
oauthButtonText: mapValueOfType<String>(json, r'oauthButtonText')!,
|
||||
publicUsers: mapValueOfType<bool>(json, r'publicUsers')!,
|
||||
trashDays: mapValueOfType<int>(json, r'trashDays')!,
|
||||
userDeleteDelay: mapValueOfType<int>(json, r'userDeleteDelay')!,
|
||||
);
|
||||
|
@ -156,6 +163,7 @@ class ServerConfigDto {
|
|||
'mapDarkStyleUrl',
|
||||
'mapLightStyleUrl',
|
||||
'oauthButtonText',
|
||||
'publicUsers',
|
||||
'trashDays',
|
||||
'userDeleteDelay',
|
||||
};
|
||||
|
|
|
@ -15,30 +15,36 @@ class SystemConfigServerDto {
|
|||
SystemConfigServerDto({
|
||||
required this.externalDomain,
|
||||
required this.loginPageMessage,
|
||||
required this.publicUsers,
|
||||
});
|
||||
|
||||
String externalDomain;
|
||||
|
||||
String loginPageMessage;
|
||||
|
||||
bool publicUsers;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigServerDto &&
|
||||
other.externalDomain == externalDomain &&
|
||||
other.loginPageMessage == loginPageMessage;
|
||||
other.loginPageMessage == loginPageMessage &&
|
||||
other.publicUsers == publicUsers;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(externalDomain.hashCode) +
|
||||
(loginPageMessage.hashCode);
|
||||
(loginPageMessage.hashCode) +
|
||||
(publicUsers.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigServerDto[externalDomain=$externalDomain, loginPageMessage=$loginPageMessage]';
|
||||
String toString() => 'SystemConfigServerDto[externalDomain=$externalDomain, loginPageMessage=$loginPageMessage, publicUsers=$publicUsers]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'externalDomain'] = this.externalDomain;
|
||||
json[r'loginPageMessage'] = this.loginPageMessage;
|
||||
json[r'publicUsers'] = this.publicUsers;
|
||||
return json;
|
||||
}
|
||||
|
||||
|
@ -53,6 +59,7 @@ class SystemConfigServerDto {
|
|||
return SystemConfigServerDto(
|
||||
externalDomain: mapValueOfType<String>(json, r'externalDomain')!,
|
||||
loginPageMessage: mapValueOfType<String>(json, r'loginPageMessage')!,
|
||||
publicUsers: mapValueOfType<bool>(json, r'publicUsers')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -102,6 +109,7 @@ class SystemConfigServerDto {
|
|||
static const requiredKeys = <String>{
|
||||
'externalDomain',
|
||||
'loginPageMessage',
|
||||
'publicUsers',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<SystemConfig>({
|
|||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
publicUsers: true,
|
||||
},
|
||||
notifications: {
|
||||
smtp: {
|
||||
|
|
|
@ -39,8 +39,8 @@ export class UserController {
|
|||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
searchUsers(): Promise<UserResponseDto[]> {
|
||||
return this.service.search();
|
||||
searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
|
|
|
@ -144,6 +144,7 @@ export class ServerConfigDto {
|
|||
isInitialized!: boolean;
|
||||
isOnboarded!: boolean;
|
||||
externalDomain!: string;
|
||||
publicUsers!: boolean;
|
||||
mapDarkStyleUrl!: string;
|
||||
mapLightStyleUrl!: string;
|
||||
}
|
||||
|
|
|
@ -404,6 +404,9 @@ class SystemConfigServerDto {
|
|||
|
||||
@IsString()
|
||||
loginPageMessage!: string;
|
||||
|
||||
@IsBoolean()
|
||||
publicUsers!: boolean;
|
||||
}
|
||||
|
||||
class SystemConfigSmtpTransportDto {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -133,6 +133,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
publicUsers: true,
|
||||
},
|
||||
storageTemplate: {
|
||||
enabled: false,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -19,8 +19,14 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
|
|||
|
||||
@Injectable()
|
||||
export class UserService extends BaseService {
|
||||
async search(): Promise<UserResponseDto[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: false });
|
||||
async search(auth: AuthDto): Promise<UserResponseDto[]> {
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
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>>;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.server_public_users')}
|
||||
subtitle={$t('admin.server_public_users_description')}
|
||||
{disabled}
|
||||
bind:checked={config.server.publicUsers}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
|
||||
|
|
|
@ -34,6 +34,7 @@ export const serverConfig = writable<ServerConfig>({
|
|||
externalDomain: '',
|
||||
mapDarkStyleUrl: '',
|
||||
mapLightStyleUrl: '',
|
||||
publicUsers: true,
|
||||
});
|
||||
|
||||
export const retrieveServerConfig = async () => {
|
||||
|
|
Loading…
Add table
Reference in a new issue