made login route for secret usage

This commit is contained in:
beckerinj
2022-12-02 00:22:04 -05:00
parent 0d4bacd892
commit 4e518d90ad
6 changed files with 294 additions and 1 deletions

3
Cargo.lock generated
View File

@@ -1246,6 +1246,9 @@ dependencies = [
"anyhow",
"monitor_types 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest",
"serde",
"serde_derive",
"serde_json",
]
[[package]]

View File

@@ -12,6 +12,7 @@ use types::CoreConfig;
mod github;
mod jwt;
mod local;
mod secret;
pub use self::jwt::{JwtClaims, JwtClient, JwtExtension, RequestUser, RequestUserExtension};
@@ -19,6 +20,7 @@ pub fn router(config: &CoreConfig) -> Router {
Router::new()
.nest("/local", local::router())
.nest("/github", github::router(config))
.nest("/secret", secret::router())
}
pub async fn auth_request(

57
core/src/auth/secret.rs Normal file
View File

@@ -0,0 +1,57 @@
use anyhow::{anyhow, Context};
use async_timing_util::unix_timestamp_ms;
use axum::{routing::post, Extension, Json, Router};
use db::DbExtension;
use helpers::handle_anyhow_error;
use mungos::{doc, Deserialize, Document, Update};
use super::JwtExtension;
#[derive(Deserialize)]
pub struct SecretLoginBody {
username: String,
secret: String,
}
pub fn router() -> Router {
Router::new().route(
"/login",
post(|db, jwt, body| async { login(db, jwt, body).await.map_err(handle_anyhow_error) }),
)
}
pub async fn login(
Extension(db): DbExtension,
Extension(jwt): JwtExtension,
Json(SecretLoginBody { username, secret }): Json<SecretLoginBody>,
) -> anyhow::Result<String> {
let user = db
.users
.find_one(doc! { "username": &username }, None)
.await
.context("failed at mongo query")?
.ok_or(anyhow!("did not find user with username {username}"))?;
let user_id = user.id.ok_or(anyhow!("user does not have id"))?.to_string();
let ts = unix_timestamp_ms() as i64;
for s in user.secrets {
if let Some(expires) = s.expires {
if expires < ts {
db.users
.update_one::<Document>(
&user_id,
Update::Custom(doc! { "$pull": { "secrets": { "_id": s.id } } }),
)
.await
.context("failed to remove expired secret")?;
continue;
}
}
if bcrypt::verify(&secret, &s.hash).context("failed at verifying hash")? {
let jwt = jwt
.generate(user_id)
.context("failed at generating jwt for user")?;
return Ok(jwt);
}
}
Err(anyhow!("invalid secret"))
}

View File

@@ -11,4 +11,7 @@ license = "MIT"
[dependencies]
monitor_types = "0.1.0"
reqwest = { version = "0.11", features = ["json"] }
anyhow = "1.0"
anyhow = "1.0"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"

View File

@@ -1 +1,217 @@
use anyhow::{anyhow, Context};
use reqwest::StatusCode;
use serde::{de::DeserializeOwned, Serialize};
pub use monitor_types as types;
use serde_json::json;
#[derive(Clone)]
pub struct MonitorClient {
http_client: reqwest::Client,
url: String,
token: String,
}
impl MonitorClient {
pub fn new_with_token(url: &str, token: impl Into<String>) -> MonitorClient {
MonitorClient {
http_client: reqwest::Client::new(),
url: parse_url(url),
token: token.into(),
}
}
pub async fn new_with_credentials(
url: &str,
username: impl Into<String>,
password: impl Into<String>,
) -> anyhow::Result<MonitorClient> {
let mut client = MonitorClient::new_with_token(url, "");
client.login(username, password).await?;
Ok(client)
}
async fn login(
&mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> anyhow::Result<()> {
let token = self
.post_string(
"/auth/local/login",
json!({ "username": username.into(), "password": password.into() }),
)
.await
.context("failed to log in")?;
self.token = token;
Ok(())
}
async fn get<R: DeserializeOwned>(&self, endpoint: &str) -> anyhow::Result<R> {
let res = self
.http_client
.get(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token))
.send()
.await
.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.json().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
async fn get_string(&self, endpoint: &str) -> anyhow::Result<String> {
let res = self
.http_client
.get(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token))
.send()
.await
.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.text().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
async fn post<B: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: impl Into<Option<B>>,
) -> anyhow::Result<R> {
let req = self
.http_client
.post(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token));
let req = if let Some(body) = body.into() {
req.header("Content-Type", "application/json").json(&body)
} else {
req
};
let res = req.send().await.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.json().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
async fn post_string<B: Serialize>(
&self,
endpoint: &str,
body: impl Into<Option<B>>,
) -> anyhow::Result<String> {
let req = self
.http_client
.post(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token));
let req = if let Some(body) = body.into() {
req.header("Content-Type", "application/json").json(&body)
} else {
req
};
let res = req.send().await.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.text().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
async fn delete<B: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: impl Into<Option<B>>,
) -> anyhow::Result<R> {
let req = self
.http_client
.delete(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token));
let req = if let Some(body) = body.into() {
req.header("Content-Type", "application/json").json(&body)
} else {
req
};
let res = req.send().await.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.json().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
async fn delete_string<B: Serialize>(
&self,
endpoint: &str,
body: impl Into<Option<B>>,
) -> anyhow::Result<String> {
let req = self
.http_client
.delete(format!("{}{endpoint}", self.url))
.header("Authorization", format!("Bearer {}", self.token));
let req = if let Some(body) = body.into() {
req.header("Content-Type", "application/json").json(&body)
} else {
req
};
let res = req.send().await.context("failed to reach monitor api")?;
let status = res.status();
if status == StatusCode::OK {
match res.text().await {
Ok(res) => Ok(res),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
} else {
match res.text().await {
Ok(res) => Err(anyhow!("{status}: {res}")),
Err(e) => Err(anyhow!("{status}: {e:#?}")),
}
}
}
}
fn parse_url(url: &str) -> String {
if url.chars().nth(url.len() - 1).unwrap() == '/' {
url[..url.len() - 1].to_string()
} else {
url.to_string()
}
}

View File

@@ -37,6 +37,8 @@ pub struct User {
pub avatar: Option<String>,
// used with auth
#[serde(default)]
pub secrets: Vec<ApiSecret>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -45,6 +47,16 @@ pub struct User {
pub google_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ApiSecret {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub name: String,
pub hash: String,
pub created_at: i64,
pub expires: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Server {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]