Skip to content
This repository was archived by the owner on Mar 6, 2025. It is now read-only.

Commit 4c0d984

Browse files
Allow user to import items from JSON exports (#56)
1 parent 49bc669 commit 4c0d984

36 files changed

+990
-4
lines changed

AuthenticatorShared/Core/Platform/Extentions/Sequence+Async.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import Foundation
22

33
extension Sequence {
4+
/// Performs an operation on each element of a sequence.
5+
/// Each operation is performed serially in order.
6+
///
7+
/// - Parameters:
8+
/// - operation: The closure to run on each element.
9+
///
10+
func asyncForEach(
11+
_ operation: (Element) async throws -> Void
12+
) async rethrows {
13+
for element in self {
14+
try await operation(element)
15+
}
16+
}
17+
418
/// Maps the elements of an array with an async Transform.
519
///
620
/// - Parameter transform: An asynchronous function mapping the sequence element.

AuthenticatorShared/Core/Platform/Services/ServiceContainer.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public class ServiceContainer: Services {
4545
/// The service used to export items.
4646
let exportItemsService: ExportItemsService
4747

48+
/// The service used to import items.
49+
let importItemsService: ImportItemsService
50+
4851
/// The service used to perform app data migrations.
4952
let migrationService: MigrationService
5053

@@ -76,6 +79,7 @@ public class ServiceContainer: Services {
7679
/// - cryptographyService: The service used by the application to encrypt and decrypt items
7780
/// - errorReporter: The service used by the application to report non-fatal errors.
7881
/// - exportItemsService: The service to export items.
82+
/// - importItemsService: The service to import items.
7983
/// - migrationService: The service to do data migrations
8084
/// - pasteboardService: The service used by the application for sharing data with other apps.
8185
/// - stateService: The service for managing account state.
@@ -93,6 +97,7 @@ public class ServiceContainer: Services {
9397
clientService: ClientService,
9498
errorReporter: ErrorReporter,
9599
exportItemsService: ExportItemsService,
100+
importItemsService: ImportItemsService,
96101
migrationService: MigrationService,
97102
pasteboardService: PasteboardService,
98103
stateService: StateService,
@@ -109,6 +114,7 @@ public class ServiceContainer: Services {
109114
self.cryptographyService = cryptographyService
110115
self.errorReporter = errorReporter
111116
self.exportItemsService = exportItemsService
117+
self.importItemsService = importItemsService
112118
self.migrationService = migrationService
113119
self.pasteboardService = pasteboardService
114120
self.timeProvider = timeProvider
@@ -194,6 +200,11 @@ public class ServiceContainer: Services {
194200
timeProvider: timeProvider
195201
)
196202

203+
let importItemsService = DefaultImportItemsService(
204+
authenticatorItemRepository: authenticatorItemRepository,
205+
errorReporter: errorReporter
206+
)
207+
197208
self.init(
198209
application: application,
199210
appSettingsStore: appSettingsStore,
@@ -205,6 +216,7 @@ public class ServiceContainer: Services {
205216
clientService: clientService,
206217
errorReporter: errorReporter,
207218
exportItemsService: exportItemsService,
219+
importItemsService: importItemsService,
208220
migrationService: migrationService,
209221
pasteboardService: pasteboardService,
210222
stateService: stateService,

AuthenticatorShared/Core/Platform/Services/Services.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ typealias Services = HasAuthenticatorItemRepository
77
& HasCryptographyService
88
& HasErrorReporter
99
& HasExportItemsService
10+
& HasImportItemsService
1011
& HasPasteboardService
1112
& HasStateService
1213
& HasTOTPService
@@ -54,6 +55,13 @@ protocol HasExportItemsService {
5455
var exportItemsService: ExportItemsService { get }
5556
}
5657

58+
/// Protocol for an object that provides an `ImportItemsService`.
59+
///
60+
protocol HasImportItemsService {
61+
/// The service used to import items.
62+
var importItemsService: ImportItemsService { get }
63+
}
64+
5765
/// Protocol for an object that provides a `PasteboardService`.
5866
///
5967
protocol HasPasteboardService {

AuthenticatorShared/Core/Platform/Services/TestHelpers/ServiceContainer+Mocks.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extension ServiceContainer {
1515
cryptographyService: CryptographyService = MockCryptographyService(),
1616
errorReporter: ErrorReporter = MockErrorReporter(),
1717
exportItemsService: ExportItemsService = MockExportItemsService(),
18+
importItemsService: ImportItemsService = MockImportItemsService(),
1819
migrationService: MigrationService = MockMigrationService(),
1920
pasteboardService: PasteboardService = MockPasteboardService(),
2021
stateService: StateService = MockStateService(),
@@ -32,6 +33,7 @@ extension ServiceContainer {
3233
clientService: clientService,
3334
errorReporter: errorReporter,
3435
exportItemsService: exportItemsService,
36+
importItemsService: importItemsService,
3537
migrationService: migrationService,
3638
pasteboardService: pasteboardService,
3739
stateService: stateService,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
typealias ClientType = String
4+
typealias DeviceType = Int
5+
6+
// MARK: - Constants
7+
8+
/// Constant values reused throughout the app.
9+
///
10+
enum Constants {
11+
// MARK: Static Properties
12+
13+
/// The default file name when the file name cannot be determined.
14+
static let unknownFileName = "unknown_file_name"
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// MARK: - ImportFileType
2+
3+
/// An enum describing the format of an import file.
4+
///
5+
public enum ImportFileType: Equatable {
6+
/// A `.json` file type.
7+
case json
8+
9+
/// The file extension type to use.
10+
var fileExtension: String {
11+
switch self {
12+
case .json:
13+
"json"
14+
}
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// MARK: - ImportFormatType
2+
3+
/// An enum describing the format of the items item.
4+
///
5+
enum ImportFormatType: Menuable {
6+
/// A JSON exported from Bitwarden
7+
case bitwardenJson
8+
9+
// MARK: Type Properties
10+
11+
/// The ordered list of options to display in the menu.
12+
static let allCases: [ImportFormatType] = [.bitwardenJson]
13+
14+
// MARK: Properties
15+
16+
/// The name of the type to display in the dropdown menu.
17+
var localizedName: String {
18+
switch self {
19+
case .bitwardenJson:
20+
"Authenticator Export (JSON)"
21+
}
22+
}
23+
}

AuthenticatorShared/Core/Vault/Services/ExportItemsService.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class DefaultExportItemsService: ExportItemsService {
7575
///
7676
/// - Parameters:
7777
/// - authenticatorItemRepository: The service for getting items.
78-
/// - cryptographyService: The service for cryptography tasks.
7978
/// - errorReporter: The service for handling errors.
8079
/// - timeProvider: The provider for current time, used in file naming and data timestamps.
8180
///
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import Foundation
2+
3+
// MARK: - ImportItemsService
4+
5+
/// A service to import items from a file.
6+
///
7+
protocol ImportItemsService: AnyObject {
8+
/// Import items with a given format.
9+
///
10+
/// - Parameters:
11+
/// - data: The data to import.
12+
/// - format: The format of the file to import.
13+
///
14+
func importItems(data: Data, format: ImportFileType) async throws
15+
}
16+
17+
extension ImportItemsService {
18+
/// Import items with a given format.
19+
///
20+
/// - Parameters:
21+
/// - url: The URL of the file to import.
22+
/// - format: The format of the file to import.
23+
///
24+
func importItems(url: URL, format: ImportFileType) async throws {
25+
let data = try Data(contentsOf: url)
26+
try await importItems(data: data, format: format)
27+
}
28+
}
29+
30+
class DefaultImportItemsService: ImportItemsService {
31+
// MARK: Properties
32+
33+
/// The item service.
34+
private let authenticatorItemRepository: AuthenticatorItemRepository
35+
36+
/// The error reporter used by this service.
37+
private let errorReporter: ErrorReporter
38+
39+
// MARK: Initilzation
40+
41+
/// Initializes a new instance of a `DefaultExportItemsService`.
42+
///
43+
/// This service handles exporting items from local storage into a file.
44+
///
45+
/// - Parameters:
46+
/// - authenticatorItemRepository: The service for storing items.
47+
/// - errorReporter: The service for handling errors.
48+
///
49+
init(
50+
authenticatorItemRepository: AuthenticatorItemRepository,
51+
errorReporter: ErrorReporter
52+
) {
53+
self.authenticatorItemRepository = authenticatorItemRepository
54+
self.errorReporter = errorReporter
55+
}
56+
57+
// MARK: Methods
58+
59+
func importItems(data: Data, format: ImportFileType) async throws {
60+
let items: [CipherLike]
61+
switch format {
62+
case .json:
63+
items = try importJson(data)
64+
}
65+
try await items.asyncForEach { cipherLike in
66+
let item = AuthenticatorItemView(
67+
favorite: cipherLike.favorite,
68+
id: cipherLike.id,
69+
name: cipherLike.name,
70+
totpKey: cipherLike.login?.totp,
71+
username: cipherLike.login?.username
72+
)
73+
try await authenticatorItemRepository.addAuthenticatorItem(item)
74+
}
75+
}
76+
77+
// MARK: Private Methods
78+
79+
private func importJson(_ data: Data) throws -> [CipherLike] {
80+
let decoder = JSONDecoder()
81+
let vaultLike = try decoder.decode(VaultLike.self, from: data)
82+
return vaultLike.items
83+
}
84+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
@testable import AuthenticatorShared
4+
5+
class MockImportItemsService: ImportItemsService {
6+
var importItemsData: Data?
7+
var importItemsUrl: URL?
8+
var importItemsFormat: ImportFileType?
9+
10+
func importItems(data: Data, format: ImportFileType) async throws {
11+
importItemsData = data
12+
importItemsFormat = format
13+
}
14+
15+
func importItems(url: URL, format: ImportFileType) async throws {
16+
importItemsUrl = url
17+
importItemsFormat = format
18+
}
19+
}

0 commit comments

Comments
 (0)