Upgrade to 0.5 complete
This commit is contained in:
parent
b654d913ad
commit
b0cdd1ba5c
@ -1,150 +0,0 @@
|
|||||||
//! # peach-web
|
|
||||||
//!
|
|
||||||
//! `peach-web` provides a web interface for monitoring and interacting with the
|
|
||||||
//! PeachCloud device. This allows administration of the single-board computer
|
|
||||||
//! (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related
|
|
||||||
//! plugins.
|
|
||||||
//!
|
|
||||||
//! ## Design
|
|
||||||
//!
|
|
||||||
//! `peach-web` is written primarily in Rust and presents a web interface for
|
|
||||||
//! interacting with the device. The stack currently consists of Rocket (Rust
|
|
||||||
//! web framework), Tera (Rust template engine inspired by Jinja2 and the Django
|
|
||||||
//! template language), HTML, CSS and JavaScript. Additional functionality is
|
|
||||||
//! provided by JSON-RPC clients for the `peach-network` and `peach-stats`
|
|
||||||
//! microservices.
|
|
||||||
//!
|
|
||||||
//! HTML is rendered server-side. Request handlers call JSON-RPC microservices
|
|
||||||
//! and serve HTML and assets. A JSON API is exposed for remote calls and
|
|
||||||
//! dynamic client-side content updates via vanilla JavaScript following
|
|
||||||
//! unobstructive design principles. A basic Websockets server is included,
|
|
||||||
//! though is not currently utilised. Each Tera template is passed a context
|
|
||||||
//! object. In the case of Rust, this object is a `struct` and must implement
|
|
||||||
//! `Serialize`. The fields of the context object are available in the context
|
|
||||||
//! of the template to be rendered.
|
|
||||||
|
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
|
|
||||||
pub mod error;
|
|
||||||
pub mod routes;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
pub mod utils;
|
|
||||||
mod ws;
|
|
||||||
|
|
||||||
use std::{env, thread};
|
|
||||||
|
|
||||||
use log::{debug, error, info};
|
|
||||||
|
|
||||||
use rocket::{catchers, routes, Rocket, Build};
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
|
|
||||||
use crate::routes::authentication::*;
|
|
||||||
use crate::routes::device::*;
|
|
||||||
use crate::routes::helpers::*;
|
|
||||||
use crate::routes::index::*;
|
|
||||||
use crate::routes::ping::*;
|
|
||||||
use crate::routes::scuttlebutt::*;
|
|
||||||
|
|
||||||
use crate::routes::settings::admin::*;
|
|
||||||
use crate::routes::settings::dns::*;
|
|
||||||
use crate::routes::settings::network::*;
|
|
||||||
use crate::ws::*;
|
|
||||||
|
|
||||||
pub type BoxError = Box<dyn std::error::Error>;
|
|
||||||
|
|
||||||
// create rocket instance & mount web & json routes (makes testing easier)
|
|
||||||
fn rocket() -> Rocket<Build> {
|
|
||||||
rocket::build()
|
|
||||||
.mount(
|
|
||||||
"/",
|
|
||||||
routes![
|
|
||||||
add_credentials, // WEB ROUTE
|
|
||||||
connect_wifi, // WEB ROUTE
|
|
||||||
disconnect_wifi, // WEB ROUTE
|
|
||||||
deploy_ap, // WEB ROUTE
|
|
||||||
deploy_client, // WEB ROUTE
|
|
||||||
device_stats, // WEB ROUTE
|
|
||||||
files, // WEB ROUTE
|
|
||||||
forget_wifi, // WEB ROUTE
|
|
||||||
help, // WEB ROUTE
|
|
||||||
index, // WEB ROUTE
|
|
||||||
login, // WEB ROUTE
|
|
||||||
logout, // WEB ROUTE
|
|
||||||
messages, // WEB ROUTE
|
|
||||||
network_home, // WEB ROUTE
|
|
||||||
network_add_ssid, // WEB ROUTE
|
|
||||||
network_add_wifi, // WEB ROUTE
|
|
||||||
network_detail, // WEB ROUTE
|
|
||||||
peers, // WEB ROUTE
|
|
||||||
profile, // WEB ROUTE
|
|
||||||
reboot_cmd, // WEB ROUTE
|
|
||||||
shutdown_cmd, // WEB ROUTE
|
|
||||||
shutdown_menu, // WEB ROUTE
|
|
||||||
wifi_list, // WEB ROUTE
|
|
||||||
wifi_password, // WEB ROUTE
|
|
||||||
wifi_set_password, // WEB ROUTE
|
|
||||||
wifi_usage, // WEB ROUTE
|
|
||||||
wifi_usage_alerts, // WEB ROUTE
|
|
||||||
wifi_usage_reset, // WEB ROUTE
|
|
||||||
configure_dns, // WEB ROUTE
|
|
||||||
configure_dns_post, // WEB ROUTE
|
|
||||||
change_password, // WEB ROUTE
|
|
||||||
reset_password, // WEB ROUTE
|
|
||||||
reset_password_post, // WEB ROUTE
|
|
||||||
send_password_reset_page, // WEB ROUTE
|
|
||||||
send_password_reset_post, // WEB ROUTE
|
|
||||||
configure_admin, // WEB ROUTE
|
|
||||||
add_admin, // WEB ROUTE
|
|
||||||
add_admin_post, // WEB ROUTE
|
|
||||||
delete_admin_post, // WEB ROUTE
|
|
||||||
activate_ap, // JSON API
|
|
||||||
activate_client, // JSON API
|
|
||||||
add_wifi, // JSON API
|
|
||||||
connect_ap, // JSON API
|
|
||||||
disconnect_ap, // JSON API
|
|
||||||
forget_ap, // JSON API
|
|
||||||
modify_password, // JSON API
|
|
||||||
ping_pong, // JSON API
|
|
||||||
test_route, // JSON API
|
|
||||||
ping_network, // JSON API
|
|
||||||
ping_oled, // JSON API
|
|
||||||
ping_stats, // JSON API
|
|
||||||
reset_data_total, // JSON API
|
|
||||||
return_ip, // JSON API
|
|
||||||
return_rssi, // JSON API
|
|
||||||
return_ssid, // JSON API
|
|
||||||
return_state, // JSON API
|
|
||||||
return_status, // JSON API
|
|
||||||
reboot_device, // JSON API
|
|
||||||
scan_networks, // JSON API
|
|
||||||
shutdown_device, // JSON API
|
|
||||||
update_wifi_alerts, // JSON API
|
|
||||||
save_dns_configuration_endpoint, // JSON API
|
|
||||||
save_password_form_endpoint, // JSON API
|
|
||||||
reset_password_form_endpoint, // JSON API
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.register("/", catchers![not_found, internal_error])
|
|
||||||
.attach(Template::fairing())
|
|
||||||
}
|
|
||||||
|
|
||||||
// launch the rocket server
|
|
||||||
pub fn run() -> Result<(), BoxError> {
|
|
||||||
info!("Starting up.");
|
|
||||||
|
|
||||||
// spawn a separate thread for rocket to prevent blocking websockets
|
|
||||||
thread::spawn(|| {
|
|
||||||
info!("Launching Rocket server.");
|
|
||||||
rocket().launch();
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: websockets are not currently in use (may be in the future)
|
|
||||||
let ws_addr = env::var("PEACH_WEB_WS").unwrap_or_else(|_| "0.0.0.0:5115".to_string());
|
|
||||||
match websocket_server(ws_addr) {
|
|
||||||
Ok(_) => debug!("Websocket server terminated without error."),
|
|
||||||
Err(e) => error!("Error starting the websocket server: {}", e),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,16 +1,144 @@
|
|||||||
//! Initialize the logger and run the application, catching any errors.
|
//! # peach-web
|
||||||
|
//!
|
||||||
|
//! `peach-web` provides a web interface for monitoring and interacting with the
|
||||||
|
//! PeachCloud device. This allows administration of the single-board computer
|
||||||
|
//! (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related
|
||||||
|
//! plugins.
|
||||||
|
//!
|
||||||
|
//! ## Design
|
||||||
|
//!
|
||||||
|
//! `peach-web` is written primarily in Rust and presents a web interface for
|
||||||
|
//! interacting with the device. The stack currently consists of Rocket (Rust
|
||||||
|
//! web framework), Tera (Rust template engine inspired by Jinja2 and the Django
|
||||||
|
//! template language), HTML, CSS and JavaScript. Additional functionality is
|
||||||
|
//! provided by JSON-RPC clients for the `peach-network` and `peach-stats`
|
||||||
|
//! microservices.
|
||||||
|
//!
|
||||||
|
//! HTML is rendered server-side. Request handlers call JSON-RPC microservices
|
||||||
|
//! and serve HTML and assets. A JSON API is exposed for remote calls and
|
||||||
|
//! dynamic client-side content updates via vanilla JavaScript following
|
||||||
|
//! unobstructive design principles. A basic Websockets server is included,
|
||||||
|
//! though is not currently utilised. Each Tera template is passed a context
|
||||||
|
//! object. In the case of Rust, this object is a `struct` and must implement
|
||||||
|
//! `Serialize`. The fields of the context object are available in the context
|
||||||
|
//! of the template to be rendered.
|
||||||
|
|
||||||
use std::process;
|
#![feature(proc_macro_hygiene, decl_macro)]
|
||||||
|
|
||||||
use log::error;
|
pub mod error;
|
||||||
|
pub mod routes;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
pub mod utils;
|
||||||
|
mod ws;
|
||||||
|
|
||||||
fn main() {
|
use std::{env, thread};
|
||||||
// initialize the logger
|
use log::{debug, error, info};
|
||||||
|
|
||||||
|
use rocket::{catchers, routes, Rocket, Build, fs::FileServer};
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
|
use crate::routes::authentication::*;
|
||||||
|
use crate::routes::device::*;
|
||||||
|
use crate::routes::helpers::*;
|
||||||
|
use crate::routes::index::*;
|
||||||
|
use crate::routes::ping::*;
|
||||||
|
use crate::routes::scuttlebutt::*;
|
||||||
|
|
||||||
|
use crate::routes::settings::admin::*;
|
||||||
|
use crate::routes::settings::dns::*;
|
||||||
|
use crate::routes::settings::network::*;
|
||||||
|
use crate::ws::*;
|
||||||
|
|
||||||
|
pub type BoxError = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
|
/// Create rocket instance & mount all routes.
|
||||||
|
fn init_rocket() -> Rocket<Build> {
|
||||||
|
rocket::build()
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![
|
||||||
|
add_credentials, // WEB ROUTE
|
||||||
|
connect_wifi, // WEB ROUTE
|
||||||
|
disconnect_wifi, // WEB ROUTE
|
||||||
|
deploy_ap, // WEB ROUTE
|
||||||
|
deploy_client, // WEB ROUTE
|
||||||
|
device_stats, // WEB ROUTE
|
||||||
|
forget_wifi, // WEB ROUTE
|
||||||
|
help, // WEB ROUTE
|
||||||
|
index, // WEB ROUTE
|
||||||
|
login, // WEB ROUTE
|
||||||
|
logout, // WEB ROUTE
|
||||||
|
messages, // WEB ROUTE
|
||||||
|
network_home, // WEB ROUTE
|
||||||
|
network_add_ssid, // WEB ROUTE
|
||||||
|
network_add_wifi, // WEB ROUTE
|
||||||
|
network_detail, // WEB ROUTE
|
||||||
|
peers, // WEB ROUTE
|
||||||
|
profile, // WEB ROUTE
|
||||||
|
reboot_cmd, // WEB ROUTE
|
||||||
|
shutdown_cmd, // WEB ROUTE
|
||||||
|
shutdown_menu, // WEB ROUTE
|
||||||
|
wifi_list, // WEB ROUTE
|
||||||
|
wifi_password, // WEB ROUTE
|
||||||
|
wifi_set_password, // WEB ROUTE
|
||||||
|
wifi_usage, // WEB ROUTE
|
||||||
|
wifi_usage_alerts, // WEB ROUTE
|
||||||
|
wifi_usage_reset, // WEB ROUTE
|
||||||
|
configure_dns, // WEB ROUTE
|
||||||
|
configure_dns_post, // WEB ROUTE
|
||||||
|
change_password, // WEB ROUTE
|
||||||
|
reset_password, // WEB ROUTE
|
||||||
|
reset_password_post, // WEB ROUTE
|
||||||
|
send_password_reset_page, // WEB ROUTE
|
||||||
|
send_password_reset_post, // WEB ROUTE
|
||||||
|
configure_admin, // WEB ROUTE
|
||||||
|
add_admin, // WEB ROUTE
|
||||||
|
add_admin_post, // WEB ROUTE
|
||||||
|
delete_admin_post, // WEB ROUTE
|
||||||
|
activate_ap, // JSON API
|
||||||
|
activate_client, // JSON API
|
||||||
|
add_wifi, // JSON API
|
||||||
|
connect_ap, // JSON API
|
||||||
|
disconnect_ap, // JSON API
|
||||||
|
forget_ap, // JSON API
|
||||||
|
modify_password, // JSON API
|
||||||
|
ping_pong, // JSON API
|
||||||
|
test_route, // JSON API
|
||||||
|
ping_network, // JSON API
|
||||||
|
ping_oled, // JSON API
|
||||||
|
ping_stats, // JSON API
|
||||||
|
reset_data_total, // JSON API
|
||||||
|
return_ip, // JSON API
|
||||||
|
return_rssi, // JSON API
|
||||||
|
return_ssid, // JSON API
|
||||||
|
return_state, // JSON API
|
||||||
|
return_status, // JSON API
|
||||||
|
reboot_device, // JSON API
|
||||||
|
scan_networks, // JSON API
|
||||||
|
shutdown_device, // JSON API
|
||||||
|
update_wifi_alerts, // JSON API
|
||||||
|
save_dns_configuration_endpoint, // JSON API
|
||||||
|
save_password_form_endpoint, // JSON API
|
||||||
|
reset_password_form_endpoint, // JSON API
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.mount("/", FileServer::from("static"))
|
||||||
|
.register("/", catchers![not_found, internal_error])
|
||||||
|
.attach(Template::fairing())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch the peach-web rocket server.
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() {
|
||||||
|
|
||||||
|
// initialize logger
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// handle errors returned from `run`
|
// initialize and launch rocket
|
||||||
if let Err(e) = peach_web::run() {
|
info!("Initializing Rocket");
|
||||||
error!("Application error: {}", e);
|
let rocket = init_rocket();
|
||||||
process::exit(1);
|
|
||||||
}
|
info!("Launching Rocket");
|
||||||
|
rocket.launch().await;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use rocket::request::{FlashMessage, Form, FromForm};
|
use rocket::request::{FlashMessage};
|
||||||
|
use rocket::form::{Form, FromForm};
|
||||||
use rocket::response::{Flash, Redirect};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket::{get, post};
|
use rocket::{get, post};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
use log::debug;
|
use log::debug;
|
||||||
use rocket::{catch, get, response::NamedFile};
|
use rocket::{catch, get};
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
#[get("/<file..>", rank = 2)]
|
|
||||||
pub fn files(file: PathBuf) -> Option<NamedFile> {
|
|
||||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR 404 ERROR
|
// HELPERS AND ROUTES FOR 404 ERROR
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use rocket::{
|
use rocket::{
|
||||||
get, post,
|
get, post,
|
||||||
request::{FlashMessage, Form, FromForm},
|
request::FlashMessage,
|
||||||
|
form::{Form, FromForm},
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
uri,
|
uri,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use log::info;
|
use log::info;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
get, post,
|
get, post,
|
||||||
request::{FlashMessage, Form, FromForm},
|
request::FlashMessage,
|
||||||
|
form::{Form, FromForm}
|
||||||
};
|
};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
|
@ -2,9 +2,9 @@ use log::{debug, warn};
|
|||||||
use percent_encoding::percent_decode;
|
use percent_encoding::percent_decode;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
get,
|
get,
|
||||||
http::RawStr,
|
|
||||||
post,
|
post,
|
||||||
request::{FlashMessage, Form, FromForm},
|
request::FlashMessage,
|
||||||
|
form::{Form, FromForm},
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
uri, UriDisplayQuery,
|
uri, UriDisplayQuery,
|
||||||
};
|
};
|
||||||
@ -52,7 +52,7 @@ pub fn wifi_usage_reset() -> Flash<Redirect> {
|
|||||||
#[post("/network/wifi/connect", data = "<network>")]
|
#[post("/network/wifi/connect", data = "<network>")]
|
||||||
pub fn connect_wifi(network: Form<Ssid>) -> Flash<Redirect> {
|
pub fn connect_wifi(network: Form<Ssid>) -> Flash<Redirect> {
|
||||||
let ssid = &network.ssid;
|
let ssid = &network.ssid;
|
||||||
let url = uri!(network_detail: ssid);
|
let url = uri!(network_detail(ssid = ssid));
|
||||||
match network_client::id("wlan0", ssid) {
|
match network_client::id("wlan0", ssid) {
|
||||||
Ok(id) => match network_client::connect(&id, "wlan0") {
|
Ok(id) => match network_client::connect(&id, "wlan0") {
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
|
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
|
||||||
@ -86,14 +86,12 @@ pub fn forget_wifi(network: Form<Ssid>) -> Flash<Redirect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/network/wifi/modify?<ssid>")]
|
#[get("/network/wifi/modify?<ssid>")]
|
||||||
pub fn wifi_password(ssid: &RawStr, flash: Option<FlashMessage>) -> Template {
|
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>) -> Template {
|
||||||
// decode ssid from url
|
|
||||||
let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap();
|
|
||||||
let mut context = NetworkAddContext {
|
let mut context = NetworkAddContext {
|
||||||
back: Some("/network/wifi".to_string()),
|
back: Some("/network/wifi".to_string()),
|
||||||
flash_name: None,
|
flash_name: None,
|
||||||
flash_msg: None,
|
flash_msg: None,
|
||||||
selected: Some(decoded_ssid.to_string()),
|
selected: Some(ssid.to_string()),
|
||||||
title: Some("Update WiFi Password".to_string()),
|
title: Some("Update WiFi Password".to_string()),
|
||||||
};
|
};
|
||||||
// check to see if there is a flash message to display
|
// check to see if there is a flash message to display
|
||||||
@ -110,7 +108,7 @@ pub fn wifi_password(ssid: &RawStr, flash: Option<FlashMessage>) -> Template {
|
|||||||
pub fn wifi_set_password(wifi: Form<WiFi>) -> Flash<Redirect> {
|
pub fn wifi_set_password(wifi: Form<WiFi>) -> Flash<Redirect> {
|
||||||
let ssid = &wifi.ssid;
|
let ssid = &wifi.ssid;
|
||||||
let pass = &wifi.pass;
|
let pass = &wifi.pass;
|
||||||
let url = uri!(network_detail: ssid);
|
let url = uri!(network_detail(ssid = ssid));
|
||||||
match network_client::update("wlan0", ssid, pass) {
|
match network_client::update("wlan0", ssid, pass) {
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
|
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
|
||||||
Err(_) => Flash::error(
|
Err(_) => Flash::error(
|
||||||
@ -542,14 +540,12 @@ impl NetworkDetailContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/network/wifi?<ssid>")]
|
#[get("/network/wifi?<ssid>")]
|
||||||
pub fn network_detail(ssid: &RawStr, flash: Option<FlashMessage>) -> Template {
|
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>) -> Template {
|
||||||
// assign context through context_builder call
|
// assign context through context_builder call
|
||||||
let mut context = NetworkDetailContext::build();
|
let mut context = NetworkDetailContext::build();
|
||||||
context.back = Some("/network/wifi".to_string());
|
context.back = Some("/network/wifi".to_string());
|
||||||
context.title = Some("WiFi Network".to_string());
|
context.title = Some("WiFi Network".to_string());
|
||||||
// decode ssid from url
|
context.selected = Some(ssid.to_string());
|
||||||
let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap();
|
|
||||||
context.selected = Some(decoded_ssid.to_string());
|
|
||||||
// check to see if there is a flash message to display
|
// check to see if there is a flash message to display
|
||||||
if let Some(flash) = flash {
|
if let Some(flash) = flash {
|
||||||
// add flash message contents to the context object
|
// add flash message contents to the context object
|
||||||
@ -613,9 +609,10 @@ impl NetworkAddContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/network/wifi/add?<ssid>")]
|
#[get("/network/wifi/add?<ssid>")]
|
||||||
pub fn network_add_ssid(ssid: &RawStr, flash: Option<FlashMessage>) -> Template {
|
pub fn network_add_ssid(ssid: &str, flash: Option<FlashMessage>) -> Template {
|
||||||
// decode ssid from url
|
// decode ssid from url
|
||||||
let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap();
|
let decoded_ssid = ssid;
|
||||||
|
// let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap();
|
||||||
let mut context = NetworkAddContext::build();
|
let mut context = NetworkAddContext::build();
|
||||||
context.back = Some("/network/wifi".to_string());
|
context.back = Some("/network/wifi".to_string());
|
||||||
context.selected = Some(decoded_ssid.to_string());
|
context.selected = Some(decoded_ssid.to_string());
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use nest::{Error, Store, Value};
|
use nest::{Error, Store, Value};
|
||||||
use rocket::request::FromForm;
|
use rocket::form::{Form, FromForm};
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user