Created
March 2, 2020 16:45
-
-
Save GoldsteinE/e4b439b4ce132d07f78fa088e71a8cdd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "transact" | |
version = "0.1.0" | |
authors = ["Maximilian Siling <[email protected]>"] | |
edition = "2018" | |
[dependencies] | |
actix-rt = "1.0.0" | |
actix-web = "2.0.0" | |
actix-identity = "0.2.1" | |
actix-threadpool = "0.3.1" | |
r2d2 = "0.8.8" | |
r2d2_sqlite = "0.14.0" | |
rusqlite = "0.21.0" | |
libsqlite3-sys = "0.17.1" | |
log = "0.4.8" | |
pretty_env_logger = "0.4.0" | |
hex = "0.4.2" | |
sha3 = "0.8.2" | |
rand = "0.7.3" | |
serde = "1.0.104" | |
serde_derive = "1.0.104" | |
dotenv = "0.15.0" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use log::{error, info}; | |
use rand::Rng; | |
use sha3::Digest; | |
use std::env; | |
use std::fmt::Debug; | |
use r2d2_sqlite::SqliteConnectionManager; | |
use rusqlite::params; | |
use actix_identity::Identity; | |
use actix_identity::{CookieIdentityPolicy, IdentityService}; | |
use actix_threadpool::BlockingError; | |
use actix_web::{get, post, web, App, HttpResponse, HttpServer}; | |
use serde_derive::Deserialize; | |
type Pool = r2d2::Pool<SqliteConnectionManager>; | |
type Connection = r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>; | |
#[derive(Deserialize)] | |
struct LoginData { | |
username: String, | |
password: String, | |
} | |
fn get_conn(pool: web::Data<Pool>) -> Result<Connection, actix_web::Error> { | |
pool.get().map_err(|err| { | |
error!("DB connection failed: {}", err); | |
HttpResponse::InternalServerError() | |
.body("DB connection failed") | |
.into() | |
}) | |
} | |
fn get_random_token() -> String { | |
rand::thread_rng() | |
.sample_iter(&rand::distributions::Alphanumeric) | |
.take(30) | |
.collect() | |
} | |
fn hash_password(password: &str, salt: &str) -> String { | |
hex::encode(sha3::Sha3_512::new().chain(password).chain(salt).result()) | |
} | |
fn convert_db_error<E: Debug>(err: E) -> actix_web::Error { | |
error!("DB interaction error: {:?}", err); | |
HttpResponse::InternalServerError() | |
.body("DB interaction error") | |
.into() | |
} | |
fn map_user_error<T: Debug>( | |
res: Result<Option<Result<T, rusqlite::Error>>, BlockingError<rusqlite::Error>>, | |
) -> Result<T, actix_web::Error> { | |
match res { | |
Ok(Some(Ok(res))) => Ok(res), | |
Err(err) => Err(convert_db_error(err)), | |
Ok(Some(Err(err))) => Err(convert_db_error(err)), | |
_ => Err(HttpResponse::NotFound().body("User is not found").into()), | |
} | |
} | |
#[post("/register")] | |
async fn register( | |
id: Identity, | |
pool: web::Data<Pool>, | |
form: web::Form<LoginData>, | |
) -> Result<HttpResponse, actix_web::Error> { | |
let conn = get_conn(pool)?; | |
let username = form.username.clone(); | |
let inserted_count = web::block(move || { | |
let salt = get_random_token(); | |
conn.execute( | |
" | |
INSERT INTO users (username, salt, password_hash, balance) | |
SELECT ?1, ?2, ?3, 0 | |
WHERE NOT EXISTS ( | |
SELECT 1 FROM users WHERE username = ?1 | |
) | |
", | |
params![&form.username, &salt, hash_password(&form.password, &salt)], | |
) | |
}) | |
.await | |
.map_err(|err| { | |
error!("DB interaction failed: {}", err); | |
HttpResponse::InternalServerError().body("DB interaction failed") | |
})?; | |
if inserted_count == 0 { | |
return Err(HttpResponse::Conflict() | |
.body("User with this username already exists") | |
.into()); | |
} | |
id.remember(username); | |
Ok(HttpResponse::Ok().body("Registered")) | |
} | |
#[post("/login")] | |
async fn login( | |
id: Identity, | |
pool: web::Data<Pool>, | |
form: web::Form<LoginData>, | |
) -> Result<HttpResponse, actix_web::Error> { | |
let conn = get_conn(pool)?; | |
let username = form.username.clone(); | |
let user_res = web::block(move || { | |
Result::<_, rusqlite::Error>::Ok( | |
conn.prepare("SELECT username, salt, password_hash FROM users WHERE username = ?1")? | |
.query_map(params![username], |row| { | |
let res: (String, String, String) = (row.get(0)?, row.get(1)?, row.get(2)?); | |
Ok(res) | |
})? | |
.next(), | |
) | |
}) | |
.await; | |
let (_username, salt, password_hash) = map_user_error(user_res)?; | |
if password_hash == hash_password(&form.password, &salt) { | |
id.remember(form.username.clone()); | |
Ok(HttpResponse::Ok().body("Logged in")) | |
} else { | |
Err(HttpResponse::Forbidden().body("Invalid password").into()) | |
} | |
} | |
#[get("/balance")] | |
async fn get_balance( | |
id: Identity, | |
pool: web::Data<Pool>, | |
) -> Result<HttpResponse, actix_web::Error> { | |
let conn = get_conn(pool)?; | |
let username = id | |
.identity() | |
.ok_or(HttpResponse::Forbidden().body("Log in first"))?; | |
let balance_res = web::block(move || { | |
Result::<_, rusqlite::Error>::Ok( | |
conn.prepare("SELECT balance FROM users WHERE username = ?1")? | |
.query_map(params![username], |row| { | |
let res: i32 = row.get(0)?; | |
Ok(res) | |
})? | |
.next(), | |
) | |
}) | |
.await; | |
Ok(HttpResponse::Ok().body(map_user_error(balance_res)?.to_string())) | |
} | |
#[post("/transfer/{to_username}/{amount}")] | |
async fn transfer_to( | |
id: Identity, | |
pool: web::Data<Pool>, | |
path: web::Path<(String, u32)>, | |
) -> Result<HttpResponse, actix_web::Error> { | |
let mut conn = get_conn(pool)?; | |
let (to_username, amount) = path.into_inner(); | |
let username = id | |
.identity() | |
.ok_or(HttpResponse::Forbidden().body("Log in first"))?; | |
let transaction_result = web::block(move || { | |
let tx = conn.transaction()?; | |
tx.execute( | |
"UPDATE users SET balance = balance - ?1 WHERE username = ?2", | |
params![amount, username], | |
)?; | |
tx.execute( | |
"UPDATE users SET balance = balance + ?1 WHERE username = ?2", | |
params![amount, to_username], | |
)?; | |
tx.commit()?; | |
Result::<_, rusqlite::Error>::Ok(()) | |
}) | |
.await; | |
if let Err(err) = transaction_result { | |
use rusqlite::Error::SqliteFailure; | |
use rusqlite::ErrorCode::ConstraintViolation; | |
match err { | |
BlockingError::Error(SqliteFailure( | |
libsqlite3_sys::Error { | |
code: ConstraintViolation, | |
.. | |
}, | |
_, | |
)) => Err(HttpResponse::Forbidden().body("Not enough tokens").into()), | |
_ => { | |
error!("{}", err); | |
Err(HttpResponse::InternalServerError() | |
.body("Transaction failed") | |
.into()) | |
} | |
} | |
} else { | |
Ok(HttpResponse::Ok().body("Transaction complete")) | |
} | |
} | |
#[actix_rt::main] | |
async fn main() -> Result<(), std::io::Error> { | |
if let Err(err) = dotenv::dotenv() { | |
if !err.not_found() { | |
panic!("Failed to use env vars from .env"); | |
} | |
} | |
let host = env::var("TRANSACT_HOST").unwrap_or("localhost".into()); | |
let port: u16 = env::var("TRANSACT_PORT") | |
.unwrap_or("9876".into()) | |
.parse() | |
.expect("Port must be a number"); | |
let token = env::var("TRANSACT_TOKEN") | |
.expect("Please, specify TRANSACT_TOKEN") | |
.into_bytes(); | |
if let Err(_) = env::var("RUST_LOG") { | |
env::set_var("RUST_LOG", "transact=info,actix_web=info"); | |
} | |
let database_name = env::var("TRANSACT_DB").unwrap_or("transact.sqlite3".into()); | |
let pool: Pool = r2d2::Pool::new(SqliteConnectionManager::file(&database_name)) | |
.expect("Failed to open database"); | |
pretty_env_logger::init(); | |
info!("Serving at http://{}:{}", host, port); | |
info!("DB file: {}", database_name); | |
HttpServer::new(move || { | |
App::new() | |
.wrap(IdentityService::new( | |
CookieIdentityPolicy::new(&token).secure(false), // Allow http | |
)) | |
.service(login) | |
.service(register) | |
.service(get_balance) | |
.service(transfer_to) | |
.default_service(web::to(|| HttpResponse::NotFound().body("Not found"))) | |
.data(pool.clone()) | |
}) | |
.bind((host.as_str(), port))? | |
.run() | |
.await | |
}[package] | |
name = "transact" | |
version = "0.1.0" | |
authors = ["Maximilian Siling <[email protected]>"] | |
edition = "2018" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
drop table users; | |
create table users ( | |
id integer primary key not null, | |
username text not null, | |
salt text not null, | |
password_hash text not null, | |
balance integer check(balance > -1) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment