Authorization
Added authorization system. All app endpoints are now bearer token verified. Added endpoints for initial setup, authorization, and updating passwords.
This commit is contained in:
parent
80d14a9688
commit
6c9ea1f774
|
@ -424,9 +424,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-rc.1"
|
||||
version = "4.0.0-rc.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7bfa913583b6cfe5f0d1588e752bcf6b107556422624fefdaf99c40ba2b7f16"
|
||||
checksum = "73170d019de2d82c0d826c1f315c3106134bd764e9247505ba6f0d78d22dfe9e"
|
||||
dependencies = [
|
||||
"actix-codec 0.4.2",
|
||||
"actix-http 3.0.0-rc.1",
|
||||
|
@ -484,6 +484,21 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-httpauth"
|
||||
version = "0.6.0-beta.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95ff332baee0f5ce55d94c690acb33a390d04a458d4bbd40a593cf847ac74ba4"
|
||||
dependencies = [
|
||||
"actix-service 2.0.2",
|
||||
"actix-utils 3.0.0",
|
||||
"actix-web 4.0.0-rc.2",
|
||||
"base64",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix_derive"
|
||||
version = "0.6.0"
|
||||
|
@ -796,6 +811,17 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f691e63585950d8c1c43644d11bab9073e40f5060dd2822734ae7c3dc69a3a80"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom 0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
|
@ -861,6 +887,17 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.3.3"
|
||||
|
@ -1032,6 +1069,15 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
|
@ -1472,7 +1518,7 @@ dependencies = [
|
|||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project 1.0.10",
|
||||
"spin",
|
||||
"spin 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2032,6 +2078,20 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "8.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "012bb02250fdd38faa5feee63235f7a459974440b9b57593822414c31f92839e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"pem",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
|
@ -2337,6 +2397,17 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
|
@ -2519,6 +2590,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
|
@ -2750,6 +2830,15 @@ version = "1.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quickcheck"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
|
||||
dependencies = [
|
||||
"rand 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
|
@ -2900,6 +2989,21 @@ dependencies = [
|
|||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin 0.5.2",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusb"
|
||||
version = "0.8.1"
|
||||
|
@ -3267,6 +3371,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745"
|
||||
dependencies = [
|
||||
"num-bigint 0.4.3",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.11.2"
|
||||
|
@ -3317,6 +3433,12 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.2"
|
||||
|
@ -3378,7 +3500,7 @@ dependencies = [
|
|||
"libsqlite3-sys",
|
||||
"log",
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"num-bigint 0.3.3",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
|
@ -3693,6 +3815,7 @@ dependencies = [
|
|||
"itoa 1.0.1",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"quickcheck",
|
||||
"time-macros 0.2.3",
|
||||
]
|
||||
|
||||
|
@ -3838,9 +3961,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.29"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
|
||||
checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"log",
|
||||
|
@ -3851,11 +3974,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-actix-web"
|
||||
version = "0.5.0-rc.1"
|
||||
version = "0.5.0-rc.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0066b8102bdd05325d02966fac8b5e360eafd8dcdf81006c4d30f3543f488714"
|
||||
checksum = "eba9ba7b978b6fd2838d47cd459cd4e23451bdd571448509e71476531b9306f2"
|
||||
dependencies = [
|
||||
"actix-web 4.0.0-rc.1",
|
||||
"actix-web 4.0.0-rc.2",
|
||||
"pin-project 1.0.10",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
|
@ -3864,9 +3987,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
|
||||
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3875,11 +3998,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
|
||||
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3905,9 +4029,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22"
|
||||
checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"lazy_static",
|
||||
|
@ -4068,6 +4192,12 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.2"
|
||||
|
@ -4097,9 +4227,14 @@ dependencies = [
|
|||
"actix",
|
||||
"actix-cors",
|
||||
"actix-rt 2.6.0",
|
||||
"actix-web 4.0.0-rc.1",
|
||||
"actix-web 4.0.0-rc.2",
|
||||
"actix-web-httpauth",
|
||||
"base64",
|
||||
"bcrypt",
|
||||
"cargo-embed",
|
||||
"chrono",
|
||||
"jsonwebtoken",
|
||||
"rand 0.8.4",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -4110,6 +4245,12 @@ dependencies = [
|
|||
"tracing-unwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.0.0-alpha.8"
|
||||
|
|
|
@ -7,9 +7,9 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
sea-orm = { version = "0.5.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros", "mock"], default-features = false }
|
||||
tracing = "0.1.29"
|
||||
tracing = "0.1.30"
|
||||
tracing-unwrap = "0.9.2"
|
||||
tracing-subscriber = { version = "0.3.7", features=["fmt"] }
|
||||
tracing-subscriber = { version = "0.3.8", features = ["fmt"] }
|
||||
actix = "0.12.0"
|
||||
actix-cors = "0.5.4"
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
@ -20,3 +20,8 @@ actix-rt = "2.6.0"
|
|||
tracing-test = "0.2.1"
|
||||
tracing-actix-web = "0.5.0-rc.1"
|
||||
cargo-embed = "0.12.0"
|
||||
bcrypt = "0.10.1"
|
||||
actix-web-httpauth = "0.6.0-beta.7"
|
||||
jsonwebtoken = "8.0.1"
|
||||
rand = "0.8.4"
|
||||
base64 = "0.13.0"
|
||||
|
|
|
@ -26,3 +26,7 @@ CREATE TABLE bookmark_category (
|
|||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
glyph TEXT
|
||||
);
|
||||
CREATE TABLE settings (
|
||||
setting_name TEXT NOT NULL,
|
||||
setting_value TEXT NOT NULL
|
||||
);
|
||||
|
|
|
@ -135,7 +135,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -159,7 +159,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
|
@ -185,7 +185,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -224,7 +224,7 @@ mod tests {
|
|||
|
||||
model.category_name = "Another name".into();
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
|
@ -258,14 +258,17 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}",
|
||||
model.id
|
||||
))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
assert_eq!(resp.status(), 200);
|
||||
let status = resp.status();
|
||||
let body = test::read_body(resp).await;
|
||||
assert_eq!(body, "");
|
||||
assert_eq!(status, 200);
|
||||
assert_eq!(
|
||||
application_category::Entity::find()
|
||||
.count(&state.db)
|
||||
|
@ -312,7 +315,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/application_categories/{}/applications",
|
||||
category.id
|
||||
))
|
||||
|
|
|
@ -123,7 +123,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -145,7 +145,8 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -171,7 +172,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/applications")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -207,10 +208,12 @@ mod tests {
|
|||
|
||||
model.url = "http://updated.com".into();
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
||||
let resp = call_endpoint!(req, state);
|
||||
assert_eq!(resp.status(), 200);
|
||||
let mut data = get_response!(resp, crate::entity::application::Model);
|
||||
|
@ -239,7 +242,8 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
use std::ops::Add;
|
||||
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::api::api_prelude::*;
|
||||
use crate::auth::{get_secret, set_password, verify_password};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[post("authorize")]
|
||||
pub async fn authorize(
|
||||
state: web::Data<AppState>,
|
||||
req: web::Json<AuthRequest>,
|
||||
) -> Result<HttpResponse> {
|
||||
if verify_password(&state.db, &req.password).await {
|
||||
let secret = get_secret(&state.db).await?;
|
||||
let exp = chrono::Utc::now()
|
||||
.add(chrono::Duration::days(30))
|
||||
.timestamp() as usize;
|
||||
let token = encode(
|
||||
&Header::default(),
|
||||
&crate::auth::AuthClaims { exp },
|
||||
&EncodingKey::from_secret(&secret),
|
||||
)
|
||||
.map_err(|_| Error::unauthorized())?;
|
||||
Ok(HttpResponse::Ok().json(AuthResponse { token }))
|
||||
} else {
|
||||
Err(Error::unauthorized())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdatePasswordRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[post("password")]
|
||||
pub async fn update_password(
|
||||
state: web::Data<AppState>,
|
||||
req: web::Json<UpdatePasswordRequest>,
|
||||
) -> Result<HttpResponse> {
|
||||
set_password(&state.db, &req.password).await?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetupRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[post("setup")]
|
||||
pub async fn initial_setup(
|
||||
state: web::Data<AppState>,
|
||||
req: web::Json<SetupRequest>,
|
||||
) -> Result<HttpResponse> {
|
||||
let count = setting::Entity::find()
|
||||
.filter(setting::Column::SettingName.eq("password_hash".to_string()))
|
||||
.count(&state.db)
|
||||
.await?;
|
||||
if count == 0 {
|
||||
set_password(&state.db, &req.password).await?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
crate::error::ErrorCode::UnAuthorized,
|
||||
"Setup has already been run",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
api::test_prelude::*,
|
||||
auth::{get_secret, AuthClaims},
|
||||
};
|
||||
use actix_web::http::Method;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_authorize() -> Result<()> {
|
||||
let state = setup_state().await?;
|
||||
|
||||
let test_password = "sshh a secret".to_string();
|
||||
let test_hash = bcrypt::hash(test_password.clone(), bcrypt::DEFAULT_COST).unwrap();
|
||||
|
||||
setting::ActiveModel {
|
||||
id: NotSet,
|
||||
setting_name: Set("password_hash".into()),
|
||||
setting_value: Set(test_hash),
|
||||
}
|
||||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/authorize")
|
||||
.method(Method::POST)
|
||||
.set_json(super::AuthRequest {
|
||||
password: test_password,
|
||||
})
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
||||
let jwt_secret = get_secret(&state.db).await?;
|
||||
|
||||
let status = resp.status();
|
||||
assert_eq!(status, 200);
|
||||
let data = get_response!(resp, super::AuthResponse);
|
||||
assert_eq!(status, 200);
|
||||
|
||||
let decoded = jsonwebtoken::decode::<AuthClaims>(
|
||||
&data.token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(&jwt_secret),
|
||||
&jsonwebtoken::Validation::default(),
|
||||
);
|
||||
|
||||
decoded.expect("Decode failure");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -135,7 +135,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -156,7 +156,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req =
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
|
@ -180,7 +180,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmark_categories")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -214,7 +214,7 @@ mod tests {
|
|||
|
||||
model.category_name = "Another name".into();
|
||||
|
||||
let req =
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
|
@ -246,7 +246,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req =
|
||||
let mut req =
|
||||
actix_web::test::TestRequest::with_uri(&format!("/bookmark_categories/{}", model.id))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
|
@ -293,7 +293,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!(
|
||||
"/bookmark_categories/{}/bookmarks",
|
||||
category.id
|
||||
))
|
||||
|
|
|
@ -116,7 +116,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -138,7 +138,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
.method(Method::GET)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
@ -162,7 +162,7 @@ mod tests {
|
|||
|
||||
let state = setup_state().await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
let mut req = actix_web::test::TestRequest::with_uri("/bookmarks")
|
||||
.method(Method::POST)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -198,7 +198,7 @@ mod tests {
|
|||
|
||||
model.url = "http://updated.com".into();
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
.method(Method::PUT)
|
||||
.set_json(model.clone())
|
||||
.to_request();
|
||||
|
@ -230,7 +230,7 @@ mod tests {
|
|||
.insert(&state.db)
|
||||
.await?;
|
||||
|
||||
let req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
let mut req = actix_web::test::TestRequest::with_uri(&format!("/bookmarks/{}", model.id))
|
||||
.method(Method::DELETE)
|
||||
.to_request();
|
||||
let resp = call_endpoint!(req, state);
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use actix_web::Scope;
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{api, auth};
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(test)]
|
||||
macro_rules! call_endpoint {
|
||||
|
@ -17,13 +21,30 @@ macro_rules! call_endpoint {
|
|||
// tracing::subscriber::set_global_default(subscriber)
|
||||
// .expect("Unable to set a global collector");
|
||||
|
||||
let jwt_secret = crate::auth::get_secret(&$state.db).await?;
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::default(),
|
||||
&crate::auth::AuthClaims {
|
||||
exp: 10_000_000_000,
|
||||
},
|
||||
&jsonwebtoken::EncodingKey::from_secret(&jwt_secret),
|
||||
)
|
||||
.map_err(|e| {
|
||||
crate::error::Error::new(
|
||||
crate::error::ErrorCode::Internal,
|
||||
&format!("Token error: {} ", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
$req.headers_mut().append(
|
||||
actix_web::http::header::AUTHORIZATION,
|
||||
actix_web::http::header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
|
||||
);
|
||||
|
||||
let a = App::new()
|
||||
.wrap(tracing_actix_web::TracingLogger::default())
|
||||
.app_data($state.clone())
|
||||
.service(crate::api::applications::routes())
|
||||
.service(crate::api::application_categories::routes())
|
||||
.service(crate::api::bookmarks::routes())
|
||||
.service(crate::api::bookmark_categories::routes());
|
||||
.service(routes());
|
||||
let app = actix_web::test::init_service(a).await;
|
||||
let resp = actix_web::test::call_service(&app, $req).await;
|
||||
resp
|
||||
|
@ -40,6 +61,7 @@ macro_rules! get_response {
|
|||
|
||||
pub mod application_categories;
|
||||
pub mod applications;
|
||||
pub mod authorization;
|
||||
pub mod bookmark_categories;
|
||||
pub mod bookmarks;
|
||||
|
||||
|
@ -51,14 +73,17 @@ mod api_prelude {
|
|||
pub use actix_web::{delete, get, post, put, web, Error, HttpResponse, Scope};
|
||||
pub use sea_orm::prelude::*;
|
||||
pub use sea_orm::{NotSet, Set};
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_prelude {
|
||||
pub mod test_prelude {
|
||||
pub use super::ListObjects;
|
||||
use crate::auth;
|
||||
pub use crate::entity::*;
|
||||
pub use crate::AppState;
|
||||
|
||||
pub use super::routes;
|
||||
pub use crate::error::Result;
|
||||
pub use actix_web::dev::ServiceResponse;
|
||||
pub use actix_web::{test, web, App};
|
||||
|
@ -87,6 +112,12 @@ mod test_prelude {
|
|||
|
||||
let stmt: TableCreateStatement = schema.create_table_from_entity(bookmark_category::Entity);
|
||||
db.execute(db.get_database_backend().build(&stmt)).await?;
|
||||
|
||||
let stmt: TableCreateStatement = schema.create_table_from_entity(setting::Entity);
|
||||
db.execute(db.get_database_backend().build(&stmt)).await?;
|
||||
|
||||
auth::generate_secret(&db).await?;
|
||||
|
||||
Ok(actix_web::web::Data::new(AppState { db }))
|
||||
}
|
||||
}
|
||||
|
@ -106,3 +137,19 @@ impl<T: Serialize> ListObjects<T> {
|
|||
Self { items, total }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Scope {
|
||||
let auth_handler = HttpAuthentication::bearer(auth::validator);
|
||||
let protected_routes = Scope::new("")
|
||||
.wrap(auth_handler)
|
||||
.service(api::applications::routes())
|
||||
.service(api::application_categories::routes())
|
||||
.service(api::bookmarks::routes())
|
||||
.service(api::bookmark_categories::routes())
|
||||
.service(api::authorization::update_password);
|
||||
|
||||
Scope::new("")
|
||||
.service(api::authorization::authorize)
|
||||
.service(api::authorization::initial_setup)
|
||||
.service(protected_routes)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
use crate::{
|
||||
entity::setting,
|
||||
error::{Error, ErrorCode},
|
||||
AppState,
|
||||
};
|
||||
use actix_web::{dev::ServiceRequest, web::Data};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use bcrypt::verify;
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
use sea_orm::{prelude::*, ActiveValue::NotSet, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct AuthClaims {
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
/// JWT validation middleware. Used to wrap protected routes in Actix .
|
||||
pub async fn validator(
|
||||
req: ServiceRequest,
|
||||
credentials: BearerAuth,
|
||||
) -> Result<ServiceRequest, actix_web::error::Error> {
|
||||
// Retrieve shared app state
|
||||
let state = req
|
||||
.app_data::<Data<AppState>>()
|
||||
.ok_or_else(|| Error::new(ErrorCode::Internal, "Could not retrieve state"))?;
|
||||
// Get the secret from the database
|
||||
let secret = get_secret(&state.db).await?;
|
||||
// decode the JWT token from the header
|
||||
let decoded = decode::<AuthClaims>(
|
||||
credentials.token(),
|
||||
&DecodingKey::from_secret(&secret),
|
||||
&Validation::default(),
|
||||
);
|
||||
// We have no users or anything else to do, if the token is properly formed we are good to go.
|
||||
match decoded {
|
||||
Ok(_) => Ok(req),
|
||||
Err(e) => Err(Error::new(ErrorCode::UnAuthorized, &e.to_string()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the passed password against the password hash stored in the settings table in the database.
|
||||
pub async fn verify_password(db: &DatabaseConnection, password: &str) -> bool {
|
||||
let rec: Option<crate::entity::setting::Model> = crate::entity::setting::Entity::find()
|
||||
.filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string()))
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if let Some(rec) = rec {
|
||||
verify(password, &rec.setting_value).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_password(db: &DatabaseConnection, password: &str) -> crate::error::Result<()> {
|
||||
let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST)
|
||||
.map_err(|e| Error::new(ErrorCode::Internal, &format!("Hash error: {}", e)))?;
|
||||
let rec = crate::entity::setting::Entity::find()
|
||||
.filter(crate::entity::setting::Column::SettingName.eq("password_hash".to_string()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if let Some(rec) = rec {
|
||||
setting::ActiveModel {
|
||||
id: Set(rec.id),
|
||||
setting_value: Set(password_hash),
|
||||
setting_name: NotSet,
|
||||
}
|
||||
.update(db)
|
||||
.await?;
|
||||
} else {
|
||||
setting::ActiveModel {
|
||||
id: NotSet,
|
||||
setting_name: Set("password_hash".into()),
|
||||
setting_value: Set(password_hash),
|
||||
}
|
||||
.insert(db)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use rand::Rng;
|
||||
use tracing::instrument;
|
||||
#[instrument(skip_all)]
|
||||
/// Generate a new secret and store it to the settings table. This is used during intiial configuration.
|
||||
pub async fn generate_secret(db: &DatabaseConnection) -> crate::error::Result<()> {
|
||||
let b = rand::thread_rng().gen::<[u8; 32]>();
|
||||
setting::ActiveModel {
|
||||
id: NotSet,
|
||||
setting_name: Set("secret".to_string()),
|
||||
setting_value: Set(base64::encode(b)),
|
||||
}
|
||||
.insert(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
/// Retrieves the JWT secret from the database.
|
||||
pub async fn get_secret(db: &DatabaseConnection) -> crate::error::Result<Vec<u8>> {
|
||||
let rec = setting::Entity::find()
|
||||
.filter(setting::Column::SettingName.eq("secret".to_string()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if let Some(rec) = rec {
|
||||
if let Ok(b64) = base64::decode(rec.setting_value) {
|
||||
Ok(b64)
|
||||
} else {
|
||||
Err(Error::new(ErrorCode::Internal, "Could not decode secret"))
|
||||
}
|
||||
} else {
|
||||
Err(Error::new(ErrorCode::Internal, "No secret found"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
|
||||
use super::{get_secret, AuthClaims};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_tokens() {
|
||||
let state = crate::api::test_prelude::setup_state().await.unwrap();
|
||||
let jwt_secret = get_secret(&state.db).await.unwrap();
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::default(),
|
||||
&crate::auth::AuthClaims {
|
||||
exp: 10_000_000_000,
|
||||
},
|
||||
&jsonwebtoken::EncodingKey::from_secret(&jwt_secret),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let decoded = decode::<AuthClaims>(
|
||||
&token,
|
||||
&DecodingKey::from_secret(&jwt_secret),
|
||||
&Validation::default(),
|
||||
);
|
||||
|
||||
decoded.expect("Decode failure");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -49,7 +47,7 @@ impl Default for Model {
|
|||
app_name: Default::default(),
|
||||
url: Default::default(),
|
||||
description: Default::default(),
|
||||
active: Default::default(),
|
||||
active: true,
|
||||
glyph: Default::default(),
|
||||
application_category_id: Default::default(),
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use sea_orm::entity::prelude::*;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "bookmark")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
|
|
|
@ -4,6 +4,7 @@ use sea_orm::entity::prelude::*;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "bookmark_category")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod application;
|
||||
pub mod application_category;
|
||||
pub mod bookmark;
|
||||
pub mod bookmark_category;
|
||||
pub mod prelude;
|
||||
pub mod setting;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "setting")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: i32,
|
||||
pub setting_name: String,
|
||||
pub setting_value: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
panic!("No RelationDef")
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
13
src/error.rs
13
src/error.rs
|
@ -6,6 +6,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||
pub enum ErrorCode {
|
||||
NotFound,
|
||||
DatabaseError,
|
||||
UnAuthorized,
|
||||
Internal,
|
||||
}
|
||||
|
||||
|
@ -17,12 +18,24 @@ pub struct Error {
|
|||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(code: ErrorCode, message: &str) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
pub fn not_found() -> Self {
|
||||
Self {
|
||||
code: ErrorCode::NotFound,
|
||||
message: "Resource not found".to_string(),
|
||||
}
|
||||
}
|
||||
pub fn unauthorized() -> Self {
|
||||
Self {
|
||||
code: ErrorCode::NotFound,
|
||||
message: "Unauthorzed".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -4,6 +4,7 @@ use tracing::{info, instrument};
|
|||
use tracing_subscriber::prelude::*;
|
||||
|
||||
mod api;
|
||||
mod auth;
|
||||
mod entity;
|
||||
mod error;
|
||||
|
||||
|
@ -27,14 +28,8 @@ async fn main() {
|
|||
let state = web::Data::new(AppState { db });
|
||||
|
||||
info!("Starting http server on 8080");
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(state.clone())
|
||||
.service(api::applications::routes())
|
||||
.service(api::application_categories::routes())
|
||||
.service(api::bookmarks::routes())
|
||||
.service(api::bookmark_categories::routes())
|
||||
})
|
||||
|
||||
HttpServer::new(move || App::new().app_data(state.clone()).service(api::routes()))
|
||||
.bind("127.0.0.1:8080")
|
||||
.unwrap()
|
||||
.run()
|
||||
|
|
Loading…
Reference in New Issue