add: icon resolver

add: config "no_track" file list option
add: rainbeam-shared -> tetratto-shared
add: l10n
This commit is contained in:
trisua 2025-03-23 12:31:48 -04:00
parent b6fe2fba37
commit d2ca9e23d3
40 changed files with 1107 additions and 583 deletions

View file

@ -0,0 +1,196 @@
use super::*;
use crate::model::{Error, Result};
use crate::{execute, get, query_row};
use tetratto_shared::hash::hash_salted;
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`User`] from an SQL row.
pub(crate) fn get_user_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> User {
User {
id: get!(x->0(u64)) as usize,
created: get!(x->1(u64)) as usize as usize,
username: get!(x->2(String)),
password: get!(x->3(String)),
salt: get!(x->4(String)),
settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(),
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
}
}
/// Get a user given just their `id`.
///
/// # Arguments
/// * `id` - the ID of the user
pub async fn get_user_by_id(&self, id: &str) -> Result<User> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(&conn, "SELECT * FROM users WHERE id = $1", &[&id], |x| {
Ok(Self::get_user_from_row(x))
});
if res.is_err() {
return Err(Error::UserNotFound);
}
Ok(res.unwrap())
}
/// Get a user given just their `username`.
///
/// # Arguments
/// * `username` - the username of the user
pub async fn get_user_by_username(&self, username: &str) -> Result<User> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM users WHERE username = $1",
&[&username],
|x| Ok(Self::get_user_from_row(x))
);
if res.is_err() {
return Err(Error::UserNotFound);
}
Ok(res.unwrap())
}
/// Get a user given just their auth token.
///
/// # Arguments
/// * `token` - the token of the user
pub async fn get_user_by_token(&self, token: &str) -> Result<User> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM users WHERE tokens LIKE $1",
&[&format!("%\"{token}\"%")],
|x| Ok(Self::get_user_from_row(x))
);
if res.is_err() {
return Err(Error::UserNotFound);
}
Ok(res.unwrap())
}
/// Create a new user in the database.
///
/// # Arguments
/// * `data` - a mock [`User`] object to insert
pub async fn create_user(&self, data: User) -> Result<()> {
if self.0.security.registration_enabled == false {
return Err(Error::RegistrationDisabled);
}
// check values
if data.username.len() < 2 {
return Err(Error::DataTooShort("username".to_string()));
} else if data.username.len() > 32 {
return Err(Error::DataTooLong("username".to_string()));
}
if data.password.len() < 6 {
return Err(Error::DataTooShort("password".to_string()));
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7)",
&[
&data.id.to_string(),
&data.created.to_string(),
&data.username,
&data.password,
&data.salt,
&serde_json::to_string(&data.settings).unwrap(),
&serde_json::to_string(&data.tokens).unwrap(),
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(())
}
/// Create a new user in the database.
///
/// # Arguments
/// * `id` - the ID of the user
/// * `password` - the current password of the user
/// * `force` - if we should delete even if the given password is incorrect
pub async fn delete_user(&self, id: &str, password: &str, force: bool) -> Result<()> {
let user = self.get_user_by_id(id).await?;
if (hash_salted(password.to_string(), user.salt) != user.password) && !force {
return Err(Error::IncorrectPassword);
}
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM users WHERE id = $1", &[&id]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(())
}
/// Update the tokens of the the specified account (by `id`).
///
/// # Arguments
/// * `id` - the ID of the user
/// * `tokens` - the **new** tokens vector for the user
pub async fn update_user_tokens(&self, id: usize, tokens: Vec<Token>) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"UPDATE users SET tokens = $1 WHERE id = $2",
&[&serde_json::to_string(&tokens).unwrap(), &id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(())
}
}

View file

@ -0,0 +1 @@
pub const CREATE_TABLE_USERS: &str = include_str!("./sql/create_users.sql");

View file

@ -0,0 +1,7 @@
pub mod common;
#[cfg(feature = "sqlite")]
pub mod sqlite;
#[cfg(feature = "postgres")]
pub mod postgres;

View file

@ -0,0 +1,100 @@
use crate::config::Config;
use bb8_postgres::{
PostgresConnectionManager,
bb8::{Pool, PooledConnection},
};
use tetratto_l10n::{LangFile, read_langs};
use tokio_postgres::{Config as PgConfig, NoTls, Row, types::ToSql};
pub type Result<T> = std::result::Result<T, tokio_postgres::Error>;
pub type Connection<'a> = PooledConnection<'a, PostgresConnectionManager<NoTls>>;
#[derive(Clone)]
pub struct DataManager(
pub Config,
pub HashMap<String, LangFile>,
pub Pool<PostgresConnectionManager<NoTls>>,
);
impl DataManager {
/// Obtain a connection to the staging database.
pub(crate) async fn connect(&self) -> Result<Connection> {
Ok(self.2.get().await.unwrap())
}
/// Create a new [`DataManager`] (and init database).
pub async fn new(config: Config) -> Result<Self> {
let manager = PostgresConnectionManager::new(
{
let mut c = PgConfig::new();
c.user(&config.database.user);
c.password(&config.database.password);
c.dbname(&config.database.name);
c
},
NoTls,
);
let pool = Pool::builder().max_size(15).build(manager).await.unwrap();
let this = Self(config.clone(), read_langs(), pool);
let c = this.clone();
let conn = c.connect().await?;
conn.execute(super::common::CREATE_TABLE_USERS, &[])
.await
.unwrap();
Ok(this)
}
}
#[cfg(feature = "postgres")]
#[macro_export]
macro_rules! get {
($row:ident->$idx:literal($t:tt)) => {
$row.get::<usize, Option<$t>>($idx).unwrap()
};
}
pub async fn query_row_helper<T, F>(
conn: &Connection<'_>,
sql: &str,
params: &[&(dyn ToSql + Sync)],
f: F,
) -> Result<T>
where
F: FnOnce(&Row) -> Result<T>,
{
let query = conn.prepare(sql).await.unwrap();
let res = conn.query_one(&query, params).await;
if let Ok(row) = res {
Ok(f(&row).unwrap())
} else {
Err(res.unwrap_err())
}
}
#[macro_export]
macro_rules! query_row {
($conn:expr, $sql:expr, $params:expr, $f:expr) => {
crate::database::query_row_helper($conn, $sql, $params, $f).await
};
}
pub async fn execute_helper(
conn: &Connection<'_>,
sql: &str,
params: &[&(dyn ToSql + Sync)],
) -> Result<()> {
let query = conn.prepare(sql).await.unwrap();
conn.execute(&query, params).await?;
Ok(())
}
#[macro_export]
macro_rules! execute {
($conn:expr, $sql:expr, $params:expr) => {
crate::database::execute_helper($conn, $sql, $params).await
};
}

View file

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY,
created INTEGER NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
salt TEXT NOT NULL,
settings TEXT NOT NULL,
tokens TEXT NOT NULL
)

View file

@ -0,0 +1,48 @@
use crate::config::Config;
use rusqlite::{Connection, Result};
use std::collections::HashMap;
use tetratto_l10n::{LangFile, read_langs};
#[derive(Clone)]
pub struct DataManager(pub Config, pub HashMap<String, LangFile>);
impl DataManager {
/// Obtain a connection to the staging database.
pub(crate) async fn connect(&self) -> Result<Connection> {
Ok(Connection::open(&self.0.database.name)?)
}
/// Create a new [`DataManager`] (and init database).
pub async fn new(config: Config) -> Result<Self> {
let this = Self(config.clone(), read_langs());
let conn = this.connect().await?;
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn.execute(super::common::CREATE_TABLE_USERS, ()).unwrap();
Ok(this)
}
}
#[cfg(feature = "sqlite")]
#[macro_export]
macro_rules! get {
($row:ident->$idx:literal($t:tt)) => {
$row.get::<usize, $t>($idx).unwrap()
};
}
#[macro_export]
macro_rules! query_row {
($conn:expr, $sql:expr, $params:expr, $f:expr) => {{
let mut query = $conn.prepare($sql).unwrap();
query.query_row($params, $f)
}};
}
#[macro_export]
macro_rules! execute {
($conn:expr, $sql:expr, $params:expr) => {
$conn.prepare($sql).unwrap().execute($params)
};
}

View file

@ -0,0 +1,10 @@
mod auth;
mod drivers;
use super::model::auth::{Token, User};
#[cfg(feature = "sqlite")]
pub use drivers::sqlite::*;
#[cfg(feature = "postgres")]
pub use drivers::postgres::*;