diff --git a/crates/cli/src/subcommands/identity.rs b/crates/cli/src/subcommands/identity.rs index b9903012c3b..c1d1e3d5ff5 100644 --- a/crates/cli/src/subcommands/identity.rs +++ b/crates/cli/src/subcommands/identity.rs @@ -3,16 +3,12 @@ use crate::{ config::{Config, IdentityConfig}, util::{init_default, y_or_n, IdentityTokenJson, InitDefaultResultType}, }; -use std::io::Write; use crate::util::print_identity_config; use anyhow::Context; use clap::{Arg, ArgAction, ArgMatches, Command}; -use email_address::EmailAddress; -use reqwest::{StatusCode, Url}; -use serde::Deserialize; +use reqwest::Url; use spacetimedb::auth::identity::decode_token; -use spacetimedb_client_api_messages::recovery::RecoveryCodeResponse; use spacetimedb_lib::Identity; use tabled::{ settings::{object::Columns, Alignment, Modify, Style}, @@ -29,15 +25,6 @@ pub fn cli() -> Command { fn get_subcommands() -> Vec { vec![ - Command::new("find").about("Find an identity for an email") - .arg( - Arg::new("email") - .required(true) - .help("The email associated with the identity that you would like to find"), - ) - .arg(common_args::server() - .help("The server to search for identities matching the email"), - ), Command::new("import") .about("Import an existing identity into your spacetime config") .arg( @@ -113,20 +100,6 @@ fn get_subcommands() -> Vec { .help("Nickname for this identity") .conflicts_with("no-save"), ) - .arg( - Arg::new("email") - .long("email") - .short('e') - .help("Recovery email for this identity") - .conflicts_with("no-email"), - ) - .arg( - Arg::new("no-email") - .long("no-email") - .help("Creates an identity without a recovery email") - .conflicts_with("email") - .action(ArgAction::SetTrue), - ) .arg( Arg::new("default") .help("Make the new identity the default for the server") @@ -135,21 +108,6 @@ fn get_subcommands() -> Vec { .conflicts_with("no-save") .action(ArgAction::SetTrue), ), - Command::new("recover") - .about("Recover an existing identity and import it into your local config") - .arg( - Arg::new("email") - .required(true) - .help("The email associated with the identity that you would like to recover."), - ) - .arg(common_args::identity().required(true).help( - "The identity you would like to recover. This identity must be associated with the email provided.", - ).value_parser(clap::value_parser!(Identity))) - .arg(common_args::server() - .help("The server from which to request recovery codes"), - ) - // TODO: project flag? - , Command::new("remove") .about("Removes a saved identity from your spacetime config") .arg(common_args::identity() @@ -191,31 +149,6 @@ fn get_subcommands() -> Vec { ) // TODO: project flag? , - Command::new("set-email") - .about("Associates an email address with an identity") - .arg( - common_args::identity() - .help("The identity string or name that should be associated with the email") - .required(true), - ) - .arg( - Arg::new("email") - .help("The email that should be assigned to the provided identity") - .required(true), - ) - .arg( - common_args::server() - .help("The server that should be informed of the email change") - .conflicts_with("all-servers") - ) - .arg( - Arg::new("all-servers") - .long("all-servers") - .short('a') - .action(ArgAction::SetTrue) - .help("Inform all known servers of the email change") - .conflicts_with("server") - ), Command::new("set-name").about("Set the name of an identity or rename an existing identity nickname").arg( common_args::identity() .help("The identity string or name to be named. If a name is supplied, the corresponding identity will be renamed.") @@ -241,10 +174,7 @@ async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Result "remove" => exec_remove(config, args).await, "set-name" => exec_set_name(config, args).await, "import" => exec_import(config, args).await, - "set-email" => exec_set_email(config, args).await, - "find" => exec_find(config, args).await, "token" => exec_token(config, args).await, - "recover" => exec_recover(config, args).await, unknown => Err(anyhow::anyhow!("Invalid subcommand: {}", unknown)), } } @@ -370,21 +300,7 @@ async fn exec_new(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E config.can_set_name(x)?; } - let email = args.get_one::("email"); - let no_email = args.get_flag("no-email"); - if email.is_none() && !no_email { - return Err(anyhow::anyhow!( - "You must either supply an email with --email , or pass the --no-email flag." - )); - } - - let mut query_params = Vec::<(&str, &str)>::new(); - if let Some(email) = email { - if !EmailAddress::is_valid(email.as_str()) { - return Err(anyhow::anyhow!("The email you provided is malformed: {}", email)); - } - query_params.push(("email", email.as_str())) - } + let query_params = Vec::<(&str, &str)>::new(); let mut builder = reqwest::Client::new().post(Url::parse_with_params( format!("{}/identity", config.get_host_url(server)?).as_str(), @@ -413,7 +329,6 @@ async fn exec_new(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E println!(" IDENTITY {}", identity); println!(" NAME {}", alias.unwrap_or(&String::new())); - println!(" EMAIL {}", email.unwrap_or(&String::new())); Ok(()) } @@ -510,45 +425,6 @@ Fetch the server's fingerprint with: Ok(()) } -#[derive(Debug, Clone, Deserialize)] -struct GetIdentityResponse { - identities: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -struct GetIdentityResponseEntry { - identity: String, - email: String, -} - -/// Executes the `identity find` command which finds an identity by email. -async fn exec_find(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> { - let email = args.get_one::("email").unwrap().clone(); - let server = args.get_one::("server").map(|s| s.as_ref()); - let client = reqwest::Client::new(); - let builder = client.get(format!("{}/identity?email={}", config.get_host_url(server)?, email)); - - let res = builder.send().await?; - - if res.status() == StatusCode::OK { - let response: GetIdentityResponse = res.json().await?; - if response.identities.is_empty() { - return Err(anyhow::anyhow!("Could not find identity for: {}", email)); - } - - for identity in response.identities { - println!("Identity"); - println!(" IDENTITY {}", identity.identity); - println!(" EMAIL {}", identity.email); - } - Ok(()) - } else if res.status() == StatusCode::NOT_FOUND { - Err(anyhow::anyhow!("Could not find identity for: {}", email)) - } else { - Err(anyhow::anyhow!("Error occurred in lookup: {}", res.status())) - } -} - /// Executes the `identity token` command which prints the token for an identity. async fn exec_token(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> { let identity = args.get_one::("identity").unwrap(); @@ -573,114 +449,3 @@ async fn exec_set_name(mut config: Config, args: &ArgMatches) -> Result<(), anyh config.save(); Ok(()) } - -/// Executes the `identity set-email` command which sets the email for an identity. -async fn exec_set_email(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> { - let email = args.get_one::("email").unwrap().clone(); - let server = args.get_one::("server").map(|s| s.as_ref()); - let identity = args.get_one::("identity").unwrap(); - let identity_config = config - .get_identity_config(identity) - .ok_or_else(|| anyhow::anyhow!("Missing identity credentials for identity: {identity}"))?; - - // TODO: check that the identity is valid for the server - - reqwest::Client::new() - .post(format!( - "{}/identity/{}/set-email?email={}", - config.get_host_url(server)?, - identity_config.identity, - email - )) - .basic_auth("token", Some(&identity_config.token)) - .send() - .await? - .error_for_status()?; - - println!(" Associated email with identity"); - print_identity_config(identity_config); - println!(" EMAIL {}", email); - - Ok(()) -} - -/// Executes the `identity recover` command which recovers an identity from an email. -async fn exec_recover(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> { - let identity = args.get_one::("identity").unwrap(); - let email = args.get_one::("email").unwrap(); - let server = args.get_one::("server").map(|s| s.as_ref()); - - let query_params = [ - ("email", email.as_str()), - ("identity", &*identity.to_hex()), - ("link", "false"), - ]; - - if config.get_identity_config_by_identity(identity).is_some() { - return Err(anyhow::anyhow!("No need to recover this identity, it is already stored in your config. Use `spacetime identity list` to list identities.")); - } - - let client = reqwest::Client::new(); - let builder = client.post(Url::parse_with_params( - format!("{}/identity/request_recovery_code", config.get_host_url(server)?,).as_str(), - query_params, - )?); - let res = builder.send().await?; - res.error_for_status()?; - - println!( - "We have successfully sent a recovery code to {}. Enter the code now.", - email - ); - for _ in 0..5 { - print!("Recovery Code: "); - std::io::stdout().flush()?; - let mut line = String::new(); - std::io::stdin().read_line(&mut line).unwrap(); - let code = match line.trim().parse::() { - Ok(value) => value, - Err(_) => { - println!("Malformed code. Please try again."); - continue; - } - }; - - let client = reqwest::Client::new(); - let builder = client.post(Url::parse_with_params( - format!("{}/identity/confirm_recovery_code", config.get_host_url(server)?,).as_str(), - vec![ - ("code", code.to_string().as_str()), - ("email", email.as_str()), - ("identity", identity.to_hex().as_str()), - ], - )?); - let res = builder.send().await?; - match res.error_for_status() { - Ok(res) => { - let buf = res.bytes().await?.to_vec(); - let utf8 = String::from_utf8(buf)?; - let response: RecoveryCodeResponse = serde_json::from_str(utf8.as_str())?; - let identity_config = IdentityConfig { - nickname: None, - identity: response.identity, - token: response.token, - }; - config.identity_configs_mut().push(identity_config.clone()); - config.set_default_identity_if_unset(server, &identity_config.identity.to_hex())?; - config.save(); - println!("Success. Identity imported."); - print_identity_config(&identity_config); - // TODO: Remove this once print_identity_config prints email - println!(" EMAIL {}", email); - return Ok(()); - } - Err(_) => { - println!("Invalid recovery code, please try again."); - } - } - } - - Err(anyhow::anyhow!( - "Maximum amount of attempts reached. Please start the process over." - )) -} diff --git a/crates/client-api-messages/src/lib.rs b/crates/client-api-messages/src/lib.rs index 504b0a7bfa8..8ff382909be 100644 --- a/crates/client-api-messages/src/lib.rs +++ b/crates/client-api-messages/src/lib.rs @@ -2,6 +2,5 @@ pub mod energy; pub mod name; -pub mod recovery; pub mod timestamp; pub mod websocket; diff --git a/crates/client-api-messages/src/recovery.rs b/crates/client-api-messages/src/recovery.rs deleted file mode 100644 index 914fe082a5c..00000000000 --- a/crates/client-api-messages/src/recovery.rs +++ /dev/null @@ -1,19 +0,0 @@ -use chrono::serde::ts_seconds; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use spacetimedb_lib::Identity; - -#[derive(Deserialize, Serialize, Clone)] -pub struct RecoveryCode { - pub code: String, - #[serde(with = "ts_seconds")] - pub generation_time: DateTime, - pub identity: Identity, -} - -#[derive(Serialize, Deserialize)] -pub struct RecoveryCodeResponse { - pub identity: Identity, - pub token: String, -} diff --git a/crates/client-api/src/auth.rs b/crates/client-api/src/auth.rs index b2539ff33a3..7e8c8d8b297 100644 --- a/crates/client-api/src/auth.rs +++ b/crates/client-api/src/auth.rs @@ -6,6 +6,7 @@ use axum::response::IntoResponse; use axum_extra::typed_header::TypedHeader; use headers::{authorization, HeaderMapExt}; use http::{request, HeaderValue, StatusCode}; +use rand::Rng; use serde::Deserialize; use spacetimedb::auth::identity::{ decode_token, encode_token, DecodingKey, EncodingKey, JwtError, JwtErrorKind, SpacetimeIdentityClaims, @@ -96,7 +97,15 @@ pub struct SpacetimeAuth { impl SpacetimeAuth { /// Allocate a new identity, and mint a new token for it. pub async fn alloc(ctx: &(impl NodeDelegate + ControlStateDelegate + ?Sized)) -> axum::response::Result { - let identity = ctx.create_identity().await.map_err(log_and_500)?; + // TODO: I'm just sticking in a random string until we change how identities are generated. + let identity = { + let mut rng = rand::thread_rng(); + let mut random_bytes = [0u8; 16]; // Example: 16 random bytes + rng.fill(&mut random_bytes); + + let preimg = [b"clockworklabs:", &random_bytes[..]].concat(); + Identity::from_hashing_bytes(preimg) + }; let creds = SpacetimeCreds::encode_token(ctx.private_key(), identity).map_err(log_and_500)?; Ok(Self { creds, identity }) } diff --git a/crates/client-api/src/lib.rs b/crates/client-api/src/lib.rs index a71497fe670..dc90e58e4ec 100644 --- a/crates/client-api/src/lib.rs +++ b/crates/client-api/src/lib.rs @@ -10,10 +10,9 @@ use spacetimedb::client::ClientActorIndex; use spacetimedb::energy::{EnergyBalance, EnergyQuanta}; use spacetimedb::host::{HostController, UpdateDatabaseResult}; use spacetimedb::identity::Identity; -use spacetimedb::messages::control_db::{Database, HostType, IdentityEmail, Node, Replica}; +use spacetimedb::messages::control_db::{Database, HostType, Node, Replica}; use spacetimedb::sendgrid_controller::SendGridController; use spacetimedb_client_api_messages::name::{DomainName, InsertDomainResult, RegisterTldResult, Tld}; -use spacetimedb_client_api_messages::recovery::RecoveryCode; pub mod auth; pub mod routes; @@ -97,11 +96,6 @@ pub trait ControlStateReadAccess { fn get_replicas(&self) -> anyhow::Result>; fn get_leader_replica_by_database(&self, database_id: u64) -> Option; - // Identities - fn get_identities_for_email(&self, email: &str) -> anyhow::Result>; - fn get_emails_for_identity(&self, identity: &Identity) -> anyhow::Result>; - fn get_recovery_codes(&self, email: &str) -> anyhow::Result>; - // Energy fn get_energy_balance(&self, identity: &Identity) -> anyhow::Result>; @@ -132,11 +126,6 @@ pub trait ControlStateWriteAccess: Send + Sync { async fn delete_database(&self, identity: &Identity, address: &Address) -> anyhow::Result<()>; - // Identities - async fn create_identity(&self) -> anyhow::Result; - async fn add_email(&self, identity: &Identity, email: &str) -> anyhow::Result<()>; - async fn insert_recovery_code(&self, identity: &Identity, email: &str, code: RecoveryCode) -> anyhow::Result<()>; - // Energy async fn add_energy(&self, identity: &Identity, amount: EnergyQuanta) -> anyhow::Result<()>; async fn withdraw_energy(&self, identity: &Identity, amount: EnergyQuanta) -> anyhow::Result<()>; @@ -185,17 +174,6 @@ impl ControlStateReadAccess for Arc { (**self).get_leader_replica_by_database(database_id) } - // Identities - fn get_identities_for_email(&self, email: &str) -> anyhow::Result> { - (**self).get_identities_for_email(email) - } - fn get_emails_for_identity(&self, identity: &Identity) -> anyhow::Result> { - (**self).get_emails_for_identity(identity) - } - fn get_recovery_codes(&self, email: &str) -> anyhow::Result> { - (**self).get_recovery_codes(email) - } - // Energy fn get_energy_balance(&self, identity: &Identity) -> anyhow::Result> { (**self).get_energy_balance(identity) @@ -229,18 +207,6 @@ impl ControlStateWriteAccess for Arc { (**self).delete_database(identity, address).await } - async fn create_identity(&self) -> anyhow::Result { - (**self).create_identity().await - } - - async fn add_email(&self, identity: &Identity, email: &str) -> anyhow::Result<()> { - (**self).add_email(identity, email).await - } - - async fn insert_recovery_code(&self, identity: &Identity, email: &str, code: RecoveryCode) -> anyhow::Result<()> { - (**self).insert_recovery_code(identity, email, code).await - } - async fn add_energy(&self, identity: &Identity, amount: EnergyQuanta) -> anyhow::Result<()> { (**self).add_energy(identity, amount).await } diff --git a/crates/client-api/src/routes/identity.rs b/crates/client-api/src/routes/identity.rs index bc466d21bdb..1db0590c8c7 100644 --- a/crates/client-api/src/routes/identity.rs +++ b/crates/client-api/src/routes/identity.rs @@ -1,27 +1,17 @@ use std::time::Duration; -use axum::extract::{Path, Query, State}; +use axum::extract::{Path, State}; use axum::response::IntoResponse; -use axum::Extension; -use chrono::Utc; use http::header::CONTENT_TYPE; use http::StatusCode; -use rand::Rng; use serde::{Deserialize, Serialize}; -use spacetimedb::auth::identity::{encode_token, encode_token_with_expiry}; -use spacetimedb::messages::control_db::IdentityEmail; -use spacetimedb_client_api_messages::recovery::{RecoveryCode, RecoveryCodeResponse}; +use spacetimedb::auth::identity::encode_token_with_expiry; use spacetimedb_lib::de::serde::DeserializeWrapper; use spacetimedb_lib::{Address, Identity}; -use crate::auth::{anon_auth_middleware, SpacetimeAuth, SpacetimeAuthRequired}; -use crate::{log_and_500, ControlStateDelegate, ControlStateReadAccess, ControlStateWriteAccess, NodeDelegate}; - -#[derive(Deserialize)] -pub struct CreateIdentityQueryParams { - email: Option, -} +use crate::auth::{SpacetimeAuth, SpacetimeAuthRequired}; +use crate::{log_and_500, ControlStateDelegate, NodeDelegate}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateIdentityResponse { @@ -31,14 +21,8 @@ pub struct CreateIdentityResponse { pub async fn create_identity( State(ctx): State, - Query(CreateIdentityQueryParams { email }): Query, ) -> axum::response::Result { let auth = SpacetimeAuth::alloc(&ctx).await?; - if let Some(email) = email { - ctx.add_email(&auth.identity, email.as_str()) - .await - .map_err(log_and_500)?; - } let identity_response = CreateIdentityResponse { identity: auth.identity, @@ -47,41 +31,6 @@ pub async fn create_identity( Ok(axum::Json(identity_response)) } -#[derive(Debug, Clone, Serialize)] -pub struct GetIdentityResponse { - identities: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct GetIdentityResponseEntry { - identity: Identity, - email: String, -} - -#[derive(Deserialize)] -pub struct GetIdentityQueryParams { - email: Option, -} -pub async fn get_identity( - State(ctx): State, - Query(GetIdentityQueryParams { email }): Query, -) -> axum::response::Result { - match email { - None => Err(StatusCode::BAD_REQUEST.into()), - Some(email) => { - let identities = ctx.get_identities_for_email(email.as_str()).map_err(log_and_500)?; - let identities = identities - .into_iter() - .map(|identity_email| GetIdentityResponseEntry { - identity: identity_email.identity, - email: identity_email.email, - }) - .collect::>(); - Ok(axum::Json(GetIdentityResponse { identities })) - } - } -} - /// A version of `Identity` appropriate for URL de/encoding. /// /// Because `Identity` is represented in SATS as a `ProductValue`, @@ -102,53 +51,6 @@ impl<'de> serde::Deserialize<'de> for IdentityForUrl { } } -#[derive(Deserialize)] -pub struct SetEmailParams { - identity: IdentityForUrl, -} - -#[derive(Deserialize)] -pub struct SetEmailQueryParams { - email: email_address::EmailAddress, -} - -pub async fn set_email( - State(ctx): State, - Path(SetEmailParams { identity }): Path, - Query(SetEmailQueryParams { email }): Query, - Extension(auth): Extension, -) -> axum::response::Result { - let identity = identity.into(); - - if auth.identity != identity { - return Err(StatusCode::UNAUTHORIZED.into()); - } - ctx.add_email(&identity, email.as_str()).await.map_err(log_and_500)?; - - Ok(()) -} - -pub async fn check_email( - State(ctx): State, - Path(SetEmailParams { identity }): Path, - Extension(auth): Extension, -) -> axum::response::Result { - let identity = identity.into(); - - if auth.identity != identity { - return Err(StatusCode::UNAUTHORIZED.into()); - } - - let emails = ctx - .get_emails_for_identity(&identity) - .map_err(log_and_500)? - .into_iter() - .map(|IdentityEmail { email, .. }| email) - .collect::>(); - - Ok(axum::Json(emails)) -} - #[derive(Deserialize)] pub struct GetDatabasesParams { identity: IdentityForUrl, @@ -216,127 +118,15 @@ pub async fn get_public_key(State(ctx): State) -> axum::resp )) } -#[derive(Deserialize)] -pub struct RequestRecoveryCodeParams { - /// Whether or not the client is requesting a login link for a web-login. This is false for CLI logins. - #[serde(default)] - link: bool, - email: String, - identity: IdentityForUrl, -} - -pub async fn request_recovery_code( - State(ctx): State, - Query(RequestRecoveryCodeParams { link, email, identity }): Query, -) -> axum::response::Result { - let identity = Identity::from(identity); - let Some(sendgrid) = ctx.sendgrid_controller() else { - log::error!("A recovery code was requested, but SendGrid is disabled."); - return Err((StatusCode::INTERNAL_SERVER_ERROR, "SendGrid is disabled.").into()); - }; - - if !ctx - .get_identities_for_email(email.as_str()) - .map_err(log_and_500)? - .iter() - .any(|a| a.identity == identity) - { - return Err(( - StatusCode::BAD_REQUEST, - "Email is not associated with the provided identity.", - ) - .into()); - } - - let code = rand::thread_rng().gen_range(0..=999999); - let code = format!("{code:06}"); - let recovery_code = RecoveryCode { - code: code.clone(), - generation_time: Utc::now(), - identity, - }; - ctx.insert_recovery_code(&identity, email.as_str(), recovery_code) - .await - .map_err(log_and_500)?; - - sendgrid - .send_recovery_email(email.as_str(), code.as_str(), &identity.to_hex(), link) - .await - .map_err(log_and_500)?; - Ok(()) -} - -#[derive(Deserialize)] -pub struct ConfirmRecoveryCodeParams { - pub email: String, - pub identity: IdentityForUrl, - pub code: String, -} - -/// Note: We should be slightly more security conscious about this function because -/// we are providing a login token to the user initiating the request. We want to make -/// sure there aren't any logical issues in here that would allow a user to request a token -/// for an identity that they don't have authority over. -pub async fn confirm_recovery_code( - State(ctx): State, - Query(ConfirmRecoveryCodeParams { email, identity, code }): Query, -) -> axum::response::Result { - let identity = Identity::from(identity); - let recovery_codes = ctx.get_recovery_codes(email.as_str()).map_err(log_and_500)?; - - let recovery_code = recovery_codes - .into_iter() - .find(|rc| rc.code == code.as_str()) - .ok_or((StatusCode::NOT_FOUND, "Recovery code not found."))?; - - let duration = Utc::now() - recovery_code.generation_time; - if duration.num_seconds() > 60 * 10 { - return Err((StatusCode::BAD_REQUEST, "Recovery code expired.").into()); - } - - // Make sure the identity provided by the request matches the recovery code registration - if recovery_code.identity != identity { - return Err(( - StatusCode::BAD_REQUEST, - "Recovery code doesn't match the provided identity.", - ) - .into()); - } - - if !ctx - .get_identities_for_email(email.as_str()) - .map_err(log_and_500)? - .iter() - .any(|a| a.identity == identity) - { - // This can happen if someone changes their associated email during a recovery request. - return Err((StatusCode::NOT_FOUND, "No identity associated with that email.").into()); - } - - // Recovery code is verified, return the identity and token to the user - let token = encode_token(ctx.private_key(), identity).map_err(log_and_500)?; - let result = RecoveryCodeResponse { identity, token }; - - Ok(axum::Json(result)) -} - -pub fn router(ctx: S) -> axum::Router +pub fn router(_: S) -> axum::Router where S: NodeDelegate + ControlStateDelegate + Clone + 'static, { use axum::routing::{get, post}; - let auth_middleware = axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::); axum::Router::new() - .route("/", get(get_identity::).post(create_identity::)) + .route("/", post(create_identity::)) .route("/public-key", get(get_public_key::)) - .route("/request_recovery_code", post(request_recovery_code::)) - .route("/confirm_recovery_code", post(confirm_recovery_code::)) .route("/websocket_token", post(create_websocket_token::)) .route("/:identity/verify", get(validate_token)) - .route( - "/:identity/set-email", - post(set_email::).route_layer(auth_middleware.clone()), - ) - .route("/:identity/emails", get(check_email::).route_layer(auth_middleware)) .route("/:identity/databases", get(get_databases::)) } diff --git a/crates/standalone/src/control_db.rs b/crates/standalone/src/control_db.rs index 6f608e3dc7e..422583ca242 100644 --- a/crates/standalone/src/control_db.rs +++ b/crates/standalone/src/control_db.rs @@ -1,13 +1,12 @@ use spacetimedb::address::Address; use spacetimedb::hash::hash_bytes; use spacetimedb::identity::Identity; -use spacetimedb::messages::control_db::{Database, EnergyBalance, IdentityEmail, Node, Replica}; +use spacetimedb::messages::control_db::{Database, EnergyBalance, Node, Replica}; use spacetimedb::{energy, stdb_path}; use spacetimedb_client_api_messages::name::{ DomainName, DomainParsingError, InsertDomainResult, RegisterTldResult, Tld, TldRef, }; -use spacetimedb_client_api_messages::recovery::RecoveryCode; use spacetimedb_lib::bsatn; #[cfg(test)] @@ -184,48 +183,6 @@ impl ControlDb { } } - /// Starts a recovery code request - /// - /// * `email` - The email to send the recovery code to - pub fn spacetime_insert_recovery_code(&self, email: &str, new_code: RecoveryCode) -> Result<()> { - // TODO(jdetter): This function should take an identity instead of an email - let tree = self.db.open_tree("recovery_codes")?; - let current_requests = tree.get(email.as_bytes())?; - match current_requests { - None => { - tree.insert(email.as_bytes(), serde_json::to_string(&vec![new_code])?.as_bytes())?; - } - Some(codes_bytes) => { - let mut codes: Vec = serde_json::from_slice(&codes_bytes[..])?; - codes.push(new_code); - tree.insert(email.as_bytes(), serde_json::to_string(&codes)?.as_bytes())?; - } - } - - Ok(()) - } - - pub fn spacetime_get_recovery_codes(&self, email: &str) -> Result> { - let tree = self.db.open_tree("recovery_codes")?; - let current_requests = tree.get(email.as_bytes())?; - current_requests - .map(|bytes| { - let codes: Vec = serde_json::from_slice(&bytes[..])?; - Ok(codes) - }) - .unwrap_or(Ok(vec![])) - } - - pub fn _spacetime_get_recovery_code(&self, email: &str, code: &str) -> Result> { - for recovery_code in self.spacetime_get_recovery_codes(email)? { - if recovery_code.code == code { - return Ok(Some(recovery_code)); - } - } - - Ok(None) - } - /// Returns the owner (or `None` if there is no owner) of the domain. /// /// # Arguments @@ -238,16 +195,6 @@ impl ControlDb { } } - pub fn alloc_spacetime_identity(&self) -> Result { - // TODO: this really doesn't need to be a single global count - let id = self.db.generate_id()?; - let bytes: &[u8] = &id.to_le_bytes(); - let name = b"clockworklabs:"; - let bytes = [name, bytes].concat(); - let hash = Identity::from_hashing_bytes(bytes); - Ok(hash) - } - pub fn alloc_spacetime_address(&self) -> Result
{ // TODO: this really doesn't need to be a single global count // We could do something more intelligent for addresses... @@ -262,43 +209,6 @@ impl ControlDb { Ok(address) } - pub async fn associate_email_spacetime_identity(&self, identity: Identity, email: &str) -> Result<()> { - // Lowercase the email before storing - let email = email.to_lowercase(); - - let tree = self.db.open_tree("email")?; - let identity_email = IdentityEmail { identity, email }; - let buf = bsatn::to_vec(&identity_email).unwrap(); - tree.insert(identity.as_bytes(), buf)?; - Ok(()) - } - - pub fn get_identities_for_email(&self, email: &str) -> Result> { - let mut result = Vec::::new(); - let tree = self.db.open_tree("email")?; - for i in tree.iter() { - let (_, value) = i?; - let iemail: IdentityEmail = bsatn::from_slice(&value)?; - if iemail.email.eq_ignore_ascii_case(email) { - result.push(iemail); - } - } - Ok(result) - } - - pub fn get_emails_for_identity(&self, identity: &Identity) -> Result> { - let mut result = Vec::::new(); - let tree = self.db.open_tree("email")?; - for i in tree.iter() { - let (_, value) = i?; - let iemail: IdentityEmail = bsatn::from_slice(&value)?; - if &iemail.identity == identity { - result.push(iemail); - } - } - Ok(result) - } - pub fn get_databases(&self) -> Result> { let tree = self.db.open_tree("database")?; let mut databases = Vec::new(); diff --git a/crates/standalone/src/control_db/tests.rs b/crates/standalone/src/control_db/tests.rs index ebeb45fe0d8..e903ab5e778 100644 --- a/crates/standalone/src/control_db/tests.rs +++ b/crates/standalone/src/control_db/tests.rs @@ -92,7 +92,8 @@ fn test_decode() -> ResultTest<()> { let cdb = ControlDb::at(path)?; - let id = cdb.alloc_spacetime_identity()?; + // TODO: Use a random identity. + let id = Identity::ZERO; let db = Database { id: 0, diff --git a/crates/standalone/src/lib.rs b/crates/standalone/src/lib.rs index 8be943c4465..c2d5b7348e6 100644 --- a/crates/standalone/src/lib.rs +++ b/crates/standalone/src/lib.rs @@ -21,12 +21,11 @@ use spacetimedb::db::{db_metrics::DB_METRICS, Config}; use spacetimedb::energy::{EnergyBalance, EnergyQuanta}; use spacetimedb::host::{DiskStorage, HostController, UpdateDatabaseResult}; use spacetimedb::identity::Identity; -use spacetimedb::messages::control_db::{Database, IdentityEmail, Node, Replica}; +use spacetimedb::messages::control_db::{Database, Node, Replica}; use spacetimedb::sendgrid_controller::SendGridController; use spacetimedb::stdb_path; use spacetimedb::worker_metrics::WORKER_METRICS; use spacetimedb_client_api_messages::name::{DomainName, InsertDomainResult, RegisterTldResult, Tld}; -use spacetimedb_client_api_messages::recovery::RecoveryCode; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; @@ -228,19 +227,6 @@ impl spacetimedb_client_api::ControlStateReadAccess for StandaloneEnv { self.control_db.get_leader_replica_by_database(database_id) } - // Identities - fn get_identities_for_email(&self, email: &str) -> anyhow::Result> { - Ok(self.control_db.get_identities_for_email(email)?) - } - - fn get_emails_for_identity(&self, identity: &Identity) -> anyhow::Result> { - Ok(self.control_db.get_emails_for_identity(identity)?) - } - - fn get_recovery_codes(&self, email: &str) -> anyhow::Result> { - Ok(self.control_db.spacetime_get_recovery_codes(email)?) - } - // Energy fn get_energy_balance(&self, identity: &Identity) -> anyhow::Result> { Ok(self.control_db.get_energy_balance(identity)?) @@ -380,21 +366,6 @@ impl spacetimedb_client_api::ControlStateWriteAccess for StandaloneEnv { Ok(()) } - async fn create_identity(&self) -> anyhow::Result { - Ok(self.control_db.alloc_spacetime_identity()?) - } - - async fn add_email(&self, identity: &Identity, email: &str) -> anyhow::Result<()> { - self.control_db - .associate_email_spacetime_identity(*identity, email) - .await?; - Ok(()) - } - - async fn insert_recovery_code(&self, _identity: &Identity, email: &str, code: RecoveryCode) -> anyhow::Result<()> { - Ok(self.control_db.spacetime_insert_recovery_code(email, code)?) - } - async fn add_energy(&self, identity: &Identity, amount: EnergyQuanta) -> anyhow::Result<()> { let balance = self .control_db diff --git a/crates/testing/src/modules.rs b/crates/testing/src/modules.rs index e0adf66e83c..69226880a68 100644 --- a/crates/testing/src/modules.rs +++ b/crates/testing/src/modules.rs @@ -5,6 +5,7 @@ use std::sync::OnceLock; use std::time::Instant; use spacetimedb::messages::control_db::HostType; +use spacetimedb::Identity; use spacetimedb_lib::ser::serde::SerializeWrapper; use tokio::runtime::{Builder, Runtime}; @@ -150,7 +151,8 @@ impl CompiledModule { crate::set_key_env_vars(&paths); let env = spacetimedb_standalone::StandaloneEnv::init(config).await.unwrap(); - let identity = env.create_identity().await.unwrap(); + // TODO: Fix this when we update identity generation. + let identity = Identity::ZERO; let db_address = env.create_address().await.unwrap(); let client_address = env.create_address().await.unwrap(); diff --git a/smoketests/__init__.py b/smoketests/__init__.py index c9028686b88..caa5866c00a 100644 --- a/smoketests/__init__.py +++ b/smoketests/__init__.py @@ -194,8 +194,8 @@ def fingerprint(self): # Fetch the server's fingerprint; required for `identity list`. self.spacetime("server", "fingerprint", "-s", "localhost", "-y") - def new_identity(self, *, email, default=False): - output = self.spacetime("identity", "new", "--no-email" if email is None else f"--email={email}") + def new_identity(self, *, default=False): + output = self.spacetime("identity", "new") identity = extract_field(output, "IDENTITY") if default: self.spacetime("identity", "set-default", "--identity", identity) diff --git a/smoketests/tests/domains.py b/smoketests/tests/domains.py index 31e067e55c5..83a65d67773 100644 --- a/smoketests/tests/domains.py +++ b/smoketests/tests/domains.py @@ -8,7 +8,7 @@ def test_register_domain(self): rand_domain = random_string() - identity = self.new_identity(email=None) + identity = self.new_identity() self.spacetime("dns", "register-tld", rand_domain) self.publish_module(rand_domain) diff --git a/smoketests/tests/identity.py b/smoketests/tests/identity.py index 88f2023d4a1..63a7a0f5498 100644 --- a/smoketests/tests/identity.py +++ b/smoketests/tests/identity.py @@ -1,7 +1,4 @@ -from .. import Smoketest, random_string, extract_field - -def random_email(): - return random_string() + "@clockworklabs.io" +from .. import Smoketest, extract_field class IdentityImports(Smoketest): AUTOPUBLISH = False @@ -16,7 +13,7 @@ def test_import(self): This test does not require a remote spacetimedb instance. """ - identity = self.new_identity(email=None) + identity = self.new_identity() token = self.token(identity) self.reset_config() @@ -26,40 +23,13 @@ def test_import(self): self.import_identity(identity, token, default=True) # [ "$(grep "$IDENT" "$TEST_OUT" | awk '{print $1}')" == '***' ] - def test_new_email(self): - """This test is designed to make sure an email can be set while creating a new identity""" - - # Create a new identity - email = random_email() - identity = self.new_identity(email=email) - token = self.token(identity) - - # Reset our config so we lose this identity - self.reset_config() - - # Import this identity, and set it as the default identity - self.import_identity(identity, token, default=True) - - # Configure our email - output = self.spacetime("identity", "set-email", "--identity", identity, email) - self.assertEqual(extract_field(output, "IDENTITY"), identity) - self.assertEqual(extract_field(output, "EMAIL"), email) - - # Reset config again - self.reset_config() - - # Find our identity by its email - output = self.spacetime("identity", "find", email) - self.assertEqual(extract_field(output, "IDENTITY"), identity) - self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) - def test_remove(self): """Test deleting an identity from your local ~/.spacetime/config.toml file.""" self.fingerprint() - self.new_identity(email=None) - identity = self.new_identity(email=None) + self.new_identity() + identity = self.new_identity() identities = self.spacetime("identity", "list") self.assertIn(identity, identities) @@ -75,8 +45,8 @@ def test_remove_all(self): self.fingerprint() - identity1 = self.new_identity(email=None) - identity2 = self.new_identity(email=None) + identity1 = self.new_identity() + identity2 = self.new_identity() identities = self.spacetime("identity", "list") self.assertIn(identity2, identities) @@ -93,8 +63,8 @@ def test_set_default(self): self.fingerprint() - self.new_identity(email=None) - identity = self.new_identity(email=None) + self.new_identity() + identity = self.new_identity() identities = self.spacetime("identity", "list").splitlines() default_identity = next(filter(lambda s: "***" in s, identities), "") @@ -106,32 +76,3 @@ def test_set_default(self): default_identity = next(filter(lambda s: "***" in s, identities), "") self.assertIn(identity, default_identity) - def test_set_email(self): - """Ensure that we are able to associate an email with an identity""" - - self.fingerprint() - - # Create a new identity - identity = self.new_identity(email=None) - email = random_email() - token = self.token(identity) - - # Reset our config so we lose this identity - self.reset_config() - - # Import this identity, and set it as the default identity - self.import_identity(identity, token, default=True) - - # Configure our email - output = self.spacetime("identity", "set-email", "--identity", identity, email) - self.assertEqual(extract_field(output, "IDENTITY"), identity) - self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) - - # Reset config again - self.reset_config() - - # Find our identity by its email - output = self.spacetime("identity", "find", email) - self.assertEqual(extract_field(output, "IDENTITY"), identity) - self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) - diff --git a/smoketests/tests/new_user_flow.py b/smoketests/tests/new_user_flow.py index 1002b9f4c96..567a77f8e9d 100644 --- a/smoketests/tests/new_user_flow.py +++ b/smoketests/tests/new_user_flow.py @@ -29,7 +29,7 @@ def test_new_user_flow(self): """Test the entirety of the new user flow.""" ## Publish your module - self.new_identity(email=None) + self.new_identity() self.publish_module() diff --git a/smoketests/tests/permissions.py b/smoketests/tests/permissions.py index f055016509e..df842bb7155 100644 --- a/smoketests/tests/permissions.py +++ b/smoketests/tests/permissions.py @@ -9,14 +9,14 @@ def setUp(self): def test_call(self): """Ensure that anyone has the permission to call any standard reducer""" - identity = self.new_identity(email=None) + identity = self.new_identity() token = self.token(identity) self.publish_module() # TODO: can a lot of the usage of reset_config be replaced with just passing -i ? or -a ? self.reset_config() - self.new_identity(email=None) + self.new_identity() self.call("say_hello") self.reset_config() @@ -26,7 +26,7 @@ def test_call(self): def test_delete(self): """Ensure that you cannot delete a database that you do not own""" - identity = self.new_identity(email=None, default=True) + identity = self.new_identity(default=True) self.publish_module() @@ -37,32 +37,32 @@ def test_delete(self): def test_describe(self): """Ensure that anyone can describe any database""" - self.new_identity(email=None) + self.new_identity() self.publish_module() self.reset_config() - self.new_identity(email=None) + self.new_identity() self.spacetime("describe", self.address) def test_logs(self): """Ensure that we are not able to view the logs of a module that we don't have permission to view""" - self.new_identity(email=None) + self.new_identity() self.publish_module() self.reset_config() - self.new_identity(email=None) + self.new_identity() self.call("say_hello") self.reset_config() - identity = self.new_identity(email=None, default=True) + identity = self.new_identity(default=True) with self.assertRaises(Exception): self.spacetime("logs", self.address, "-n", "10000") def test_publish(self): """This test checks to make sure that you cannot publish to an address that you do not own.""" - self.new_identity(email=None, default=True) + self.new_identity(default=True) self.publish_module() self.reset_config() @@ -112,7 +112,7 @@ def test_private_table(self): """) self.reset_config() - self.new_identity(email=None) + self.new_identity() with self.assertRaises(Exception): self.spacetime("sql", self.address, "select * from secret") diff --git a/smoketests/tests/template b/smoketests/tests/template index 46d21673020..c773961e631 100644 --- a/smoketests/tests/template +++ b/smoketests/tests/template @@ -9,7 +9,7 @@ set -euox pipefail source "./test/lib.include" -run_test cargo run identity new --no-domain --no-email +run_test cargo run identity new --no-domain IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') EMAIL="$(random_string)@clockworklabs.io" TOKEN=$(grep token "$HOME/.spacetime/config.toml" | awk '{print $3}' | tr -d \') @@ -18,19 +18,11 @@ reset_config run_test cargo run identity add "$IDENT" "$TOKEN" run_test cargo run identity set-default "$IDENT" -run_test cargo run identity set-email "$IDENT" "$EMAIL" [ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] reset_config -run_test cargo run identity find "$EMAIL" -[ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] - -run_test cargo run identity new --email "$EMAIL" --no-domain -run_test cargo run identity find "$EMAIL" -[ "2" == "$(grep EMAIL "$TEST_OUT" | wc -l | awk '{print $1}')" ] +run_test cargo run identity new --no-domain run_test cargo run publish ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')"