Compare commits
10
Commits
689105c949
...
e04e70db2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04e70db2e | ||
|
|
75517057d0 | ||
|
|
3efc91ff9a | ||
|
|
7c07c001cd | ||
|
|
f1f7f82e91 | ||
|
|
b3bd5d1fd1 | ||
|
|
ad9b368cb8 | ||
|
|
7c78b00df6 | ||
|
|
4dfb6e88af | ||
|
|
744797e59d |
@@ -567,7 +567,7 @@ describe('/search', () => {
|
||||
'New York',
|
||||
'Shanghai',
|
||||
'State of Berlin',
|
||||
'St.-Petersburg',
|
||||
'St. Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
'Virginia',
|
||||
@@ -591,7 +591,7 @@ describe('/search', () => {
|
||||
'New York',
|
||||
'Shanghai',
|
||||
'State of Berlin',
|
||||
'St.-Petersburg',
|
||||
'St. Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
'Virginia',
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { LoginResponseDto, getMyUser, login } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures.js';
|
||||
import { app, asBearerAuth, utils } from 'src/utils.js';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/users', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
|
||||
});
|
||||
|
||||
describe('PUT /users/me', () => {
|
||||
/** @deprecated */
|
||||
it('should allow a user to change their password (deprecated)', async () => {
|
||||
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
||||
|
||||
expect(user.shouldChangePassword).toBe(true);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/users/me`)
|
||||
.send({ password: 'super-secret' })
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
email: nonAdmin.userEmail,
|
||||
shouldChangePassword: false,
|
||||
});
|
||||
|
||||
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
|
||||
|
||||
expect(token.accessToken).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,6 +71,7 @@ test.describe('Registration', () => {
|
||||
// change password
|
||||
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||
await expect(page).toHaveURL('/auth/change-password');
|
||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||
await page.getByLabel('New Password').fill('new-password');
|
||||
await page.getByLabel('Confirm Password').fill('new-password');
|
||||
await page.getByRole('button', { name: 'Change password' }).click();
|
||||
|
||||
@@ -323,6 +323,13 @@
|
||||
"oauth_timeout": "Таймаут для запросов",
|
||||
"oauth_timeout_description": "Максимальное время, в течение которого ожидать ответа, в миллисекундах",
|
||||
"ocr_job_description": "Использование машинного обучения для распознавания текста на изображениях",
|
||||
"passkey_additional_domains": "Дополнительные домены",
|
||||
"passkey_additional_domains_description": "Дополнительные доверенные домены. Для их работы основной домен ключей доступа также должен быть доступен.",
|
||||
"passkey_domain": "Домен ключей доступа",
|
||||
"passkey_domain_description": "Домен для ключей доступа. Оставьте пустым для определения по адресу сайта. При изменении домена существующие ключи перестанут работать.",
|
||||
"passkey_enable_description": "Вход с помощью ключа доступа",
|
||||
"passkey_settings": "Вход по ключу доступа",
|
||||
"passkey_settings_description": "Настройки входа по ключу доступа",
|
||||
"password_enable_description": "Вход по электронной почте и паролю",
|
||||
"password_settings": "Настройки входа с паролем",
|
||||
"password_settings_description": "Управление настройками входа по паролю",
|
||||
@@ -873,6 +880,8 @@
|
||||
"delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
|
||||
"delete_local_dialog_ok_force": "Все равно удалить",
|
||||
"delete_others": "Удалить остальные",
|
||||
"delete_passkey": "Удалить ключ доступа",
|
||||
"delete_passkey_prompt": "Удалить этот ключ доступа?",
|
||||
"delete_permanently": "Удалить навсегда",
|
||||
"delete_permanently_action_prompt": "Объекты удалены безвозвратно ({count} шт.)",
|
||||
"delete_shared_link": "Удалить публичную ссылку",
|
||||
@@ -942,6 +951,7 @@
|
||||
"edit_location_action_prompt": "Места изменены ({count} шт.)",
|
||||
"edit_location_dialog_title": "Местоположение",
|
||||
"edit_name": "Изменить имя",
|
||||
"edit_passkey": "Изменить ключ доступа",
|
||||
"edit_people": "Изменить людей",
|
||||
"edit_person": "Изменить человека",
|
||||
"edit_tag": "Изменить тег",
|
||||
@@ -1287,6 +1297,7 @@
|
||||
"last": "Последний",
|
||||
"last_months": "{count, plural, one {Последний месяц} many {# последних месяцев} other {# последних месяца}}",
|
||||
"last_seen": "Последний доступ",
|
||||
"last_used": "Последнее использование",
|
||||
"latest_version": "Последняя версия",
|
||||
"latitude": "Широта",
|
||||
"leave": "Покинуть",
|
||||
@@ -1379,6 +1390,7 @@
|
||||
"login_has_been_disabled": "Вход в систему был отключён",
|
||||
"login_password_changed_error": "Произошла ошибка при обновлении пароля",
|
||||
"login_password_changed_success": "Пароль успешно обновлен",
|
||||
"login_with_passkey": "Войти с ключом доступа",
|
||||
"logout_all_device_confirmation": "Вы действительно хотите завершить все сеансы, кроме текущего?",
|
||||
"logout_this_device_confirmation": "Вы действительно хотите завершить сеанс на этом устройстве?",
|
||||
"logs": "Журнал событий",
|
||||
@@ -1427,6 +1439,7 @@
|
||||
"manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами",
|
||||
"manage_your_devices": "Управление устройствами, на которых вы входили в свой аккаунт",
|
||||
"manage_your_oauth_connection": "Настройки подключённого OAuth",
|
||||
"manage_your_passkeys": "Управление ключами доступа",
|
||||
"map": "Карта",
|
||||
"map_assets_in_bounds": "{count, plural, =0 {Нет фото в этой области} other {# фото}}",
|
||||
"map_cannot_get_user_location": "Невозможно получить местоположение пользователя",
|
||||
@@ -1546,6 +1559,7 @@
|
||||
"new_album": "Новый альбом",
|
||||
"new_api_key": "Новый API ключ",
|
||||
"new_feature": "Новая функциональность",
|
||||
"new_passkey": "Новый ключ доступа",
|
||||
"new_password": "Новый пароль",
|
||||
"new_person": "Новый человек",
|
||||
"new_pin_code": "Новый PIN-код",
|
||||
@@ -1658,6 +1672,8 @@
|
||||
"partner_sharing": "Совместное использование",
|
||||
"partners": "Партнёры",
|
||||
"partners_description": "Делитесь своими фото и видео с другими пользователями",
|
||||
"passkeys": "{count, plural, one {Ключ доступа} few {Ключи доступа} many {Ключи доступа} other {Ключи доступа}}",
|
||||
"passkeys_not_supported": "Этот браузер не поддерживает ключи доступа",
|
||||
"password": "Пароль",
|
||||
"password_does_not_match": "Пароли не совпадают",
|
||||
"password_required": "Требуется пароль",
|
||||
@@ -1842,6 +1858,7 @@
|
||||
"removed_from_favorites_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}} из избранного",
|
||||
"removed_from_stack": "Объект удалён из группы",
|
||||
"removed_memory": "Воспоминание удалено",
|
||||
"removed_passkey": "Удалён ключ доступа: {name}",
|
||||
"removed_tagged_assets": "Тег удалён у {count, plural, one {# объекта} other {# объектов}}",
|
||||
"rename": "Переименовать",
|
||||
"repository": "Репозиторий",
|
||||
@@ -2027,6 +2044,7 @@
|
||||
"share_everyone": "Поделиться со всеми",
|
||||
"share_everyone_description": "Предоставить пользователю {user} доступ всем своим людям.",
|
||||
"share_link": "Создать ссылку",
|
||||
"share_logs": "Поделиться журналами",
|
||||
"share_original": "Оригинал (большой размер)",
|
||||
"share_preview": "Предпросмотр (небольшой размер)",
|
||||
"share_quality_body": "Нажмите и удерживайте кнопку «Поделиться», чтобы выбрать качество изображения перед отправкой.",
|
||||
@@ -2206,6 +2224,8 @@
|
||||
"theme": "Тема",
|
||||
"theme_selection": "Выбор темы",
|
||||
"theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера",
|
||||
"theme_setting_asset_list_smooth_scrubbing_subtitle": "Прокручивать ленту плавно при перетаскивании полосы прокрутки, без переходов по месяцам",
|
||||
"theme_setting_asset_list_smooth_scrubbing_title": "Плавная прокрутка ленты",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Отображать индикатор хранилища на плитках объектов",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})",
|
||||
"theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.",
|
||||
|
||||
@@ -212,37 +212,37 @@ checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.node]]
|
||||
version = "24.22.0"
|
||||
version = "24.22.1"
|
||||
backend = "core:node"
|
||||
specifiers = ["24.22.0"]
|
||||
specifiers = ["24.22.1"]
|
||||
|
||||
[tools.node."platforms.linux-arm64"]
|
||||
checksum = "sha256:b7e7991d5407815adfa74a2a9cd4db683552c8ce32ff4a295c22e972849c3f5d"
|
||||
url = "https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-arm64.tar.gz"
|
||||
checksum = "sha256:33c37be234ba8bee041961047b19160f4d7d0f164e8e246f78bdd10feff148b3"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64"]
|
||||
checksum = "sha256:28ea0a438de9a13bd7891fd758eee6c7819ee8af5442dfd8d45bd65e56d4228c"
|
||||
url = "https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-x64.tar.gz"
|
||||
checksum = "sha256:eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-arm64"]
|
||||
checksum = "sha256:6a9724d9b50a5f22b4816cded441b5a8d9ddae546191a5e5b7efa5a5e2ee26d5"
|
||||
url = "https://nodejs.org/dist/v24.22.0/node-v24.22.0-darwin-arm64.tar.gz"
|
||||
checksum = "sha256:89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-x64"]
|
||||
checksum = "sha256:495e3f273c645f3c4677d438304cdfc701729ea5dec5728f5c8e76a2dece4b91"
|
||||
url = "https://nodejs.org/dist/v24.22.0/node-v24.22.0-darwin-x64.tar.gz"
|
||||
checksum = "sha256:d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.windows-x64"]
|
||||
checksum = "sha256:f9683a19877e12a9b167305c8db71040d812876359de21f0d2e97c3f321f80c3"
|
||||
url = "https://nodejs.org/dist/v24.22.0/node-v24.22.0-win-x64.zip"
|
||||
checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip"
|
||||
|
||||
[[tools."npm:@openapitools/openapi-generator-cli"]]
|
||||
version = "2.41.0"
|
||||
|
||||
@@ -15,7 +15,7 @@ config_roots = [
|
||||
]
|
||||
|
||||
[tools]
|
||||
node = "24.22.0"
|
||||
node = "24.22.1"
|
||||
pnpm = "12.14.0"
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.13.1"
|
||||
|
||||
@@ -24,7 +24,8 @@ abstract final class ImmichLogger {
|
||||
try {
|
||||
return await LogService.I.getMessages();
|
||||
} on LoggerUnInitializedException {
|
||||
final logs = DriftLogger.sqlite(await openSqliteConnection(name: 'immich_logs'));
|
||||
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||
final logs = DriftLogger.sqlite(connection);
|
||||
try {
|
||||
return [
|
||||
// if the log service had started, main would have logged this already
|
||||
@@ -34,6 +35,7 @@ abstract final class ImmichLogger {
|
||||
];
|
||||
} finally {
|
||||
await logs.close();
|
||||
await connection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +43,8 @@ abstract final class ImmichLogger {
|
||||
static Future<void> shareLogs(BuildContext context, {String? startupError}) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final dateTime = DateTime.now().toIso8601String();
|
||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
||||
final safeTimestamp = dateTime.replaceAll(':', '-');
|
||||
final filePath = '${tempDir.path}/Immich_log_$safeTimestamp.log';
|
||||
final logFile = await File(filePath).create();
|
||||
final io = logFile.openWrite();
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,9 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
).toJson(),
|
||||
'ffmpeg': {'realtime': {'enabled': false}},
|
||||
'ffmpeg': {
|
||||
'realtime': {'enabled': false},
|
||||
},
|
||||
'passkey': {'enabled': false},
|
||||
},
|
||||
'ServerConfigDto': {
|
||||
@@ -59,7 +61,9 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
'passkey': {'enabled': false, 'domain': null, 'additionalDomains': const []},
|
||||
},
|
||||
'UserConfigDto': {
|
||||
'notifications': {'smtp': {'enabled': false}},
|
||||
'notifications': {
|
||||
'smtp': {'enabled': false},
|
||||
},
|
||||
'passkey': {'enabled': false},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,64 +36,63 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||
);
|
||||
|
||||
return isMultiSelectEnabled
|
||||
? SliverToBoxAdapter(
|
||||
child: switch (_scrollProgress) {
|
||||
< 0.8 => const SizedBox(height: 120),
|
||||
_ => const SizedBox(height: 352),
|
||||
},
|
||||
)
|
||||
: SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
return SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.title,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||
);
|
||||
},
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.title,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||
);
|
||||
final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress);
|
||||
final List<Shadow> actionIconShadows = [
|
||||
if (_scrollProgress < 0.95)
|
||||
@@ -62,74 +66,74 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
||||
const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
];
|
||||
|
||||
return isMultiSelectEnabled
|
||||
? SliverToBoxAdapter(
|
||||
child: switch (_scrollProgress) {
|
||||
< 0.8 => const SizedBox(height: 120),
|
||||
_ => const SizedBox(height: 352),
|
||||
},
|
||||
)
|
||||
: SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
||||
onPressed: widget.onShowOptions,
|
||||
),
|
||||
return SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
||||
onPressed: widget.onShowOptions,
|
||||
),
|
||||
),
|
||||
],
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.person.name,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
person: widget.person,
|
||||
onNameTap: widget.onNameTap,
|
||||
onBirthdayTap: widget.onBirthdayTap,
|
||||
),
|
||||
);
|
||||
},
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.person.name,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
person: widget.person,
|
||||
onNameTap: widget.onNameTap,
|
||||
onBirthdayTap: widget.onBirthdayTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.3.3-local.6+3030404
|
||||
version: 3.3.5-local.7+3030500
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -28,7 +28,8 @@ void main() {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
|
||||
await tester.runAsync(() async {
|
||||
final logs = DriftLogger.sqlite(await openSqliteConnection(name: 'immich_logs'));
|
||||
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||
final logs = DriftLogger.sqlite(connection);
|
||||
await LogRepository(logs).insertAll([
|
||||
LogMessage(
|
||||
message: 'Failed to connect to old.example.net/203.0.113.9:443, uri=https://old.example.net/api',
|
||||
@@ -37,6 +38,7 @@ void main() {
|
||||
),
|
||||
]);
|
||||
await logs.close();
|
||||
await connection.close();
|
||||
|
||||
await ImmichLogger.shareLogs(
|
||||
tester.element(find.byType(SizedBox)),
|
||||
|
||||
@@ -18620,7 +18620,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
@@ -34312,11 +34312,6 @@
|
||||
"name": {
|
||||
"description": "User name",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"deprecated": true,
|
||||
"description": "User password (deprecated, use change password endpoint)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.22.0-trixie-slim@sha256:7f486cabe892bff8fef04c0f7da8dba84a560be3e4e2740b7a86afc57ebec315 AS core
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e AS core
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package* pnpm* .pnpmfile.cjs ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.22.0-trixie-slim@sha256:7f486cabe892bff8fef04c0f7da8dba84a560be3e4e2740b7a86afc57ebec315
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -3266,8 +3266,6 @@ export type UserUpdateMeDto = {
|
||||
email?: string;
|
||||
/** User name */
|
||||
name?: string;
|
||||
/** User password (deprecated, use change password endpoint) */
|
||||
password?: string;
|
||||
};
|
||||
export type OnboardingResponseDto = {
|
||||
/** Is user onboarded */
|
||||
|
||||
Generated
+706
-416
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610131110@sha256:082dc5e2611953d63d7b30dc8427da37ff5cd1d932aa9e99ccc4a26298b5f7a5 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610211607@sha256:f99c605b44b291834cb680b80fd606f2b7c4bee5d8f9591bfdac51fdbd68c21d AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -86,7 +86,7 @@ RUN --mount=type=cache,id=pnpm,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202610131110@sha256:9d4ce4bfd72d77edddd3072fbca78b7f4307a32c8cbe75276349944dd6405be3
|
||||
FROM ghcr.io/immich-app/base-server-prod:202610211607@sha256:e50a1b36e2638dce42981ddfa84c134246c48fb2ad290fab6acfc1bb9b2bd891
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610131110@sha256:082dc5e2611953d63d7b30dc8427da37ff5cd1d932aa9e99ccc4a26298b5f7a5 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610211607@sha256:f99c605b44b291834cb680b80fd606f2b7c4bee5d8f9591bfdac51fdbd68c21d AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.15@sha256:0593528c637b0dae8a0acca66b5d49266cf262a811f06530d7273d3cc002f3a0 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -82,7 +82,7 @@
|
||||
"geo-tz": "^8.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"ioredis": "^6.0.0",
|
||||
"jose": "^6.0.0",
|
||||
"js-yaml": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -11,11 +11,6 @@ import { isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/
|
||||
export const UserUpdateMeSchema = z
|
||||
.object({
|
||||
email: toEmail.optional().describe('User email'),
|
||||
password: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('User password (deprecated, use change password endpoint)')
|
||||
.meta({ deprecated: true }),
|
||||
name: z.string().optional().describe('User name'),
|
||||
avatarColor: UserAvatarColorSchema.nullish(),
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Updateable } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { ArgOf } from 'src/repositories/event.repository.js';
|
||||
import type { JobOf, UserMetadataItem } from 'src/types.js';
|
||||
import { SALT_ROUNDS } from 'src/constants.js';
|
||||
import { StorageCore } from 'src/cores/storage.core.js';
|
||||
import { OnEvent, OnJob } from 'src/decorators.js';
|
||||
import { AuthDto } from 'src/dtos/auth.dto.js';
|
||||
@@ -68,12 +67,6 @@ export class UserService extends BaseService {
|
||||
avatarColor: dto.avatarColor,
|
||||
};
|
||||
|
||||
if (dto.password) {
|
||||
const hashedPassword = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
||||
update.password = hashedPassword;
|
||||
update.shouldChangePassword = false;
|
||||
}
|
||||
|
||||
const updatedUser = await this.userRepository.update(user.id, update);
|
||||
|
||||
return mapUserAdmin(updatedUser);
|
||||
|
||||
@@ -436,6 +436,11 @@ export function searchAssetBuilderLegacy(kysely: Kysely<DB>, options: AssetSearc
|
||||
? qb.where('asset.visibility', '!=', AssetVisibility.Locked)
|
||||
: qb.where('asset.visibility', '=', options.visibility!),
|
||||
)
|
||||
.$if(!!options.viewingUserId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', options.viewingUserId!)]),
|
||||
),
|
||||
)
|
||||
.$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!))
|
||||
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
|
||||
.$if(options.tagIds === null, (qb) =>
|
||||
|
||||
@@ -123,6 +123,31 @@ describe(SearchService.name, () => {
|
||||
|
||||
expect(result).toEqual({ total: 0 });
|
||||
});
|
||||
|
||||
it('should not return locked assets of partner in elevated session without a visibility filter', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: partner } = await ctx.newUser();
|
||||
|
||||
await ctx.newPartner({ sharedById: partner.id, sharedWithId: user.id });
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
|
||||
await ctx.newAsset({ ownerId: partner.id, visibility: AssetVisibility.Locked });
|
||||
await ctx.newAsset({ ownerId: partner.id });
|
||||
|
||||
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
|
||||
|
||||
await expect(sut.searchStatistics(auth, {})).resolves.toEqual({ total: 2 });
|
||||
|
||||
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
|
||||
expect(response.assets.items.map(({ ownerId, visibility }) => ({ ownerId, visibility }))).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ ownerId: user.id, visibility: AssetVisibility.Locked },
|
||||
{ ownerId: partner.id, visibility: AssetVisibility.Timeline },
|
||||
]),
|
||||
);
|
||||
expect(response.assets.items).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withStacked option', () => {
|
||||
|
||||
@@ -198,17 +198,6 @@ describe(UserService.name, () => {
|
||||
await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto);
|
||||
await expect(sut.getMe(auth)).resolves.toMatchObject(dto);
|
||||
});
|
||||
|
||||
it('should clear shouldChangePassword when the password is updated', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser({ shouldChangePassword: true });
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
|
||||
await expect(sut.updateMe(auth, { password: 'super-secret' })).resolves.toMatchObject({
|
||||
shouldChangePassword: false,
|
||||
});
|
||||
await expect(sut.getMe(auth)).resolves.toMatchObject({ shouldChangePassword: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMyPreferences', () => {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -9,7 +9,7 @@
|
||||
"build:stats": "BUILD_STATS=true vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'state_referenced_locally:ignore'",
|
||||
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings \"state_referenced_locally:ignore\"",
|
||||
"check:typescript": "tsc --noEmit",
|
||||
"check:watch": "pnpm run check:svelte --watch",
|
||||
"check:code": "pnpm run format && pnpm run lint && pnpm run check:svelte && pnpm run check:typescript",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { updateMyUser } from '@immich/sdk';
|
||||
import { handleChangePassword } from '$lib/services/user.service';
|
||||
import { Alert, Button, Field, HelperText, PasswordInput, Stack, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -14,9 +14,10 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let currentPassword = $state('');
|
||||
let password = $state('');
|
||||
let passwordConfirm = $state('');
|
||||
const valid = $derived(password === passwordConfirm && passwordConfirm.length > 0);
|
||||
const valid = $derived(currentPassword.length > 0 && password === passwordConfirm && passwordConfirm.length > 0);
|
||||
const errorMessage = $derived(passwordConfirm.length === 0 || valid ? '' : $t('password_does_not_match'));
|
||||
|
||||
const onSubmit = async () => {
|
||||
@@ -24,8 +25,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await updateMyUser({ userUpdateMeDto: { password } });
|
||||
await goto(Route.logout());
|
||||
const success = await handleChangePassword({ password: currentPassword, newPassword: password });
|
||||
if (success) {
|
||||
await goto(Route.logout());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -38,6 +41,10 @@
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
<Field label={$t('password')} required>
|
||||
<PasswordInput bind:value={currentPassword} autocomplete="current-password" />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('new_password')} required>
|
||||
<PasswordInput bind:value={password} autocomplete="new-password" />
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user