Skip to content

Instantly share code, notes, and snippets.

@GoldsteinE
Created March 2, 2020 16:45
Show Gist options
  • Save GoldsteinE/e4b439b4ce132d07f78fa088e71a8cdd to your computer and use it in GitHub Desktop.
Save GoldsteinE/e4b439b4ce132d07f78fa088e71a8cdd to your computer and use it in GitHub Desktop.
[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"
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"
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