Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use toml_edit::ArrayOfTables;
const DEFAULT_SERVER_KEY: &str = "default_server";
const WEB_SESSION_TOKEN_KEY: &str = "web_session_token";
const SPACETIMEDB_TOKEN_KEY: &str = "spacetimedb_token";
const ROOT_DATABASE_NAMESPACE_KEY: &str = "root_database_namespace";
const SERVER_CONFIGS_KEY: &str = "server_configs";
const NICKNAME_KEY: &str = "nickname";
const HOST_KEY: &str = "host";
Expand Down Expand Up @@ -124,6 +125,7 @@ pub struct RawConfig {
// TODO: Move these IDs/tokens out of config so we're no longer storing sensitive tokens in a human-edited file.
web_session_token: Option<String>,
spacetimedb_token: Option<String>,
root_database_namespace: Option<String>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -173,6 +175,7 @@ impl RawConfig {
server_configs: vec![maincloud, local],
web_session_token: None,
spacetimedb_token: None,
root_database_namespace: None,
}
}

Expand Down Expand Up @@ -448,9 +451,14 @@ Fetch the server's fingerprint with:
self.spacetimedb_token = Some(token);
}

pub fn set_root_database_namespace(&mut self, namespace: Option<String>) {
self.root_database_namespace = namespace;
}

pub fn clear_login_tokens(&mut self) {
self.web_session_token = None;
self.spacetimedb_token = None;
self.root_database_namespace = None;
}
}

Expand All @@ -461,6 +469,7 @@ impl TryFrom<&toml_edit::DocumentMut> for RawConfig {
let default_server = read_opt_str(value, DEFAULT_SERVER_KEY)?;
let web_session_token = read_opt_str(value, WEB_SESSION_TOKEN_KEY)?;
let spacetimedb_token = read_opt_str(value, SPACETIMEDB_TOKEN_KEY)?;
let root_database_namespace = read_opt_str(value, ROOT_DATABASE_NAMESPACE_KEY)?;

let mut server_configs = Vec::new();
if let Some(arr) = read_table(value, SERVER_CONFIGS_KEY)? {
Expand All @@ -474,6 +483,7 @@ impl TryFrom<&toml_edit::DocumentMut> for RawConfig {
server_configs,
web_session_token,
spacetimedb_token,
root_database_namespace,
})
}
}
Expand Down Expand Up @@ -654,11 +664,13 @@ impl Config {
server_configs: old_server_configs,
web_session_token,
spacetimedb_token,
root_database_namespace,
} = &self.home;

set_value(DEFAULT_SERVER_KEY, default_server.as_deref());
set_value(WEB_SESSION_TOKEN_KEY, web_session_token.as_deref());
set_value(SPACETIMEDB_TOKEN_KEY, spacetimedb_token.as_deref());
set_value(ROOT_DATABASE_NAMESPACE_KEY, root_database_namespace.as_deref());

// Short-circuit if there are no servers.
if old_server_configs.is_empty() {
Expand Down Expand Up @@ -808,6 +820,10 @@ Update the server's fingerprint with:
self.home.set_spacetimedb_token(token);
}

pub fn set_root_database_namespace(&mut self, namespace: Option<String>) {
self.home.set_root_database_namespace(namespace);
}

pub fn clear_login_tokens(&mut self) {
self.home.clear_login_tokens();
}
Expand All @@ -819,6 +835,13 @@ Update the server's fingerprint with:
pub fn spacetimedb_token(&self) -> Option<&String> {
self.home.spacetimedb_token.as_ref()
}

pub fn root_database_namespace(&self) -> Option<&str> {
self.home
.root_database_namespace
.as_deref()
.and_then(|value| (!value.is_empty()).then_some(value))
}
}

/// Update the value of a key in a `TOML` document, preserving the formatting and comments of the original value.
Expand Down
75 changes: 61 additions & 14 deletions crates/cli/src/subcommands/dns.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::common_args;
use crate::config::Config;
use crate::util::{add_auth_header_opt, get_auth_header, ResponseExt};
use crate::util::{add_auth_header_opt, get_auth_header, prepend_root_database_namespace, ResponseExt};
use anyhow::ensure;
use clap::ArgMatches;
use clap::{Arg, Command};
use reqwest::StatusCode;

use spacetimedb_client_api_messages::name::{DomainName, SetDomainsResult};
use spacetimedb_client_api_messages::name::{parse_database_name, parse_domain_name, DomainName, SetDomainsResult};

pub fn cli() -> Command {
Command::new("rename")
Expand All @@ -26,21 +28,22 @@ pub fn cli() -> Command {
}

pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let domain = args.get_one::<String>("new-name").unwrap();
let requested_name = args.get_one::<String>("new-name").unwrap();
let database_identity = args.get_one::<String>("database-identity").unwrap();
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
let root_database_namespace = config.root_database_namespace();
let host_url = config.get_host_url(server)?;
let client = reqwest::Client::new();

let domain: DomainName = domain.parse()?;
let current_default_name = get_default_name(&client, &host_url, database_identity, &auth_header).await?;
let name = resolve_name_for_rename(requested_name, current_default_name.as_ref(), root_database_namespace)?;

let builder = reqwest::Client::new()
.put(format!(
"{}/v1/database/{database_identity}/names",
config.get_host_url(server)?
))
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(serde_json::to_string(&[&domain])?);
let builder = client
.post(format!("{host_url}/v1/database/default-name/{database_identity}"))
.header(reqwest::header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(name.to_string());
let builder = add_auth_header_opt(builder, &auth_header);

let response = builder.send().await?;
Expand All @@ -50,17 +53,61 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
if !status.is_success() {
anyhow::bail!(match result {
SetDomainsResult::Success => "".to_string(),
SetDomainsResult::PermissionDenied { domain } => format!("Permission denied for domain: {domain}"),
SetDomainsResult::PermissionDenied { domain } => format!("Permission denied for database name: {domain}"),
SetDomainsResult::PermissionDeniedOnAny { domains } =>
format!("Permission denied for domains: {domains:?}"),
format!("Permission denied for database names: {domains:?}"),
SetDomainsResult::DatabaseNotFound => format!("Database {database_identity} not found"),
SetDomainsResult::NotYourDatabase { .. } =>
format!("You cannot rename {database_identity} because it is owned by another identity."),
SetDomainsResult::OtherError(err) => err,
});
}

println!("Name set to {domain} for identity {database_identity}.");
println!("Name set to {name} for identity {database_identity}.");

Ok(())
}

async fn get_default_name(
client: &reqwest::Client,
host_url: &str,
database_identity: &str,
auth_header: &crate::util::AuthHeader,
) -> Result<Option<DomainName>, anyhow::Error> {
let builder = client.get(format!("{host_url}/v1/database/default-name/{database_identity}"));
let builder = add_auth_header_opt(builder, auth_header);
let response = builder.send().await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(None);
}

response.json_or_error().await.map(Some)
}

fn resolve_name_for_rename(
requested_name: &str,
current_default_name: Option<&DomainName>,
root_database_namespace: Option<&str>,
) -> Result<DomainName, anyhow::Error> {
if let Some(current_default_name) = current_default_name {
if current_default_name
.sub_domain()
.is_some_and(|sub_domain| sub_domain.contains('/'))
{
ensure!(
!requested_name.contains('/'),
"Child database rename target cannot contain `/`"
);
parse_database_name(requested_name)?;
let parent = current_default_name
.as_ref()
.rsplit_once('/')
.map(|(parent, _)| parent)
.ok_or_else(|| anyhow::anyhow!("Failed to determine parent database path"))?;
return parse_domain_name(format!("{parent}/{requested_name}")).map_err(Into::into);
}
}

let qualified = prepend_root_database_namespace(requested_name, root_database_namespace);
parse_domain_name(qualified).map_err(Into::into)
}
29 changes: 25 additions & 4 deletions crates/cli/src/subcommands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ pub fn cli() -> Command {

#[derive(Deserialize)]
struct DatabasesResult {
pub identities: Vec<IdentityRow>,
pub identities: Vec<IdentityOnlyRow>,
}

#[derive(Tabled, Deserialize)]
#[derive(Deserialize)]
#[serde(transparent)]
struct IdentityOnlyRow {
pub db_identity: Identity,
}

#[derive(Tabled)]
struct IdentityRow {
pub db_identity: Identity,
pub default_name: String,
}

pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
Expand Down Expand Up @@ -58,11 +64,26 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
.context("unable to retrieve databases for identity")?;

if !result.identities.is_empty() {
let mut table = Table::new(result.identities);
let mut rows = Vec::with_capacity(result.identities.len());
for row in result.identities {
let default_name = util::spacetime_reverse_dns(&config, &row.db_identity.to_string(), server)
.await
.with_context(|| format!("unable to retrieve database names for {}", row.db_identity))?
.names
.first()
.map(ToString::to_string)
.unwrap_or_else(|| "<unnamed>".to_owned());
rows.push(IdentityRow {
db_identity: row.db_identity,
default_name,
});
}

let mut table = Table::new(rows);
table
.with(Style::psql())
.with(Modify::new(Columns::first()).with(Alignment::left()));
println!("Associated database identities for {identity}:\n");
println!("Associated databases for {identity}:\n");
println!("{table}");
} else {
println!("No databases found for {identity}.");
Expand Down
15 changes: 14 additions & 1 deletion crates/cli/src/subcommands/login.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::util::decode_identity;
use crate::util::{decode_identity, decode_root_database_namespace};
use crate::Config;
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
use reqwest::Url;
Expand Down Expand Up @@ -64,6 +64,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E

if let Some(token) = spacetimedb_token {
config.set_spacetimedb_token(token.clone());
config.set_root_database_namespace(Some(String::new()));
config.save();
return Ok(());
}
Expand Down Expand Up @@ -138,6 +139,18 @@ pub async fn spacetimedb_login_force(
spacetimedb_login(host, &session_token).await?
};
config.set_spacetimedb_token(token.clone());
let root_database_namespace = if direct_login {
Some(String::new())
} else {
match decode_root_database_namespace(&token) {
Ok(namespace) => Some(namespace.unwrap_or_default()),
Err(err) => {
eprintln!("WARNING: failed to extract root database namespace from token: {err}");
Some(String::new())
}
}
};
config.set_root_database_namespace(root_database_namespace);
config.save();

Ok(token)
Expand Down
Loading