mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
Merge remote-tracking branch 'origin/main' into fix/android-locked-folder-delete
# Conflicts: # mobile/lib/services/action.service.dart
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -35,6 +35,7 @@ Found Admin:
|
||||
- Email=admin@example.com
|
||||
- Name=Immich Admin
|
||||
? Please choose a new password (optional) immich-is-cool
|
||||
? Invalidate existing sessions? Yes
|
||||
The admin password has been updated.
|
||||
```
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ describe(`immich-admin`, () => {
|
||||
child.stdout.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
if (data.includes('Please choose a new password (optional)')) {
|
||||
child.stdin.write('\n');
|
||||
}
|
||||
|
||||
if (data.includes('Invalidate existing sessions?')) {
|
||||
child.stdin.end('\n');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ enum SortOrder {
|
||||
|
||||
enum TextSearchType { context, filename, description, ocr }
|
||||
|
||||
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||
|
||||
enum ActionSource { timeline, viewer }
|
||||
|
||||
enum ShareAssetType { original, preview }
|
||||
|
||||
@@ -6,6 +6,7 @@ const Map<String, Locale> locales = {
|
||||
// Additional locales
|
||||
'Arabic (ar)': Locale('ar'),
|
||||
'Basque (eu)': Locale('eu'),
|
||||
'Belarusian (be)': Locale('be'),
|
||||
'Bosnian (bl)': Locale('bn'),
|
||||
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
|
||||
'Bulgarian (bg)': Locale('bg'),
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
@@ -292,4 +293,20 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
..orderBy([(row) => OrderingTerm.asc(row.sequence)]);
|
||||
return query.map((row) => row.toDto()!).get();
|
||||
}
|
||||
|
||||
Future<void> update(
|
||||
List<String> remoteIds, {
|
||||
Option<bool> isFavorite = const .none(),
|
||||
Option<AssetVisibility> visibility = const .none(),
|
||||
}) {
|
||||
final companion = RemoteAssetEntityCompanion(
|
||||
visibility: visibility.toDriftValue(),
|
||||
isFavorite: isFavorite.toDriftValue(),
|
||||
);
|
||||
return _db.batch((batch) {
|
||||
for (final remoteId in remoteIds) {
|
||||
batch.update(_db.remoteAssetEntity, companion, where: (e) => e.id.equals(remoteId));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,7 +886,6 @@ class _QuickLinkList extends StatelessWidget {
|
||||
_QuickLink(
|
||||
title: context.t.recently_added,
|
||||
icon: Icons.upload_outlined,
|
||||
isTop: true,
|
||||
onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()),
|
||||
),
|
||||
_QuickLink(
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/repositories/toast.repository.dart';
|
||||
|
||||
final toastRepositoryProvider = Provider<ToastRepository>((ref) => const .new());
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart' hide AssetEditAction;
|
||||
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:openapi/api.dart' as api show AssetVisibility;
|
||||
import 'package:openapi/api.dart' hide AssetVisibility;
|
||||
|
||||
final assetApiRepositoryProvider = Provider(
|
||||
(ref) => AssetApiRepository(
|
||||
@@ -41,7 +43,7 @@ class AssetApiRepository extends ApiRepository {
|
||||
return response?.count ?? 0;
|
||||
}
|
||||
|
||||
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
|
||||
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
|
||||
}
|
||||
|
||||
@@ -77,11 +79,11 @@ class AssetApiRepository extends ApiRepository {
|
||||
return _api.downloadAssetWithHttpInfo(id, edited: edited);
|
||||
}
|
||||
|
||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||
AssetVisibilityEnum.locked => AssetVisibility.locked,
|
||||
AssetVisibilityEnum.archive => AssetVisibility.archive,
|
||||
api.AssetVisibility _mapVisibility(AssetVisibility visibility) => switch (visibility) {
|
||||
AssetVisibility.timeline => api.AssetVisibility.timeline,
|
||||
AssetVisibility.hidden => api.AssetVisibility.hidden,
|
||||
AssetVisibility.locked => api.AssetVisibility.locked,
|
||||
AssetVisibility.archive => api.AssetVisibility.archive,
|
||||
};
|
||||
|
||||
Future<String?> getAssetMIMEType(String assetId) async {
|
||||
@@ -106,6 +108,20 @@ class AssetApiRepository extends ApiRepository {
|
||||
Future<void> removeEdits(String assetId) async {
|
||||
return _api.removeAssetEdits(assetId);
|
||||
}
|
||||
|
||||
Future<void> update(
|
||||
List<String> remoteIds, {
|
||||
Option<bool> isFavorite = const .none(),
|
||||
Option<AssetVisibility> visibility = const .none(),
|
||||
}) {
|
||||
return _api.updateAssets(
|
||||
AssetBulkUpdateDto(
|
||||
ids: remoteIds,
|
||||
isFavorite: isFavorite.toOptional(),
|
||||
visibility: visibility.map(_mapVisibility).toOptional(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on StackResponseDto {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class ToastOption {
|
||||
final Duration? timeout;
|
||||
final FutureOr<void> Function()? onUndo;
|
||||
|
||||
const ToastOption({this.timeout, this.onUndo});
|
||||
}
|
||||
|
||||
class ToastRepository {
|
||||
const ToastRepository();
|
||||
|
||||
FutureOr<void> success(String message, {ToastOption? toast}) {
|
||||
snackbar.success(message, duration: toast?.timeout);
|
||||
}
|
||||
|
||||
FutureOr<void> info(String message, {ToastOption? toast}) {
|
||||
snackbar.info(message, duration: toast?.timeout);
|
||||
}
|
||||
|
||||
FutureOr<void> error(String message, {ToastOption? toast}) {
|
||||
snackbar.error(message, duration: toast?.timeout);
|
||||
}
|
||||
}
|
||||
@@ -79,17 +79,17 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<void> archive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.archive);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .archive);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive);
|
||||
}
|
||||
|
||||
Future<void> unArchive(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
Future<int> moveToLockFolder(List<String> remoteIds, List<String> localIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .locked);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||
|
||||
// Locked assets stay on the server, so permanently delete the local copies instead of trashing them
|
||||
@@ -100,7 +100,7 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<void> removeFromLockFolder(List<String> remoteIds) async {
|
||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.timeline);
|
||||
await _assetApiRepository.updateVisibility(remoteIds, .timeline);
|
||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:openapi/api.dart' show Optional;
|
||||
|
||||
sealed class Option<T> {
|
||||
@@ -21,6 +22,11 @@ sealed class Option<T> {
|
||||
None() => null,
|
||||
};
|
||||
|
||||
Option<U> map<U>(U Function(T value) f) => switch (this) {
|
||||
Some(:final value) => Some(f(value)),
|
||||
None() => None<U>(),
|
||||
};
|
||||
|
||||
U fold<U>(U Function(T value) onSome, U Function() onNone) => switch (this) {
|
||||
Some(:final value) => onSome(value),
|
||||
None() => onNone(),
|
||||
@@ -65,3 +71,10 @@ extension OptionToOptional<T> on Option<T> {
|
||||
Some(:final value) => Optional.present(value),
|
||||
};
|
||||
}
|
||||
|
||||
extension OptionToDriftValue<T> on Option<T> {
|
||||
Value<T> toDriftValue() => switch (this) {
|
||||
Some(:final value) => Value(value),
|
||||
None() => const Value.absent(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,18 +6,23 @@ final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
class SnackbarManager {
|
||||
const SnackbarManager();
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(String message, SnackbarType type) {
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(
|
||||
String message,
|
||||
SnackbarType type, {
|
||||
Duration? duration,
|
||||
}) {
|
||||
final messenger = scaffoldMessengerKey.currentState;
|
||||
final context = scaffoldMessengerKey.currentContext;
|
||||
if (messenger == null || context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
duration ??= const .new(seconds: 4);
|
||||
messenger.hideCurrentSnackBar();
|
||||
return messenger.showSnackBar(_build(context, message, type));
|
||||
return messenger.showSnackBar(_build(context, message, type, duration));
|
||||
}
|
||||
|
||||
SnackBar _build(BuildContext context, String message, SnackbarType type) {
|
||||
SnackBar _build(BuildContext context, String message, SnackbarType type, Duration duration) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.extension<ImmichColors>() ?? ImmichColors.harmonized(theme.colorScheme);
|
||||
final (IconData icon, Color background, Color foreground) = switch (type) {
|
||||
@@ -29,7 +34,7 @@ class SnackbarManager {
|
||||
return SnackBar(
|
||||
behavior: .floating,
|
||||
backgroundColor: background,
|
||||
duration: const .new(seconds: 4),
|
||||
duration: duration,
|
||||
shape: const RoundedRectangleBorder(borderRadius: .all(.circular(ImmichRadius.sm))),
|
||||
content: Row(
|
||||
children: [
|
||||
@@ -48,11 +53,14 @@ class SnackbarManager {
|
||||
);
|
||||
}
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message) => show(message, .info);
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? info(String message, {Duration? duration}) =>
|
||||
show(message, .info, duration: duration);
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message) => show(message, .success);
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? success(String message, {Duration? duration}) =>
|
||||
show(message, .success, duration: duration);
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message) => show(message, .error);
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? error(String message, {Duration? duration}) =>
|
||||
show(message, .error, duration: duration);
|
||||
}
|
||||
|
||||
const snackbar = SnackbarManager();
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
@@ -197,7 +196,7 @@ void main() {
|
||||
test('permanently deletes local copies without trashing, even when Android trash handling is on', () async {
|
||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => assetMediaRepository.deleteAll(localIds, trash: false)).thenAnswer((_) async => localIds);
|
||||
when(() => localAssetRepository.delete(localIds)).thenAnswer((_) async {});
|
||||
@@ -205,7 +204,7 @@ void main() {
|
||||
final result = await sut.moveToLockFolder(remoteIds, localIds);
|
||||
|
||||
expect(result, localIds.length);
|
||||
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).called(1);
|
||||
verify(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).called(1);
|
||||
verify(() => assetMediaRepository.deleteAll(localIds, trash: false)).called(1);
|
||||
verify(() => localAssetRepository.delete(localIds)).called(1);
|
||||
@@ -213,18 +212,18 @@ void main() {
|
||||
});
|
||||
|
||||
test('locks remote assets without touching local media when there are no local copies', () async {
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
|
||||
final result = await sut.moveToLockFolder(remoteIds, const []);
|
||||
|
||||
expect(result, 0);
|
||||
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).called(1);
|
||||
verifyNever(() => assetMediaRepository.deleteAll(any(), trash: any(named: 'trash')));
|
||||
});
|
||||
|
||||
test('returns zero when local deletion is cancelled', () async {
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => assetMediaRepository.deleteAll(localIds, trash: false)).thenAnswer((_) async => <String>[]);
|
||||
|
||||
@@ -237,7 +236,7 @@ void main() {
|
||||
|
||||
test('returns the number of local copies deleted from a partial result', () async {
|
||||
const deletedIds = ['l1'];
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||
when(() => assetMediaRepository.deleteAll(localIds, trash: false)).thenAnswer((_) async => deletedIds);
|
||||
when(() => localAssetRepository.delete(deletedIds)).thenAnswer((_) async {});
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
} from 'src/commands/media-location.command';
|
||||
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
||||
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
||||
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||
import { PromptPasswordResetQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||
import { SchemaCheck } from 'src/commands/schema-check';
|
||||
import { VersionCommand } from 'src/commands/version.command';
|
||||
|
||||
export const commandsAndQuestions = [
|
||||
ResetAdminPasswordCommand,
|
||||
PromptPasswordQuestions,
|
||||
PromptPasswordResetQuestions,
|
||||
PromptEmailQuestion,
|
||||
EnablePasswordLoginCommand,
|
||||
DisablePasswordLoginCommand,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
const prompt = (inquirer: InquirerService) => {
|
||||
return function ask(admin: UserAdminResponseDto) {
|
||||
return (admin: UserAdminResponseDto) => {
|
||||
const { id, oauthId, email, name } = admin;
|
||||
console.log(`Found Admin:
|
||||
- ID=${id}
|
||||
@@ -11,7 +11,7 @@ const prompt = (inquirer: InquirerService) => {
|
||||
- Email=${email}
|
||||
- Name=${name}`);
|
||||
|
||||
return inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
|
||||
return inquirer.ask<{ newPassword: string; invalidateSessions: boolean }>('prompt-password-reset', {});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,13 +43,23 @@ export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
@QuestionSet({ name: 'prompt-password' })
|
||||
export class PromptPasswordQuestions {
|
||||
@QuestionSet({ name: 'prompt-password-reset' })
|
||||
export class PromptPasswordResetQuestions {
|
||||
@Question({
|
||||
message: 'Please choose a new password (optional)',
|
||||
name: 'password',
|
||||
name: 'newPassword',
|
||||
})
|
||||
parsePassword(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Question({
|
||||
type: 'confirm',
|
||||
message: 'Invalidate existing sessions?',
|
||||
default: true,
|
||||
name: 'invalidateSessions',
|
||||
})
|
||||
parseInvalidate(value: boolean): boolean {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ describe(CliService.name, () => {
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(UserFactory.create({ isAdmin: true }));
|
||||
|
||||
const ask = vitest.fn().mockImplementation(() => {});
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: undefined, invalidateSessions: false });
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
@@ -47,6 +47,7 @@ describe(CliService.name, () => {
|
||||
expect(ask).toHaveBeenCalled();
|
||||
expect(id).toEqual(admin.id);
|
||||
expect(update.password).toBeDefined();
|
||||
expect(mocks.session.invalidateAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use the supplied password', async () => {
|
||||
@@ -55,7 +56,7 @@ describe(CliService.name, () => {
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(admin);
|
||||
|
||||
const ask = vitest.fn().mockResolvedValue('new-password');
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: 'new-password', invalidateSessions: false });
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
@@ -66,6 +67,20 @@ describe(CliService.name, () => {
|
||||
expect(id).toEqual(admin.id);
|
||||
expect(update.password).toBeDefined();
|
||||
});
|
||||
|
||||
it('should invalidate existing sessions when requested', async () => {
|
||||
const admin = UserFactory.create({ isAdmin: true });
|
||||
|
||||
mocks.user.getAdmin.mockResolvedValue(admin);
|
||||
mocks.user.update.mockResolvedValue(admin);
|
||||
mocks.session.invalidateAll.mockResolvedValue(void 0);
|
||||
|
||||
const ask = vitest.fn().mockResolvedValue({ newPassword: 'new-password', invalidateSessions: true });
|
||||
|
||||
await sut.resetAdminPassword(ask);
|
||||
|
||||
expect(mocks.session.invalidateAll).toHaveBeenCalledWith({ userId: admin.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('disablePasswordLogin', () => {
|
||||
|
||||
@@ -58,18 +58,24 @@ export class CliService extends BaseService {
|
||||
return users.map((user) => mapUserAdmin(user));
|
||||
}
|
||||
|
||||
async resetAdminPassword(ask: (admin: UserAdminResponseDto) => Promise<string | undefined>) {
|
||||
async resetAdminPassword(
|
||||
ask: (admin: UserAdminResponseDto) => Promise<{ newPassword: string | undefined; invalidateSessions: boolean }>,
|
||||
) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
throw new Error('Admin account does not exist');
|
||||
}
|
||||
|
||||
const providedPassword = await ask(mapUserAdmin(admin));
|
||||
const { newPassword: providedPassword, invalidateSessions } = await ask(mapUserAdmin(admin));
|
||||
const password = providedPassword || this.cryptoRepository.randomBytesAsText(24);
|
||||
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
|
||||
|
||||
await this.userRepository.update(admin.id, { password: hashedPassword });
|
||||
|
||||
if (invalidateSessions) {
|
||||
await this.sessionRepository.invalidateAll({ userId: admin.id });
|
||||
}
|
||||
|
||||
return { admin, password, provided: !!providedPassword };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user