242 lines
6.7 KiB
Rust
242 lines
6.7 KiB
Rust
use s3::error::S3Error;
|
|
use s3::request::ResponseData;
|
|
use s3::{Bucket, BucketConfiguration};
|
|
use std::str::FromStr;
|
|
use thiserror::Error;
|
|
|
|
use crate::app_config::AppConfig;
|
|
use crate::constants::TOKEN_LEN;
|
|
use crate::utils::base_utils::{curr_time, format_time, rand_str};
|
|
|
|
type HttpClient = ruma::client::http_client::HyperNativeTls;
|
|
pub type RumaClient = ruma::Client<HttpClient>;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum UserError {
|
|
#[error("failed to fetch user configuration: {0}")]
|
|
FetchUserConfig(S3Error),
|
|
#[error("missing matrix token")]
|
|
MissingMatrixToken,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct UserID(pub String);
|
|
|
|
impl UserID {
|
|
fn conf_path_in_bucket(&self) -> String {
|
|
format!("confs/{}.json", urlencoding::encode(&self.0))
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
|
pub struct User {
|
|
pub id: UserID,
|
|
pub name: String,
|
|
pub email: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
|
|
pub struct APIClientID(pub uuid::Uuid);
|
|
|
|
impl APIClientID {
|
|
pub fn generate() -> Self {
|
|
Self(uuid::Uuid::new_v4())
|
|
}
|
|
}
|
|
|
|
impl FromStr for APIClientID {
|
|
type Err = uuid::Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self(uuid::Uuid::from_str(s)?))
|
|
}
|
|
}
|
|
|
|
/// Single API client information
|
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
|
pub struct APIClient {
|
|
/// Client unique ID
|
|
pub id: APIClientID,
|
|
|
|
/// Client description
|
|
pub description: String,
|
|
|
|
/// Restricted API network for token
|
|
pub network: Option<ipnet::IpNet>,
|
|
|
|
/// Client secret
|
|
pub secret: String,
|
|
|
|
/// Client creation time
|
|
pub created: u64,
|
|
|
|
/// Client last usage time
|
|
pub used: u64,
|
|
|
|
/// Read only access
|
|
pub readonly_client: bool,
|
|
}
|
|
|
|
impl APIClient {
|
|
pub fn fmt_created(&self) -> String {
|
|
format_time(self.created).unwrap_or_default()
|
|
}
|
|
|
|
pub fn fmt_used(&self) -> String {
|
|
format_time(self.used).unwrap_or_default()
|
|
}
|
|
|
|
pub fn need_update_last_used(&self) -> bool {
|
|
self.used + 60 * 15 < curr_time().unwrap()
|
|
}
|
|
}
|
|
|
|
impl APIClient {
|
|
/// Generate a new API client
|
|
pub fn generate(description: String, network: Option<ipnet::IpNet>) -> Self {
|
|
Self {
|
|
id: APIClientID::generate(),
|
|
description,
|
|
network,
|
|
secret: rand_str(TOKEN_LEN),
|
|
created: curr_time().unwrap(),
|
|
used: curr_time().unwrap(),
|
|
readonly_client: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
|
pub struct UserConfig {
|
|
/// Target user ID
|
|
pub user_id: UserID,
|
|
|
|
/// Configuration creation time
|
|
pub created: u64,
|
|
|
|
/// Configuration last update time
|
|
pub updated: u64,
|
|
|
|
/// Current user matrix token
|
|
pub matrix_token: String,
|
|
|
|
/// API clients
|
|
pub clients: Vec<APIClient>,
|
|
}
|
|
|
|
impl UserConfig {
|
|
/// Create S3 bucket if required
|
|
pub async fn create_bucket_if_required() -> anyhow::Result<()> {
|
|
if AppConfig::get().s3_skip_auto_create_bucket {
|
|
log::debug!("Skipping bucket existence check");
|
|
return Ok(());
|
|
}
|
|
|
|
let bucket = AppConfig::get().s3_bucket()?;
|
|
|
|
match bucket.location().await {
|
|
Ok(_) => {
|
|
log::debug!("The bucket already exists.");
|
|
return Ok(());
|
|
}
|
|
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchKey</Code>") => {
|
|
log::warn!("Failed to fetch bucket location, but it seems that bucket exists.");
|
|
return Ok(());
|
|
}
|
|
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => {
|
|
log::warn!("The bucket does not seem to exists, trying to create it!")
|
|
}
|
|
Err(e) => {
|
|
log::error!("Got unexpected error when querying bucket info: {}", e);
|
|
return Err(e.into());
|
|
}
|
|
}
|
|
|
|
Bucket::create_with_path_style(
|
|
&bucket.name,
|
|
bucket.region,
|
|
AppConfig::get().s3_credentials()?,
|
|
BucketConfiguration::private(),
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current user configuration
|
|
pub async fn load(user_id: &UserID, allow_non_existing: bool) -> anyhow::Result<Self> {
|
|
let res: Result<ResponseData, S3Error> = AppConfig::get()
|
|
.s3_bucket()?
|
|
.get_object(user_id.conf_path_in_bucket())
|
|
.await;
|
|
|
|
match (res, allow_non_existing) {
|
|
(Ok(res), _) => Ok(serde_json::from_slice(res.as_slice())?),
|
|
(Err(S3Error::HttpFailWithBody(404, _)), true) => {
|
|
log::warn!("User configuration does not exists, generating a new one...");
|
|
Ok(Self {
|
|
user_id: user_id.clone(),
|
|
created: curr_time()?,
|
|
updated: curr_time()?,
|
|
matrix_token: "".to_string(),
|
|
clients: vec![],
|
|
})
|
|
}
|
|
(Err(e), _) => Err(UserError::FetchUserConfig(e).into()),
|
|
}
|
|
}
|
|
|
|
/// Set user configuration
|
|
pub async fn save(&mut self) -> anyhow::Result<()> {
|
|
log::info!("Saving new configuration for user {:?}", self.user_id);
|
|
|
|
self.updated = curr_time()?;
|
|
|
|
// Save updated configuration
|
|
AppConfig::get()
|
|
.s3_bucket()?
|
|
.put_object(
|
|
self.user_id.conf_path_in_bucket(),
|
|
&serde_json::to_vec(self)?,
|
|
)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current user matrix token, in an obfuscated form
|
|
pub fn obfuscated_matrix_token(&self) -> String {
|
|
self.matrix_token
|
|
.chars()
|
|
.enumerate()
|
|
.map(|(num, c)| match num {
|
|
0 | 1 => c,
|
|
_ => 'X',
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Find a client by its id
|
|
pub fn find_client_by_id(&self, id: &APIClientID) -> Option<&APIClient> {
|
|
self.clients.iter().find(|c| &c.id == id)
|
|
}
|
|
|
|
/// Find a client by its id and get a mutable reference
|
|
pub fn find_client_by_id_mut(&mut self, id: &APIClientID) -> Option<&mut APIClient> {
|
|
self.clients.iter_mut().find(|c| &c.id == id)
|
|
}
|
|
|
|
/// Get a matrix client instance for the current user
|
|
pub async fn matrix_client(&self) -> anyhow::Result<RumaClient> {
|
|
if self.matrix_token.is_empty() {
|
|
return Err(UserError::MissingMatrixToken.into());
|
|
}
|
|
|
|
Ok(ruma::Client::builder()
|
|
.homeserver_url(AppConfig::get().matrix_homeserver.to_string())
|
|
.access_token(Some(self.matrix_token.clone()))
|
|
.build()
|
|
.await?)
|
|
}
|
|
}
|