add: icon resolver
add: config "no_track" file list option add: rainbeam-shared -> tetratto-shared add: l10n
This commit is contained in:
parent
b6fe2fba37
commit
d2ca9e23d3
40 changed files with 1107 additions and 583 deletions
22
crates/core/Cargo.toml
Normal file
22
crates/core/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "tetratto-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
postgres = ["dep:tokio-postgres", "dep:bb8-postgres"]
|
||||
sqlite = ["dep:rusqlite"]
|
||||
default = ["sqlite"]
|
||||
|
||||
[dependencies]
|
||||
pathbufd = "0.1.4"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.20"
|
||||
tetratto-shared = { path = "../shared" }
|
||||
tetratto-l10n = { path = "../l10n" }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
rusqlite = { version = "0.34.0", optional = true }
|
||||
|
||||
tokio-postgres = { version = "0.7.13", optional = true }
|
||||
bb8-postgres = { version = "0.9.0", optional = true }
|
1
crates/core/LICENSE
Symbolic link
1
crates/core/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
217
crates/core/src/config.rs
Normal file
217
crates/core/src/config.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
use pathbufd::PathBufD;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::Result;
|
||||
|
||||
/// Security configuration.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct SecurityConfig {
|
||||
/// If registrations are enabled.
|
||||
#[serde(default = "default_security_registration_enabled")]
|
||||
pub registration_enabled: bool,
|
||||
/// If registrations are enabled.
|
||||
#[serde(default = "default_security_admin_user")]
|
||||
pub admin_user: String,
|
||||
/// If registrations are enabled.
|
||||
#[serde(default = "default_real_ip_header")]
|
||||
pub real_ip_header: String,
|
||||
}
|
||||
|
||||
fn default_security_registration_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_security_admin_user() -> String {
|
||||
"admin".to_string()
|
||||
}
|
||||
|
||||
fn default_real_ip_header() -> String {
|
||||
"CF-Connecting-IP".to_string()
|
||||
}
|
||||
|
||||
impl Default for SecurityConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
registration_enabled: default_security_registration_enabled(),
|
||||
admin_user: default_security_admin_user(),
|
||||
real_ip_header: default_real_ip_header(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Directories configuration.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct DirsConfig {
|
||||
/// HTML templates directory.
|
||||
#[serde(default = "default_dir_templates")]
|
||||
pub templates: String,
|
||||
/// Static files directory.
|
||||
#[serde(default = "default_dir_assets")]
|
||||
pub assets: String,
|
||||
/// Media (user avatars/banners) files directory.
|
||||
#[serde(default = "default_dir_media")]
|
||||
pub media: String,
|
||||
/// The icons files directory.
|
||||
#[serde(default = "default_dir_icons")]
|
||||
pub icons: String,
|
||||
}
|
||||
|
||||
fn default_dir_templates() -> String {
|
||||
"html".to_string()
|
||||
}
|
||||
|
||||
fn default_dir_assets() -> String {
|
||||
"public".to_string()
|
||||
}
|
||||
|
||||
fn default_dir_media() -> String {
|
||||
"media".to_string()
|
||||
}
|
||||
|
||||
fn default_dir_icons() -> String {
|
||||
"icons".to_string()
|
||||
}
|
||||
|
||||
impl Default for DirsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
templates: default_dir_templates(),
|
||||
assets: default_dir_assets(),
|
||||
media: default_dir_media(),
|
||||
icons: default_dir_icons(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Database configuration.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct DatabaseConfig {
|
||||
pub name: String,
|
||||
#[cfg(feature = "postgres")]
|
||||
pub url: String,
|
||||
#[cfg(feature = "postgres")]
|
||||
pub user: String,
|
||||
#[cfg(feature = "postgres")]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "atto.db".to_string(),
|
||||
#[cfg(feature = "postgres")]
|
||||
url: "localhost:5432".to_string(),
|
||||
#[cfg(feature = "postgres")]
|
||||
user: "postgres".to_string(),
|
||||
#[cfg(feature = "postgres")]
|
||||
password: "postgres".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration file
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
/// The name of the app.
|
||||
#[serde(default = "default_name")]
|
||||
pub name: String,
|
||||
/// The description of the app.
|
||||
#[serde(default = "default_description")]
|
||||
pub description: String,
|
||||
/// The theme color of the app.
|
||||
#[serde(default = "default_color")]
|
||||
pub color: String,
|
||||
/// The port to serve the server on.
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
/// Database security.
|
||||
#[serde(default = "default_security")]
|
||||
pub security: SecurityConfig,
|
||||
/// The locations where different files should be matched.
|
||||
#[serde(default = "default_dirs")]
|
||||
pub dirs: DirsConfig,
|
||||
/// Database configuration.
|
||||
#[serde(default = "default_database")]
|
||||
pub database: DatabaseConfig,
|
||||
/// A list of files (just their name, no full path) which are NOT updated to match the
|
||||
/// version built with the server binary.
|
||||
#[serde(default = "default_no_track")]
|
||||
pub no_track: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_name() -> String {
|
||||
"Tetratto".to_string()
|
||||
}
|
||||
|
||||
fn default_description() -> String {
|
||||
"🐐 tetratto!".to_string()
|
||||
}
|
||||
|
||||
fn default_color() -> String {
|
||||
"#c9b1bc".to_string()
|
||||
}
|
||||
fn default_port() -> u16 {
|
||||
4118
|
||||
}
|
||||
|
||||
fn default_security() -> SecurityConfig {
|
||||
SecurityConfig::default()
|
||||
}
|
||||
|
||||
fn default_dirs() -> DirsConfig {
|
||||
DirsConfig::default()
|
||||
}
|
||||
|
||||
fn default_database() -> DatabaseConfig {
|
||||
DatabaseConfig::default()
|
||||
}
|
||||
|
||||
fn default_no_track() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: default_name(),
|
||||
description: default_description(),
|
||||
color: default_color(),
|
||||
port: default_port(),
|
||||
database: default_database(),
|
||||
security: default_security(),
|
||||
dirs: default_dirs(),
|
||||
no_track: default_no_track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Read configuration file into [`Config`]
|
||||
pub fn read(contents: String) -> Self {
|
||||
toml::from_str::<Self>(&contents).unwrap()
|
||||
}
|
||||
|
||||
/// Pull configuration file
|
||||
pub fn get_config() -> Self {
|
||||
let path = PathBufD::current().join("tetratto.toml");
|
||||
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(c) => Config::read(c),
|
||||
Err(_) => {
|
||||
Self::update_config(Self::default()).expect("failed to write default config");
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update configuration file
|
||||
pub fn update_config(contents: Self) -> Result<()> {
|
||||
let c = fs::canonicalize(".").unwrap();
|
||||
let here = c.to_str().unwrap();
|
||||
|
||||
fs::write(
|
||||
format!("{here}/tetratto.toml"),
|
||||
toml::to_string_pretty::<Self>(&contents).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
196
crates/core/src/database/auth.rs
Normal file
196
crates/core/src/database/auth.rs
Normal 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(())
|
||||
}
|
||||
}
|
1
crates/core/src/database/drivers/common.rs
Normal file
1
crates/core/src/database/drivers/common.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub const CREATE_TABLE_USERS: &str = include_str!("./sql/create_users.sql");
|
7
crates/core/src/database/drivers/mod.rs
Normal file
7
crates/core/src/database/drivers/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod common;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub mod sqlite;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
pub mod postgres;
|
100
crates/core/src/database/drivers/postgres.rs
Normal file
100
crates/core/src/database/drivers/postgres.rs
Normal 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
|
||||
};
|
||||
}
|
9
crates/core/src/database/drivers/sql/create_users.sql
Normal file
9
crates/core/src/database/drivers/sql/create_users.sql
Normal 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
|
||||
)
|
48
crates/core/src/database/drivers/sqlite.rs
Normal file
48
crates/core/src/database/drivers/sqlite.rs
Normal 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)
|
||||
};
|
||||
}
|
10
crates/core/src/database/mod.rs
Normal file
10
crates/core/src/database/mod.rs
Normal 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::*;
|
5
crates/core/src/lib.rs
Normal file
5
crates/core/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod model;
|
||||
|
||||
pub use database::DataManager;
|
71
crates/core/src/model/auth.rs
Normal file
71
crates/core/src/model/auth.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tetratto_shared::{
|
||||
hash::{hash_salted, salt},
|
||||
snow::AlmostSnowflake,
|
||||
unix_epoch_timestamp,
|
||||
};
|
||||
|
||||
/// `(ip, token, creation timestamp)`
|
||||
pub type Token = (String, String, usize);
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct User {
|
||||
pub id: usize,
|
||||
pub created: usize,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub salt: String,
|
||||
pub settings: UserSettings,
|
||||
pub tokens: Vec<Token>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserSettings;
|
||||
|
||||
impl Default for UserSettings {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new [`User`].
|
||||
pub fn new(username: String, password: String) -> Self {
|
||||
let salt = salt();
|
||||
let password = hash_salted(password, salt.clone());
|
||||
|
||||
Self {
|
||||
id: AlmostSnowflake::new(1234567890)
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap(),
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
username,
|
||||
password,
|
||||
salt,
|
||||
settings: UserSettings::default(),
|
||||
tokens: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
///
|
||||
/// # Returns
|
||||
/// `(unhashed id, token)`
|
||||
pub fn create_token(ip: &str) -> (String, Token) {
|
||||
let unhashed = tetratto_shared::hash::uuid();
|
||||
(
|
||||
unhashed.clone(),
|
||||
(
|
||||
ip.to_string(),
|
||||
tetratto_shared::hash::hash(unhashed),
|
||||
unix_epoch_timestamp() as usize,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if the given password is correct for the user.
|
||||
pub fn check_password(&self, against: String) -> bool {
|
||||
self.password == hash_salted(against, self.salt.clone())
|
||||
}
|
||||
}
|
56
crates/core/src/model/mod.rs
Normal file
56
crates/core/src/model/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
pub mod auth;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiReturn<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
pub ok: bool,
|
||||
pub message: String,
|
||||
pub payload: T,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DatabaseConnection(String),
|
||||
UserNotFound,
|
||||
RegistrationDisabled,
|
||||
DatabaseError(String),
|
||||
IncorrectPassword,
|
||||
AlreadyAuthenticated,
|
||||
DataTooLong(String),
|
||||
DataTooShort(String),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ToString for Error {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Error::DatabaseConnection(msg) => msg.to_owned(),
|
||||
Error::DatabaseError(msg) => format!("Database error: {msg}"),
|
||||
Error::UserNotFound => "Unable to find user with given parameters".to_string(),
|
||||
Error::RegistrationDisabled => "Registration is disabled".to_string(),
|
||||
Error::IncorrectPassword => "The given password is invalid".to_string(),
|
||||
Error::AlreadyAuthenticated => "Already authenticated".to_string(),
|
||||
Error::DataTooLong(name) => format!("Given {name} is too long!"),
|
||||
Error::DataTooShort(name) => format!("Given {name} is too short!"),
|
||||
_ => format!("An unknown error as occurred: ({:?})", self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Into<ApiReturn<T>> for Error
|
||||
where
|
||||
T: Default + Serialize,
|
||||
{
|
||||
fn into(self) -> ApiReturn<T> {
|
||||
ApiReturn {
|
||||
ok: false,
|
||||
message: self.to_string(),
|
||||
payload: T::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
Loading…
Add table
Add a link
Reference in a new issue