Create basic backend server
This commit is contained in:
commit
1e3c7d5e0b
|
@ -0,0 +1,2 @@
|
||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
linker = "aarch64-linux-gnu-gcc"
|
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/*_db
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
||||||
|
[default]
|
||||||
|
port = 8200
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
port = 8200
|
|
@ -0,0 +1,8 @@
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
name = "rust-dev";
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
rustc cargo rust-analyzer rustfmt
|
||||||
|
];
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
Loading…
Reference in New Issue