Add register and password verification

This commit is contained in:
lemonsh 2022-05-30 20:10:06 +02:00
parent 681b9cf85d
commit 87c58d0bc9
5 changed files with 71 additions and 180 deletions

View file

@ -18,9 +18,9 @@
use serde::Deserialize;
use std::env;
use std::fs;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::fs;
#[derive(Deserialize)]
pub struct Config {
@ -31,11 +31,11 @@ pub struct Config {
}
impl Config {
pub async fn load() -> anyhow::Result<Arc<Self>> {
pub fn load() -> anyhow::Result<Arc<Self>> {
let config_var = env::var("TMTD_CONFIG");
let config_path = config_var.as_deref().unwrap_or("tmtd.toml");
println!("Loading config from '{}'...", config_path);
let config_str = fs::read_to_string(config_path).await?;
let config_str = fs::read_to_string(config_path)?;
Ok(Arc::new(toml::from_str(&config_str)?))
}
}

View file

@ -17,15 +17,21 @@
*/
use crate::{task, templates};
use anyhow::anyhow;
use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::{Argon2, PasswordHasher};
use sqlx::postgres::{PgConnectOptions, PgConnectionInfo, PgPoolOptions};
use sqlx::Executor;
use argon2::{password_hash, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use sqlx::postgres::{PgConnectOptions, PgConnectionInfo, PgPoolOptions, PgRow};
use sqlx::{ConnectOptions, PgPool};
use sqlx::{Executor, Row};
use tracing::info;
use tracing::log::LevelFilter;
pub struct Database(PgPool);
pub struct Database {
pool: PgPool,
argon2: Argon2<'static>,
}
type SqlxResult<T> = Result<T, sqlx::Error>;
impl Database {
pub async fn connect(conn_string: &str) -> anyhow::Result<Self> {
@ -41,35 +47,14 @@ impl Database {
);
conn.execute(include_str!("sql/schema.sql")).await?;
Ok(Self(pool))
}
pub fn pool(&self) -> PgPool {
self.0.clone()
Ok(Self {
pool,
argon2: Argon2::default(),
})
}
pub async fn close(&self) {
self.0.close().await;
}
// Async might become a problem here
pub async fn get_tasks(&self) -> templates::Tasks {
// TODO: actually get the issues from the db based on the category cookies
let vec = vec![templates::Task {
title: "finish tmtd".to_string(),
date: "yesterday".to_string(), // Convert from unix timestamps to
// the actual date, also timezone info?
status: "assigned".to_string(),
assignee: "tmtd contributers".to_string(),
description: "DO SOMETHING AAAAAAA".to_string(),
id: 1,
}];
let logged_in = self.logged_in().await;
templates::Tasks {
tasks: vec,
logged_in,
}
self.pool.close().await;
}
pub async fn create_task(&self, task: &task::CreateTask) {
@ -80,42 +65,36 @@ impl Database {
// TODO: change the category of the task inside the db
}
fn hash(&self, password: &str, salt: SaltString) -> Result<String, ()> {
let argon2 = Argon2::default();
let hash = argon2.hash_password(password.as_bytes(), &salt);
if let Ok(ref hash) = hash {
if let Some(ref hash) = hash.hash {
return Ok(hash.to_string());
}
}
tracing::error!("Error hashing password: {:?}", hash);
Err(())
}
pub async fn register(&self, username: &str, password: &str) {
pub async fn register(&self, username: &str, password: &str) -> anyhow::Result<()> {
let salt = SaltString::generate(&mut OsRng);
let hash = self.hash(password, salt);
if let Err(_) = hash {
return;
}
tracing::debug!("{}", hash.unwrap());
// TODO: insert the salt and hash into the DB
let hash = match self.argon2.hash_password(password.as_bytes(), &salt) {
Ok(o) => o.to_string(),
Err(e) => return Err(anyhow!("Hashing error: {}", e)),
};
let mut conn = self.pool.acquire().await?;
sqlx::query("insert into users(username,hash) values($1,$2)")
.bind(username)
.bind(hash)
.execute(&mut conn)
.await?;
Ok(())
}
pub async fn login(&self, username: &str, password: &str) {
// TODO: get the salt from the DB
let salt = SaltString::generate(&mut OsRng);
let hash = self.hash(password, salt);
if let Err(_) = hash {
return;
}
tracing::debug!("{}", hash.unwrap());
// TODO: find user in DB and check if the password matches
// TODO: save that the user is logged in a cookie or something
}
pub async fn check_password(&self, username: &str, password: &str) -> anyhow::Result<bool> {
let mut conn = self.pool.acquire().await?;
pub async fn logged_in(&self) -> Option<String> {
// TODO: find out if the user is logged in and return the username if yes
None
let phc = sqlx::query("select hash from users where username=$1")
.bind(username)
.map(move |row: PgRow| row.get::<String, _>(0))
.fetch_one(&mut conn)
.await?;
match PasswordHash::new(&phc)
.and_then(|v| self.argon2.verify_password(password.as_bytes(), &v))
{
Ok(_) => Ok(true),
Err(password_hash::Error::Password) => Ok(false),
Err(e) => Err(anyhow!("Hashing error: {}", e)),
}
}
}

View file

@ -17,14 +17,9 @@
*/
use crate::{config::Config, database::Database};
use async_sqlx_session::PostgresSessionStore;
use std::str::FromStr;
use std::time::Duration;
use std::{env, sync::Arc};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::{error, info, Level};
use tracing::{info, Level};
mod config;
mod database;
@ -34,7 +29,7 @@ mod web;
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let cfg = Config::load().await?;
let cfg = Config::load()?;
tracing_subscriber::fmt::fmt()
.with_max_level({
if let Some(o) = cfg.log_level.as_deref() {
@ -46,47 +41,12 @@ async fn main() -> anyhow::Result<()> {
.init();
info!(concat!("Initializing - tmtd ", env!("CARGO_PKG_VERSION")));
let (ctx, _) = broadcast::channel(1);
let database = Arc::new(Database::connect(&cfg.connection_string).await?);
let session_store =
PostgresSessionStore::from_client(database.pool()).with_table_name("sessions");
session_store.migrate().await?;
let cleanup_task =
spawn_session_cleanup_task(&session_store, Duration::from_secs(600), ctx.subscribe());
info!("Started session cleanup task");
let web = web::App::new(cfg.listen_addr, database.clone()).await?;
info!("Started the web app at http://{}", cfg.listen_addr);
web.server.await?;
ctx.send(()).unwrap();
cleanup_task
.await
.unwrap_or_else(|e| error!("Couldn't join cleanup task: {}", e));
database.close().await;
Ok(())
}
fn spawn_session_cleanup_task(
store: &PostgresSessionStore,
period: Duration,
mut cancel: broadcast::Receiver<()>,
) -> JoinHandle<()> {
let store = store.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = sleep(period) => {
if let Err(error) = store.cleanup().await {
error!("Error in cleanup task: {}", error);
}
}
_ = cancel.recv() => break
}
}
info!("Cleanup task has been shut down");
})
}

View file

@ -4,58 +4,32 @@ create table if not exists users(
hash varchar(128) not null
);
create table if not exists org(
id serial primary key,
name varchar(32) not null unique
);
create table if not exists boards(
id serial primary key,
name varchar(32) not null,
description varchar(256),
org integer not null references org(id) on delete cascade
);
create table if not exists categories(
id serial primary key,
name varchar(32) not null,
board integer not null references boards(id) on delete cascade
board integer not null references boards(id) on delete cascade,
unique(name, board)
);
create table if not exists status(
id serial primary key,
name varchar(16),
board integer not null references boards(id) on delete cascade
);
create table if not exists org_members(
org integer not null references org(id) on delete cascade,
member integer not null references users(id) on delete cascade
);
create table if not exists org_boards(
org integer not null references org(id) on delete cascade,
board integer not null references boards(id) on delete cascade
);
create table if not exists tasks(
id serial primary key,
title varchar(128) not null,
description varchar(32768) not null,
author integer references users(id) not null,
author integer references users(id),
category integer not null references categories(id) on delete cascade,
org integer not null references org(id) on delete cascade,
status integer not null references status(id) on delete cascade,
created timestamp not null,
deadline timestamp
);
create table if not exists task_assigned(
task integer references task(id),
member integer references users(id)
id serial primary key,
task integer references task(id) on delete cascade,
member integer references users(id) on delete cascade
);

View file

@ -30,45 +30,23 @@ impl App {
pub async fn new(addr: SocketAddr, db: Arc<Database>) -> anyhow::Result<Self> {
let db = db.clone();
let server = HttpServer::new(move || {
actix_web::App::new().service((
web::resource("/")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(index)),
web::resource("/users")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(users)),
web::resource("/task")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(tasks)),
web::resource("/admin")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(admin)),
web::resource("/create")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(create)),
web::resource("/login")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(login)),
web::resource("/register")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(register)),
web::resource("/api/task/create")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(create_task)),
web::resource("/api/task/move")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(move_task)),
//web::resource("/api/task/sort").route(web::post().to(sort_task)),
web::resource("/api/login")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(login_user)),
web::resource("/api/register")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(register_user)),
web::resource("/api/logout")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(logout)),
))
actix_web::App::new()
.app_data(web::Data::new(db.clone()))
.service((
web::resource("/").route(web::get().to(index)),
web::resource("/users").route(web::get().to(users)),
web::resource("/task").route(web::get().to(tasks)),
web::resource("/admin").route(web::get().to(admin)),
web::resource("/create").route(web::get().to(create)),
web::resource("/login").route(web::get().to(login)),
web::resource("/register").route(web::get().to(register)),
web::resource("/api/task/create").route(web::post().to(create_task)),
web::resource("/api/task/move").route(web::post().to(move_task)),
//web::resource("/api/task/sort").route(web::post().to(sort_task)),
web::resource("/api/login").route(web::post().to(login_user)),
web::resource("/api/register").route(web::post().to(register_user)),
web::resource("/api/logout").route(web::get().to(logout)),
))
})
.bind(addr)?
.run();
@ -151,7 +129,7 @@ async fn sort_task(req: web::Form<task::SortTask>) -> impl Responder {
}
async fn login_user(req: web::Form<task::Login>, db: web::Data<Arc<Database>>) -> impl Responder {
db.login(&req.username, &req.password).await;
db.check_password(&req.username, &req.password).await;
HttpResponse::SeeOther()
.insert_header(("Location", "/"))
.finish()