mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Update to account recovery UX (#859)
* JSON-formatted output for recover_account, moved a bunch of logs to debug instead of info * updated documentation
This commit is contained in:
parent
4b1989ee22
commit
57f8fa9d2b
|
@ -148,7 +148,7 @@ in `/tmp/kanidm.db`.
|
|||
|
||||
Create the initial database and generate an `admin` username:
|
||||
|
||||
cargo run --bin kanidmd recover_account -c ./examples/insecure_server.toml -n admin
|
||||
cargo run --bin kanidmd recover_account -c ./examples/insecure_server.toml admin
|
||||
<snip>
|
||||
Success - password reset to -> Et8QRJgQkMJu3v1AQxcbxRWW44qRUZPpr6BJ9fCGapAB9cT4
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ You should test your configuration is valid before you proceed.
|
|||
Then you can setup the initial admin account and initialise the database into your volume.
|
||||
|
||||
docker run --rm -i -t -v kanidmd:/data \
|
||||
kanidm/server:latest /sbin/kanidmd recover_account -c /data/server.toml -n admin
|
||||
kanidm/server:latest /sbin/kanidmd recover_account -c /data/server.toml admin
|
||||
|
||||
### Run the Server
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ tracing = "^0.1.35"
|
|||
reqwest = { version = "^0.11.11", features=["cookies", "json", "native-tls"] }
|
||||
kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha.8" }
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
toml = "^0.5.9"
|
||||
uuid = { version = "^1.1.2", features = ["serde", "v4"] }
|
||||
url = { version = "^2.2.2", features = ["serde"] }
|
||||
|
|
|
@ -12,7 +12,7 @@ repository = "https://github.com/kanidm/kanidm/"
|
|||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
uuid = { version = "^1.1.2", features = ["serde"] }
|
||||
base32 = "^0.4.0"
|
||||
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }
|
||||
|
|
|
@ -37,7 +37,7 @@ rpassword = "^6.0.1"
|
|||
clap = { version = "^3.2", features = ["derive", "env"] }
|
||||
libc = "^0.2.126"
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
shellexpand = "^2.1.0"
|
||||
rayon = "^1.5.3"
|
||||
time = { version = "=0.2.27", features = ["serde", "std"] }
|
||||
|
|
|
@ -60,7 +60,7 @@ bytes = "^1.1.0"
|
|||
|
||||
libc = "^0.2.126"
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
clap = { version = "^3.2", features = ["derive"] }
|
||||
|
||||
libsqlite3-sys = "0.24.2"
|
||||
|
|
|
@ -28,7 +28,7 @@ use std::path::PathBuf;
|
|||
use std::str::FromStr;
|
||||
|
||||
use kanidm::audit::LogLevel;
|
||||
use kanidm::config::{Configuration, OnlineBackup, ServerRole};
|
||||
use kanidm::config::{Configuration, ConsoleOutputMode, OnlineBackup, ServerRole};
|
||||
use kanidm::tracing_tree;
|
||||
use kanidm::utils::file_permissions_readonly;
|
||||
use score::{
|
||||
|
@ -221,6 +221,9 @@ async fn main() {
|
|||
config.update_domain(&sconfig.domain.as_str());
|
||||
config.update_db_arc_size(sconfig.db_arc_size);
|
||||
config.update_role(sconfig.role);
|
||||
config.update_output_mode(
|
||||
ConsoleOutputMode::from_str(opt.commands.commonopt().output_mode.as_str()).unwrap(),
|
||||
);
|
||||
|
||||
// Apply any cli overrides, normally debug level.
|
||||
if let Some(dll) = opt.commands.commonopt().debug.as_ref() {
|
||||
|
|
|
@ -6,6 +6,10 @@ struct CommonOpt {
|
|||
#[clap(parse(from_os_str), short, long = "config", env = "KANIDM_CONFIG")]
|
||||
/// Path to the server's configuration file. If it does not exist, it will be created.
|
||||
config_path: PathBuf,
|
||||
//TODO: remove this once we work out the format
|
||||
/// Log format (still in very early development)
|
||||
#[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value="text")]
|
||||
output_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
@ -28,7 +32,7 @@ struct RestoreOpt {
|
|||
|
||||
#[derive(Debug, Args)]
|
||||
struct RecoverAccountOpt {
|
||||
#[clap(short)]
|
||||
#[clap(value_parser)]
|
||||
/// The account name to recover credentials for.
|
||||
name: String,
|
||||
#[clap(flatten)]
|
||||
|
|
|
@ -48,7 +48,7 @@ uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
|
|||
compiled-uuid = "0.1.2"
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_cbor = "^0.11.2"
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
|
||||
libsqlite3-sys = "0.24.2"
|
||||
rusqlite = "^0.27.0"
|
||||
|
|
|
@ -1255,7 +1255,7 @@ impl<'a> BackendWriteTransaction<'a> {
|
|||
|
||||
pub fn upgrade_reindex(&self, v: i64) -> Result<(), OperationError> {
|
||||
let dbv = self.get_db_index_version();
|
||||
admin_info!(?dbv, ?v, "upgrade_reindex");
|
||||
admin_debug!(?dbv, ?v, "upgrade_reindex");
|
||||
if dbv < v {
|
||||
limmediate_warning!(
|
||||
"NOTICE: A system reindex is required. This may take a long time ...\n"
|
||||
|
@ -1514,9 +1514,9 @@ impl Backend {
|
|||
idxkeys: Vec<IdxKey>,
|
||||
vacuum: bool,
|
||||
) -> Result<Self, OperationError> {
|
||||
info!("DB tickets -> {:?}", cfg.pool_size);
|
||||
info!("Profile -> {}", env!("KANIDM_PROFILE_NAME"));
|
||||
info!("CPU Flags -> {}", env!("KANIDM_CPU_FLAGS"));
|
||||
debug!("DB tickets -> {:?}", cfg.pool_size);
|
||||
debug!("Profile -> {}", env!("KANIDM_PROFILE_NAME"));
|
||||
debug!("CPU Flags -> {}", env!("KANIDM_CPU_FLAGS"));
|
||||
|
||||
// If in memory, reduce pool to 1
|
||||
if cfg.path.is_empty() {
|
||||
|
|
|
@ -74,6 +74,31 @@ impl FromStr for ServerRole {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: this should probably be in the kanidm crate
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ConsoleOutputMode {
|
||||
Text,
|
||||
JSON,
|
||||
}
|
||||
impl Default for ConsoleOutputMode {
|
||||
fn default() -> Self {
|
||||
ConsoleOutputMode::Text
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ConsoleOutputMode {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"json" => Ok(ConsoleOutputMode::JSON),
|
||||
"text" => Ok(ConsoleOutputMode::Text),
|
||||
_ => Err("Must be one of json, text"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct Configuration {
|
||||
pub address: String,
|
||||
|
@ -93,6 +118,7 @@ pub struct Configuration {
|
|||
pub domain: String,
|
||||
pub origin: String,
|
||||
pub role: ServerRole,
|
||||
pub output_mode: ConsoleOutputMode,
|
||||
}
|
||||
|
||||
impl fmt::Display for Configuration {
|
||||
|
@ -112,21 +138,23 @@ impl fmt::Display for Configuration {
|
|||
.and_then(|_| write!(f, "secure cookies: {}, ", self.secure_cookies))
|
||||
.and_then(|_| write!(f, "with TLS: {}, ", self.tls_config.is_some()))
|
||||
.and_then(|_| match self.log_level {
|
||||
Some(u) => write!(f, "with log_level: {:x}, ", u),
|
||||
None => write!(f, "with log_level: default, "),
|
||||
Some(u) => write!(f, "log_level: {:x}, ", u),
|
||||
None => write!(f, "log_level: default, "),
|
||||
})
|
||||
// TODO: include the backup timings
|
||||
.and_then(|_| match &self.online_backup {
|
||||
Some(_) => write!(f, "with online_backup: enabled, "),
|
||||
None => write!(f, "with online_backup: disabled, "),
|
||||
Some(_) => write!(f, "online_backup: enabled, "),
|
||||
None => write!(f, "online_backup: disabled, "),
|
||||
})
|
||||
.and_then(|_| write!(f, "role: {}, ", self.role.to_string()))
|
||||
.and_then(|_| {
|
||||
write!(
|
||||
f,
|
||||
"integration mode: {}",
|
||||
"integration mode: {}, ",
|
||||
self.integration_test_config.is_some()
|
||||
)
|
||||
})
|
||||
.and_then(|_| write!(f, "console output format: {:?} ", self.output_mode))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +172,7 @@ impl Configuration {
|
|||
db_path: String::from(""),
|
||||
db_fs_type: None,
|
||||
db_arc_size: None,
|
||||
maximum_request: 262_144, // 256k
|
||||
maximum_request: 256 * 1024, // 256k
|
||||
// log type
|
||||
// log path
|
||||
// TODO #63: default true in prd
|
||||
|
@ -157,6 +185,7 @@ impl Configuration {
|
|||
domain: "idm.example.com".to_string(),
|
||||
origin: "https://idm.example.com".to_string(),
|
||||
role: ServerRole::WriteReplica,
|
||||
output_mode: ConsoleOutputMode::default(),
|
||||
};
|
||||
let mut rng = StdRng::from_entropy();
|
||||
rng.fill(&mut c.cookie_key);
|
||||
|
@ -218,6 +247,11 @@ impl Configuration {
|
|||
self.role = r;
|
||||
}
|
||||
|
||||
/// Sets the output mode for writing to the console
|
||||
pub fn update_output_mode(&mut self, om: ConsoleOutputMode) {
|
||||
self.output_mode = om;
|
||||
}
|
||||
|
||||
pub fn update_tls(&mut self, chain: &Option<String>, key: &Option<String>) {
|
||||
match (chain, key) {
|
||||
(None, None) => {}
|
||||
|
|
|
@ -93,9 +93,9 @@ pub mod prelude {
|
|||
ValueSetUuid,
|
||||
};
|
||||
pub use crate::{
|
||||
admin_error, admin_info, admin_warn, filter_error, filter_info, filter_trace, filter_warn,
|
||||
perf_trace, request_error, request_info, request_trace, request_warn, security_access,
|
||||
security_critical, security_error, security_info, spanned,
|
||||
admin_debug, admin_error, admin_info, admin_warn, filter_error, filter_info, filter_trace,
|
||||
filter_warn, perf_trace, request_error, request_info, request_trace, request_warn,
|
||||
security_access, security_critical, security_error, security_info, spanned,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1354,10 +1354,10 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
|
||||
let r = self.validate();
|
||||
if r.is_empty() {
|
||||
admin_info!("schema validate -> passed");
|
||||
admin_debug!("schema validate -> passed");
|
||||
Ok(())
|
||||
} else {
|
||||
admin_info!(err = ?r, "schema validate -> errors");
|
||||
admin_error!(err = ?r, "schema validate -> errors");
|
||||
Err(OperationError::ConsistencyError(r))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1094,7 +1094,7 @@ impl QueryServer {
|
|||
Err(OperationError::NoMatchingEntries) => Ok(0),
|
||||
Err(r) => Err(r),
|
||||
}?;
|
||||
admin_info!(?system_info_version);
|
||||
admin_debug!(?system_info_version);
|
||||
|
||||
if system_info_version < 3 {
|
||||
migrate_txn.migrate_2_to_3()?;
|
||||
|
@ -1119,8 +1119,8 @@ impl QueryServer {
|
|||
ts_write_3
|
||||
.initialise_idm()
|
||||
.and_then(|_| ts_write_3.commit())?;
|
||||
|
||||
admin_info!("migrations success! ☀️ ");
|
||||
// TODO: work out if we've actually done any migrations before printing this
|
||||
admin_debug!("Database version check and migrations success! ☀️ ");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -2267,7 +2267,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
*/
|
||||
|
||||
pub fn initialise_schema_core(&self) -> Result<(), OperationError> {
|
||||
admin_info!("initialise_schema_core -> start ...");
|
||||
admin_debug!("initialise_schema_core -> start ...");
|
||||
// Load in all the "core" schema, that we already have in "memory".
|
||||
let entries = self.schema.to_entries();
|
||||
|
||||
|
@ -2277,9 +2277,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
self.internal_migrate_or_create(e)
|
||||
});
|
||||
if r.is_ok() {
|
||||
admin_info!("initialise_schema_core -> Ok!");
|
||||
admin_debug!("initialise_schema_core -> Ok!");
|
||||
} else {
|
||||
admin_info!(?r, "initialise_schema_core -> Error");
|
||||
admin_error!(?r, "initialise_schema_core -> Error");
|
||||
}
|
||||
// why do we have error handling if it's always supposed to be `Ok`?
|
||||
debug_assert!(r.is_ok());
|
||||
|
@ -2287,7 +2287,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
|
||||
pub fn initialise_schema_idm(&self) -> Result<(), OperationError> {
|
||||
admin_info!("initialise_schema_idm -> start ...");
|
||||
admin_debug!("initialise_schema_idm -> start ...");
|
||||
// List of IDM schemas to init.
|
||||
let idm_schema: Vec<&str> = vec![
|
||||
JSON_SCHEMA_ATTR_DISPLAYNAME,
|
||||
|
@ -2338,7 +2338,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.try_for_each(|e_str| self.internal_migrate_or_create_str(e_str));
|
||||
|
||||
if r.is_ok() {
|
||||
admin_info!("initialise_schema_idm -> Ok!");
|
||||
admin_debug!("initialise_schema_idm -> Ok!");
|
||||
} else {
|
||||
admin_error!(res = ?r, "initialise_schema_idm -> Error");
|
||||
}
|
||||
|
@ -2467,7 +2467,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.iter()
|
||||
.try_for_each(|e_str| self.internal_migrate_or_create_str(e_str));
|
||||
if res.is_ok() {
|
||||
admin_info!("initialise_idm -> result Ok!");
|
||||
admin_debug!("initialise_idm -> result Ok!");
|
||||
} else {
|
||||
admin_error!(?res, "initialise_idm p3 -> result");
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
|
|||
#[derive(Debug, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u64)]
|
||||
pub enum EventTag {
|
||||
AdminDebug,
|
||||
AdminError,
|
||||
AdminWarn,
|
||||
AdminInfo,
|
||||
|
@ -24,6 +25,7 @@ pub enum EventTag {
|
|||
impl EventTag {
|
||||
pub fn pretty(self) -> &'static str {
|
||||
match self {
|
||||
EventTag::AdminDebug => "admin.debug",
|
||||
EventTag::AdminError => "admin.error",
|
||||
EventTag::AdminWarn => "admin.warn",
|
||||
EventTag::AdminInfo => "admin.info",
|
||||
|
@ -46,8 +48,9 @@ impl EventTag {
|
|||
pub fn emoji(self) -> &'static str {
|
||||
use EventTag::*;
|
||||
match self {
|
||||
AdminDebug => "🐛",
|
||||
AdminError | FilterError | RequestError | SecurityError => "🚨",
|
||||
AdminWarn | FilterWarn | RequestWarn => "⚠️ ",
|
||||
AdminWarn | FilterWarn | RequestWarn => "⚠️",
|
||||
AdminInfo | FilterInfo | RequestInfo | SecurityInfo => " ",
|
||||
RequestTrace | FilterTrace | PerfTrace => "📍",
|
||||
SecurityCritical => "🔐",
|
||||
|
|
|
@ -25,6 +25,11 @@ macro_rules! tagged_event {
|
|||
}}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! admin_debug {
|
||||
($($arg:tt)*) => { tagged_event!(DEBUG, EventTag::AdminDebug, $($arg)*) }
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! admin_error {
|
||||
($($arg:tt)*) => { tagged_event!(ERROR, EventTag::AdminError, $($arg)*) }
|
||||
|
|
|
@ -31,6 +31,8 @@ ldap3_proto = "^0.2.3"
|
|||
|
||||
tracing = { version = "^0.1.35", features = ["attributes"] }
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.81"
|
||||
|
||||
async-trait = "^0.1.53"
|
||||
async-std = { version = "^1.12.0", features = ["tokio1"] }
|
||||
compact_jwt = "^0.2.1"
|
||||
|
@ -44,7 +46,6 @@ tracing-subscriber = "^0.3.11"
|
|||
# kanidm = { path = "../kanidmd" }
|
||||
# score = { path = "../kanidmd/score" }
|
||||
futures = "^0.3.21"
|
||||
serde_json = "^1.0.80"
|
||||
# async-std = { version = "1.6", features = ["tokio1"] }
|
||||
|
||||
webauthn-authenticator-rs = "^0.3.2"
|
||||
|
|
|
@ -27,19 +27,17 @@ extern crate kanidm;
|
|||
|
||||
mod https;
|
||||
mod ldaps;
|
||||
use libc::umask;
|
||||
|
||||
// use crossbeam::channel::unbounded;
|
||||
use async_std::task;
|
||||
use compact_jwt::JwsSigner;
|
||||
use kanidm::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use libc::umask;
|
||||
|
||||
use kanidm::config::Configuration;
|
||||
|
||||
// SearchResult
|
||||
// use self::ctx::ServerCtx;
|
||||
use kanidm::actors::v1_read::QueryServerReadV1;
|
||||
use kanidm::actors::v1_write::QueryServerWriteV1;
|
||||
use kanidm::be::{Backend, BackendConfig, BackendTransaction, FsType};
|
||||
use kanidm::config::{Configuration, ConsoleOutputMode};
|
||||
use kanidm::crypto::setup_tls;
|
||||
use kanidm::idm::server::{IdmServer, IdmServerDelayed};
|
||||
use kanidm::interval::IntervalActor;
|
||||
|
@ -47,11 +45,12 @@ use kanidm::ldap::LdapServer;
|
|||
use kanidm::schema::Schema;
|
||||
use kanidm::status::StatusActor;
|
||||
use kanidm::utils::{duration_from_epoch_now, touch_file_or_quit};
|
||||
|
||||
use kanidm_proto::v1::OperationError;
|
||||
|
||||
use async_std::task;
|
||||
use compact_jwt::JwsSigner;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
// === internal setup helpers
|
||||
|
||||
|
@ -484,6 +483,42 @@ pub fn verify_server_core(config: &Configuration) {
|
|||
// Now add IDM server verifications?
|
||||
}
|
||||
|
||||
// TODO: should this go somewhere else
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum MessageStatus {
|
||||
Failure,
|
||||
Success,
|
||||
}
|
||||
|
||||
impl fmt::Display for MessageStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
|
||||
match *self {
|
||||
MessageStatus::Failure => f.write_str("failure"),
|
||||
MessageStatus::Success => f.write_str("status"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AccountChangeMessage {
|
||||
action: String,
|
||||
result: String,
|
||||
status: MessageStatus,
|
||||
src_user: String,
|
||||
dest_user: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for AccountChangeMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string(self).unwrap_or(format!("{:?}", self))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recover_account_core(config: &Configuration, name: &str) {
|
||||
let schema = match Schema::new() {
|
||||
Ok(s) => s,
|
||||
|
@ -527,7 +562,26 @@ pub fn recover_account_core(config: &Configuration, name: &str) {
|
|||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
eprintln!("Success - password reset to -> {}", new_pw);
|
||||
match config.output_mode {
|
||||
ConsoleOutputMode::JSON => {
|
||||
println!(
|
||||
"{}",
|
||||
AccountChangeMessage {
|
||||
status: MessageStatus::Success,
|
||||
src_user: String::from("command-line invocation"),
|
||||
dest_user: name.to_string(),
|
||||
result: new_pw,
|
||||
action: String::from("recover_account"),
|
||||
}
|
||||
);
|
||||
}
|
||||
ConsoleOutputMode::Text => {
|
||||
println!(
|
||||
"Successfully recovered account '{}' - password reset to -> {}",
|
||||
name, new_pw
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_server_core(config: Configuration, config_test: bool) -> Result<(), ()> {
|
||||
|
|
|
@ -18,7 +18,7 @@ crate-type = ["cdylib", "rlib"]
|
|||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
|
||||
wasm-bindgen = { version = "^0.2.81", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = { version = "^0.4.30" }
|
||||
|
|
|
@ -91,7 +91,7 @@ impl Component for DeleteApp {
|
|||
type Message = Msg;
|
||||
type Properties = ModalProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
console::log!("delete modal create");
|
||||
|
||||
DeleteApp { state: State::Init }
|
||||
|
|
|
@ -24,7 +24,7 @@ clap = { version = "^3.2", features = ["derive"] }
|
|||
uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
|
||||
csv = "1.1.6"
|
||||
serde = { version = "^1.0.137", features = ["derive"] }
|
||||
serde_json = "^1.0.80"
|
||||
serde_json = "^1.0.81"
|
||||
|
||||
rand = "^0.8.5"
|
||||
toml = "^0.5.9"
|
||||
|
|
Loading…
Reference in a new issue