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