cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 1 | use std::sync::Arc; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 2 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 3 | use std::{collections::BTreeSet, path::PathBuf}; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 4 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 5 | use ruma::TransactionId; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 6 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 7 | use crate::{ |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 8 | AccountError, EncryptedSessionStore, OAuthLogin, PasswordLogin, PendingOAuthLogin, Restore, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 9 | Session, SessionRecord, SessionStore, SoftLogout, StoreConfig, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 10 | account::{callback_state, homeserver_input}, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 11 | engine::StoreLock, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 12 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 13 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 14 | /// every account one owner has, a store each under one root and one passphrase. whoever holds |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 15 | /// the set can resume any soft-logged-out device in it with just its password, and the device's |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 16 | /// store has its e2ee keys, so a set belongs to one person and never to a whole machine |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 17 | pub struct Accounts { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 18 | root: Root, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 19 | passphrase: String, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 20 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 21 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 22 | enum Root { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 23 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 24 | Directory(PathBuf), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 25 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 26 | Prefix(String), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 27 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 28 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 29 | /// one account's record and store |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 30 | #[derive(Clone)] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 31 | pub struct Account { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 32 | id: String, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 33 | sessions: Arc<EncryptedSessionStore>, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 34 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 35 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 36 | impl Account { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 37 | /// stable for the account's life, and safe in a cookie or a url |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 38 | #[must_use] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 39 | pub fn id(&self) -> &str { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 40 | &self.id |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 41 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 42 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 43 | /// for `Termination::retain`. a discarded account drops out of `Accounts::list` |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 44 | #[must_use] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 45 | pub fn sessions(&self) -> Arc<dyn SessionStore> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 46 | self.sessions.clone() |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 47 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 48 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 49 | pub async fn restore(&self) -> Result<Option<Restore>, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 50 | Session::restore(self.sessions()).await |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 51 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 52 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 53 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 54 | impl std::fmt::Debug for Account { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 55 | fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 56 | formatter |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 57 | .debug_struct("Account") |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 58 | .field("id", &self.id) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 59 | .finish_non_exhaustive() |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 60 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 61 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 62 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 63 | enum Found { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 64 | Nobody, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 65 | SoftLoggedOut(Account, SoftLogout), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 66 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 67 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 68 | impl Accounts { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 69 | /// each account is a store directory under `root` with its record beside it |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 70 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 71 | #[must_use] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 72 | pub fn sqlite(root: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 73 | Self { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 74 | root: Root::Directory(root.into()), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 75 | passphrase: passphrase.into(), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 76 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 77 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 78 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 79 | /// each account is a store named `<prefix>::<id>`, listed in `<prefix>::macltok-accounts` |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 80 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 81 | #[must_use] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 82 | pub fn indexeddb(prefix: impl Into<String>, passphrase: impl Into<String>) -> Self { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 83 | Self { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 84 | root: Root::Prefix(prefix.into()), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 85 | passphrase: passphrase.into(), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 86 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 87 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 88 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 89 | /// `None` for anything `Account::id` couldn't have been |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 90 | #[must_use] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 91 | pub fn account(&self, id: &str) -> Option<Account> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 92 | let valid = |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 93 | !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric()); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 94 | valid.then(|| self.open(id.to_owned())) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 95 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 96 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 97 | /// every account with a record. a store left without one, by a login that died halfway or a |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 98 | /// discard that couldn't finish, is deleted on the way past, and so is an oauth login whose |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 99 | /// redirect never came back. one that resumed an account puts that account back |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 100 | pub async fn list(&self) -> Result<Vec<Account>, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 101 | let mut accounts = Vec::new(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 102 | for account in self.slots().await? { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 103 | match account.sessions.load().await { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 104 | Ok(None) => self.sweep(&account).await, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 105 | Ok(Some(record)) if record.oauth_expired() => { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 106 | if self.expire(&account).await { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 107 | accounts.push(account); |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 108 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 109 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 110 | Ok(Some(_)) | Err(_) => accounts.push(account), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 111 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 112 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 113 | Ok(accounts) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 114 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 115 | |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 116 | /// finishes an oauth login begun before a redirect, finding its account by the callback's |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 117 | /// state, so the host needs to have kept nothing across the redirect |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 118 | pub async fn finish_oauth( |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 119 | &self, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 120 | callback_url: &str, |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 121 | ) -> Result<(Account, Session), AccountError> { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 122 | let state = url::Url::parse(callback_url) |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 123 | .ok() |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 124 | .as_ref() |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 125 | .and_then(callback_state) |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 126 | .ok_or(AccountError::NoPendingOAuth)?; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 127 | for account in self.slots().await? { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 128 | let Ok(Some(record)) = account.sessions.load().await else { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 129 | continue; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 130 | }; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 131 | if record.pending_oauth_state() != Some(state.as_str()) { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 132 | continue; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 133 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 134 | let store_lock = account.sessions.store().lock().await?; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 135 | let session = |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 136 | Session::finish_oauth_locked(account.sessions(), store_lock, callback_url).await?; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 137 | return Ok((account, session)); |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 138 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 139 | Err(AccountError::NoPendingOAuth) |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 140 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 141 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 142 | /// a soft-logged-out account the login names is resumed as its own device, keeping its keys. |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 143 | /// anyone else gets a new account |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 144 | pub async fn login_password( |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 145 | &self, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 146 | login: PasswordLogin, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 147 | ) -> Result<(Account, Session), AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 148 | homeserver_input(&login.homeserver, login.allow_insecure_loopback)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 149 | let found = self |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 150 | .find(|record| record_names(record, &login.homeserver, &login.username)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 151 | .await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 152 | match found { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 153 | Found::SoftLoggedOut(account, soft) => { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 154 | let store_lock = account.sessions.store().lock().await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 155 | let login = PasswordLogin { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 156 | device_display_name: login.device_display_name, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 157 | ..soft.password_login(login.password) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 158 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 159 | let session = |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 160 | Session::login_password_locked(login, account.sessions(), store_lock).await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 161 | Ok((account, session)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 162 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 163 | Found::Nobody => { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 164 | let (account, store_lock) = self.fresh().await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 165 | let login = PasswordLogin { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 166 | device_id: None, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 167 | ..login |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 168 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 169 | match Session::login_password_locked(login, account.sessions(), store_lock).await { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 170 | Ok(session) => Ok((account, session)), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 171 | Err(error) => Err(self.abandon(&account, error).await), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 172 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 173 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 174 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 175 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 176 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 177 | /// resumes the soft-logged-out account whose device `login` names, as |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 178 | /// `SoftLogout::oauth_login` sets it. without one it's a new account, since which account an |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 179 | /// OAuth login is for isn't known until it finishes |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 180 | pub async fn begin_oauth( |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 181 | &self, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 182 | login: OAuthLogin, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 183 | ) -> Result<(Account, PendingOAuthLogin), AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 184 | homeserver_input(&login.homeserver, login.allow_insecure_loopback)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 185 | let device_id = login.device_id.clone(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 186 | let found = self |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 187 | .find(|record| { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 188 | record |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 189 | .soft_logout() |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 190 | .is_some_and(|soft| device_id.as_ref() == Some(&soft.device_id)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 191 | }) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 192 | .await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 193 | match found { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 194 | Found::SoftLoggedOut(account, soft) => { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 195 | let store_lock = account.sessions.store().lock().await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 196 | let login = OAuthLogin { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 197 | homeserver: soft.homeserver, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 198 | client_id: login.client_id.or(soft.client_id), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 199 | allow_insecure_loopback: true, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 200 | ..login |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 201 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 202 | let pending = |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 203 | Session::begin_oauth_locked(login, account.sessions(), store_lock).await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 204 | Ok((account, pending)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 205 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 206 | Found::Nobody => { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 207 | let (account, store_lock) = self.fresh().await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 208 | let login = OAuthLogin { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 209 | device_id: None, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 210 | ..login |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 211 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 212 | match Session::begin_oauth_locked(login, account.sessions(), store_lock).await { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 213 | Ok(pending) => Ok((account, pending)), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 214 | Err(error) => Err(self.abandon(&account, error).await), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 215 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 216 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 217 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 218 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 219 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 220 | /// a live login for the same person is refused rather than given a second device |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 221 | async fn find(&self, resumes: impl Fn(&SessionRecord) -> bool) -> Result<Found, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 222 | for account in self.slots().await? { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 223 | let Ok(Some(record)) = account.sessions.load().await else { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 224 | continue; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 225 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 226 | if !resumes(&record) { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 227 | continue; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 228 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 229 | return match (record.soft_logout(), record.user_id()) { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 230 | (Some(soft), _) => Ok(Found::SoftLoggedOut(account, soft)), |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 231 | (None, Some(user_id)) => Err(AccountError::AlreadyLoggedIn(user_id.clone())), |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 232 | (None, None) => continue, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 233 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 234 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 235 | Ok(Found::Nobody) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 236 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 237 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 238 | /// locked before anything else can see it, so `list` never sweeps a login still under way |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 239 | async fn fresh(&self) -> Result<(Account, StoreLock), AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 240 | let account = self.open(TransactionId::new().to_string()); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 241 | let store_lock = account.sessions.store().lock().await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 242 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 243 | { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 244 | let Root::Prefix(prefix) = &self.root; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 245 | index::add(prefix, &account.id).await?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 246 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 247 | Ok((account, store_lock)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 248 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 249 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 250 | /// a failed first login leaves nothing behind. one whose record didn't save has a live device |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 251 | /// on the server, so that stays for `UnsavedSession::save` |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 252 | async fn abandon(&self, account: &Account, error: AccountError) -> AccountError { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 253 | if !matches!(error, AccountError::Unsaved { .. }) { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 254 | self.sweep(account).await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 255 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 256 | error |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 257 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 258 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 259 | /// best effort, since whatever it leaves is found and swept again by the next `list` |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 260 | async fn sweep(&self, account: &Account) { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 261 | let Ok(_store_lock) = account.sessions.store().lock().await else { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 262 | return; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 263 | }; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 264 | if matches!(account.sessions.load().await, Ok(None)) { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 265 | self.remove(account).await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 266 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 267 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 268 | |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 269 | /// whether the account is still there. a login running in another tab holds the lock and |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 270 | /// is left alone |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 271 | async fn expire(&self, account: &Account) -> bool { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 272 | let Ok(_store_lock) = account.sessions.store().lock().await else { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 273 | return true; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 274 | }; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 275 | let Ok(Some(record)) = account.sessions.load().await else { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 276 | return true; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 277 | }; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 278 | if !record.oauth_expired() { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 279 | return true; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 280 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 281 | match record.resumable() { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 282 | Some(previous) => { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 283 | let _ = account.sessions.save(&previous).await; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 284 | true |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 285 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 286 | None => { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 287 | self.remove(account).await; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 288 | false |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 289 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 290 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 291 | } |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 292 | |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 293 | async fn remove(&self, account: &Account) { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 294 | if account.sessions.discard().await.is_err() { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 295 | return; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 296 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 297 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 298 | { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 299 | let Root::Prefix(prefix) = &self.root; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 300 | let _ = index::remove(prefix, &account.id).await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 301 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 302 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 303 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 304 | fn open(&self, id: String) -> Account { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 305 | let sessions = match &self.root { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 306 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 307 | Root::Directory(root) => EncryptedSessionStore::file( |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 308 | root.join(format!("{id}.session")), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 309 | StoreConfig::sqlite(root.join(&id), self.passphrase.clone()), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 310 | ), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 311 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 312 | Root::Prefix(prefix) => EncryptedSessionStore::indexeddb(StoreConfig::indexeddb( |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 313 | format!("{prefix}::{id}"), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 314 | self.passphrase.clone(), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 315 | )), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 316 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 317 | Account { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 318 | id, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 319 | sessions: Arc::new(sessions), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 320 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 321 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 322 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 323 | async fn slots(&self) -> Result<Vec<Account>, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 324 | let ids = match &self.root { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 325 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 326 | Root::Directory(root) => directory_slots(root)?, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 327 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 328 | Root::Prefix(prefix) => index::list(prefix).await?, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 329 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 330 | Ok(ids.iter().filter_map(|id| self.account(id)).collect()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 331 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 332 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 333 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 334 | /// a localpart or a full user id names the same account, but only on the same server. the server |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 335 | /// is compared by name as well as by url, so a delegated `example.org` still finds the account |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 336 | /// discovery took to `matrix.example.org` |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 337 | fn record_names(record: &SessionRecord, homeserver: &str, username: &str) -> bool { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 338 | let Some(user_id) = record.user_id() else { |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 339 | return false; |
d8d8a9fe770a | atagen | 2026-10-22 07:44 UTC | 340 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 341 | let username = username.trim(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 342 | let user = if username.starts_with('@') { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 343 | username.eq_ignore_ascii_case(user_id.as_str()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 344 | } else { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 345 | username.eq_ignore_ascii_case(user_id.localpart()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 346 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 347 | let typed = authority(homeserver); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 348 | user && (typed.eq_ignore_ascii_case(user_id.server_name().as_str()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 349 | || typed.eq_ignore_ascii_case(authority(record.homeserver()))) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 350 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 351 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 352 | fn authority(homeserver: &str) -> &str { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 353 | let homeserver = homeserver.trim(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 354 | let rest = homeserver |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 355 | .split_once("://") |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 356 | .map_or(homeserver, |(_, rest)| rest); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 357 | rest.split('/').next().unwrap_or(rest) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 358 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 359 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 360 | #[cfg(feature = "sqlite")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 361 | fn directory_slots(root: &std::path::Path) -> Result<BTreeSet<String>, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 362 | use std::io::ErrorKind; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 363 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 364 | let entries = match std::fs::read_dir(root) { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 365 | Ok(entries) => entries, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 366 | Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BTreeSet::new()), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 367 | Err(error) => return Err(crate::account::account_error("listing the accounts", error)), |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 368 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 369 | let mut ids = BTreeSet::new(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 370 | for entry in entries { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 371 | let entry = |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 372 | entry.map_err(|error| crate::account::account_error("listing the accounts", error))?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 373 | let name = entry.file_name(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 374 | let Some(name) = name.to_str() else { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 375 | continue; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 376 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 377 | if entry.path().is_dir() { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 378 | ids.insert(name.to_owned()); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 379 | } else if let Some(id) = name.strip_suffix(".session") { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 380 | ids.insert(id.to_owned()); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 381 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 382 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 383 | Ok(ids) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 384 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 385 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 386 | /// a key per account, so two tabs adding at once can't lose one to the other |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 387 | #[cfg(feature = "indexeddb")] |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 388 | mod index { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 389 | use std::collections::BTreeSet; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 390 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 391 | use indexed_db_futures::{ |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 392 | Build, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 393 | database::Database, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 394 | error::{Error, OpenDbError}, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 395 | prelude::*, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 396 | transaction::{Transaction, TransactionMode}, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 397 | }; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 398 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 399 | use crate::{AccountError, account::account_error}; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 400 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 401 | const ACCOUNTS: &str = "accounts"; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 402 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 403 | fn name(prefix: &str) -> String { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 404 | format!("{prefix}::macltok-accounts") |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 405 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 406 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 407 | async fn open(prefix: &str) -> Result<Database, OpenDbError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 408 | Database::open(&name(prefix)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 409 | .with_version(1u32) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 410 | .with_on_upgrade_needed(|_, transaction: &Transaction<'_>| -> Result<(), Error> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 411 | transaction.db().create_object_store(ACCOUNTS).build()?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 412 | Ok(()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 413 | }) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 414 | .build()? |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 415 | .await |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 416 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 417 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 418 | pub(super) async fn list(prefix: &str) -> Result<BTreeSet<String>, AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 419 | let failed = |error| account_error("listing the accounts", error); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 420 | let database = open(prefix).await.map_err(failed)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 421 | let ids = keys(&database).await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 422 | database.close(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 423 | ids.map_err(|error| account_error("listing the accounts", error)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 424 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 425 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 426 | pub(super) async fn add(prefix: &str, id: &str) -> Result<(), AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 427 | let failed = |error| account_error("adding the account", error); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 428 | let database = open(prefix).await.map_err(failed)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 429 | let added = write(&database, |store| { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 430 | store.put(true).with_key(id).build()?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 431 | Ok(()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 432 | }) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 433 | .await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 434 | database.close(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 435 | added.map_err(|error| account_error("adding the account", error)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 436 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 437 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 438 | pub(super) async fn remove(prefix: &str, id: &str) -> Result<(), AccountError> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 439 | let failed = |error| account_error("removing the account", error); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 440 | let database = open(prefix).await.map_err(failed)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 441 | let removed = write(&database, |store| { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 442 | store.delete(id).build()?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 443 | Ok(()) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 444 | }) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 445 | .await; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 446 | database.close(); |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 447 | removed.map_err(|error| account_error("removing the account", error)) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 448 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 449 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 450 | async fn keys(database: &Database) -> Result<BTreeSet<String>, Error> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 451 | let transaction = database |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 452 | .transaction(ACCOUNTS) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 453 | .with_mode(TransactionMode::Readonly) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 454 | .build()?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 455 | let store = transaction.object_store(ACCOUNTS)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 456 | store.get_all_keys::<String>().await?.collect() |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 457 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 458 | |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 459 | async fn write( |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 460 | database: &Database, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 461 | change: impl FnOnce(&indexed_db_futures::object_store::ObjectStore<'_>) -> Result<(), Error>, |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 462 | ) -> Result<(), Error> { |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 463 | let transaction = database |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 464 | .transaction(ACCOUNTS) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 465 | .with_mode(TransactionMode::Readwrite) |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 466 | .build()?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 467 | change(&transaction.object_store(ACCOUNTS)?)?; |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 468 | transaction.commit().await |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 469 | } |
cb0668d30c14 | atagen | 2026-10-08 11:34 UTC | 470 | } |