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