Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI - Implement new login flow #1863

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
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
651 changes: 488 additions & 163 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ urlencoding = "2.1.2"
uuid = { version = "1.2.1", features = ["v4"] }
walkdir = "2.2.5"
wasmbin = "0.6"
webbrowser = "1.0.2"

# Vendor the openssl we rely on, rather than depend on a
# potentially very old system version.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,19 @@ fn table_impl(mut args: TableArgs, mut item: MutItem<syn::DeriveInput>) -> syn::
let unique_col_ids = unique_columns.iter().map(|col| col.index);
let primary_col_id = primary_key_column.iter().map(|col| col.index);
let sequence_col_ids = sequenced_columns.iter().map(|col| col.index);
let scheduled_reducer_ident = args.scheduled.iter();

let schedule = args
.scheduled
.as_ref()
.map(|reducer| {
// scheduled_at was inserted as the last field
let scheduled_at_id = (fields.len() - 1) as u16;
quote!(spacetimedb::table::ScheduleDesc {
reducer_name: <#reducer as spacetimedb::rt::ReducerInfo>::NAME,
scheduled_at_column: #scheduled_at_id,
})
})
.into_iter();

let unique_err = if !unique_columns.is_empty() {
quote!(spacetimedb::UniqueConstraintViolation)
Expand Down Expand Up @@ -1063,7 +1075,7 @@ fn table_impl(mut args: TableArgs, mut item: MutItem<syn::DeriveInput>) -> syn::
const INDEXES: &'static [spacetimedb::table::IndexDesc<'static>] = &[#(#index_descs),*];
#(const PRIMARY_KEY: Option<u16> = Some(#primary_col_id);)*
const SEQUENCES: &'static [u16] = &[#(#sequence_col_ids),*];
#(const SCHEDULED_REDUCER_NAME: Option<&'static str> = Some(<#scheduled_reducer_ident as spacetimedb::rt::ReducerInfo>::NAME);)*
#(const SCHEDULE: Option<spacetimedb::table::ScheduleDesc<'static>> = Some(#schedule);)*

#table_id_from_name_func
}
Expand Down
1 change: 1 addition & 0 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub use spacetimedb_lib::ser::Serialize;
pub use spacetimedb_lib::Address;
pub use spacetimedb_lib::AlgebraicValue;
pub use spacetimedb_lib::Identity;
pub use spacetimedb_lib::ScheduleAt;
pub use spacetimedb_primitives::TableId;
pub use sys::Errno;
pub use table::{AutoIncOverflow, BTreeIndex, Table, TryInsertError, UniqueColumn, UniqueConstraintViolation};
Expand Down
18 changes: 16 additions & 2 deletions crates/bindings/src/rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ pub trait TableColumn {
}
impl<T: SpacetimeType> TableColumn for T {}

/// Assert that the primary_key column of a scheduled table is a u64.
pub const fn assert_scheduled_table_primary_key<T: ScheduledTablePrimaryKey>() {}

mod sealed {
pub trait Sealed {}
}
#[diagnostic::on_unimplemented(
message = "scheduled table primary key must be a `u64`",
label = "should be `u64`, not `{Self}`"
)]
pub trait ScheduledTablePrimaryKey: sealed::Sealed {}
impl sealed::Sealed for u64 {}
impl ScheduledTablePrimaryKey for u64 {}

/// Used in the last type parameter of `Reducer` to indicate that the
/// context argument *should* be passed to the reducer logic.
pub struct ContextArg;
Expand Down Expand Up @@ -331,8 +345,8 @@ pub fn register_table<T: Table>() {
for &col in T::SEQUENCES {
table = table.with_column_sequence(col, None);
}
if let Some(scheduled_reducer) = T::SCHEDULED_REDUCER_NAME {
table = table.with_schedule(scheduled_reducer, None);
if let Some(schedule) = T::SCHEDULE {
table = table.with_schedule(schedule.reducer_name, schedule.scheduled_at_column, None);
}

table.finish();
Expand Down
7 changes: 6 additions & 1 deletion crates/bindings/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub trait TableInternal: Sized {
const INDEXES: &'static [IndexDesc<'static>];
const PRIMARY_KEY: Option<u16> = None;
const SEQUENCES: &'static [u16];
const SCHEDULED_REDUCER_NAME: Option<&'static str> = None;
const SCHEDULE: Option<ScheduleDesc<'static>> = None;

/// Returns the ID of this table.
fn table_id() -> TableId;
Expand All @@ -143,6 +143,11 @@ pub enum IndexAlgo<'a> {
BTree { columns: &'a [u16] },
}

pub struct ScheduleDesc<'a> {
pub reducer_name: &'a str,
pub scheduled_at_column: u16,
}

#[doc(hidden)]
pub trait __MapRowTypeToTable {
type Table: Table;
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ tokio-tungstenite.workspace = true
toml.workspace = true
wasmbin.workspace = true
wasmtime.workspace = true
webbrowser.workspace = true

[dev-dependencies]
insta.workspace = true
Expand Down
10 changes: 10 additions & 0 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub struct RawConfig {
identity_configs: Vec<IdentityConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
server_configs: Vec<ServerConfig>,
login_token: Option<String>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -154,6 +155,7 @@ impl RawConfig {
default_server: local.nickname.clone(),
identity_configs: Vec::new(),
server_configs: vec![local, testnet],
login_token: None,
}
}

Expand Down Expand Up @@ -578,6 +580,10 @@ Fetch the server's fingerprint with:
cfg.ecdsa_public_key = None;
Ok(())
}

pub fn set_login_token(&mut self, token: String) {
self.login_token = Some(token);
}
}

impl Config {
Expand Down Expand Up @@ -1045,4 +1051,8 @@ Update the server's fingerprint with:
self.home.delete_default_server_fingerprint()
}
}

pub fn set_login_token(&mut self, token: String) {
self.home.set_login_token(token);
}
}
2 changes: 2 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn get_subcommands() -> Vec<Command> {
dns::cli(),
generate::cli(),
list::cli(),
login::cli(),
init::cli(),
build::cli(),
server::cli(),
Expand Down Expand Up @@ -61,6 +62,7 @@ pub async fn exec_subcommand(config: Config, cmd: &str, args: &ArgMatches) -> Re
"subscribe" => subscribe::exec(config, args).await,
#[cfg(feature = "standalone")]
"start" => start::exec(args).await,
"login" => login::exec(config, args).await,
"upgrade" => upgrade::exec(config, args).await,
unknown => Err(anyhow::anyhow!("Invalid subcommand: {}", unknown)),
}
Expand Down
78 changes: 78 additions & 0 deletions crates/cli/src/subcommands/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::Config;
use clap::{Arg, ArgMatches, Command};
use reqwest::Url;
use serde::Deserialize;
use webbrowser;

pub fn cli() -> Command {
Command::new("login")
.arg(
Arg::new("host")
.long("host")
.default_value("https://spacetimedb.com")
.help("Fetch login token from a different host"),
)
.about("Login the CLI in to SpacetimeDB")
}

#[derive(Deserialize)]
struct TokenResponse {
token: String,
}

#[derive(Deserialize)]
struct LoginTokenResponse {
approved: bool,
session: Option<LoginTokenResponseSession>,
}

#[derive(Deserialize)]
struct LoginTokenResponseSession {
token: String,
}

pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let remote: &String = args.get_one("host").unwrap();
// Users like to provide URLs with trailing slashes, which can cause issues due to double-slashes in the routes below.
let remote = remote.trim_end_matches('/');

let route = |path| format!("{}{}", remote, path);

let client = reqwest::Client::new();

let response: TokenResponse = client
.get(route("/api/auth/cli/request-login-token"))
.send()
.await?
.json()
.await?;
let temp_token = response.token.as_str();

let browser_url = Url::parse_with_params(route("/login/cli").as_str(), vec![("token", temp_token)])?;
if webbrowser::open(browser_url.as_str()).is_err() {
println!("Please open the following URL in your browser: {}", browser_url);
}

println!("Waiting to hear response from the server...");
loop {
let response: LoginTokenResponse = client
.get(Url::parse_with_params(
route("/api/auth/cli/status").as_str(),
vec![("token", temp_token)],
)?)
.send()
.await?
.json()
.await?;
if response.approved {
config.set_login_token(response.session.unwrap().token);
println!("Login successful!");
break;
}

tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
// TODO: test that Ctrl-C returns non-zero, rather than falling through to the Ok(()) here

Ok(())
}
1 change: 1 addition & 0 deletions crates/cli/src/subcommands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod generate;
pub mod identity;
pub mod init;
pub mod list;
pub mod login;
pub mod logs;
pub mod publish;
pub mod repl;
Expand Down
7 changes: 7 additions & 0 deletions crates/client-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ bytestring = "1"
tokio-tungstenite.workspace = true
itoa = "1.0.9"
derive_more = "0.99.17"
uuid.workspace = true
blake3.workspace = true
jsonwebtoken.workspace = true
scopeguard.workspace = true

[dev-dependencies]
jsonwebkey = { version = "0.3.5", features = ["generate","jwt-convert"] }
jsonwebtoken.workspace = true
Loading