Add basic JWT authentication for app API
This commit is contained in:
parent
7ad2ed8ff1
commit
3825263fa3
14 changed files with 289 additions and 40 deletions
89
Cargo.lock
generated
89
Cargo.lock
generated
|
@ -311,9 +311,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.82"
|
version = "1.0.83"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01"
|
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -986,8 +986,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1414,6 +1416,21 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "9.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"js-sys",
|
||||||
|
"pem",
|
||||||
|
"ring 0.17.7",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1422,9 +1439,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.147"
|
version = "0.2.151"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linereader"
|
name = "linereader"
|
||||||
|
@ -1890,6 +1907,16 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
@ -2225,12 +2252,26 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"spin",
|
"spin 0.5.2",
|
||||||
"untrusted",
|
"untrusted 0.7.1",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
"spin 0.9.8",
|
||||||
|
"untrusted 0.9.0",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rmp"
|
name = "rmp"
|
||||||
version = "0.8.12"
|
version = "0.8.12"
|
||||||
|
@ -2305,7 +2346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
|
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring",
|
"ring 0.16.20",
|
||||||
"sct",
|
"sct",
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
@ -2349,8 +2390,8 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring 0.16.20",
|
||||||
"untrusted",
|
"untrusted 0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2533,6 +2574,18 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
@ -2601,6 +2654,12 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlite"
|
name = "sqlite"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
|
@ -3011,9 +3070,11 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
|
"chrono",
|
||||||
"cozo",
|
"cozo",
|
||||||
"i2p",
|
"i2p",
|
||||||
"itertools 0.12.0",
|
"itertools 0.12.0",
|
||||||
|
"jsonwebtoken",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -3076,6 +3137,12 @@ version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -3219,8 +3286,8 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring 0.16.20",
|
||||||
"untrusted",
|
"untrusted 0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -7,7 +7,9 @@ edition = "2021"
|
||||||
anyhow = "1.0.71"
|
anyhow = "1.0.71"
|
||||||
async-trait = "0.1.73"
|
async-trait = "0.1.73"
|
||||||
axum = { version = "0.7.2", features = [ "macros" ] }
|
axum = { version = "0.7.2", features = [ "macros" ] }
|
||||||
|
chrono = "0.4.31"
|
||||||
itertools = "0.12.0"
|
itertools = "0.12.0"
|
||||||
|
jsonwebtoken = "9.2.0"
|
||||||
serde = { version = "1.0.166", features = [ "derive" ] }
|
serde = { version = "1.0.166", features = [ "derive" ] }
|
||||||
serde_json = "1.0.99"
|
serde_json = "1.0.99"
|
||||||
serde_with = "3.3.0"
|
serde_with = "3.3.0"
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use tokio::{net::TcpListener, task::JoinHandle};
|
use tokio::{net::TcpListener, task::JoinHandle};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{state::ApiState, config::ApiConfig};
|
use crate::{state::ApiState, config::ApiConfig};
|
||||||
|
|
||||||
mod v0;
|
mod v0;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct AppId(Uuid);
|
||||||
|
|
||||||
|
impl AppId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AppId { 0: Uuid::new_v4() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AppDescription {
|
||||||
|
pub name: String,
|
||||||
|
pub desc_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
server_thread: JoinHandle<()>,
|
server_thread: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
67
src/api/v0/app.rs
Normal file
67
src/api/v0/app.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use axum::{Extension, extract::Request, body::Body, middleware::Next, response::{IntoResponse, Response}, http::{header::AUTHORIZATION, StatusCode}, Json};
|
||||||
|
use jsonwebtoken::{decode, Header};
|
||||||
|
use tracing::{debug, warn, error};
|
||||||
|
|
||||||
|
use crate::{state::ApiState, api::{AppId, AppDescription}};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct JWTClaims {
|
||||||
|
sub: AppId,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub(super) async fn auth(s: Extension<Arc<ApiState>>, mut request: Request<Body>, next: Next) -> Response {
|
||||||
|
if let Some(auth_header) = request.headers().get(AUTHORIZATION) {
|
||||||
|
if let Ok(header_string) = auth_header.to_str() {
|
||||||
|
if header_string.starts_with("Bearer") {
|
||||||
|
if let Ok(token) = decode::<JWTClaims>(
|
||||||
|
&header_string[7..],
|
||||||
|
s.jwt_decoding_key(),
|
||||||
|
s.jwt_validation(),
|
||||||
|
) {
|
||||||
|
if let Ok(true) = s.app_exists(&token.claims.sub) {
|
||||||
|
debug!("Authentication for {:?} succeeded.", &token.claims.sub);
|
||||||
|
request.extensions_mut().insert(token.claims.sub);
|
||||||
|
return next.run(request).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Authentication failed for request '{:?}'.", &request);
|
||||||
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub(super) async fn register(s: Extension<Arc<ApiState>>, Json(data): Json<AppDescription>) -> Response {
|
||||||
|
// Maybe ask for consent by user
|
||||||
|
|
||||||
|
// If user wants registration, proceed
|
||||||
|
let result = s.add_app(&data);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(id) => {
|
||||||
|
// Build JWT, respond
|
||||||
|
let jwt = jsonwebtoken::encode(
|
||||||
|
&Header::default(),
|
||||||
|
&JWTClaims {sub: id},
|
||||||
|
&s.jwt_encoding_key(),
|
||||||
|
);
|
||||||
|
match jwt {
|
||||||
|
Ok(token) => (StatusCode::OK, token).into_response(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to encode token: {:?}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to register new application! {:?}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,12 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Router, routing::{put, get}, extract::{Path, Json}, Extension, response::{IntoResponse, Response}, http::StatusCode};
|
use axum::{extract::{Path, Json}, Extension, response::{IntoResponse, Response}, http::StatusCode};
|
||||||
use tracing::{warn, debug};
|
use tracing::{warn, debug};
|
||||||
|
|
||||||
use crate::state::{types::{ElementId, ElementContent}, ApiState};
|
use crate::state::{types::{ElementId, ElementContent}, ApiState};
|
||||||
|
|
||||||
|
|
||||||
|
pub(super) async fn get(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
|
||||||
|
|
||||||
pub fn get_router(state: ApiState) -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/element", put(create_element))
|
|
||||||
.route("/element/:id", get(get_element).post(set_element).delete(remove_element))
|
|
||||||
.layer(Extension(Arc::new(state)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
|
|
||||||
let element = s.get_element(&id);
|
let element = s.get_element(&id);
|
||||||
match element {
|
match element {
|
||||||
Ok(el) => (StatusCode::OK, Json{0: el}).into_response(),
|
Ok(el) => (StatusCode::OK, Json{0: el}).into_response(),
|
||||||
|
@ -26,7 +17,7 @@ async fn get_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_element(s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
|
pub(super) async fn create(s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
|
||||||
let element_id = s.create_element(&content);
|
let element_id = s.create_element(&content);
|
||||||
debug!("{:?}", element_id);
|
debug!("{:?}", element_id);
|
||||||
match element_id {
|
match element_id {
|
||||||
|
@ -35,7 +26,7 @@ async fn create_element(s: Extension<Arc<ApiState>>, Json(content): Json<Element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
|
pub(super) async fn set(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>, Json(content): Json<ElementContent>) -> Response {
|
||||||
let res = s.write_element_content(&id, &content);
|
let res = s.write_element_content(&id, &content);
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => StatusCode::OK.into_response(),
|
Ok(_) => StatusCode::OK.into_response(),
|
||||||
|
@ -43,7 +34,7 @@ async fn set_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>, Jso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_element(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
|
pub(super) async fn remove(Path(id): Path<ElementId>, s: Extension<Arc<ApiState>>) -> Response {
|
||||||
let res = s.remove_element(&id);
|
let res = s.remove_element(&id);
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => StatusCode::OK.into_response(),
|
Ok(_) => StatusCode::OK.into_response(),
|
20
src/api/v0/mod.rs
Normal file
20
src/api/v0/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{Router, routing::{put, get}, Extension, middleware::from_fn};
|
||||||
|
|
||||||
|
use crate::state::ApiState;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod element;
|
||||||
|
|
||||||
|
pub fn get_router(state: ApiState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
// authenticated routes
|
||||||
|
.route("/element", put(element::create))
|
||||||
|
.route("/element/:id", get(element::get).post(element::set).delete(element::remove))
|
||||||
|
.layer(from_fn(app::auth))
|
||||||
|
// public / unauthenticated routes
|
||||||
|
.route("/app/register", put(app::register))
|
||||||
|
.layer(Extension(Arc::new(state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ use serde::{Serialize, Deserialize};
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub i2p_private_key: Option<String>,
|
pub i2p_private_key: Option<String>,
|
||||||
pub api_config: ApiConfig,
|
pub api_config: ApiConfig,
|
||||||
|
pub jwt_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config { i2p_private_key: None, api_config: Default::default() }
|
Config { i2p_private_key: None, api_config: Default::default(), jwt_secret: "insecuresecret".to_string() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ use i2p::net::I2pSocketAddr;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use state::{State, types::{MessageId, PeerId}, CommState, ApiState};
|
use state::{State, types::{MessageId, PeerId}, CommState, ApiState};
|
||||||
|
|
||||||
mod api;
|
pub mod api;
|
||||||
pub mod comm;
|
pub mod comm;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
@ -31,7 +31,7 @@ impl Ubisync {
|
||||||
let comm_handle = Arc::new(CommHandle::new(CommState::new(state.clone()), config)?);
|
let comm_handle = Arc::new(CommHandle::new(CommState::new(state.clone()), config)?);
|
||||||
state.set_comm_handle(comm_handle.clone());
|
state.set_comm_handle(comm_handle.clone());
|
||||||
|
|
||||||
let api = Arc::new(ApiBuilder::from(config.api_config.clone()).build(ApiState::new(state.clone())).await);
|
let api = Arc::new(ApiBuilder::from(config.api_config.clone()).build(ApiState::new(state.clone(), &config.jwt_secret)).await);
|
||||||
|
|
||||||
comm_handle.run().await;
|
comm_handle.run().await;
|
||||||
Ok(Ubisync {
|
Ok(Ubisync {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use cozo::DbInstance;
|
use cozo::DbInstance;
|
||||||
|
use jsonwebtoken::{EncodingKey, DecodingKey, Validation};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{state::{types::ElementId, queries}, comm::messages::MessageContent};
|
use crate::{state::{types::ElementId, queries}, comm::messages::MessageContent, api::{AppDescription, AppId}};
|
||||||
|
|
||||||
use super::{State, types::{ElementContent, Element}};
|
use super::{State, types::{ElementContent, Element}};
|
||||||
|
|
||||||
|
@ -11,11 +13,34 @@ use super::{State, types::{ElementContent, Element}};
|
||||||
|
|
||||||
pub struct ApiState {
|
pub struct ApiState {
|
||||||
state: Arc<State>,
|
state: Arc<State>,
|
||||||
|
jwt_encoding_key: EncodingKey,
|
||||||
|
jwt_decoding_key: DecodingKey,
|
||||||
|
jwt_validation: Validation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiState {
|
impl ApiState {
|
||||||
pub fn new(state: Arc<State>) -> Self {
|
pub fn new(state: Arc<State>, jwt_secret: &str) -> Self {
|
||||||
ApiState { state: state }
|
let mut validation = Validation::default();
|
||||||
|
validation.set_required_spec_claims(&vec!["sub"]);
|
||||||
|
ApiState {
|
||||||
|
state: state,
|
||||||
|
jwt_encoding_key: EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||||
|
jwt_decoding_key: DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||||
|
jwt_validation: validation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_app(&self, description: &AppDescription) -> anyhow::Result<AppId> {
|
||||||
|
let id = AppId::new();
|
||||||
|
let last_access = Utc::now();
|
||||||
|
queries::apps::add(self.db(), &id, &last_access, &description.name, &description.desc_text)?;
|
||||||
|
debug!("Successfully added app");
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_exists(&self, id: &AppId) -> anyhow::Result<bool> {
|
||||||
|
queries::apps::exists(self.db(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> {
|
pub fn create_element(&self, content: &ElementContent) -> anyhow::Result<ElementId> {
|
||||||
|
@ -50,6 +75,18 @@ impl ApiState {
|
||||||
fn db(&self) -> &DbInstance {
|
fn db(&self) -> &DbInstance {
|
||||||
&self.state.db
|
&self.state.db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn jwt_encoding_key(&self) -> &EncodingKey {
|
||||||
|
&self.jwt_encoding_key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jwt_decoding_key(&self) -> &DecodingKey {
|
||||||
|
&self.jwt_decoding_key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jwt_validation(&self) -> &Validation {
|
||||||
|
&self.jwt_validation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -61,7 +98,7 @@ mod tests {
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_element_create() {
|
async fn test_element_create() {
|
||||||
tracing_subscriber::fmt().pretty().init();
|
tracing_subscriber::fmt().pretty().init();
|
||||||
let state = ApiState::new(State::new().await.unwrap());
|
let state = ApiState::new(State::new().await.unwrap(), "abcdabcdabcdabcdabcdabcdabcdabcd");
|
||||||
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
|
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
|
||||||
let el = state.get_element(&id).unwrap();
|
let el = state.get_element(&id).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -74,7 +111,7 @@ mod tests {
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_element_write() {
|
async fn test_element_write() {
|
||||||
tracing_subscriber::fmt().pretty().init();
|
tracing_subscriber::fmt().pretty().init();
|
||||||
let state = ApiState::new(State::new().await.unwrap());
|
let state = ApiState::new(State::new().await.unwrap(), "abcdabcdabcdabcdabcdabcdabcdabcd");
|
||||||
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
|
let id = state.create_element(&ElementContent::Text("Test-text".to_string())).unwrap();
|
||||||
state.write_element_content(&id,&ElementContent::Text("Test-text 2".to_string())).unwrap();
|
state.write_element_content(&id,&ElementContent::Text("Test-text 2".to_string())).unwrap();
|
||||||
let el = state.get_element(&id).unwrap();
|
let el = state.get_element(&id).unwrap();
|
||||||
|
|
36
src/state/queries/apps.rs
Normal file
36
src/state/queries/apps.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use anyhow::{bail, Error};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use cozo::{DbInstance, DataValue, Num};
|
||||||
|
|
||||||
|
use crate::{run_query, api::AppId};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn add(db: &DbInstance, id: &AppId, last_access: &DateTime<Utc>, name: &str, description: &str) -> anyhow::Result<()> {
|
||||||
|
let params = vec![
|
||||||
|
("id", DataValue::Str(serde_json::to_string(&id).unwrap().into())),
|
||||||
|
("last_access", DataValue::Num(Num::Int(last_access.timestamp()))),
|
||||||
|
("name", DataValue::Str(name.into())),
|
||||||
|
("description", DataValue::Str(description.into())),
|
||||||
|
];
|
||||||
|
|
||||||
|
match run_query!(&db, ":insert apps {id => last_access, name, description}", params, cozo::ScriptMutability::Mutable) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(report) => bail!(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(db: &DbInstance, id: &AppId) -> anyhow::Result<bool> {
|
||||||
|
let mut params = BTreeMap::new();
|
||||||
|
params.insert("id".to_string(), DataValue::Str(serde_json::to_string(&id)?.into()));
|
||||||
|
|
||||||
|
let result = db.run_script("?[name] := *apps[$id, last_access, name, description]", params, cozo::ScriptMutability::Immutable);
|
||||||
|
|
||||||
|
if let Ok(rows) = result {
|
||||||
|
return Ok(rows.rows.len() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::msg("Could not check whether app is registered"))
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
|
pub mod apps;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
|
|
||||||
pub mod peers;
|
pub mod peers;
|
||||||
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! build_query {
|
macro_rules! build_query {
|
||||||
($payload:expr, $params:expr) => {
|
($payload:expr, $params:expr) => {
|
||||||
{
|
{
|
||||||
|
use cozo::DataValue;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
// Build parameters map
|
// Build parameters map
|
||||||
let mut params_map: BTreeMap<String, DataValue> = Default::default();
|
let mut params_map: BTreeMap<String, DataValue> = Default::default();
|
||||||
let mut parameters_init = String::new();
|
let mut parameters_init = String::new();
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use cozo::{DbInstance, DataValue, ScriptMutability};
|
use cozo::{DbInstance, DataValue, ScriptMutability};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,13 @@ use cozo::DbInstance;
|
||||||
pub fn add_schema(db: &DbInstance) -> anyhow::Result<()> {
|
pub fn add_schema(db: &DbInstance) -> anyhow::Result<()> {
|
||||||
let params = BTreeMap::new();
|
let params = BTreeMap::new();
|
||||||
match db.run_script("
|
match db.run_script("
|
||||||
|
{:create apps {
|
||||||
|
id: String,
|
||||||
|
=>
|
||||||
|
last_access: Int,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
}}
|
||||||
{:create peers {
|
{:create peers {
|
||||||
id: String,
|
id: String,
|
||||||
=>
|
=>
|
||||||
|
|
10
tests/api.rs
10
tests/api.rs
|
@ -1,7 +1,7 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tracing::{Level, debug};
|
use tracing::{Level, debug};
|
||||||
use ubisync::{Ubisync, config::Config, state::types::{ElementContent, Element, ElementId}};
|
use ubisync::{Ubisync, config::Config, state::types::{ElementContent, Element, ElementId}, api::AppDescription};
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
@ -15,9 +15,13 @@ async fn two_nodes_element_creation() {
|
||||||
ubi1.add_peer_from_id(ubi2.get_destination().unwrap().into()).unwrap();
|
ubi1.add_peer_from_id(ubi2.get_destination().unwrap().into()).unwrap();
|
||||||
|
|
||||||
let http_client = reqwest::Client::new();
|
let http_client = reqwest::Client::new();
|
||||||
|
let register_response = http_client.put("http://localhost:9981/v0/app/register").json(&AppDescription{name: "Test".to_string(), desc_text: "desc".to_string()}).send().await.unwrap();
|
||||||
|
let jwt1 = register_response.text().await.expect("Couldn't fetch token from response");
|
||||||
|
let register_response = http_client.put("http://localhost:9982/v0/app/register").json(&AppDescription{name: "Test".to_string(), desc_text: "desc".to_string()}).send().await.unwrap();
|
||||||
|
let jwt2 = register_response.text().await.expect("Couldn't fetch token from response");
|
||||||
|
|
||||||
let test_element_content = ElementContent::Text("Text".to_string());
|
let test_element_content = ElementContent::Text("Text".to_string());
|
||||||
let put_resp = http_client.put(&format!("http://localhost:9981/v0/element")).json(&test_element_content).send().await.unwrap();
|
let put_resp = http_client.put(&format!("http://localhost:9981/v0/element")).json(&test_element_content).header("Authorization", &format!("Bearer {}", &jwt1)).send().await.unwrap();
|
||||||
debug!("{:?}", &put_resp);
|
debug!("{:?}", &put_resp);
|
||||||
let put_resp_text = put_resp.text().await.expect("No put response body");
|
let put_resp_text = put_resp.text().await.expect("No put response body");
|
||||||
debug!("{}", put_resp_text);
|
debug!("{}", put_resp_text);
|
||||||
|
@ -25,7 +29,7 @@ async fn two_nodes_element_creation() {
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(3000)).await;
|
tokio::time::sleep(Duration::from_millis(3000)).await;
|
||||||
|
|
||||||
let get_resp = http_client.get(&format!("http://localhost:9982/v0/element/{}", Into::<String>::into(&id))).send().await.expect("Get request failed");
|
let get_resp = http_client.get(&format!("http://localhost:9982/v0/element/{}", Into::<String>::into(&id))).header("Authorization", &format!("Bearer {}", &jwt2)).send().await.expect("Get request failed");
|
||||||
let get_resp_text = get_resp.text().await.expect("No get request body");
|
let get_resp_text = get_resp.text().await.expect("No get request body");
|
||||||
debug!("{}", get_resp_text);
|
debug!("{}", get_resp_text);
|
||||||
let received_element = serde_json::from_str::<Element>(&get_resp_text).expect("Could not deserialize Element");
|
let received_element = serde_json::from_str::<Element>(&get_resp_text).expect("Could not deserialize Element");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue