1use bitwarden_api_api::models::{
2 DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
3};
4use bitwarden_collections::{collection::Collection, error::CollectionsParseError};
5use bitwarden_core::{
6 Client, MissingFieldError, OrganizationId, UserId,
7 client::encryption_settings::EncryptionSettingsError, require,
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 = config
39 .api_client
40 .sync_api()
41 .get(input.exclude_subdomains)
42 .await
43 .map_err(|e| SyncError::Api(e.into()))?;
44
45 let org_keys: Vec<_> = require!(sync.profile.as_ref())
46 .organizations
47 .as_deref()
48 .unwrap_or_default()
49 .iter()
50 .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
51 .map(|(id, key)| (OrganizationId::new(id), key))
52 .collect();
53
54 client.internal.initialize_org_crypto(org_keys)?;
55
56 SyncResponse::process_response(sync)
57}
58
59#[derive(Serialize, Deserialize, Debug)]
60#[serde(rename_all = "camelCase", deny_unknown_fields)]
61pub struct ProfileResponse {
62 pub id: UserId,
63 pub name: String,
64 pub email: String,
65
66 pub organizations: Vec<ProfileOrganizationResponse>,
69}
70
71#[derive(Serialize, Deserialize, Debug)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct ProfileOrganizationResponse {
74 pub id: OrganizationId,
75}
76
77#[derive(Serialize, Deserialize, Debug)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct DomainResponse {
80 pub equivalent_domains: Vec<Vec<String>>,
81 pub global_equivalent_domains: Vec<GlobalDomains>,
82}
83
84#[allow(missing_docs)]
85#[derive(Serialize, Deserialize, Debug)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct SyncResponse {
88 pub profile: ProfileResponse,
91 pub folders: Vec<Folder>,
92 pub collections: Vec<Collection>,
93 pub ciphers: Vec<Cipher>,
95 pub domains: Option<DomainResponse>,
96 }
99
100impl SyncResponse {
101 pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
102 let profile = require!(response.profile);
103 let ciphers = require!(response.ciphers);
104
105 fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
106 where
107 In: IntoIterator<Item = InItem>,
108 InItem: TryInto<OutItem>,
109 Out: FromIterator<OutItem>,
110 {
111 iter.into_iter().map(|i| i.try_into()).collect()
112 }
113
114 Ok(SyncResponse {
115 profile: ProfileResponse::process_response(*profile)?,
116 folders: try_into_iter(require!(response.folders))?,
117 collections: try_into_iter(require!(response.collections))?,
118 ciphers: try_into_iter(ciphers)?,
119 domains: response.domains.map(|d| (*d).try_into()).transpose()?,
120 })
123 }
124}
125
126impl ProfileOrganizationResponse {
127 fn process_response(
128 response: ProfileOrganizationResponseModel,
129 ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
130 Ok(ProfileOrganizationResponse {
131 id: OrganizationId::new(require!(response.id)),
132 })
133 }
134}
135
136impl ProfileResponse {
137 fn process_response(
138 response: ProfileResponseModel,
139 ) -> Result<ProfileResponse, MissingFieldError> {
140 Ok(ProfileResponse {
141 id: UserId::new(require!(response.id)),
142 name: require!(response.name),
143 email: require!(response.email),
144 organizations: response
147 .organizations
148 .unwrap_or_default()
149 .into_iter()
150 .map(ProfileOrganizationResponse::process_response)
151 .collect::<Result<_, _>>()?,
152 })
153 }
154}
155
156impl TryFrom<DomainsResponseModel> for DomainResponse {
157 type Error = SyncError;
158
159 fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
160 Ok(Self {
161 equivalent_domains: value.equivalent_domains.unwrap_or_default(),
162 global_equivalent_domains: value
163 .global_equivalent_domains
164 .unwrap_or_default()
165 .into_iter()
166 .map(|s| s.try_into())
167 .collect::<Result<Vec<GlobalDomains>, _>>()?,
168 })
169 }
170}