use serde::{Deserialize, Serialize}; use actix::Addr; use actix_identity::Identity; use actix_web::{http::header, web, HttpResponse, Responder}; use askama::Template; use log::error; use crate::model::DbExecutor; use crate::util::UserRequest; use crate::Secret; pub mod login; use login::{HashVerifyExecutor, HashVerifyRequest, LoginRequest}; mod settings; use settings::UpdatePasswordRequest; mod transactions; use transactions::{Transaction, TransactionsRequest, TransferRequest}; use crate::util::filters; use crate::admin::user_creation::{PassHashExecutor, PassHashRequest}; pub async fn index(ident: Identity) -> impl Responder { if ident.identity().is_some() { HttpResponse::Found() .header(header::LOCATION, "/transactions") .finish() } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } #[derive(Template)] #[template(path = "login.htm")] struct LoginTemplate { failed: bool, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct LoginError { #[serde(default)] pub error: bool, } pub async fn login(ident: Identity, params: web::Query) -> impl Responder { if ident.identity().is_some() { HttpResponse::Found() .header(header::LOCATION, "/transactions") .finish() } else { let failed = params.into_inner().error; HttpResponse::Ok() .content_type("text/html") .body(LoginTemplate { failed }.render().unwrap()) } } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct LoginData { pub user: String, pub password: String, } pub async fn confirm_login( ident: Identity, params: web::Form, db: web::Data>, verifier: web::Data>, sec: web::Data, ) -> impl Responder { let login = params.into_inner(); let (user, hash) = match db.send(LoginRequest(login.user)).await.unwrap() { Ok(v) => v, Err(_) => { return HttpResponse::Found() .header(header::LOCATION, "/login?error=true") .finish() } }; if !verifier .send(HashVerifyRequest::new(hash, login.password, sec.0.clone())) .await .unwrap() .unwrap_or(false) { return HttpResponse::Found() .header(header::LOCATION, "/login?error=true") .finish(); } ident.remember(user.name); HttpResponse::Found() .header(header::LOCATION, "/transactions") .finish() } #[derive(Template)] #[template(path = "transactions.htm")] struct TransactionsTemplate<'a> { name: &'a str, amount: u64, transactions: &'a [Transaction], } pub async fn transactions(ident: Identity, db: web::Data>) -> HttpResponse { if let Some(name) = ident.identity() { ident.remember(name.clone()); let user = match db.send(UserRequest(name.clone())).await.unwrap() { Ok(v) => v, Err(_) => { ident.forget(); return HttpResponse::Found() .header(header::LOCATION, "/login") .finish(); } }; let transactions = match db.send(TransactionsRequest::new(user.id)).await.unwrap() { Ok(transactions) => transactions, Err(e) => { error!("Transactions error: {}", e); Vec::with_capacity(0) } }; let page = TransactionsTemplate { name: &name, amount: user.balance, transactions: &transactions, } .render() .unwrap(); HttpResponse::Ok().content_type("text/html").body(page) } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } #[derive(Template)] #[template(path = "transfer.htm")] struct TransferTemplate { balance: u64, recipient: String, amount: u64, purpose: String, error: TransferResult, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TransferQuery { #[serde(default)] pub result: TransferResult, #[serde(default)] pub recipient: String, #[serde(default)] pub amount: u64, #[serde(default)] pub purpose: String, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum TransferResult { ZeroPoints, SelfSend, TransferError, Empty, } impl Default for TransferResult { fn default() -> Self { TransferResult::Empty } } pub async fn transfer( ident: Identity, db: web::Data>, params: web::Query, ) -> HttpResponse { if let Some(name) = ident.identity() { ident.remember(name.clone()); let user = match db.send(UserRequest(name.clone())).await.unwrap() { Ok(v) => v, Err(_) => { ident.forget(); return HttpResponse::Found() .header(header::LOCATION, "/login") .finish(); } }; let query = params.into_inner(); let page = TransferTemplate { balance: user.balance, recipient: query.recipient, amount: query.amount, purpose: query.purpose, error: query.result, } .render() .unwrap(); HttpResponse::Ok().content_type("text/html").body(page) } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TransferData { pub recipient: String, pub amount: u64, pub purpose: String, } pub async fn confirm_transfer( ident: Identity, params: web::Form, db: web::Data>, ) -> HttpResponse { if let Some(name) = ident.identity() { ident.remember(name.clone()); let form = params.into_inner(); if form.amount == 0 { let query = TransferQuery { result: TransferResult::ZeroPoints, recipient: form.recipient, amount: form.amount, purpose: form.purpose, }; return HttpResponse::Found() .header( header::LOCATION, format!( "/transfer?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish(); } if name.to_lowercase() == form.recipient.to_lowercase() { let query = TransferQuery { result: TransferResult::SelfSend, recipient: form.recipient, amount: form.amount, purpose: form.purpose, }; return HttpResponse::Found() .header( header::LOCATION, format!( "/transfer?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish(); } let request = TransferRequest { from: name, to: form.recipient.clone(), amount: form.amount, purpose: form.purpose.clone(), }; if let Err(e) = db.send(request).await.unwrap() { error!("Failed to transfer points: {}", e); let query = TransferQuery { result: TransferResult::TransferError, recipient: form.recipient, amount: form.amount, purpose: form.purpose, }; return HttpResponse::Found() .header( header::LOCATION, format!( "/transfer?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish(); } HttpResponse::Found() .header(header::LOCATION, "/transactions") .finish() } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } #[derive(Template)] #[template(path = "settings.htm")] struct SettingsTemplate { result: PasswordResult, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct PasswordQuery { #[serde(default)] pub result: PasswordResult, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum PasswordResult { Success, InvalidOld, Missmatch, Empty, } impl Default for PasswordResult { fn default() -> Self { PasswordResult::Empty } } pub async fn settings(ident: Identity, params: web::Query) -> HttpResponse { if let Some(name) = ident.identity() { ident.remember(name.clone()); let query = params.into_inner(); HttpResponse::Ok().content_type("text/html").body( SettingsTemplate { result: query.result, } .render() .unwrap(), ) } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct PasswordReset { pub old_password: String, pub new_password: String, pub confirm_password: String, } pub async fn reset_password( ident: Identity, params: web::Form, db: web::Data>, verifier: web::Data>, hasher: web::Data>, sec: web::Data, ) -> HttpResponse { if let Some(name) = ident.identity() { ident.remember(name.clone()); let (user, hash) = match db.send(LoginRequest(name)).await.unwrap() { Ok(v) => v, Err(_) => { ident.forget(); return HttpResponse::Found() .header(header::LOCATION, "/login") .finish(); } }; let reset = params.into_inner(); if reset.new_password != reset.confirm_password { let query = PasswordQuery { result: PasswordResult::Missmatch, }; return HttpResponse::Found() .header( header::LOCATION, format!( "/settings?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish(); } if !verifier .send(HashVerifyRequest::new( hash, reset.old_password, sec.0.clone(), )) .await .unwrap() .unwrap_or(false) { let query = PasswordQuery { result: PasswordResult::InvalidOld, }; return HttpResponse::Found() .header( header::LOCATION, format!( "/settings?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish(); } let hash = hasher .send(PassHashRequest::new(reset.new_password, sec.0.clone())) .await .unwrap() .unwrap(); db.send(UpdatePasswordRequest::new(user.id, hash)) .await .unwrap() .unwrap(); let query = PasswordQuery { result: PasswordResult::Success, }; HttpResponse::Found() .header( header::LOCATION, format!( "/settings?{}", serde_urlencoded::to_string(query).unwrap_or(String::with_capacity(0)) ), ) .finish() } else { HttpResponse::Found() .header(header::LOCATION, "/login") .finish() } } pub fn logout(ident: Identity) -> HttpResponse { ident.forget(); HttpResponse::Found() .header(header::LOCATION, "/login") .finish() }