Create basic backend server

This commit is contained in:
~erin 2021-11-10 15:28:08 -05:00
commit 1e3c7d5e0b
No known key found for this signature in database
GPG Key ID: 597FF22FE9F78895
11 changed files with 2052 additions and 0 deletions

2
.cargo/config Normal file
View File

@ -0,0 +1,2 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

1
.envrc Normal file
View File

@ -0,0 +1 @@
eval "$(lorri direnv)"

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/*_db

1692
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "pharmacy"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.0-rc.1", features = ["json"] }
log = "0.4.14"
serde = "1.0.130"
bincode = "1.3.3"
sled = "0.34.7"
argon2 = "0.3.1"
rand_core = { version = "0.6", features = ["std"] }
uuid = { version = "0.8", features = ["serde", "v5"] }
rand = "0.8.4"

27
README.md Normal file
View File

@ -0,0 +1,27 @@
# Pharmacy Server
A simple server for dealing with products & orders for the Catgirl Pharmacy.
## ToDo:
- [x] Client verification for admin actions
- [x] Webpage for admin actions
- [x] Fetch info with JS
- [x] Fix DB lock error
- [x] Keep track of orders
- [ ] Add proper error handling
- [ ] Return Json data
- [ ] Cleanup code
- [ ] Add comments
## API:
`GET /api/price/<shortcode>`
`GET /api/stock/<shortcode>`
`POST /api/update shortcode=string field=[price,stock] value=int/float password=string`
`POST /api/new full_name=string short_name=string price_usd=float stock=int password=string`
`POST /api/order name=string user_message=string email=string payment_type=[xmr,eth,btc]`
Start the server with the environment variable `ADMIN_PASSWD` set to your admin password.

5
Rocket.toml Normal file
View File

@ -0,0 +1,5 @@
[default]
port = 8200
[debug]
port = 8200

8
shell.nix Normal file
View File

@ -0,0 +1,8 @@
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "rust-dev";
buildInputs = with pkgs; [
rustc cargo rust-analyzer rustfmt
];
}

64
src/database.rs Normal file
View File

@ -0,0 +1,64 @@
// File for database functions
use crate::structures::*;
type MyErrorType = Box<dyn std::error::Error>;
pub fn open() -> sled::Db {
sled::open("products_db").unwrap()
}
// Register a new product
pub fn register_product(db: &sled::Db, short_name: &str, product: &Product) {
let bytes = bincode::serialize(&product).unwrap();
db.insert(short_name, bytes).unwrap();
db.flush().unwrap();
}
// Read info about a product
pub fn read_product(db: &sled::Db, short_name: &str) -> std::result::Result<Option<Product>, MyErrorType> {
let entry = db.get(short_name)?;
if let Some(product_entry) = entry {
let read_product: Product = bincode::deserialize(&product_entry)?;
db.flush().unwrap();
Ok(Some(read_product))
} else {
db.flush().unwrap();
Ok(None)
}
}
pub fn make_order(order: &Order) {
let db = sled::open("order_db").unwrap();
let bytes = bincode::serialize(&order).unwrap();
db.insert(order.uuid.to_hyphenated().to_string(), bytes).unwrap();
db.flush().unwrap();
}
pub fn read_order(uuid: &str) -> std::result::Result<Option<Order>, MyErrorType> {
let db = sled::open("order_db").unwrap();
let entry = db.get(uuid)?;
if let Some(order_entry) = entry {
let read_order: Order = bincode::deserialize(&order_entry)?;
db.flush().unwrap();
Ok(Some(read_order))
} else {
db.flush().unwrap();
Ok(None)
}
}
pub fn read_all_orders() -> Vec<Order> {
let db = sled::open("order_db").unwrap();
let first_key: &[u8] = &db.first().unwrap().unwrap().0;
let mut iter = db.range(first_key..);
let mut all_orders: Vec<Order> = Vec::new();
for order in iter {
if let order_entry = order.unwrap().1 {
let read_order: Order = bincode::deserialize(&order_entry).unwrap();
db.flush().unwrap();
all_orders.push(read_order);
} else {
db.flush().unwrap();
}
}
return all_orders;
}

176
src/main.rs Normal file
View File

@ -0,0 +1,176 @@
#[macro_use]
extern crate rocket;
use rocket::{form::Form, State, Request, Response, http::Header};
use rocket::fairing::{Fairing, Info, Kind};
use uuid::Uuid;
use rand::Rng;
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
use std::env;
mod database;
use crate::database::*;
mod structures;
use crate::structures::*;
pub struct CORS;
#[rocket::async_trait]
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Attaching CORS headers to responses",
kind: Kind::Response
}
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}
#[post("/api/new", data = "<request>")]
fn new_product(db: &State<sled::Db>, argon2: &State<Argon2>, salt: &State<SaltString>, request: Form<ProductRequest>) -> String {
let password = request.password.as_bytes();
let hashed_password = argon2.hash_password(password, &salt.as_ref()).unwrap().to_string();
let mut admin_password = env::var("ADMIN_PASSWD").unwrap();
admin_password = argon2.hash_password(admin_password.as_bytes(), &salt.as_ref()).unwrap().to_string();
println!("salt is: {}\n hashed password is: {}\nadmin password is: {}", &salt.as_ref(), hashed_password, admin_password);
if hashed_password == admin_password {
let new_product: Product = Product {
short_name: request.short_name.clone(),
full_name: request.full_name.clone(),
price_usd: request.price_usd,
stock: request.stock,
};
register_product(db, &new_product.short_name, &new_product);
let new = read_product(db, &request.short_name).unwrap().unwrap();
return format!(
"registered new produt {} ${}. There are {} left in stock.",
new.full_name, new.price_usd, new.stock
);
};
return format!("wrong password!");
}
#[post("/api/order", data = "<order>")]
fn new_order(order: Form<OrderRequest<'_>>) -> String {
let mut rng = rand::thread_rng();
let to_hash = format!("{}{}{}", order.user_message.to_string(), order.encryption_key.to_string(), rng.gen::<f64>().to_string());
let db_order: Order = Order {
name: order.name.to_string(),
message: order.user_message.to_string(),
encryption_key: order.encryption_key.to_string(),
email: order.email.to_string(),
payment_type: order.payment_type.clone(),
uuid: Uuid::new_v5(&Uuid::NAMESPACE_X500, to_hash.as_bytes()),
};
make_order(&db_order);
format!("Thank you for ordering from Catgirl Pharmacy!\nYour order Uuid is {}", db_order.uuid.to_hyphenated().to_string())
}
#[get("/api/order/<uuid>")]
fn order_info(uuid: &str) -> String {
let order = read_order(uuid).unwrap().unwrap();
format!("Uuid: {}\nName: {}\nEmail: {}\nMessage: {}\nEncryption Key: {}\nPayment Type: {:?}",
order.uuid,
order.name,
order.email,
order.message,
order.encryption_key,
order.payment_type
)
}
#[post("/api/orders", data="<auth>")]
fn all_orders(auth: Form<Authenticate>, argon2: &State<Argon2>, salt: &State<SaltString>) -> String {
let entered_password = auth.password.as_bytes();
let hashed_password = argon2.hash_password(entered_password, &salt.as_ref()).unwrap().to_string();
let mut admin_password = env::var("ADMIN_PASSWD").unwrap();
admin_password = argon2.hash_password(admin_password.as_bytes(), &salt.as_ref()).unwrap().to_string();
if admin_password == hashed_password {
let all_orders = read_all_orders();
let mut return_string = String::new();
for x in &all_orders {
return_string = format!("{}\n{:?}",return_string, x);
}
return return_string;
} else {
return "wrong password".to_string();
}
}
#[post("/api/update", data = "<update>")]
fn update_product(db: &State<sled::Db>, argon2: &State<Argon2>, salt: &State<SaltString>, update: Form<Update<'_>>) -> String {
let password = update.password.as_bytes();
let hashed_password = argon2.hash_password(password, &salt.as_ref()).unwrap().to_string();
let mut admin_password = env::var("ADMIN_PASSWD").unwrap();
admin_password = argon2.hash_password(admin_password.as_bytes(), &salt.as_ref()).unwrap().to_string();
if admin_password == hashed_password {
match update.field {
"price_usd" => {
let mut new_product = read_product(db, &update.product).unwrap().unwrap();
new_product.price_usd = update.value;
register_product(db, &update.product, &new_product);
return "price changed".to_string();
}
"stock" => {
let mut new_product = read_product(db, &update.product).unwrap().unwrap();
new_product.stock = update.value as u32;
register_product(db, &update.product, &new_product);
return "stock changed".to_string();
}
_ => return "field not found".to_string(),
}
};
return "wrong password".to_string();
}
#[get("/api/stock/<product>")]
fn get_stock(db: &State<sled::Db>, product: &str) -> String {
read_product(db, &product)
.unwrap()
.unwrap()
.stock
.to_string()
}
#[get("/api/price/<product>")]
fn get_price(db: &State<sled::Db>, product: &str) -> String {
read_product(db, &product)
.unwrap()
.unwrap()
.price_usd
.to_string()
}
#[launch]
fn rocket() -> _ {
rocket::build()
.manage(database::open())
.manage(Argon2::default())
.manage(SaltString::generate(&mut OsRng))
.mount("/",
routes![get_stock, get_price, new_order, order_info, all_orders, new_product, update_product])
.attach(CORS)
}

58
src/structures.rs Normal file
View File

@ -0,0 +1,58 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, FromForm)]
pub struct Authenticate {
pub password: String,
}
#[derive(Clone, Debug, FromForm)]
pub struct ProductRequest {
pub full_name: String,
pub short_name: String,
pub price_usd: f64,
pub stock: u32,
pub password: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, FromForm)]
pub struct Product {
pub full_name: String,
pub short_name: String,
pub price_usd: f64,
pub stock: u32,
}
#[derive(FromForm)]
pub struct OrderRequest<'r> {
pub r#name: &'r str,
pub r#email: &'r str,
pub r#user_message: &'r str,
pub r#encryption_key: &'r str,
pub r#payment_type: PaymentOption,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Order {
pub name: String,
pub email: String,
pub message: String,
pub encryption_key: String,
pub payment_type: PaymentOption,
pub uuid: Uuid,
}
#[derive(FromForm)]
pub struct Update<'r> {
pub r#product: &'r str,
pub r#field: &'r str,
pub value: f64,
pub r#password: &'r str,
}
#[derive(Debug, PartialEq, FromFormField, Serialize, Clone, Deserialize)]
pub enum PaymentOption {
XMR,
ETH,
BTC,
}