Implemented database (untested), managed to make actix pass it to routes

This commit is contained in:
Gvidas Juknevičius 2024-03-25 23:26:56 +02:00
parent b18e193173
commit 2cb4acc604
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
14 changed files with 1125 additions and 19 deletions

908
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,28 @@ authors = [
]
[dependencies]
#? base
clap = { version = "4.5.3", features = ["derive"] }
anyhow = "1.0.81"
camino = "1.1.6"
simplelog = { version = "0.12.2", features = ["paris"] }
toml = "0.8.12"
serde = { version = "1.0.197", features = ["derive"] }
log = "0.4.21"
#? web
actix-files = "0.6.5"
actix-web = "4.5.1"
actix-web-lab = "0.20.2"
anyhow = "1.0.81"
askama = "0.12.1"
camino = "1.1.6"
clap = { version = "4.5.3", features = ["derive"] }
log = "0.4.21"
serde = { version = "1.0.197", features = ["derive"] }
simplelog = { version = "0.12.2", features = ["paris"] }
toml = "0.8.12"
# time = { version = "0.3.34", features = ["macros"] }
#? database
sqlx = { version = "0.7.4", features = ["tls-rustls", "runtime-tokio", "postgres", "uuid"] }
uuid = { version = "1.8.0", features = ["v4"] }
time = { version = "0.3.34", features = ["macros"] }
crypto = { version = "0.5.1", features = ["digest", "password-hash"] }
bcrypt = "0.15.1"
futures = "0.3.30"
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

@ -2,4 +2,7 @@ debug=true
[webserver]
host="0.0.0.0"
port=8080
port=8080
[database]
url="postgresql://postgres@localhost/mctest"

1
env.sh Normal file
View File

@ -0,0 +1 @@
export DATABASE_URL="postgresql://postgres@localhost/mctest"

0
migrations/.keep Normal file
View File

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE Users

View File

@ -0,0 +1,9 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS Users (
id UUID NOT NULL UNIQUE,
email TEXT NOT NULL,
username TEXT NOT NULL,
pw_hash TEXT NOT NULL,
permissions BIGINT NOT NULL,
PRIMARY KEY (id)
)

View File

@ -3,11 +3,17 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
pub debug: bool,
pub webserver: ConfigWebserver
pub webserver: ConfigWebserver,
pub database: ConfigDatabase
}
#[derive(Debug, Deserialize)]
pub struct ConfigWebserver {
pub host: String,
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct ConfigDatabase {
pub url: String,
}

28
src/database/mod.rs Normal file
View File

@ -0,0 +1,28 @@
use sqlx::{postgres::PgPoolOptions, Postgres};
use crate::config::definition::Config;
pub mod models;
#[derive(Debug, Clone)]
pub struct Database {
connection: sqlx::Pool<Postgres>
}
impl Database {
pub async fn new(config: &Config) -> anyhow::Result<Self> {
let conn = PgPoolOptions::new()
.max_connections(5)
.connect(&config.database.url).await?;
Ok(Self {
connection: conn
})
}
pub fn connection(&self) -> &sqlx::Pool<Postgres> {
&self.connection
}
}

View File

@ -0,0 +1 @@
pub mod users;

View File

@ -0,0 +1,131 @@
use uuid::Uuid;
use sqlx::Row;
use crate::database::Database;
use futures::TryStreamExt;
#[derive(sqlx::FromRow)]
pub struct Users {
pub id: uuid::Uuid,
pub email: String,
pub username: String,
pub pw_hash: String,
pub permissions: i64,
}
#[allow(dead_code)]
impl Users {
pub async fn create_new(db: &mut Database, email: String, username: String, password: String) -> anyhow::Result<Uuid> {
let id = Uuid::new_v4();
let hash = bcrypt::hash(password, 15)?;
sqlx::query(r#"
INSERT INTO users ( id, email, username, pw_hash, permissions )
VALUES ( $1, $2, $3, $4, 0 )
RETURNING id
"#)
.bind(id)
.bind(email)
.bind(username)
.bind(hash)
.execute(db.connection())
.await?;
Ok(id)
}
pub async fn get_by_id(db: &mut Database, id: Uuid) -> anyhow::Result<Option<Self>>{
let mut rows = sqlx::query("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch(db.connection());
while let Some(row) = rows.try_next().await? {
return Ok(Some(Self {
id: row.try_get("id")?,
email: row.try_get("email")?,
username: row.try_get("username")?,
pw_hash: row.try_get("pw_hash")?,
permissions: row.try_get("permissions")?,
}));
}
Ok(None)
}
pub async fn get_by_email(db: &mut Database, email: String) -> anyhow::Result<Option<Self>>{
let mut rows = sqlx::query("SELECT * FROM users WHERE email = $1")
.bind(email)
.fetch(db.connection());
while let Some(row) = rows.try_next().await? {
return Ok(Some(Self {
id: row.try_get("id")?,
email: row.try_get("email")?,
username: row.try_get("username")?,
pw_hash: row.try_get("pw_hash")?,
permissions: row.try_get("permissions")?,
}));
}
Ok(None)
}
pub async fn get_by_username(db: &mut Database, username: String) -> anyhow::Result<Option<Self>>{
let mut rows = sqlx::query("SELECT * FROM users WHERE username = $1")
.bind(username)
.fetch(db.connection());
while let Some(row) = rows.try_next().await? {
return Ok(Some(Self {
id: row.try_get("id")?,
email: row.try_get("email")?,
username: row.try_get("username")?,
pw_hash: row.try_get("pw_hash")?,
permissions: row.try_get("permissions")?,
}));
}
Ok(None)
}
pub async fn check_login(db: &mut Database, email: String, password: String) -> anyhow::Result<Option<Self>>{
let Some(user) = Self::get_by_email(db, email).await? else {return Ok(None)};
if bcrypt::verify(password, &user.pw_hash)? {
Ok(Some(user))
} else {
Ok(None)
}
}
pub async fn save(&self, db: &mut Database) -> anyhow::Result<()> {
let _ = sqlx::query(r#"
UPDATE users
SET id = $1
SET email = $2
SET username = $3
SET pw_hash = $4
SET permissions = $5
"#)
.bind(&self.id)
.bind(&self.email)
.bind(&self.username)
.bind(&self.pw_hash)
.bind(&self.permissions)
.execute(db.connection()).await?;
Ok(())
}
pub async fn remove_user(&self, db: &mut Database) -> anyhow::Result<()> {
sqlx::query(r#"
DELETE FROM users
WHERE id = $1
"#)
.bind(self.id)
.execute(db.connection())
.await?;
Ok(())
}
}

View File

@ -4,6 +4,7 @@ mod public;
mod logger;
mod cli;
mod config;
mod database;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@ -18,8 +19,9 @@ async fn main() -> std::io::Result<()> {
}
};
let Ok(database) = database::Database::new(config.get_ref()).await else {return Ok(())};
if let Err(e) = public::start_actix(config.get_ref()).await {
if let Err(e) = public::start_actix(config.get_ref(), database).await {
log::error!("Actix had an error: {e}");
}
Ok(())

View File

@ -4,15 +4,17 @@ mod templates;
use actix_web::{web, App, HttpServer};
use actix_files as actix_fs;
use sqlx::database;
use crate::config::definition::Config;
use crate::{config::definition::Config, database::Database};
pub(crate) async fn start_actix(config: &Config) -> anyhow::Result<()> {
pub(crate) async fn start_actix(config: &Config, database: Database) -> anyhow::Result<()> {
let bindip = format!("{}:{}", config.webserver.host, config.webserver.port);
log::info!("Serving an http server at http://{bindip}");
HttpServer::new(|| {
HttpServer::new(move || {
App::new()
.app_data(actix_web::web::Data::new(database.clone()))
.route("/", web::get().to(routes::index)) // index.html
.service(actix_fs::Files::new("/static", "./static").index_file("index.html")) // static directoryh
})

View File

@ -1,10 +1,14 @@
use std::sync::Mutex;
use actix_web_lab::respond::Html;
use actix_web::{Result, Responder};
use actix_web::{web::Data, Responder, Result};
use askama::Template;
use crate::public::templates::IndexTemplate;
use crate::{database::Database, public::templates::IndexTemplate};
pub async fn index() -> Result<impl Responder> {
// NOTE: Not usefull to have database here but just so u know how
pub async fn index(_: Data<Mutex<Database>>) -> Result<impl Responder> {
let html = IndexTemplate {
placeholder: "hewwo world"
}.render().expect("Failed to render index.html");