Add register and password verification
This commit is contained in:
parent
681b9cf85d
commit
87c58d0bc9
|
@ -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)?))
|
||||
}
|
||||
}
|
||||
|
|
105
src/database.rs
105
src/database.rs
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
44
src/main.rs
44
src/main.rs
|
@ -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");
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
58
src/web.rs
58
src/web.rs
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue