!!+ Optional backend code for admin feature

Features implemented thus far are proven to be working however
they will be left as optional features until they are more properly
tested in some kind of staging environment
This commit is contained in:
shockrah 2022-02-02 23:02:39 -08:00
parent 91d3d2b9fc
commit ff73995ff3
4 changed files with 270 additions and 14 deletions

95
api/src/admin.rs Normal file
View File

@ -0,0 +1,95 @@
#[cfg(feature = "admin")]
// This module deals with all the routes which are protected by an api key
// Without a proper api key sent to the server these routes will
// respond with a 401 UNAUTHORIZED response
// Below is the implementation required to have custom api keys which
// allow people to actually use the service
use std::env;
use std::collections::HashMap;
use rocket::serde::Serialize;
use rocket::serde::json::Json;
use rocket_dyn_templates::Template;
use rocket::request::{self, Outcome, Request, FromRequest};
use rocket::http::Status;
use crate::db;
use crate::page;
lazy_static! {
static ref DB_PATH: String = {
env::var("DB_PATH").unwrap_or("keys.db".into())
};
}
#[derive(Serialize)]
struct ActionResponse(&'static str);
pub struct ApiKey {
uid: String,
key: String
}
#[derive(Debug)]
pub enum ApiKeyError {
Missing,
Invalid,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey {
type Error = ApiKeyError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let key = req.headers().get_one("ADMIN-API-KEY");
let uid = req.headers().get_one("ADMIN-API-UID");
if key.is_none() || uid.is_none() {
return Outcome::Failure((Status::Forbidden, ApiKeyError::Missing));
}
let (key, uid) = (key.unwrap(), uid.unwrap());
println!("Path to use for db file {:?}", DB_PATH.to_string());
let db = db::Database::load(DB_PATH.as_str().into()).unwrap();
if let Some(stored) = db.get(uid) {
if stored == key {
return Outcome::Success(ApiKey {
key: key.into(),
uid: uid.into()
})
}
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
}
return Outcome::Failure((Status::Forbidden, ApiKeyError::Invalid))
}
}
#[get("/dashboard")]
pub async fn login_dashboard() -> Template {
// This page is basically just a login form
// However the rest of the form is present on this page, just hidden
let h: HashMap<i32,i32> = HashMap::new(); // does not allocate
return Template::render("admin", &h);
}
#[post("/dashboard")]
pub async fn dashboard(_key: ApiKey) -> Json<ActionResponse> {
// API Key auth'd for us so we don't need to bother checking,
// this just serves to confirm the credentials are correct
Json(ActionResponse("ok"))
}
#[post("/upload-video?<category>&<filename>")]
async fn updload_video(_key: ApiKey, category: String, filename: String) -> &'static str {
todo!()
}
#[delete("/remove-video?<category>&<filename>")]
async fn remove_video(_key: ApiKey, category: String, filename: String) -> &'static str {
todo!()
}

113
api/src/db.rs Normal file
View File

@ -0,0 +1,113 @@
#[cfg(feature = "admin")]
// This module defines a tiny async interface for the "database" that this
// project uses for interfacing with the key store
// WARN: at the moment there are no guarantees as far as data integrity is
// concerned. This means there are no real transactions
use std::fs::OpenOptions;
use std::io::prelude::Write;
use std::io::{BufWriter, BufReader};
use std::path::PathBuf;
use std::collections::HashMap;
use rocket::serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Database {
// uid's while random are fine to release as public as the key is more
// important however ideally neither should be release. Furthermore
// the frontend assists in keeping these secret by treating both as
// password fields as they are both randomly generated via a script
// uid -> key
users: HashMap<String,String>,
#[serde(skip)]
path: PathBuf
}
impl Database {
// Opens a handle to a database file
// if none is found then one is created with the new path
// if there is one then the existing database is used
// any thing else is invalid and causes this to return Err
pub fn new(path: PathBuf) -> Result<Self, std::io::Error> {
let file = OpenOptions::new()
.write(true)
.create(true)
.open(&path)?;
let writer = BufWriter::new(&file);
// Dummy value to write in place
let empty = Database { users: HashMap::new(), path: "".into() };
serde_json::to_writer(writer, &empty)?;
Ok(empty)
}
pub fn load(path: PathBuf) -> Result<Self, std::io::Error> {
let file = OpenOptions::new()
.read(true)
.open(&path)?;
let reader = BufReader::new(&file);
let mut data: Database = serde_json::from_reader(reader)?;
data.path = path;
return Ok(data);
}
pub fn get(&self, uid: &str) -> Option<&String> {
return self.users.get(uid);
}
fn write(&self) -> Result<(), std::io::Error> {
let file = OpenOptions::new()
.write(true)
.open(&self.path)?;
let writer = BufWriter::new(file);
serde_json::to_writer(writer, &self.path)?;
return Ok(())
}
pub fn remove(&mut self, uid: &str) -> Result<(), std::io::Error> {
self.users.remove_entry(uid);
self.write()
}
pub fn add(&mut self, key: &str, value: &str) -> Result<(), std::io::Error> {
println!("{:?}", self.path);
self.users.insert(key.into(), value.into());
self.write()
}
}
#[cfg(test)]
mod db_tests {
use super::Database;
use rocket::tokio;
const DB: &'static str = "new.db";
#[test]
fn load_db() {
match Database::new(DB.into()) {
Ok(db) => println!("Loaded new sample.db: {:?}", db),
Err(e) => panic!("Error fetching database: {}", e)
}
}
#[test]
fn add_simple_entries() {
match Database::load(DB.into()) {
Ok(mut db) => db.add("key", "value").unwrap(),
Err(e) => println!("Error adding entries: {}", e)
}
}
#[test]
fn remove_simple_entries() {
match Database::load(DB.into()) {
Ok(mut db) => db.remove("key").unwrap(),
Err(e) => println!("Error removing simple entries: {}", e)
}
}
}

View File

@ -10,21 +10,43 @@ mod video;
mod thumbnail;
mod common;
#[cfg(feature = "admin")]
mod admin;
#[cfg(feature = "admin")]
mod sec;
#[cfg(feature = "admin")]
mod db;
#[rocket::main]
async fn main() {
// emoji's crash my terminal
// rid ourselves of random emoji's in logs
env::set_var("ROCKET_CLI_COLORS", "false");
/*
* Some of the target vars that we look for
* CLIP_DIR
*/
// Comments denote what kind of data each sub uri actually returns
let _ = rocket::build()
.mount("/", routes![page::home, page::category, page::video]) // html
.mount("/static", routes![page::files]) // files
.mount("/api", routes![category::list, video::list]) // json
.mount("/thumbnail", routes![thumbnail::get]) // images
.mount("/video", routes![video::get_video]) // videos
.attach(Template::fairing())
.launch().await;
if cfg!(feature = "admin") {
#[cfg(feature = "admin")]
let _ = rocket::build()
.mount("/", routes![page::home, page::category, page::video]) // html
.mount("/static", routes![page::files]) // files
.mount("/api", routes![category::list, video::list]) // json
.mount("/thumbnail", routes![thumbnail::get]) // images
.mount("/video", routes![video::get_video]) // videos
.mount("/admin?", routes![
admin::login_dashboard,
admin::dashboard
])
.attach(Template::fairing())
.launch().await;
} else {
let _ = rocket::build()
.mount("/", routes![page::home, page::category, page::video]) // html
.mount("/static", routes![page::files]) // files
.mount("/api", routes![category::list, video::list]) // json
.mount("/thumbnail", routes![thumbnail::get]) // images
.mount("/video", routes![video::get_video]) // videos
.attach(Template::fairing())
.launch().await;
}
}

26
api/src/sec.rs Normal file
View File

@ -0,0 +1,26 @@
#[cfg(feature = "admin")]
// This module concerns itself with the encrypting/decrypting of passwords
// as well as the storage of those items
use rocket::tokio::io::AsyncReadExt;
use rocket::tokio::fs;
async fn random_string() -> Result<String, std::io::Error> {
// First we read in some bytes from /dev/urandom
let mut handle = fs::File::open("/dev/urandom").await?;
let mut buffer = [0;32];
handle.read(&mut buffer[..]).await?;
Ok(base64::encode_config(buffer, base64::URL_SAFE_NO_PAD))
}
#[cfg(test)]
mod sec_api_tests {
use rocket::tokio;
use super::random_string;
#[tokio::test]
async fn generate_string() {
println!("{:?}", random_string().await);
}
}