Add all connectors
This commit is contained in:
		
							
								
								
									
										2904
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2904
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -5,4 +5,19 @@ edition = "2024"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
env_logger = "0.11.7"
 | 
					env_logger = "0.11.7"
 | 
				
			||||||
diesel = { version = "2.2.0", features = ["postgres"] }
 | 
					log = "0.4.26"
 | 
				
			||||||
 | 
					diesel = { version = "2.2.0", features = ["postgres", "r2d2"] }
 | 
				
			||||||
 | 
					diesel_migrations = "2.1.0"
 | 
				
			||||||
 | 
					clap = { version = "4.5.32", features = ["env", "derive"] }
 | 
				
			||||||
 | 
					actix-web = "4"
 | 
				
			||||||
 | 
					actix-cors = "0.7.0"
 | 
				
			||||||
 | 
					actix-multipart = "0.7.0"
 | 
				
			||||||
 | 
					actix-remote-ip = "0.1.0"
 | 
				
			||||||
 | 
					actix-session = { version = "0.10.0", features = ["redis-session"] }
 | 
				
			||||||
 | 
					lazy_static = "1.5.0"
 | 
				
			||||||
 | 
					anyhow = "1.0.97"
 | 
				
			||||||
 | 
					serde = { version = "1.0.219", features = ["derive"] }
 | 
				
			||||||
 | 
					rust-s3 = "0.36.0-beta.2"
 | 
				
			||||||
 | 
					thiserror = "1.0.69"
 | 
				
			||||||
 | 
					tokio = "1.44.1"
 | 
				
			||||||
 | 
					futures-util = "0.3.31"
 | 
				
			||||||
@@ -14,7 +14,7 @@ CREATE TABLE token
 | 
				
			|||||||
    time_create      BIGINT       NOT NULL,
 | 
					    time_create      BIGINT       NOT NULL,
 | 
				
			||||||
    time_update      BIGINT       NOT NULL,
 | 
					    time_update      BIGINT       NOT NULL,
 | 
				
			||||||
    user_id          INTEGER      NOT NULL REFERENCES users ON DELETE CASCADE,
 | 
					    user_id          INTEGER      NOT NULL REFERENCES users ON DELETE CASCADE,
 | 
				
			||||||
    token            VARCHAR(150) NOT NULL,
 | 
					    token_value      VARCHAR(150) NOT NULL,
 | 
				
			||||||
    time_used        BIGINT       NOT NULL,
 | 
					    time_used        BIGINT       NOT NULL,
 | 
				
			||||||
    max_inactivity   INTEGER,
 | 
					    max_inactivity   INTEGER,
 | 
				
			||||||
    ip_restriction   VARCHAR(50),
 | 
					    ip_restriction   VARCHAR(50),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										261
									
								
								moneymgr_backend/src/app_config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								moneymgr_backend/src/app_config.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,261 @@
 | 
				
			|||||||
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					use s3::creds::Credentials;
 | 
				
			||||||
 | 
					use s3::{Bucket, Region};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Money Manager backend API
 | 
				
			||||||
 | 
					#[derive(Parser, Debug, Clone)]
 | 
				
			||||||
 | 
					#[clap(author, version, about, long_about = None)]
 | 
				
			||||||
 | 
					pub struct AppConfig {
 | 
				
			||||||
 | 
					    /// Listen address
 | 
				
			||||||
 | 
					    #[clap(short, long, env, default_value = "0.0.0.0:8000")]
 | 
				
			||||||
 | 
					    pub listen_address: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Website origin
 | 
				
			||||||
 | 
					    #[clap(short, long, env, default_value = "http://localhost:5173")]
 | 
				
			||||||
 | 
					    pub website_origin: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Proxy IP, might end with a star "*"
 | 
				
			||||||
 | 
					    #[clap(short, long, env)]
 | 
				
			||||||
 | 
					    pub proxy_ip: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Secret key, used to sign some resources. Must be randomly generated
 | 
				
			||||||
 | 
					    #[clap(short = 'S', long, env, default_value = "")]
 | 
				
			||||||
 | 
					    secret: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Specify whether the cookie should be transmitted only over secure connections
 | 
				
			||||||
 | 
					    #[clap(long, env)]
 | 
				
			||||||
 | 
					    pub cookie_secure: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Unsecure : for development, bypass authentication, using the account with the given
 | 
				
			||||||
 | 
					    /// email address by default
 | 
				
			||||||
 | 
					    #[clap(long, env)]
 | 
				
			||||||
 | 
					    pub unsecure_auto_login_email: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// PostgreSQL database host
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "localhost")]
 | 
				
			||||||
 | 
					    db_host: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// PostgreSQL database port
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value_t = 5432)]
 | 
				
			||||||
 | 
					    db_port: u16,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// PostgreSQL username
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "user")]
 | 
				
			||||||
 | 
					    db_username: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// PostgreSQL password
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "pass")]
 | 
				
			||||||
 | 
					    db_password: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// PostgreSQL database name
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "moneymgr")]
 | 
				
			||||||
 | 
					    db_name: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// URL where the OpenID configuration can be found
 | 
				
			||||||
 | 
					    #[arg(
 | 
				
			||||||
 | 
					        long,
 | 
				
			||||||
 | 
					        env,
 | 
				
			||||||
 | 
					        default_value = "http://127.0.0.1:9001/dex/.well-known/openid-configuration"
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    pub oidc_configuration_url: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// OpenID provider name
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "3rd party provider")]
 | 
				
			||||||
 | 
					    pub oidc_provider_name: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// OpenID client ID
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "foo")]
 | 
				
			||||||
 | 
					    pub oidc_client_id: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// OpenID client secret
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "bar")]
 | 
				
			||||||
 | 
					    pub oidc_client_secret: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// OpenID login redirect URL
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "APP_ORIGIN/web/oidc_cb")]
 | 
				
			||||||
 | 
					    oidc_redirect_url: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 Bucket name
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "moneymgr-data")]
 | 
				
			||||||
 | 
					    s3_bucket_name: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 region (if not using Minio)
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "eu-central-1")]
 | 
				
			||||||
 | 
					    s3_region: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 API endpoint
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "http://localhost:9000")]
 | 
				
			||||||
 | 
					    s3_endpoint: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 access key
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "topsecret")]
 | 
				
			||||||
 | 
					    s3_access_key: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 secret key
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "topsecret")]
 | 
				
			||||||
 | 
					    s3_secret_key: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// S3 skip auto create bucket if not existing
 | 
				
			||||||
 | 
					    #[arg(long, env)]
 | 
				
			||||||
 | 
					    pub s3_skip_auto_create_bucket: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Directory where temporary files are stored
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value = "/tmp")]
 | 
				
			||||||
 | 
					    pub temp_dir: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Maintenance routine execution interval
 | 
				
			||||||
 | 
					    #[arg(long, env, default_value_t = 3600)]
 | 
				
			||||||
 | 
					    pub routine_interval: u64,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Redis connection hostname
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "localhost")]
 | 
				
			||||||
 | 
					    redis_hostname: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Redis connection port
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value_t = 6379)]
 | 
				
			||||||
 | 
					    redis_port: u16,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Redis database number
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value_t = 0)]
 | 
				
			||||||
 | 
					    redis_db_number: i64,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Redis username
 | 
				
			||||||
 | 
					    #[clap(long, env)]
 | 
				
			||||||
 | 
					    redis_username: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Redis password
 | 
				
			||||||
 | 
					    #[clap(long, env, default_value = "secretredis")]
 | 
				
			||||||
 | 
					    redis_password: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static::lazy_static! {
 | 
				
			||||||
 | 
					    static ref ARGS: AppConfig = {
 | 
				
			||||||
 | 
					        AppConfig::parse()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AppConfig {
 | 
				
			||||||
 | 
					    /// Get parsed command line arguments
 | 
				
			||||||
 | 
					    pub fn get() -> &'static AppConfig {
 | 
				
			||||||
 | 
					        &ARGS
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Check if auth is disabled
 | 
				
			||||||
 | 
					    pub fn is_auth_disabled(&self) -> bool {
 | 
				
			||||||
 | 
					        self.unsecure_auto_login_email.is_some()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get auth cookie domain
 | 
				
			||||||
 | 
					    pub fn cookie_domain(&self) -> Option<String> {
 | 
				
			||||||
 | 
					        if cfg!(debug_assertions) {
 | 
				
			||||||
 | 
					            let domain = self.website_origin.split_once("://")?.1;
 | 
				
			||||||
 | 
					            Some(
 | 
				
			||||||
 | 
					                domain
 | 
				
			||||||
 | 
					                    .split_once(':')
 | 
				
			||||||
 | 
					                    .map(|s| s.0)
 | 
				
			||||||
 | 
					                    .unwrap_or(domain)
 | 
				
			||||||
 | 
					                    .to_string(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // In release mode, the web app is hosted on the same origin as the API
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get app secret
 | 
				
			||||||
 | 
					    pub fn secret(&self) -> &str {
 | 
				
			||||||
 | 
					        let mut secret = self.secret.as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if cfg!(debug_assertions) && secret.is_empty() {
 | 
				
			||||||
 | 
					            secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if secret.is_empty() {
 | 
				
			||||||
 | 
					            panic!("SECRET is undefined or too short (min 64 chars)!")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        secret
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get full db connection chain
 | 
				
			||||||
 | 
					    pub fn db_connection_chain(&self) -> String {
 | 
				
			||||||
 | 
					        format!(
 | 
				
			||||||
 | 
					            "postgres://{}:{}@{}:{}/{}",
 | 
				
			||||||
 | 
					            self.db_username, self.db_password, self.db_host, self.db_port, self.db_name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get OpenID provider configuration
 | 
				
			||||||
 | 
					    pub fn openid_provider(&self) -> OIDCProvider<'_> {
 | 
				
			||||||
 | 
					        OIDCProvider {
 | 
				
			||||||
 | 
					            client_id: self.oidc_client_id.as_str(),
 | 
				
			||||||
 | 
					            client_secret: self.oidc_client_secret.as_str(),
 | 
				
			||||||
 | 
					            configuration_url: self.oidc_configuration_url.as_str(),
 | 
				
			||||||
 | 
					            name: self.oidc_provider_name.as_str(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get OIDC callback URL
 | 
				
			||||||
 | 
					    pub fn oidc_redirect_url(&self) -> String {
 | 
				
			||||||
 | 
					        self.oidc_redirect_url
 | 
				
			||||||
 | 
					            .replace("APP_ORIGIN", &self.website_origin)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get s3 credentials
 | 
				
			||||||
 | 
					    pub fn s3_credentials(&self) -> anyhow::Result<Credentials> {
 | 
				
			||||||
 | 
					        Ok(Credentials::new(
 | 
				
			||||||
 | 
					            Some(&self.s3_access_key),
 | 
				
			||||||
 | 
					            Some(&self.s3_secret_key),
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					        )?)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get S3 bucket
 | 
				
			||||||
 | 
					    pub fn s3_bucket(&self) -> anyhow::Result<Box<Bucket>> {
 | 
				
			||||||
 | 
					        Ok(Bucket::new(
 | 
				
			||||||
 | 
					            &self.s3_bucket_name,
 | 
				
			||||||
 | 
					            Region::Custom {
 | 
				
			||||||
 | 
					                region: self.s3_region.to_string(),
 | 
				
			||||||
 | 
					                endpoint: self.s3_endpoint.to_string(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            self.s3_credentials()?,
 | 
				
			||||||
 | 
					        )?
 | 
				
			||||||
 | 
					        .with_path_style())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get Redis connection configuration
 | 
				
			||||||
 | 
					    pub fn redis_connection_string(&self) -> String {
 | 
				
			||||||
 | 
					        format!(
 | 
				
			||||||
 | 
					            "redis://{}:{}@{}:{}/{}",
 | 
				
			||||||
 | 
					            self.redis_username.as_deref().unwrap_or(""),
 | 
				
			||||||
 | 
					            self.redis_password,
 | 
				
			||||||
 | 
					            self.redis_hostname,
 | 
				
			||||||
 | 
					            self.redis_port,
 | 
				
			||||||
 | 
					            self.redis_db_number
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, serde::Serialize)]
 | 
				
			||||||
 | 
					pub struct OIDCProvider<'a> {
 | 
				
			||||||
 | 
					    #[serde(skip_serializing)]
 | 
				
			||||||
 | 
					    pub client_id: &'a str,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing)]
 | 
				
			||||||
 | 
					    pub client_secret: &'a str,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing)]
 | 
				
			||||||
 | 
					    pub configuration_url: &'a str,
 | 
				
			||||||
 | 
					    pub name: &'a str,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn verify_cli() {
 | 
				
			||||||
 | 
					        use clap::CommandFactory;
 | 
				
			||||||
 | 
					        AppConfig::command().debug_assert()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								moneymgr_backend/src/connections/db_connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								moneymgr_backend/src/connections/db_connection.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use diesel::PgConnection;
 | 
				
			||||||
 | 
					use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
 | 
				
			||||||
 | 
					use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub type DBConn = Arc<Pool<ConnectionManager<PgConnection>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_db_connection_pool() -> anyhow::Result<DBConn> {
 | 
				
			||||||
 | 
					    log::info!("Connect to PostgresSQL database...");
 | 
				
			||||||
 | 
					    let url = AppConfig::get().db_connection_chain();
 | 
				
			||||||
 | 
					    let manager = ConnectionManager::<PgConnection>::new(url);
 | 
				
			||||||
 | 
					    // Refer to the `r2d2` documentation for more methods to use
 | 
				
			||||||
 | 
					    // when building a connection pool
 | 
				
			||||||
 | 
					    Ok(Arc::new(
 | 
				
			||||||
 | 
					        Pool::builder().test_on_check_out(true).build(manager)?,
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    static ref DB_POOL: DBConn = get_db_connection_pool().expect("Failed to connect to database");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn db() -> anyhow::Result<PooledConnection<ConnectionManager<PgConnection>>> {
 | 
				
			||||||
 | 
					    Ok(DB_POOL.clone().get()?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn initialize_conn() -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    // Run pending diesel migrations
 | 
				
			||||||
 | 
					    let mut db = db()?;
 | 
				
			||||||
 | 
					    db.run_pending_migrations(MIGRATIONS)
 | 
				
			||||||
 | 
					        .expect("Failed to run DB migrations");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								moneymgr_backend/src/connections/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								moneymgr_backend/src/connections/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					pub mod db_connection;
 | 
				
			||||||
 | 
					pub mod s3_connection;
 | 
				
			||||||
							
								
								
									
										72
									
								
								moneymgr_backend/src/connections/s3_connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								moneymgr_backend/src/connections/s3_connection.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use s3::error::S3Error;
 | 
				
			||||||
 | 
					use s3::{Bucket, BucketConfiguration};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(thiserror::Error, Debug)]
 | 
				
			||||||
 | 
					enum BucketServiceError {
 | 
				
			||||||
 | 
					    #[error("Failed to fetch bucket information!")]
 | 
				
			||||||
 | 
					    FailedFetchBucketInfo,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 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(BucketServiceError::FailedFetchBucketInfo.into());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Bucket::create_with_path_style(
 | 
				
			||||||
 | 
					        &bucket.name,
 | 
				
			||||||
 | 
					        bucket.region,
 | 
				
			||||||
 | 
					        AppConfig::get().s3_credentials()?,
 | 
				
			||||||
 | 
					        BucketConfiguration::private(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Upload a new file to the bucket
 | 
				
			||||||
 | 
					pub async fn upload_file(path: &str, content: &[u8]) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    let bucket = AppConfig::get().s3_bucket()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bucket.put_object(path, content).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get a  file
 | 
				
			||||||
 | 
					pub async fn get_file(path: &str) -> anyhow::Result<Vec<u8>> {
 | 
				
			||||||
 | 
					    let bucket = AppConfig::get().s3_bucket()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(bucket.get_object(path).await?.to_vec())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Delete a file, if it exists
 | 
				
			||||||
 | 
					pub async fn delete_file_if_exists(path: &str) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    let bucket = AppConfig::get().s3_bucket()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bucket.delete_object(path).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								moneymgr_backend/src/constants.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/src/constants.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					// TODO
 | 
				
			||||||
							
								
								
									
										1
									
								
								moneymgr_backend/src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod server_controller;
 | 
				
			||||||
							
								
								
									
										7
									
								
								moneymgr_backend/src/controllers/server_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								moneymgr_backend/src/controllers/server_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn robots_txt() -> HttpResponse {
 | 
				
			||||||
 | 
					    HttpResponse::Ok()
 | 
				
			||||||
 | 
					        .content_type("text/plain")
 | 
				
			||||||
 | 
					        .body("User-agent: *\nDisallow: /\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								moneymgr_backend/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								moneymgr_backend/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					pub mod app_config;
 | 
				
			||||||
 | 
					pub mod connections;
 | 
				
			||||||
 | 
					pub mod constants;
 | 
				
			||||||
 | 
					pub mod controllers;
 | 
				
			||||||
 | 
					pub mod models;
 | 
				
			||||||
 | 
					pub mod routines;
 | 
				
			||||||
 | 
					pub mod schema;
 | 
				
			||||||
 | 
					pub mod services;
 | 
				
			||||||
 | 
					pub mod utils;
 | 
				
			||||||
@@ -1,5 +1,79 @@
 | 
				
			|||||||
fn main() {
 | 
					use actix_cors::Cors;
 | 
				
			||||||
 | 
					use actix_multipart::form::tempfile::TempFileConfig;
 | 
				
			||||||
 | 
					use actix_remote_ip::RemoteIPConfig;
 | 
				
			||||||
 | 
					use actix_session::SessionMiddleware;
 | 
				
			||||||
 | 
					use actix_session::config::SessionLifecycle;
 | 
				
			||||||
 | 
					use actix_session::storage::RedisSessionStore;
 | 
				
			||||||
 | 
					use actix_web::cookie::Key;
 | 
				
			||||||
 | 
					use actix_web::middleware::Logger;
 | 
				
			||||||
 | 
					use actix_web::{App, HttpServer, web};
 | 
				
			||||||
 | 
					use moneymgr_backend::app_config::AppConfig;
 | 
				
			||||||
 | 
					use moneymgr_backend::connections::{db_connection, s3_connection};
 | 
				
			||||||
 | 
					use moneymgr_backend::controllers::server_controller;
 | 
				
			||||||
 | 
					use moneymgr_backend::routines;
 | 
				
			||||||
 | 
					use moneymgr_backend::services::users_service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[actix_web::main]
 | 
				
			||||||
 | 
					async fn main() -> std::io::Result<()> {
 | 
				
			||||||
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 | 
					    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    println!("Hello, world!");
 | 
					    log::info!(
 | 
				
			||||||
 | 
					        "Money manager server, start to listen on {}",
 | 
				
			||||||
 | 
					        AppConfig::get().listen_address
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize bucket
 | 
				
			||||||
 | 
					    log::info!("Initialize S3 bucket");
 | 
				
			||||||
 | 
					    s3_connection::create_bucket_if_required()
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect("Failed to initialize S3 bucket!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Connect to Redis
 | 
				
			||||||
 | 
					    let cookie_secret_key = Key::from(AppConfig::get().secret().as_bytes());
 | 
				
			||||||
 | 
					    let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect("Failed to connect to Redis!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize database connection
 | 
				
			||||||
 | 
					    db_connection::initialize_conn().expect("Failed to connect to PostgresSQL database!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Auto create default account, if requested
 | 
				
			||||||
 | 
					    if let Some(mail) = &AppConfig::get().unsecure_auto_login_email {
 | 
				
			||||||
 | 
					        users_service::create_or_update_user(mail, "Anonymous")
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect("Failed to create default account!");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Automatically execute routines
 | 
				
			||||||
 | 
					    tokio::spawn(routines::main_routine());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    HttpServer::new(move || {
 | 
				
			||||||
 | 
					        let session_mw = SessionMiddleware::builder(redis_store.clone(), cookie_secret_key.clone())
 | 
				
			||||||
 | 
					            .cookie_name("matrixgw-session".to_string())
 | 
				
			||||||
 | 
					            .session_lifecycle(SessionLifecycle::BrowserSession(Default::default()))
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cors = Cors::default()
 | 
				
			||||||
 | 
					            .allowed_origin(&AppConfig::get().website_origin)
 | 
				
			||||||
 | 
					            .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
 | 
				
			||||||
 | 
					            .allowed_header("X-Auth-Token")
 | 
				
			||||||
 | 
					            .allow_any_header()
 | 
				
			||||||
 | 
					            .supports_credentials()
 | 
				
			||||||
 | 
					            .max_age(3600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        App::new()
 | 
				
			||||||
 | 
					            .wrap(Logger::default())
 | 
				
			||||||
 | 
					            .wrap(session_mw)
 | 
				
			||||||
 | 
					            .wrap(cors)
 | 
				
			||||||
 | 
					            .app_data(web::Data::new(RemoteIPConfig {
 | 
				
			||||||
 | 
					                proxy: AppConfig::get().proxy_ip.clone(),
 | 
				
			||||||
 | 
					            }))
 | 
				
			||||||
 | 
					            // Uploaded files
 | 
				
			||||||
 | 
					            .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
 | 
				
			||||||
 | 
					            // Server controller
 | 
				
			||||||
 | 
					            .route("/robots.txt", web::get().to(server_controller::robots_txt))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .bind(AppConfig::get().listen_address.as_str())?
 | 
				
			||||||
 | 
					    .run()
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								moneymgr_backend/src/models/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/src/models/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod users;
 | 
				
			||||||
							
								
								
									
										29
									
								
								moneymgr_backend/src/models/users.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								moneymgr_backend/src/models/users.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					use crate::schema::*;
 | 
				
			||||||
 | 
					use diesel::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub struct UserID(pub i32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Queryable, Debug, Clone, serde::Serialize)]
 | 
				
			||||||
 | 
					pub struct User {
 | 
				
			||||||
 | 
					    id: i32,
 | 
				
			||||||
 | 
					    pub mail: String,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub time_create: i64,
 | 
				
			||||||
 | 
					    pub time_update: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl User {
 | 
				
			||||||
 | 
					    pub fn id(&self) -> UserID {
 | 
				
			||||||
 | 
					        UserID(self.id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Insertable)]
 | 
				
			||||||
 | 
					#[diesel(table_name = users)]
 | 
				
			||||||
 | 
					pub struct NewUser<'a> {
 | 
				
			||||||
 | 
					    pub time_create: i64,
 | 
				
			||||||
 | 
					    pub time_update: i64,
 | 
				
			||||||
 | 
					    pub name: &'a str,
 | 
				
			||||||
 | 
					    pub mail: &'a str,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								moneymgr_backend/src/routines.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								moneymgr_backend/src/routines.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The "cron" of the project
 | 
				
			||||||
 | 
					pub async fn main_routine() {
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        tokio::time::sleep(Duration::from_secs(AppConfig::get().routine_interval)).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        log::info!("Start to execute regular routine");
 | 
				
			||||||
 | 
					        match exec_routine().await {
 | 
				
			||||||
 | 
					            Ok(_) => {
 | 
				
			||||||
 | 
					                log::info!("Routine successfully executed")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                log::error!("Failed to execute routine! {e}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn exec_routine() -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    // TODO
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -60,7 +60,7 @@ diesel::table! {
 | 
				
			|||||||
        time_update -> Int8,
 | 
					        time_update -> Int8,
 | 
				
			||||||
        user_id -> Int4,
 | 
					        user_id -> Int4,
 | 
				
			||||||
        #[max_length = 150]
 | 
					        #[max_length = 150]
 | 
				
			||||||
        token -> Varchar,
 | 
					        token_value -> Varchar,
 | 
				
			||||||
        time_used -> Int8,
 | 
					        time_used -> Int8,
 | 
				
			||||||
        max_inactivity -> Nullable<Int4>,
 | 
					        max_inactivity -> Nullable<Int4>,
 | 
				
			||||||
        #[max_length = 50]
 | 
					        #[max_length = 50]
 | 
				
			||||||
@@ -95,11 +95,4 @@ diesel::joinable!(movement -> account (account_id));
 | 
				
			|||||||
diesel::joinable!(movement -> attachment (attachment_id));
 | 
					diesel::joinable!(movement -> attachment (attachment_id));
 | 
				
			||||||
diesel::joinable!(token -> users (user_id));
 | 
					diesel::joinable!(token -> users (user_id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
diesel::allow_tables_to_appear_in_same_query!(
 | 
					diesel::allow_tables_to_appear_in_same_query!(account, attachment, inbox, movement, token, users,);
 | 
				
			||||||
    account,
 | 
					 | 
				
			||||||
    attachment,
 | 
					 | 
				
			||||||
    inbox,
 | 
					 | 
				
			||||||
    movement,
 | 
					 | 
				
			||||||
    token,
 | 
					 | 
				
			||||||
    users,
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								moneymgr_backend/src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod users_service;
 | 
				
			||||||
							
								
								
									
										59
									
								
								moneymgr_backend/src/services/users_service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								moneymgr_backend/src/services/users_service.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					use crate::connections::db_connection::db;
 | 
				
			||||||
 | 
					use crate::models::users::{NewUser, User, UserID};
 | 
				
			||||||
 | 
					use crate::schema::users;
 | 
				
			||||||
 | 
					use crate::utils::time_utils::time;
 | 
				
			||||||
 | 
					use diesel::prelude::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create or update a user's information
 | 
				
			||||||
 | 
					pub async fn create_or_update_user(mail: &str, name: &str) -> anyhow::Result<User> {
 | 
				
			||||||
 | 
					    let mut user = match get_user_by_email(mail) {
 | 
				
			||||||
 | 
					        Ok(u) => u,
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            log::info!("Failed to fetch user info, attempting to create a new user... {e}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            diesel::insert_into(users::table)
 | 
				
			||||||
 | 
					                .values(&NewUser {
 | 
				
			||||||
 | 
					                    time_create: time() as i64,
 | 
				
			||||||
 | 
					                    time_update: time() as i64,
 | 
				
			||||||
 | 
					                    name,
 | 
				
			||||||
 | 
					                    mail,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .get_result(&mut db()?)?
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if user.name != name {
 | 
				
			||||||
 | 
					        log::info!("Name needs to be updated for user {:?}", user.id());
 | 
				
			||||||
 | 
					        user.name = name.to_string();
 | 
				
			||||||
 | 
					        update(&user)?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(user)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get user information by its ID
 | 
				
			||||||
 | 
					pub fn get_user_by_id(id: UserID) -> anyhow::Result<User> {
 | 
				
			||||||
 | 
					    Ok(users::table
 | 
				
			||||||
 | 
					        .filter(users::dsl::id.eq(id.0))
 | 
				
			||||||
 | 
					        .first(&mut db()?)?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get user information by its email
 | 
				
			||||||
 | 
					pub fn get_user_by_email(mail: &str) -> anyhow::Result<User> {
 | 
				
			||||||
 | 
					    Ok(users::table
 | 
				
			||||||
 | 
					        .filter(users::dsl::mail.eq(mail))
 | 
				
			||||||
 | 
					        .first(&mut db()?)?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Update user information
 | 
				
			||||||
 | 
					pub fn update(user: &User) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					    diesel::update(users::dsl::users.filter(users::dsl::id.eq(user.id().0)))
 | 
				
			||||||
 | 
					        .set((
 | 
				
			||||||
 | 
					            users::dsl::time_update.eq(time() as i64),
 | 
				
			||||||
 | 
					            users::dsl::name.eq(user.name.to_string()),
 | 
				
			||||||
 | 
					            users::dsl::mail.eq(user.mail.to_string()),
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					        .execute(&mut db()?)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								moneymgr_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								moneymgr_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub mod time_utils;
 | 
				
			||||||
							
								
								
									
										11
									
								
								moneymgr_backend/src/utils/time_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								moneymgr_backend/src/utils/time_utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					//! # Time utilities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get the current time since epoch
 | 
				
			||||||
 | 
					pub fn time() -> u64 {
 | 
				
			||||||
 | 
					    SystemTime::now()
 | 
				
			||||||
 | 
					        .duration_since(UNIX_EPOCH)
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .as_secs()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user