lb_rs/service/
account.rs

1use crate::model::account::{Account, MAX_USERNAME_LENGTH};
2use crate::model::api::{
3    DeleteAccountRequest, GetPublicKeyRequest, GetUsernameRequest, NewAccountRequest,
4};
5use crate::model::errors::{core_err_unexpected, LbErrKind, LbResult};
6use crate::model::file_like::FileLike;
7use crate::model::file_metadata::{FileMetadata, FileType, Owner};
8use crate::{Lb, DEFAULT_API_LOCATION};
9use libsecp256k1::SecretKey;
10use qrcode_generator::QrCodeEcc;
11
12use crate::io::network::ApiError;
13
14impl Lb {
15    /// CoreError::AccountExists,
16    /// CoreError::UsernameTaken,
17    /// CoreError::UsernameInvalid,
18    /// CoreError::ServerDisabled,
19    /// CoreError::ServerUnreachable,
20    /// CoreError::ClientUpdateRequired,
21    #[instrument(level = "debug", skip(self), err(Debug))]
22    pub async fn create_account(
23        &self, username: &str, api_url: &str, welcome_doc: bool,
24    ) -> LbResult<Account> {
25        let username = String::from(username).to_lowercase();
26
27        if username.len() > MAX_USERNAME_LENGTH {
28            return Err(LbErrKind::UsernameInvalid.into());
29        }
30
31        let mut tx = self.begin_tx().await;
32        let db = tx.db();
33
34        if db.account.get().is_some() {
35            return Err(LbErrKind::AccountExists.into());
36        }
37
38        let account = Account::new(username.clone(), api_url.to_string());
39
40        let root = FileMetadata::create_root(&account)?.sign_with(&account)?;
41        let root_id = *root.id();
42
43        let last_synced = self
44            .client
45            .request(&account, NewAccountRequest::new(&account, &root))
46            .await?
47            .last_synced;
48
49        db.account.insert(account.clone())?;
50        db.base_metadata.insert(root_id, root)?;
51        db.last_synced.insert(last_synced as i64)?;
52        db.root.insert(root_id)?;
53        db.pub_key_lookup
54            .insert(Owner(account.public_key()), account.username.clone())?;
55
56        self.keychain.cache_account(account.clone()).await?;
57
58        tx.end();
59
60        self.events.meta_changed(root_id);
61
62        if welcome_doc {
63            let welcome_doc = self
64                .create_file("welcome.md", &root_id, FileType::Document)
65                .await?;
66            self.write_document(welcome_doc.id, &Self::welcome_message(&username))
67                .await?;
68            self.sync(None).await?;
69        }
70
71        Ok(account)
72    }
73
74    #[instrument(level = "debug", skip(self, key), err(Debug))]
75    pub async fn import_account(&self, key: &str, api_url: Option<&str>) -> LbResult<Account> {
76        if self.get_account().is_ok() {
77            warn!("tried to import an account, but account exists already.");
78            return Err(LbErrKind::AccountExists.into());
79        }
80
81        if let Ok(key) = base64::decode(key) {
82            if let Ok(account) = bincode::deserialize(&key[..]) {
83                return self.import_account_private_key_v1(account).await;
84            } else if let Ok(key) = SecretKey::parse_slice(&key) {
85                return self
86                    .import_account_private_key_v2(key, api_url.unwrap_or(DEFAULT_API_LOCATION))
87                    .await;
88            }
89        }
90
91        let phrase: [&str; 24] = key
92            .split([' ', ','])
93            .filter(|maybe_word| !maybe_word.is_empty())
94            .collect::<Vec<_>>()
95            .try_into()
96            .map_err(|_| LbErrKind::AccountStringCorrupted)?;
97
98        self.import_account_phrase(phrase, api_url.unwrap_or(DEFAULT_API_LOCATION))
99            .await
100    }
101
102    pub async fn import_account_private_key_v1(&self, account: Account) -> LbResult<Account> {
103        let server_public_key = self
104            .client
105            .request(&account, GetPublicKeyRequest { username: account.username.clone() })
106            .await?
107            .key;
108
109        let account_public_key = account.public_key();
110
111        if account_public_key != server_public_key {
112            return Err(LbErrKind::UsernamePublicKeyMismatch.into());
113        }
114
115        let mut tx = self.begin_tx().await;
116        let db = tx.db();
117        db.account.insert(account.clone())?;
118        self.keychain.cache_account(account.clone()).await?;
119
120        Ok(account)
121    }
122
123    pub async fn import_account_private_key_v2(
124        &self, private_key: SecretKey, api_url: &str,
125    ) -> LbResult<Account> {
126        let mut account =
127            Account { username: "".to_string(), api_url: api_url.to_string(), private_key };
128        let public_key = account.public_key();
129
130        account.username = self
131            .client
132            .request(&account, GetUsernameRequest { key: public_key })
133            .await?
134            .username;
135
136        let mut tx = self.begin_tx().await;
137        let db = tx.db();
138        db.account.insert(account.clone())?;
139        self.keychain.cache_account(account.clone()).await?;
140
141        Ok(account)
142    }
143
144    pub async fn import_account_phrase(
145        &self, phrase: [&str; 24], api_url: &str,
146    ) -> LbResult<Account> {
147        let private_key = Account::phrase_to_private_key(phrase)?;
148        self.import_account_private_key_v2(private_key, api_url)
149            .await
150    }
151
152    #[instrument(level = "debug", skip(self), err(Debug))]
153    pub fn export_account_private_key(&self) -> LbResult<String> {
154        self.export_account_private_key_v1()
155    }
156
157    pub(crate) fn export_account_private_key_v1(&self) -> LbResult<String> {
158        let account = self.get_account()?;
159        let encoded: Vec<u8> = bincode::serialize(account).map_err(core_err_unexpected)?;
160        Ok(base64::encode(encoded))
161    }
162
163    #[allow(dead_code)]
164    pub(crate) fn export_account_private_key_v2(&self) -> LbResult<String> {
165        let account = self.get_account()?;
166        Ok(base64::encode(account.private_key.serialize()))
167    }
168
169    pub fn export_account_phrase(&self) -> LbResult<String> {
170        let account = self.get_account()?;
171        Ok(account.get_phrase()?.join(" "))
172    }
173
174    pub fn export_account_qr(&self) -> LbResult<Vec<u8>> {
175        let acct_secret = self.export_account_private_key_v1()?;
176        qrcode_generator::to_png_to_vec(acct_secret, QrCodeEcc::Low, 1024)
177            .map_err(|err| core_err_unexpected(err).into())
178    }
179
180    #[instrument(level = "debug", skip(self), err(Debug))]
181    pub async fn delete_account(&self) -> LbResult<()> {
182        let account = self.get_account()?;
183
184        self.client
185            .request(account, DeleteAccountRequest {})
186            .await
187            .map_err(|err| match err {
188                ApiError::SendFailed(_) => LbErrKind::ServerUnreachable,
189                ApiError::ClientUpdateRequired => LbErrKind::ClientUpdateRequired,
190                _ => core_err_unexpected(err),
191            })?;
192
193        let mut tx = self.begin_tx().await;
194        let db = tx.db();
195
196        db.account.clear()?;
197        db.last_synced.clear()?;
198        db.base_metadata.clear()?;
199        db.root.clear()?;
200        db.local_metadata.clear()?;
201        db.pub_key_lookup.clear()?;
202
203        // todo: clear cache?
204
205        Ok(())
206    }
207
208    fn welcome_message(username: &str) -> Vec<u8> {
209        format!(r#"# Hello {username}
210
211Welcome to Lockbook! This is an example note to help you get started with our note editor. You can keep it to use as a cheat sheet or delete it anytime.
212
213Lockbook uses Markdown, a lightweight language for formatting plain text. You can use all our supported formatting just by typing. Here’s how it works:
214
215# This is a heading
216
217## This is a smaller heading
218
219### This is an even smaller heading
220
221###### Headings have 6 levels
222
223For italic, use single *asterisks* or _underscores_.
224
225For bold, use double **asterisks** or __underscores__.
226
227For inline code, use single `backticks`
228
229For code blocks, use
230```
231triple
232backticks
233```
234
235>For block quotes,
236use a greater-than sign
237
238Bulleted list items
239* start
240* with
241* asterisks
242- or
243- hyphens
244+ or
245+ plus
246+ signs
247
248Numbered list items
2491. start
2502. with
2513. numbers
2524. and
2535. periods
254
255Happy note taking! You can report any issues to our [Github project](https://github.com/lockbook/lockbook/issues/new) or join our [Discord server](https://discord.gg/qv9fmAZCm6)."#).into()
256    }
257}