use anyhow::{Context, Result};
use futures::future::BoxFuture;
use crate::server::{
AccessToken, AuditPage, AuditQuery, DevicePoll, DeviceRequest, Invite, Member, Role,
SecretInfo, Via, Who,
};
pub trait AccountStore: Send + Sync {
fn tidy(&self) {}
fn member(&self, email: &str) -> Result<Option<Who>>;
fn members(&self) -> Result<Vec<Member>>;
fn set_role(&self, by: &str, email: &str, role: Role) -> Result<()>;
fn remove(&self, by: &str, email: &str) -> Result<()>;
fn invite(&self, by: &str, email: &str, role: Role, code: bool) -> Result<String>;
fn invites(&self) -> Result<Vec<Invite>>;
fn revoke_invite(&self, by: &str, email: &str) -> Result<()>;
fn sign_in_link(&self, email: &str) -> Result<Option<String>>;
fn redeem(&self, token: &str, client: &str) -> Result<(String, Who)>;
fn sign_in_code(
&self,
email: &str,
code: &str,
client: &str,
) -> Result<(String, Who, Option<String>)>;
fn sign_in_verified(&self, email: &str, how: &str, join: bool) -> Result<(String, Who)>;
fn admit(&self, email: &str, join: bool) -> Result<Who>;
fn reset_code(&self, by: &str, email: &str, keep: Option<&str>) -> Result<String>;
fn sign_out(&self, session: &str, everywhere: bool) -> Result<()>;
fn create_token(&self, who: &Who, name: &str, expires_in_days: Option<u32>) -> Result<String>;
fn tokens(&self, who: &Who) -> Result<Vec<AccessToken>>;
fn revoke_token(&self, who: &Who, id: &str) -> Result<()>;
fn revoke_own_token(&self, token: &str) -> Result<bool>;
}
pub trait AuditLog: Send + Sync {
fn audit_entries(&self, query: &AuditQuery) -> Result<AuditPage>;
fn record(&self, actor: &str, action: &str, detail: &str);
}
pub trait DeviceSignIn: Send + Sync {
fn device_start(&self, client: &str, from: &str) -> Result<(String, String)>;
fn device_info(&self, user_code: &str) -> Option<DeviceRequest>;
fn device_approve(&self, who: &Who, user_code: &str, approve: bool) -> Result<()>;
fn device_poll(&self, device_code: &str) -> Result<DevicePoll>;
}
pub trait Authenticator: Send + Sync {
fn authenticate(
&self,
session: Option<&str>,
bearer: Option<&str>,
) -> Result<Option<(Who, Via)>>;
}
pub trait SecretStore: Send + Sync {
fn set_secret(&self, by: &str, name: &str, value: &str) -> Result<()>;
fn secret(&self, name: &str) -> Option<String>;
fn secret_info(&self, name: &str) -> Option<SecretInfo>;
fn secret_names(&self) -> Vec<String>;
}
pub trait SettingsStore: Send + Sync {
fn repos(&self) -> Vec<crate::config::RepoEntry>;
fn set_repos(&self, by: &str, repos: &[crate::config::RepoEntry]) -> Result<()>;
fn legacy_config(&self) -> Option<String>;
fn drop_legacy_config(&self) -> Result<()>;
fn log_skip(&self) -> Option<String>;
fn set_log_skip(&self, by: &str, skip: &str) -> Result<()>;
}
pub trait Mailer: Send + Sync {
fn send<'a>(
&'a self,
to: &'a str,
subject: &'a str,
body: &'a str,
) -> BoxFuture<'a, Result<()>>;
}
pub struct Smtp {
pub url: Option<String>,
pub from: String,
}
impl Mailer for Smtp {
fn send<'a>(
&'a self,
to: &'a str,
subject: &'a str,
body: &'a str,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
use lettre::message::header::ContentType;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
let url = self.url.as_deref().context("no mail server configured")?;
let msg = Message::builder()
.from(
self.from
.parse()
.context("COMINDLY_SMTP_FROM is not an address")?,
)
.to(to.parse().context("not an email address")?)
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body.to_string())?;
AsyncSmtpTransport::<Tokio1Executor>::from_url(url)
.context("COMINDLY_SMTP_URL is not a valid smtp:// or smtps:// URL")?
.build()
.send(msg)
.await
.map_err(|e| anyhow::anyhow!("sending mail failed: {e}"))?;
Ok(())
})
}
}