✨ cli + user api
This commit is contained in:
parent
46cf2f4572
commit
b010027549
12 changed files with 504 additions and 25 deletions
65
src/api.rs
65
src/api.rs
|
@ -3,6 +3,20 @@ use rumqttc::{AsyncClient, Event, EventLoop, MqttOptions, Packet, Transport};
|
|||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub fn domain(host: &str) -> String {
|
||||
if host.starts_with("http") {
|
||||
return host.to_string();
|
||||
} else {
|
||||
format!("https://{host}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginParam {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
/// Join Request
|
||||
pub struct JoinParams {
|
||||
|
@ -89,20 +103,57 @@ where
|
|||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
/// Generic JSON API result
|
||||
pub struct Result {
|
||||
pub ok: u32,
|
||||
pub struct DeviceList {
|
||||
pub devices: Vec<DeviceEntry>,
|
||||
}
|
||||
|
||||
impl Result {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct DeviceEntry {
|
||||
pub id: String,
|
||||
pub hostname: String,
|
||||
pub online: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
/// Generic JSON API result
|
||||
pub struct Result<T: Serialize> {
|
||||
pub ok: Option<T>,
|
||||
pub err: Option<String>,
|
||||
}
|
||||
|
||||
impl<T: Serialize> Result<T> {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn OkVal(val: T) -> Self {
|
||||
Self {
|
||||
ok: Some(val),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_result(self) -> std::result::Result<T, String> {
|
||||
if let Some(ok) = self.ok {
|
||||
Ok(ok)
|
||||
} else {
|
||||
Err(self.err.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Result<i32> {
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Ok() -> Self {
|
||||
Self { ok: 1 }
|
||||
Self {
|
||||
ok: Some(1),
|
||||
err: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Err() -> Self {
|
||||
Self { ok: 0 }
|
||||
pub fn Err(msg: &str) -> Self {
|
||||
Self {
|
||||
ok: None,
|
||||
err: Some(msg.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ use std::{net::SocketAddr, path::PathBuf};
|
|||
mod api;
|
||||
mod herd_core;
|
||||
use crate::herd_core::mqtt::{handle_mqtt, listen_to_devices};
|
||||
use herd_core::model::Machine;
|
||||
use herd_core::{
|
||||
config::Config,
|
||||
route::{join_device, login_user},
|
||||
};
|
||||
use herd_core::{model::Machine, route::devices_list};
|
||||
use sage::Identity;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
|
@ -59,7 +59,9 @@ async fn main() {
|
|||
.layer(ClientIpSource::ConnectInfo.into_extension()); // Direct IP
|
||||
// .layer(ClientIpSource::XRealIp.into_extension()) // Proxy
|
||||
|
||||
let user = Router::new().route("/login", post(login_user));
|
||||
let user = Router::new()
|
||||
.route("/login", post(login_user))
|
||||
.route("/devices", get(devices_list));
|
||||
|
||||
let app = Router::new().merge(device).merge(user);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use owl::{Serialize, get, query};
|
|||
use rumqttc::AsyncClient;
|
||||
use sage::PersonaIdentity;
|
||||
|
||||
fn is_within_80_seconds(time: chrono::DateTime<chrono::Utc>) -> bool {
|
||||
pub fn is_within_80_seconds(time: chrono::DateTime<chrono::Utc>) -> bool {
|
||||
let now = chrono::Utc::now();
|
||||
now.signed_duration_since(time).num_seconds() <= 80
|
||||
}
|
||||
|
|
|
@ -1,19 +1,55 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use crate::api;
|
||||
use crate::api::JoinResponse;
|
||||
use crate::api::LoginParam;
|
||||
use crate::herd_core::model::Machine;
|
||||
use axum::Json;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::StatusCode;
|
||||
use axum_client_ip::ClientIp;
|
||||
use axum_extra::TypedHeader;
|
||||
use axum_extra::headers::Authorization;
|
||||
use axum_extra::headers::authorization::Bearer;
|
||||
use based::auth::Sessions;
|
||||
use based::auth::User;
|
||||
use owl::prelude::Model;
|
||||
use owl::query;
|
||||
use owl::save;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use sheepd::DeviceEntry;
|
||||
use sheepd::DeviceList;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginParam {
|
||||
username: String,
|
||||
password: String,
|
||||
use super::mqtt::is_within_80_seconds;
|
||||
|
||||
pub async fn devices_list(
|
||||
session: TypedHeader<Authorization<Bearer>>,
|
||||
) -> (StatusCode, Json<api::Result<DeviceList>>) {
|
||||
let machines: Vec<Model<Machine>> = query!(|_| true);
|
||||
|
||||
let mut ret = vec![];
|
||||
|
||||
for mac in machines {
|
||||
let id = mac.read().id.to_string().replace("-", "");
|
||||
let online_state = crate::ONLINE
|
||||
.get()
|
||||
.unwrap()
|
||||
.get(&id)
|
||||
.map(|x| is_within_80_seconds(*x.deref()))
|
||||
.unwrap_or(false);
|
||||
|
||||
ret.push(DeviceEntry {
|
||||
id: id,
|
||||
hostname: mac.read().hostname.clone(),
|
||||
online: online_state,
|
||||
});
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(api::Result::OkVal(DeviceList { devices: ret })),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn login_user(Json(payload): Json<LoginParam>) -> (StatusCode, Json<serde_json::Value>) {
|
||||
|
@ -21,9 +57,15 @@ pub async fn login_user(Json(payload): Json<LoginParam>) -> (StatusCode, Json<se
|
|||
let u = User::find(&payload.username).await.unwrap();
|
||||
if u.read().verify_pw(&payload.password) {
|
||||
let ses = u.read().session().await;
|
||||
(StatusCode::OK, Json(json!({"token": ses.read().token})))
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(json!(api::Result::OkVal(ses.read().token.as_str()))),
|
||||
)
|
||||
} else {
|
||||
(StatusCode::FORBIDDEN, Json(json!({"error": "invalid"})))
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(json!(api::Result::Err("invalid"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod api;
|
||||
pub use api::*;
|
18
src/sheepctl.rs
Normal file
18
src/sheepctl.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use sheepctl_core::{
|
||||
args::{DeviceCommands, SheepctlArgs, SheepctlCommand},
|
||||
cmd::{list_devices, login},
|
||||
};
|
||||
|
||||
mod api;
|
||||
mod sheepctl_core;
|
||||
|
||||
fn main() {
|
||||
let args: SheepctlArgs = argh::from_env();
|
||||
|
||||
match args.command {
|
||||
SheepctlCommand::Device(device_command) => match device_command.command {
|
||||
DeviceCommands::List(list_devices_command) => list_devices(list_devices_command),
|
||||
},
|
||||
SheepctlCommand::Login(login_command) => login(login_command),
|
||||
}
|
||||
}
|
51
src/sheepctl_core/args.rs
Normal file
51
src/sheepctl_core/args.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use argh::FromArgs;
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Control your herd
|
||||
pub struct SheepctlArgs {
|
||||
#[argh(subcommand)]
|
||||
pub command: SheepctlCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum SheepctlCommand {
|
||||
Login(LoginCommand),
|
||||
Device(DeviceCommand),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Login to a homeserver
|
||||
#[argh(subcommand, name = "login")]
|
||||
pub struct LoginCommand {
|
||||
#[argh(positional)]
|
||||
/// homeserver
|
||||
pub home: String,
|
||||
|
||||
#[argh(positional)]
|
||||
/// username
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand, name = "device")]
|
||||
/// Commands for devices
|
||||
pub struct DeviceCommand {
|
||||
#[argh(subcommand)]
|
||||
pub command: DeviceCommands,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum DeviceCommands {
|
||||
List(ListDevicesCommand),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// List devices
|
||||
#[argh(subcommand, name = "ls")]
|
||||
pub struct ListDevicesCommand {
|
||||
#[argh(switch)]
|
||||
/// only show online devices
|
||||
pub online: bool,
|
||||
}
|
105
src/sheepctl_core/cmd.rs
Normal file
105
src/sheepctl_core/cmd.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use owl::{Deserialize, Serialize};
|
||||
use sheepd::{DeviceList, LoginParam};
|
||||
|
||||
use super::args::{ListDevicesCommand, LoginCommand};
|
||||
use crate::api::domain;
|
||||
|
||||
pub fn api_call<T: Serialize + for<'a> Deserialize<'a>, I: Serialize>(
|
||||
server: &str,
|
||||
path: &str,
|
||||
data: I,
|
||||
) -> crate::api::Result<T> {
|
||||
let url = format!("{}/{path}", domain(server));
|
||||
let mut res = ureq::post(url).send_json(data).unwrap();
|
||||
let res: crate::api::Result<T> = res.body_mut().read_json().unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
pub fn api_call_get<T: Serialize + for<'a> Deserialize<'a>>(
|
||||
server: &str,
|
||||
path: &str,
|
||||
token: &str,
|
||||
) -> crate::api::Result<T> {
|
||||
let url = format!("{}/{path}", domain(server));
|
||||
let mut res = ureq::get(url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.force_send_body()
|
||||
.send_empty()
|
||||
.unwrap();
|
||||
let res: crate::api::Result<T> = res.body_mut().read_json().unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
fn get_config_path() -> Option<PathBuf> {
|
||||
directories::ProjectDirs::from("de", "Hydrar", "sheepd")
|
||||
.map(|proj_dirs| proj_dirs.config_dir().join("config.toml"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CtlConfig {
|
||||
pub home: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl CtlConfig {
|
||||
pub fn load() -> Option<Self> {
|
||||
let c = std::fs::read_to_string(get_config_path()?).ok()?;
|
||||
toml::from_str(&c).ok()
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let s = toml::to_string(self).unwrap();
|
||||
let config = get_config_path().unwrap();
|
||||
let _ = std::fs::create_dir_all(config.parent().unwrap());
|
||||
std::fs::write(get_config_path().unwrap(), s).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_devices(arg: ListDevicesCommand) {
|
||||
let conf = CtlConfig::load().unwrap();
|
||||
|
||||
if let Ok(devices) = api_call_get::<DeviceList>(&conf.home, "devices", &conf.token).as_result()
|
||||
{
|
||||
println!("Hosts:");
|
||||
for d in devices.devices {
|
||||
println!(
|
||||
"- {} [{}]{}",
|
||||
d.hostname,
|
||||
d.id,
|
||||
if d.online { " [ONLINE]" } else { "" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn login(arg: LoginCommand) {
|
||||
if let Some(conf) = CtlConfig::load() {
|
||||
println!("You are already logged in to {}", conf.home);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let password = inquire::prompt_secret("Password: ").unwrap();
|
||||
|
||||
// login request
|
||||
if let Result::Ok(token) = api_call::<String, _>(
|
||||
&arg.home,
|
||||
"login",
|
||||
LoginParam {
|
||||
username: arg.username,
|
||||
password: password,
|
||||
},
|
||||
)
|
||||
.as_result()
|
||||
{
|
||||
// save token to config
|
||||
CtlConfig {
|
||||
home: arg.home,
|
||||
token,
|
||||
}
|
||||
.save();
|
||||
} else {
|
||||
println!("Login failed");
|
||||
}
|
||||
}
|
2
src/sheepctl_core/mod.rs
Normal file
2
src/sheepctl_core/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod args;
|
||||
pub mod cmd;
|
|
@ -8,14 +8,7 @@ use crate::{
|
|||
};
|
||||
|
||||
use super::args::JoinCommand;
|
||||
|
||||
fn domain(host: &str) -> String {
|
||||
if host.starts_with("http") {
|
||||
return host.to_string();
|
||||
} else {
|
||||
format!("https://{host}")
|
||||
}
|
||||
}
|
||||
use crate::api::domain;
|
||||
|
||||
/// Join a herd as client
|
||||
pub fn join(conf: JoinCommand) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue