forked from github-starred/komodo
made login route for secret usage
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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
57
core/src/auth/secret.rs
Normal 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"))
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user