10 Commits
Author SHA1 Message Date
Codex e04e70db2e Close startup log connection and use portable export filenames 2026-10-22 05:39:19 +03:00
Codex 75517057d0 Keep Svelte compiler warning option portable with pnpm 12 2026-10-22 05:33:35 +03:00
Codex 3efc91ff9a Prepare localized 3.3.5-local.7 release with updated dependencies 2026-10-22 05:29:35 +03:00
Clément DelestreandCodex 7c07c001cd fix(mobile): keep header image when starting selection (#32356)
* fix(mobile): keep header image when starting selection

* fix(mobile): fade out header actions during selection

Hide the back button and person options menu with the same fade as the
timeline app bar, and block taps on the person name and birthday while
assets are selected.

---------

Co-authored-by: Santo Shakil <santoshakil@users.noreply.github.com>
(cherry picked from commit 745bdd2edf72ce0f75e748e2d68c37b6ea196cc5)
2026-10-22 05:25:45 +03:00
renovate[bot]andCodex f1f7f82e91 chore(deps): update base-image to v202610211607 (#32644)
Co-authored-by: bo0tzz <git@bo0tzz.me>
(cherry picked from commit b9269f873982c9b1c0f9267aefad8b8b9eacb7d9)
2026-10-22 05:25:45 +03:00
bo0tzzandCodex b3bd5d1fd1 feat(server): remove deprecated password field from update-me (#32623)
* feat(server): remove deprecated password field from update-me

* fix: tests for removed password field

(cherry picked from commit 3f6a071203187681b6c06b977665dc624c431336)
2026-10-22 05:25:45 +03:00
bo0tzzandCodex ad9b368cb8 fix(server): exclude partners' locked assets from legacy search (#32604)
(cherry picked from commit 8dab52c7491c79cba742315dea8d7f5212915875)
2026-10-22 05:25:45 +03:00
renovate[bot]andCodex 7c78b00df6 fix(deps): update dependency ioredis to v6 (#32137)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 48474ecff56fe02cb84a8136d923863d4978a7e0)
2026-10-22 05:25:45 +03:00
renovate[bot]andCodex 4dfb6e88af chore(deps): update node.js to v24.22.1 (#32633)
Co-authored-by: bo0tzz <git@bo0tzz.me>
(cherry picked from commit c5086b68f76b78964e22c52bd78060344d82aebf)
2026-10-22 05:25:45 +03:00
renovate[bot]andCodex 744797e59d chore(deps): update node.js to v24.22.1 (#32634)
(cherry picked from commit c78e5c37843916199e380f077add45bd15a8bba3)
2026-10-22 05:25:45 +03:00
31 changed files with 931 additions and 641 deletions
+1 -1
View File
@@ -1 +1 @@
24.22.0
24.22.1
+2 -2
View File
@@ -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',
-40
View File
@@ -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();
});
});
});
+1
View File
@@ -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();
+20
View File
@@ -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": "Добавить оттенок к фону.",
+12 -12
View File
@@ -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"
+1 -1
View File
@@ -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 {
+6 -2
View File
@@ -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
View File
@@ -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)),
+1 -6
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "3.3.3",
"version": "3.3.5",
"description": "Monorepo for Immich",
"type": "module",
"private": true,
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
-2
View File
@@ -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 */
+706 -416
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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",
-5
View File
@@ -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(),
})
-7
View File
@@ -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);
+5
View File
@@ -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
View File
@@ -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>