1use bitwarden_api_api::models::{
2 DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
3};
4use bitwarden_collections::{collection::Collection, error::CollectionsParseError};
5use bitwarden_core::{
6 client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError,
7 OrganizationId, UserId,
8};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::{Cipher, Folder, GlobalDomains, VaultParseError};
13
14#[derive(Debug, Error)]
15pub enum SyncError {
16 #[error(transparent)]
17 Api(#[from] bitwarden_core::ApiError),
18 #[error(transparent)]
19 MissingField(#[from] MissingFieldError),
20 #[error(transparent)]
21 VaultParse(#[from] VaultParseError),
22 #[error(transparent)]
23 CollectionParse(#[from] CollectionsParseError),
24 #[error(transparent)]
25 EncryptionSettings(#[from] EncryptionSettingsError),
26}
27
28#[allow(missing_docs)]
29#[derive(Serialize, Deserialize, Debug)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct SyncRequest {
32 pub exclude_subdomains: Option<bool>,
34}
35
36pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result<SyncResponse, SyncError> {
37 let config = client.internal.get_api_configurations().await;
38 let sync = bitwarden_api_api::apis::sync_api::sync_get(&config.api, input.exclude_subdomains)
39 .await
40 .map_err(|e| SyncError::Api(e.into()))?;
41
42 let org_keys: Vec<_> = require!(sync.profile.as_ref())
43 .organizations
44 .as_deref()
45 .unwrap_or_default()
46 .iter()
47 .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
48 .map(|(id, key)| (OrganizationId::new(id), key))
49 .collect();
50
51 client.internal.initialize_org_crypto(org_keys)?;
52
53 SyncResponse::process_response(sync)
54}
55
56#[derive(Serialize, Deserialize, Debug)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct ProfileResponse {
59 pub id: UserId,
60 pub name: String,
61 pub email: String,
62
63 pub organizations: Vec<ProfileOrganizationResponse>,
66}
67
68#[derive(Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct ProfileOrganizationResponse {
71 pub id: OrganizationId,
72}
73
74#[derive(Serialize, Deserialize, Debug)]
75#[serde(rename_all = "camelCase", deny_unknown_fields)]
76pub struct DomainResponse {
77 pub equivalent_domains: Vec<Vec<String>>,
78 pub global_equivalent_domains: Vec<GlobalDomains>,
79}
80
81#[allow(missing_docs)]
82#[derive(Serialize, Deserialize, Debug)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct SyncResponse {
85 pub profile: ProfileResponse,
88 pub folders: Vec<Folder>,
89 pub collections: Vec<Collection>,
90 pub ciphers: Vec<Cipher>,
92 pub domains: Option<DomainResponse>,
93 }
96
97impl SyncResponse {
98 pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
99 let profile = require!(response.profile);
100 let ciphers = require!(response.ciphers);
101
102 fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
103 where
104 In: IntoIterator<Item = InItem>,
105 InItem: TryInto<OutItem>,
106 Out: FromIterator<OutItem>,
107 {
108 iter.into_iter().map(|i| i.try_into()).collect()
109 }
110
111 Ok(SyncResponse {
112 profile: ProfileResponse::process_response(*profile)?,
113 folders: try_into_iter(require!(response.folders))?,
114 collections: try_into_iter(require!(response.collections))?,
115 ciphers: try_into_iter(ciphers)?,
116 domains: response.domains.map(|d| (*d).try_into()).transpose()?,
117 })
120 }
121}
122
123impl ProfileOrganizationResponse {
124 fn process_response(
125 response: ProfileOrganizationResponseModel,
126 ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
127 Ok(ProfileOrganizationResponse {
128 id: OrganizationId::new(require!(response.id)),
129 })
130 }
131}
132
133impl ProfileResponse {
134 fn process_response(
135 response: ProfileResponseModel,
136 ) -> Result<ProfileResponse, MissingFieldError> {
137 Ok(ProfileResponse {
138 id: UserId::new(require!(response.id)),
139 name: require!(response.name),
140 email: require!(response.email),
141 organizations: response
144 .organizations
145 .unwrap_or_default()
146 .into_iter()
147 .map(ProfileOrganizationResponse::process_response)
148 .collect::<Result<_, _>>()?,
149 })
150 }
151}
152
153impl TryFrom<DomainsResponseModel> for DomainResponse {
154 type Error = SyncError;
155
156 fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
157 Ok(Self {
158 equivalent_domains: value.equivalent_domains.unwrap_or_default(),
159 global_equivalent_domains: value
160 .global_equivalent_domains
161 .unwrap_or_default()
162 .into_iter()
163 .map(|s| s.try_into())
164 .collect::<Result<Vec<GlobalDomains>, _>>()?,
165 })
166 }
167}