use crate::store::AccountStore;
use origin_auth::{TokenSet, TokenStore};
use origin_domain::{Account, AccountId, AccountStatus, AppError, Clock, ConnectorId, Result};
use origin_events::{AccountExpired, EventBus, PlatformEvent};
use origin_storage::{Storage, namespace};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct AccountService {
accounts: AccountStore,
tokens: TokenStore,
events: EventBus,
storage: Arc<dyn Storage>,
clock: Arc<dyn Clock>,
}
impl AccountService {
pub fn new(
accounts: AccountStore,
tokens: TokenStore,
events: EventBus,
storage: Arc<dyn Storage>,
clock: Arc<dyn Clock>,
) -> Self {
Self {
accounts,
tokens,
events,
storage,
clock,
}
}
pub async fn connect(
&self,
connector: &ConnectorId,
display_name: impl Into<String>,
tokens: &TokenSet,
) -> Result<Account> {
let account = Account {
id: AccountId::generate(),
connector: connector.clone(),
display_name: display_name.into(),
status: AccountStatus::Active,
connected_at: self.clock.now(),
};
self.tokens.save(connector, &account.id, tokens).await?;
if let Err(save_error) = self.accounts.save(&account).await {
if let Err(cleanup_error) = self.tokens.delete(connector, &account.id).await {
return Err(AppError::storage(format!(
"{save_error}; rolling back credentials also failed: {cleanup_error}"
)));
}
return Err(save_error);
}
tracing::info!(
%connector,
account = %account.id,
"account connected"
);
Ok(account)
}
pub async fn connect_with_token(
&self,
connector: &ConnectorId,
display_name: impl Into<String>,
token: impl Into<String>,
scopes: Vec<String>,
) -> Result<Account> {
let tokens = TokenSet::personal_access_token(token, scopes);
self.connect(connector, display_name, &tokens).await
}
pub async fn list(&self) -> Result<Vec<Account>> {
self.accounts.list().await
}
pub async fn list_for(&self, connector: &ConnectorId) -> Result<Vec<Account>> {
self.accounts.list_for(connector).await
}
pub async fn get(&self, account: &AccountId) -> Result<Account> {
self.accounts
.get(account)
.await?
.ok_or_else(|| AppError::validation(format!("unknown account {account}")))
}
pub async fn disconnect(&self, account: &AccountId) -> Result<()> {
let record = self.get(account).await?;
self.tokens.delete(&record.connector, account).await?;
let prefix = namespace::account_prefix(&record.connector, account);
let removed = self.storage.clear_prefix(&prefix).await?;
self.accounts.remove(account).await?;
tracing::info!(
connector = %record.connector,
%account,
records_removed = removed,
"account disconnected"
);
Ok(())
}
pub async fn mark_expired(&self, account: &AccountId) -> Result<()> {
let mut record = self.get(account).await?;
if record.status == AccountStatus::Expired {
return Ok(());
}
record.status = AccountStatus::Expired;
self.accounts.save(&record).await?;
let _ = self
.events
.publish(PlatformEvent::AccountExpired(AccountExpired {
account: account.clone(),
connector: record.connector.clone(),
}));
tracing::warn!(connector = %record.connector, %account, "account marked as expired");
Ok(())
}
pub async fn mark_active(&self, account: &AccountId) -> Result<()> {
let mut record = self.get(account).await?;
if record.status == AccountStatus::Active {
return Ok(());
}
record.status = AccountStatus::Active;
self.accounts.save(&record).await
}
}