!!+ 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:
parent
91d3d2b9fc
commit
ff73995ff3
95
api/src/admin.rs
Normal file
95
api/src/admin.rs
Normal 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
113
api/src/db.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
26
api/src/sec.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user