1.5.1 move routes to /user

This commit is contained in:
mbecker20
2024-05-25 20:36:52 -07:00
parent 5594d3c1d9
commit 40fe76cf27
19 changed files with 557 additions and 422 deletions

20
Cargo.lock generated
View File

@@ -32,7 +32,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"axum 0.7.5",
@@ -1948,7 +1948,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "logger"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"monitor_client",
@@ -2017,7 +2017,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"chrono",
@@ -2140,7 +2140,7 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"clap",
@@ -2159,7 +2159,7 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2191,7 +2191,7 @@ dependencies = [
[[package]]
name = "monitor_core"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2237,7 +2237,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2569,7 +2569,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"monitor_client",
@@ -3547,7 +3547,7 @@ dependencies = [
[[package]]
name = "tests"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"dotenv",
@@ -4090,7 +4090,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.5.0"
version = "1.5.1"
dependencies = [
"anyhow",
"logger",

View File

@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.5.0"
version = "1.5.1"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"

View File

@@ -2,3 +2,4 @@ pub mod auth;
pub mod execute;
pub mod read;
pub mod write;
pub mod user;

217
bin/core/src/api/user.rs Normal file
View File

@@ -0,0 +1,217 @@
use std::{collections::VecDeque, time::Instant};
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Json, Router};
use axum_extra::{headers::ContentType, TypedHeader};
use mongo_indexed::doc;
use monitor_client::{
api::user::{
CreateApiKey, CreateApiKeyResponse, DeleteApiKey,
DeleteApiKeyResponse, PushRecentlyViewed,
PushRecentlyViewedResponse, SetLastSeenUpdate,
SetLastSeenUpdateResponse,
},
entities::{
api_key::ApiKey, monitor_timestamp, update::ResourceTarget,
user::User,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_bson};
use resolver_api::{derive::Resolver, Resolve, Resolver};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use uuid::Uuid;
use crate::{
auth::{auth_request, random_string},
helpers::query::get_user,
state::{db_client, State},
};
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[resolver_target(State)]
#[resolver_args(User)]
#[serde(tag = "type", content = "params")]
enum UserRequest {
PushRecentlyViewed(PushRecentlyViewed),
SetLastSeenUpdate(SetLastSeenUpdate),
CreateApiKey(CreateApiKey),
DeleteApiKey(DeleteApiKey),
}
pub fn router() -> Router {
Router::new()
.route("/", post(handler))
.layer(middleware::from_fn(auth_request))
}
#[instrument(name = "UserHandler", level = "debug", skip(user))]
async fn handler(
Extension(user): Extension<User>,
Json(request): Json<UserRequest>,
) -> serror::Result<(TypedHeader<ContentType>, String)> {
let timer = Instant::now();
let req_id = Uuid::new_v4();
debug!(
"/user request {req_id} | user: {} ({})",
user.username, user.id
);
let res =
State
.resolve_request(request, user)
.await
.map_err(|e| match e {
resolver_api::Error::Serialization(e) => {
anyhow!("{e:?}").context("response serialization error")
}
resolver_api::Error::Inner(e) => e,
});
if let Err(e) = &res {
warn!("/user request {req_id} error: {e:#}");
}
let elapsed = timer.elapsed();
debug!("/user request {req_id} | resolve time: {elapsed:?}");
Ok((TypedHeader(ContentType::json()), res?))
}
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<PushRecentlyViewed, User> for State {
#[instrument(name = "PushRecentlyViewed", skip(self, user))]
async fn resolve(
&self,
PushRecentlyViewed { resource }: PushRecentlyViewed,
user: User,
) -> anyhow::Result<PushRecentlyViewedResponse> {
let user = get_user(&user.id).await?;
let (recents, id, field) = match resource {
ResourceTarget::Server(id) => {
(user.recent_servers, id, "recent_servers")
}
ResourceTarget::Deployment(id) => {
(user.recent_deployments, id, "recent_deployments")
}
ResourceTarget::Build(id) => {
(user.recent_builds, id, "recent_builds")
}
ResourceTarget::Repo(id) => {
(user.recent_repos, id, "recent_repos")
}
ResourceTarget::Procedure(id) => {
(user.recent_procedures, id, "recent_procedures")
}
_ => return Ok(PushRecentlyViewedResponse {}),
};
let mut recents = recents
.into_iter()
.filter(|_id| !id.eq(_id))
.take(RECENTLY_VIEWED_MAX - 1)
.collect::<VecDeque<_>>();
recents.push_front(id);
let update = doc! { field: to_bson(&recents)? };
update_one_by_id(
&db_client().await.users,
&user.id,
mungos::update::Update::Set(update),
None,
)
.await
.with_context(|| format!("failed to update {field}"))?;
Ok(PushRecentlyViewedResponse {})
}
}
impl Resolve<SetLastSeenUpdate, User> for State {
#[instrument(name = "SetLastSeenUpdate", skip(self, user))]
async fn resolve(
&self,
SetLastSeenUpdate {}: SetLastSeenUpdate,
user: User,
) -> anyhow::Result<SetLastSeenUpdateResponse> {
update_one_by_id(
&db_client().await.users,
&user.id,
mungos::update::Update::Set(doc! {
"last_update_view": monitor_timestamp()
}),
None,
)
.await
.context("failed to update user last_update_view")?;
Ok(SetLastSeenUpdateResponse {})
}
}
const SECRET_LENGTH: usize = 40;
const BCRYPT_COST: u32 = 10;
impl Resolve<CreateApiKey, User> for State {
#[instrument(
name = "CreateApiKey",
level = "debug",
skip(self, user)
)]
async fn resolve(
&self,
CreateApiKey { name, expires }: CreateApiKey,
user: User,
) -> anyhow::Result<CreateApiKeyResponse> {
let user = get_user(&user.id).await?;
let key = format!("K-{}", random_string(SECRET_LENGTH));
let secret = format!("S-{}", random_string(SECRET_LENGTH));
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
.context("failed at hashing secret string")?;
let api_key = ApiKey {
name,
key: key.clone(),
secret: secret_hash,
user_id: user.id.clone(),
created_at: monitor_timestamp(),
expires,
};
db_client()
.await
.api_keys
.insert_one(api_key, None)
.await
.context("failed to create api key on db")?;
Ok(CreateApiKeyResponse { key, secret })
}
}
impl Resolve<DeleteApiKey, User> for State {
#[instrument(
name = "DeleteApiKey",
level = "debug",
skip(self, user)
)]
async fn resolve(
&self,
DeleteApiKey { key }: DeleteApiKey,
user: User,
) -> anyhow::Result<DeleteApiKeyResponse> {
let client = db_client().await;
let key = client
.api_keys
.find_one(doc! { "key": &key }, None)
.await
.context("failed at db query")?
.context("no api key with key found")?;
if user.id != key.user_id {
return Err(anyhow!("api key does not belong to user"));
}
client
.api_keys
.delete_one(doc! { "key": key.key }, None)
.await
.context("failed to delete api key from db")?;
Ok(DeleteApiKeyResponse {})
}
}

View File

@@ -1,147 +0,0 @@
use anyhow::{anyhow, Context};
use monitor_client::{
api::write::*,
entities::{
api_key::ApiKey,
monitor_timestamp,
user::{User, UserConfig},
},
};
use mungos::{by_id::find_one_by_id, mongodb::bson::doc};
use resolver_api::Resolve;
use crate::{
auth::random_string,
helpers::query::get_user,
state::{db_client, State},
};
const SECRET_LENGTH: usize = 40;
const BCRYPT_COST: u32 = 10;
impl Resolve<CreateApiKey, User> for State {
#[instrument(
name = "CreateApiKey",
level = "debug",
skip(self, user)
)]
async fn resolve(
&self,
CreateApiKey { name, expires }: CreateApiKey,
user: User,
) -> anyhow::Result<CreateApiKeyResponse> {
let user = get_user(&user.id).await?;
let key = format!("K-{}", random_string(SECRET_LENGTH));
let secret = format!("S-{}", random_string(SECRET_LENGTH));
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
.context("failed at hashing secret string")?;
let api_key = ApiKey {
name,
key: key.clone(),
secret: secret_hash,
user_id: user.id.clone(),
created_at: monitor_timestamp(),
expires,
};
db_client()
.await
.api_keys
.insert_one(api_key, None)
.await
.context("failed to create api key on db")?;
Ok(CreateApiKeyResponse { key, secret })
}
}
impl Resolve<DeleteApiKey, User> for State {
#[instrument(
name = "DeleteApiKey",
level = "debug",
skip(self, user)
)]
async fn resolve(
&self,
DeleteApiKey { key }: DeleteApiKey,
user: User,
) -> anyhow::Result<DeleteApiKeyResponse> {
let client = db_client().await;
let key = client
.api_keys
.find_one(doc! { "key": &key }, None)
.await
.context("failed at db query")?
.context("no api key with key found")?;
if user.id != key.user_id {
return Err(anyhow!("api key does not belong to user"));
}
client
.api_keys
.delete_one(doc! { "key": key.key }, None)
.await
.context("failed to delete api key from db")?;
Ok(DeleteApiKeyResponse {})
}
}
impl Resolve<CreateApiKeyForServiceUser, User> for State {
#[instrument(name = "CreateApiKeyForServiceUser", skip(self, user))]
async fn resolve(
&self,
CreateApiKeyForServiceUser {
user_id,
name,
expires,
}: CreateApiKeyForServiceUser,
user: User,
) -> anyhow::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
}
let service_user =
find_one_by_id(&db_client().await.users, &user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
};
self
.resolve(CreateApiKey { name, expires }, service_user)
.await
}
}
impl Resolve<DeleteApiKeyForServiceUser, User> for State {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(self, user))]
async fn resolve(
&self,
DeleteApiKeyForServiceUser { key }: DeleteApiKeyForServiceUser,
user: User,
) -> anyhow::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
}
let db = db_client().await;
let api_key = db
.api_keys
.find_one(doc! { "key": &key }, None)
.await
.context("failed to query db for api key")?
.context("did not find matching api key")?;
let service_user =
find_one_by_id(&db_client().await.users, &api_key.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
};
db.api_keys
.delete_one(doc! { "key": key }, None)
.await
.context("failed to delete api key on db")?;
Ok(DeleteApiKeyForServiceUserResponse {})
}
}

View File

@@ -13,7 +13,6 @@ use uuid::Uuid;
use crate::{auth::auth_request, state::State};
mod alerter;
mod api_key;
mod build;
mod builder;
mod deployment;
@@ -23,8 +22,8 @@ mod procedure;
mod repo;
mod server;
mod server_template;
mod service_user;
mod tag;
mod user;
mod user_group;
mod variable;
@@ -34,17 +33,11 @@ mod variable;
#[resolver_args(User)]
#[serde(tag = "type", content = "params")]
enum WriteRequest {
// ==== API KEY ====
CreateApiKey(CreateApiKey),
DeleteApiKey(DeleteApiKey),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER ====
PushRecentlyViewed(PushRecentlyViewed),
SetLastSeenUpdate(SetLastSeenUpdate),
// ==== SERVICE USER ====
CreateServiceUser(CreateServiceUser),
UpdateServiceUserDescription(UpdateServiceUserDescription),
CreateApiKeyForServiceUser(CreateApiKeyForServiceUser),
DeleteApiKeyForServiceUser(DeleteApiKeyForServiceUser),
// ==== USER GROUP ====
CreateUserGroup(CreateUserGroup),

View File

@@ -1,101 +1,29 @@
use std::{collections::VecDeque, str::FromStr};
use std::str::FromStr;
use anyhow::{anyhow, Context};
use monitor_client::{
api::write::{
CreateServiceUser, CreateServiceUserResponse, PushRecentlyViewed,
PushRecentlyViewedResponse, SetLastSeenUpdate,
SetLastSeenUpdateResponse, UpdateServiceUserDescription,
UpdateServiceUserDescriptionResponse,
api::{
user::CreateApiKey,
write::{
CreateApiKeyForServiceUser, CreateApiKeyForServiceUserResponse,
CreateServiceUser, CreateServiceUserResponse,
DeleteApiKeyForServiceUser, DeleteApiKeyForServiceUserResponse,
UpdateServiceUserDescription,
UpdateServiceUserDescriptionResponse,
},
},
entities::{
monitor_timestamp,
update::ResourceTarget,
user::{User, UserConfig},
},
};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{doc, oid::ObjectId, to_bson},
by_id::find_one_by_id,
mongodb::bson::{doc, oid::ObjectId},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_user,
state::{db_client, State},
};
const RECENTLY_VIEWED_MAX: usize = 10;
impl Resolve<PushRecentlyViewed, User> for State {
#[instrument(name = "PushRecentlyViewed", skip(self, user))]
async fn resolve(
&self,
PushRecentlyViewed { resource }: PushRecentlyViewed,
user: User,
) -> anyhow::Result<PushRecentlyViewedResponse> {
let user = get_user(&user.id).await?;
let (recents, id, field) = match resource {
ResourceTarget::Server(id) => {
(user.recent_servers, id, "recent_servers")
}
ResourceTarget::Deployment(id) => {
(user.recent_deployments, id, "recent_deployments")
}
ResourceTarget::Build(id) => {
(user.recent_builds, id, "recent_builds")
}
ResourceTarget::Repo(id) => {
(user.recent_repos, id, "recent_repos")
}
ResourceTarget::Procedure(id) => {
(user.recent_procedures, id, "recent_procedures")
}
_ => return Ok(PushRecentlyViewedResponse {}),
};
let mut recents = recents
.into_iter()
.filter(|_id| !id.eq(_id))
.take(RECENTLY_VIEWED_MAX - 1)
.collect::<VecDeque<_>>();
recents.push_front(id);
let update = doc! { field: to_bson(&recents)? };
update_one_by_id(
&db_client().await.users,
&user.id,
mungos::update::Update::Set(update),
None,
)
.await
.with_context(|| format!("failed to update {field}"))?;
Ok(PushRecentlyViewedResponse {})
}
}
impl Resolve<SetLastSeenUpdate, User> for State {
#[instrument(name = "SetLastSeenUpdate", skip(self, user))]
async fn resolve(
&self,
SetLastSeenUpdate {}: SetLastSeenUpdate,
user: User,
) -> anyhow::Result<SetLastSeenUpdateResponse> {
update_one_by_id(
&db_client().await.users,
&user.id,
mungos::update::Update::Set(doc! {
"last_update_view": monitor_timestamp()
}),
None,
)
.await
.context("failed to update user last_update_view")?;
Ok(SetLastSeenUpdateResponse {})
}
}
use crate::state::{db_client, State};
impl Resolve<CreateServiceUser, User> for State {
#[instrument(name = "CreateServiceUser", skip(self, user))]
@@ -185,3 +113,64 @@ impl Resolve<UpdateServiceUserDescription, User> for State {
.context("user with username not found")
}
}
impl Resolve<CreateApiKeyForServiceUser, User> for State {
#[instrument(name = "CreateApiKeyForServiceUser", skip(self, user))]
async fn resolve(
&self,
CreateApiKeyForServiceUser {
user_id,
name,
expires,
}: CreateApiKeyForServiceUser,
user: User,
) -> anyhow::Result<CreateApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
}
let service_user =
find_one_by_id(&db_client().await.users, &user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
};
self
.resolve(CreateApiKey { name, expires }, service_user)
.await
}
}
impl Resolve<DeleteApiKeyForServiceUser, User> for State {
#[instrument(name = "DeleteApiKeyForServiceUser", skip(self, user))]
async fn resolve(
&self,
DeleteApiKeyForServiceUser { key }: DeleteApiKeyForServiceUser,
user: User,
) -> anyhow::Result<DeleteApiKeyForServiceUserResponse> {
if !user.admin {
return Err(anyhow!("user not admin"));
}
let db = db_client().await;
let api_key = db
.api_keys
.find_one(doc! { "key": &key }, None)
.await
.context("failed to query db for api key")?
.context("did not find matching api key")?;
let service_user =
find_one_by_id(&db_client().await.users, &api_key.user_id)
.await
.context("failed to query db for user")?
.context("no user found with id")?;
let UserConfig::Service { .. } = &service_user.config else {
return Err(anyhow!("user is not service user"));
};
db.api_keys
.delete_one(doc! { "key": key }, None)
.await
.context("failed to delete api key on db")?;
Ok(DeleteApiKeyForServiceUserResponse {})
}
}

View File

@@ -48,6 +48,7 @@ async fn app() -> anyhow::Result<()> {
let app = Router::new()
.nest("/auth", api::auth::router())
.nest("/user", api::user::router())
.nest("/read", api::read::router())
.nest("/write", api::write::router())
.nest("/execute", api::execute::router())

View File

@@ -4,7 +4,7 @@
//!
//! All calls share some common HTTP params:
//! - Method: `POST`
//! - Path: `/auth`, `/read`, `/write`, `/execute`
//! - Path: `/auth`, `/user`, `/read`, `/write`, `/execute`
//! - Headers:
//! - Content-Type: `application/json`
//! - Authorication: `your_jwt`
@@ -30,7 +30,8 @@
//!
//! ## Modules
//!
//! - [auth]: Requests relating to loggins in / obtaining authentication tokens.
//! - [auth]: Requests relating to logging in / obtaining authentication tokens.
//! - [user]: User self-management actions (manage api keys, etc.)
//! - [read]: Read only requests which retrieve data from Monitor.
//! - [execute]: Run actions on monitor resources, eg [execute::RunBuild].
//! - [mod@write]: Requests which alter data, like create / update / delete resources.
@@ -52,4 +53,5 @@
pub mod auth;
pub mod execute;
pub mod read;
pub mod user;
pub mod write;

View File

@@ -0,0 +1,97 @@
use derive_empty_traits::EmptyTraits;
use resolver_api::{derive::Request, HasResponse};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{update::ResourceTarget, NoData, I64};
pub trait MonitorUserRequest: HasResponse {}
//
/// Push a resource to the front of the users 10 most recently viewed resources.
/// Response: [NoData].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorUserRequest)]
#[response(PushRecentlyViewedResponse)]
pub struct PushRecentlyViewed {
/// The target to push.
pub resource: ResourceTarget,
}
#[typeshare]
pub type PushRecentlyViewedResponse = NoData;
//
/// Set the time the user last opened the UI updates.
/// Used for unseen notification dot.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorUserRequest)]
#[response(SetLastSeenUpdateResponse)]
pub struct SetLastSeenUpdate {}
#[typeshare]
pub type SetLastSeenUpdateResponse = NoData;
//
/// Create an api key for the calling user.
/// Response: [CreateApiKeyResponse].
///
/// Note. After the response is served, there will be no way
/// to get the secret later.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorUserRequest)]
#[response(CreateApiKeyResponse)]
pub struct CreateApiKey {
/// The name for the api key.
pub name: String,
/// A unix timestamp in millseconds specifying api key expire time.
/// Default is 0, which means no expiry.
#[serde(default)]
pub expires: I64,
}
/// Response for [CreateApiKey].
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateApiKeyResponse {
/// X-API-KEY
pub key: String,
/// X-API-SECRET
///
/// Note.
/// There is no way to get the secret again after it is distributed in this message
pub secret: String,
}
//
/// Delete an api key for the calling user.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorUserRequest)]
#[response(DeleteApiKeyResponse)]
pub struct DeleteApiKey {
/// The key which the user intends to delete.
pub key: String,
}
#[typeshare]
pub type DeleteApiKeyResponse = NoData;

View File

@@ -3,67 +3,15 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{NoData, I64};
use crate::{
api::user::CreateApiKeyResponse,
entities::{NoData, I64},
};
use super::MonitorWriteRequest;
//
/// Create an api key for the calling user.
/// Response: [CreateApiKeyResponse].
///
/// Note. After the response is served, there will be no way
/// to get the secret later.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorWriteRequest)]
#[response(CreateApiKeyResponse)]
pub struct CreateApiKey {
/// The name for the api key.
pub name: String,
/// A unix timestamp in millseconds specifying api key expire time.
/// Default is 0, which means no expiry.
#[serde(default)]
pub expires: I64,
}
/// Response for [CreateApiKey].
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateApiKeyResponse {
/// X-API-KEY
pub key: String,
/// X-API-SECRET
///
/// Note.
/// There is no way to get the secret again after it is distributed in this message
pub secret: String,
}
//
/// Delete an api key for the calling user.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorWriteRequest)]
#[response(DeleteApiKeyResponse)]
pub struct DeleteApiKey {
/// The key which the user intends to delete.
pub key: String,
}
#[typeshare]
pub type DeleteApiKeyResponse = NoData;
//
/// Admin only method to create an api key for a service user.
/// Response: [CreateApiKeyResponse].
#[typeshare]

View File

@@ -3,46 +3,12 @@ use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{update::ResourceTarget, user::User, NoData};
use crate::entities::user::User;
use super::MonitorWriteRequest;
//
/// Push a resource to the front of the users 10 most recently viewed resources.
/// Response: [NoData].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorWriteRequest)]
#[response(PushRecentlyViewedResponse)]
pub struct PushRecentlyViewed {
/// The target to push.
pub resource: ResourceTarget,
}
#[typeshare]
pub type PushRecentlyViewedResponse = NoData;
//
/// Set the time the user last opened the UI updates.
/// Used for unseen notification dot.
/// Response: [NoData]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorWriteRequest)]
#[response(SetLastSeenUpdateResponse)]
pub struct SetLastSeenUpdate {}
#[typeshare]
pub type SetLastSeenUpdateResponse = NoData;
//
/// **Admin only.** Create a service user.
/// Response: [User].
#[typeshare]

View File

@@ -6,8 +6,7 @@ use serror::deserialize_error;
use crate::{
api::{
auth::MonitorAuthRequest, execute::MonitorExecuteRequest,
read::MonitorReadRequest, write::MonitorWriteRequest,
auth::MonitorAuthRequest, execute::MonitorExecuteRequest, read::MonitorReadRequest, user::MonitorUserRequest, write::MonitorWriteRequest
},
MonitorClient,
};
@@ -29,6 +28,22 @@ impl MonitorClient {
.await
}
#[tracing::instrument(skip(self))]
pub async fn user<T: MonitorUserRequest>(
&self,
request: T,
) -> anyhow::Result<T::Response> {
self
.post(
"/auth",
json!({
"type": T::req_type(),
"params": request
}),
)
.await
}
#[tracing::instrument(skip(self))]
pub async fn read<T: MonitorReadRequest>(
&self,

View File

@@ -3,12 +3,14 @@ import {
AuthResponses,
ExecuteResponses,
ReadResponses,
UserResponses,
WriteResponses,
} from "./responses";
import {
AuthRequest,
ExecuteRequest,
ReadRequest,
UserRequest,
WriteRequest,
} from "./types";
@@ -39,6 +41,9 @@ export function MonitorClient(url: string, options: InitOptions) {
const auth = async <Req extends AuthRequest>(req: Req) =>
await request<Req, AuthResponses[Req["type"]]>("/auth", req);
const user = async <Req extends UserRequest>(req: Req) =>
await request<Req, UserResponses[Req["type"]]>("/user", req);
const read = async <Req extends ReadRequest>(req: Req) =>
await request<Req, ReadResponses[Req["type"]]>("/read", req);
@@ -48,5 +53,5 @@ export function MonitorClient(url: string, options: InitOptions) {
const execute = async <Req extends ExecuteRequest>(req: Req) =>
await request<Req, ExecuteResponses[Req["type"]]>("/execute", req);
return { request, auth, read, write, execute };
return { request, auth, user, read, write, execute };
}

View File

@@ -8,6 +8,13 @@ export type AuthResponses = {
GetUser: Types.GetUserResponse;
};
export type UserResponses = {
PushRecentlyViewed: Types.PushRecentlyViewedResponse;
SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;
CreateApiKey: Types.CreateApiKeyResponse;
DeleteApiKey: Types.DeleteApiKeyResponse;
};
export type ReadResponses = {
GetVersion: Types.GetVersionResponse;
GetCoreInfo: Types.GetCoreInfoResponse;
@@ -118,18 +125,12 @@ export type ReadResponses = {
};
export type WriteResponses = {
// ==== API KEY ====
CreateApiKey: Types.CreateApiKeyResponse;
DeleteApiKey: Types.DeleteApiKeyResponse;
// ==== SERVICE USER ====
CreateServiceUser: Types.CreateServiceUserResponse;
UpdateServiceUserDescription: Types.UpdateServiceUserDescriptionResponse;
CreateApiKeyForServiceUser: Types.CreateApiKeyForServiceUserResponse;
DeleteApiKeyForServiceUser: Types.DeleteApiKeyForServiceUserResponse;
// ==== USER ====
PushRecentlyViewed: Types.PushRecentlyViewedResponse;
SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;
CreateServiceUser: Types.CreateServiceUserResponse;
UpdateServiceUserDescription: Types.UpdateServiceUserDescription;
// ==== USER GROUP ====
CreateUserGroup: Types.UserGroup;
RenameUserGroup: Types.UserGroup;

View File

@@ -1301,6 +1301,10 @@ export interface Variable {
export type GetVariableResponse = Variable;
export type PushRecentlyViewedResponse = NoData;
export type SetLastSeenUpdateResponse = NoData;
export type DeleteApiKeyResponse = NoData;
/** Response for [CreateApiKey]. */
@@ -1336,10 +1340,6 @@ export type UpdateProcedureResponse = Procedure;
export type UpdateTagsOnResourceResponse = NoData;
export type PushRecentlyViewedResponse = NoData;
export type SetLastSeenUpdateResponse = NoData;
export type CreateServiceUserResponse = User;
export type UpdateServiceUserDescriptionResponse = User;
@@ -2591,6 +2591,49 @@ export interface ListVariablesResponse {
secrets: string[];
}
/**
* Push a resource to the front of the users 10 most recently viewed resources.
* Response: [NoData].
*/
export interface PushRecentlyViewed {
/** The target to push. */
resource: ResourceTarget;
}
/**
* Set the time the user last opened the UI updates.
* Used for unseen notification dot.
* Response: [NoData]
*/
export interface SetLastSeenUpdate {
}
/**
* Create an api key for the calling user.
* Response: [CreateApiKeyResponse].
*
* Note. After the response is served, there will be no way
* to get the secret later.
*/
export interface CreateApiKey {
/** The name for the api key. */
name: string;
/**
* A unix timestamp in millseconds specifying api key expire time.
* Default is 0, which means no expiry.
*/
expires?: I64;
}
/**
* Delete an api key for the calling user.
* Response: [NoData]
*/
export interface DeleteApiKey {
/** The key which the user intends to delete. */
key: string;
}
export type PartialAlerterConfig =
| { type: "Custom", params: _PartialCustomAlerterConfig }
| { type: "Slack", params: _PartialSlackAlerterConfig };
@@ -2638,32 +2681,6 @@ export interface UpdateAlerter {
config: PartialAlerterConfig;
}
/**
* Create an api key for the calling user.
* Response: [CreateApiKeyResponse].
*
* Note. After the response is served, there will be no way
* to get the secret later.
*/
export interface CreateApiKey {
/** The name for the api key. */
name: string;
/**
* A unix timestamp in millseconds specifying api key expire time.
* Default is 0, which means no expiry.
*/
expires?: I64;
}
/**
* Delete an api key for the calling user.
* Response: [NoData]
*/
export interface DeleteApiKey {
/** The key which the user intends to delete. */
key: string;
}
/**
* Admin only method to create an api key for a service user.
* Response: [CreateApiKeyResponse].
@@ -3120,23 +3137,6 @@ export interface UpdateTagsOnResource {
tags: string[];
}
/**
* Push a resource to the front of the users 10 most recently viewed resources.
* Response: [NoData].
*/
export interface PushRecentlyViewed {
/** The target to push. */
resource: ResourceTarget;
}
/**
* Set the time the user last opened the UI updates.
* Used for unseen notification dot.
* Response: [NoData]
*/
export interface SetLastSeenUpdate {
}
/**
* **Admin only.** Create a service user.
* Response: [User].
@@ -3571,15 +3571,17 @@ export type ReadRequest =
| { type: "GetVariable", params: GetVariable }
| { type: "ListVariables", params: ListVariables };
export type WriteRequest =
| { type: "CreateApiKey", params: CreateApiKey }
| { type: "DeleteApiKey", params: DeleteApiKey }
| { type: "CreateApiKeyForServiceUser", params: CreateApiKeyForServiceUser }
| { type: "DeleteApiKeyForServiceUser", params: DeleteApiKeyForServiceUser }
export type UserRequest =
| { type: "PushRecentlyViewed", params: PushRecentlyViewed }
| { type: "SetLastSeenUpdate", params: SetLastSeenUpdate }
| { type: "CreateApiKey", params: CreateApiKey }
| { type: "DeleteApiKey", params: DeleteApiKey };
export type WriteRequest =
| { type: "CreateServiceUser", params: CreateServiceUser }
| { type: "UpdateServiceUserDescription", params: UpdateServiceUserDescription }
| { type: "CreateApiKeyForServiceUser", params: CreateApiKeyForServiceUser }
| { type: "DeleteApiKeyForServiceUser", params: DeleteApiKeyForServiceUser }
| { type: "CreateUserGroup", params: CreateUserGroup }
| { type: "RenameUserGroup", params: RenameUserGroup }
| { type: "DeleteUserGroup", params: DeleteUserGroup }

View File

@@ -1,4 +1,4 @@
import { useRead, useUser, useUserInvalidate, useWrite } from "@lib/hooks";
import { useManageUser, useRead, useUser, useUserInvalidate } from "@lib/hooks";
import {
DropdownMenu,
DropdownMenuContent,
@@ -25,7 +25,7 @@ export const TopbarUpdates = () => {
);
const userInvalidate = useUserInvalidate();
const { mutate } = useWrite("SetLastSeenUpdate", {
const { mutate } = useManageUser("SetLastSeenUpdate", {
onSuccess: userInvalidate,
});

View File

@@ -4,6 +4,7 @@ import {
AuthResponses,
ExecuteResponses,
ReadResponses,
UserResponses,
WriteResponses,
} from "@monitor/client/dist/responses";
import {
@@ -80,6 +81,35 @@ export const useInvalidate = () => {
) => keys.forEach((key) => qc.invalidateQueries({ queryKey: key }));
};
export const useManageUser = <
T extends Types.UserRequest["type"],
R extends Extract<Types.UserRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseMutationOptions<UserResponses[T], unknown, P, unknown>,
"mutationKey" | "mutationFn"
>
>(
type: T,
config?: C
) => {
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().user({ type, params } as R),
onError: (e, v, c) => {
console.log("useManageUser error:", e);
toast({
title: `Request ${type} Failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
export const useWrite = <
T extends Types.WriteRequest["type"],
R extends Extract<Types.WriteRequest, { type: T }>,
@@ -96,12 +126,16 @@ export const useWrite = <
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().write({ type, params } as R),
...config,
onError: (e, v, c) => {
console.log("useWrite error:", e);
toast({ title: `Request ${type} Failed`, description: "See console for details", variant: "destructive" });
toast({
title: `Request ${type} Failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
@@ -116,12 +150,23 @@ export const useExecute = <
>(
type: T,
config?: C
) =>
useMutation({
) => {
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().execute({ type, params } as R),
onError: (e, v, c) => {
console.log("useExecute error:", e);
toast({
title: `Request ${type} Failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
export const useAuth = <
T extends Types.AuthRequest["type"],
@@ -153,7 +198,7 @@ export const useResourceParamType = () => {
export const usePushRecentlyViewed = ({ type, id }: Types.ResourceTarget) => {
const userInvalidate = useUserInvalidate();
const push = useWrite("PushRecentlyViewed", {
const push = useManageUser("PushRecentlyViewed", {
onSuccess: userInvalidate,
}).mutate;
@@ -244,4 +289,4 @@ export const useFilterResources = <Info>(
: true)
) ?? []
);
};
};

View File

@@ -1,6 +1,6 @@
import { Page } from "@components/layouts";
import { ConfirmButton, CopyButton } from "@components/util";
import { useInvalidate, useRead, useSetTitle, useWrite } from "@lib/hooks";
import { useInvalidate, useManageUser, useRead, useSetTitle } from "@lib/hooks";
import {
Dialog,
DialogContent,
@@ -47,7 +47,7 @@ const CreateKey = () => {
const [expires, setExpires] = useState<ExpiresOptions>("never");
const [submitted, setSubmitted] = useState<{ key: string; secret: string }>();
const invalidate = useInvalidate();
const { mutate, isPending } = useWrite("CreateApiKey", {
const { mutate, isPending } = useManageUser("CreateApiKey", {
onSuccess: ({ key, secret }) => {
invalidate(["ListApiKeys"]);
setSubmitted({ key, secret });
@@ -159,7 +159,7 @@ const CreateKey = () => {
const DeleteKey = ({ api_key }: { api_key: string }) => {
const invalidate = useInvalidate();
const { toast } = useToast();
const { mutate, isPending } = useWrite("DeleteApiKey", {
const { mutate, isPending } = useManageUser("DeleteApiKey", {
onSuccess: () => {
invalidate(["ListApiKeys"]);
toast({ title: "Api Key Deleted" });