Skip to content

Commit d2ab902

Browse files
committed
Basic envio auth command
1 parent ebe4625 commit d2ab902

File tree

4 files changed

+143
-0
lines changed

4 files changed

+143
-0
lines changed

codegenerator/cli/CommandLineHelp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ This document contains the help content for the `envio` command-line program.
4141
* `benchmark-summary` — Prints a summary of the benchmark data after running the indexer with envio start --bench flag or setting 'ENVIO_SAVE_BENCHMARK_DATA=true'
4242
* `local` — Prepare local environment for envio testing
4343
* `start` — Start the indexer without any automatic codegen
44+
* `auth` — Authenticate with envio hosted services and print a JWT
4445

4546
###### **Options:**
4647

codegenerator/cli/src/cli_args/clap_definitions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ pub enum CommandType {
6666
///Start the indexer without any automatic codegen
6767
Start(StartArgs),
6868

69+
///Authenticate with envio hosted services and print a JWT
70+
Auth,
71+
6972
#[clap(hide = true)]
7073
#[command(subcommand)]
7174
Script(Script),

codegenerator/cli/src/commands.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,137 @@ pub mod benchmark {
280280
Ok(())
281281
}
282282
}
283+
284+
pub mod auth {
285+
use anyhow::{anyhow, Context, Result};
286+
use open;
287+
use reqwest::StatusCode;
288+
use serde::{Deserialize, Serialize};
289+
use std::time::Duration;
290+
use tokio::time::sleep;
291+
292+
/// Default UI/API base URL. Change this constant to point to your deployment.
293+
pub const AUTH_BASE_URL: &str = "http://localhost:3000";
294+
295+
fn get_api_base_url() -> String {
296+
// Allow override via ENVIO_API_URL, otherwise use the constant above.
297+
std::env::var("ENVIO_API_URL").unwrap_or_else(|_| AUTH_BASE_URL.to_string())
298+
}
299+
300+
#[derive(Debug, Deserialize)]
301+
#[serde(rename_all = "camelCase")]
302+
struct CliAuthSession {
303+
code: String,
304+
auth_url: String,
305+
expires_in: i32,
306+
}
307+
308+
#[derive(Debug, Deserialize)]
309+
struct User {
310+
#[allow(dead_code)]
311+
id: Option<String>,
312+
#[allow(dead_code)]
313+
name: Option<String>,
314+
#[allow(dead_code)]
315+
email: Option<String>,
316+
#[allow(dead_code)]
317+
#[serde(rename = "githubId")]
318+
github_id: Option<String>,
319+
#[allow(dead_code)]
320+
#[serde(rename = "githubLogin")]
321+
github_login: Option<String>,
322+
}
323+
324+
#[derive(Debug, Deserialize)]
325+
struct CliAuthStatus {
326+
completed: bool,
327+
#[allow(dead_code)]
328+
user: Option<User>,
329+
#[allow(dead_code)]
330+
error: Option<String>,
331+
token: Option<String>,
332+
}
333+
334+
#[derive(Debug, Serialize)]
335+
struct EmptyBody {}
336+
337+
pub async fn run_auth() -> Result<()> {
338+
let base = get_api_base_url();
339+
let client = reqwest::Client::new();
340+
341+
// 1) Create a CLI auth session
342+
let create_url = format!("{}/api/auth/cli-session", base);
343+
let session: CliAuthSession = client
344+
.post(&create_url)
345+
.json(&EmptyBody {})
346+
.send()
347+
.await
348+
.with_context(|| format!("Failed to POST {}", create_url))?
349+
.error_for_status()
350+
.with_context(|| format!("Non-200 from {}", create_url))?
351+
.json()
352+
.await
353+
.context("Failed to decode CLI session response")?;
354+
355+
println!("Opening browser for authentication...\nIf it doesn't open, visit: {}", session.auth_url);
356+
let _ = open::that_detached(&session.auth_url);
357+
358+
// 2) Poll for completion
359+
let poll_url = format!("{}/api/auth/cli-session?code={}", base, session.code);
360+
let poll_interval = Duration::from_secs(2);
361+
// Add a small grace window to handle cold starts or UI recompiles wiping in-memory state
362+
let extra_grace_attempts = 15; // ~30s grace
363+
let max_attempts = (session.expires_in.max(0) as u64) / 2 + extra_grace_attempts;
364+
365+
// Give the UI a brief warm-up before first poll
366+
sleep(Duration::from_secs(2)).await;
367+
368+
let mut consecutive_not_found = 0u32;
369+
370+
for _ in 0..max_attempts {
371+
sleep(poll_interval).await;
372+
373+
let resp = match client.get(&poll_url).send().await {
374+
Ok(r) => r,
375+
Err(_) => {
376+
// transient network error; try again
377+
continue;
378+
}
379+
};
380+
381+
if resp.status() == StatusCode::NOT_FOUND {
382+
consecutive_not_found += 1;
383+
// Keep polling; in-memory session store may not be ready yet
384+
if consecutive_not_found % 10 == 1 {
385+
// Print a lightweight status occasionally
386+
eprintln!("Waiting for session to become available...");
387+
}
388+
continue;
389+
} else {
390+
consecutive_not_found = 0;
391+
}
392+
393+
if resp.status().is_success() {
394+
let status: CliAuthStatus = match resp.json().await {
395+
Ok(s) => s,
396+
Err(_) => continue,
397+
};
398+
399+
if let Some(err) = status.error {
400+
if !err.is_empty() {
401+
return Err(anyhow!("authentication error: {}", err));
402+
}
403+
}
404+
405+
if status.completed {
406+
if let Some(token) = status.token {
407+
println!("{}", token);
408+
return Ok(());
409+
}
410+
}
411+
}
412+
}
413+
414+
Err(anyhow!("authentication timed out"))
415+
}
416+
}

codegenerator/cli/src/executor/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ pub async fn execute(command_line_args: CommandLineArgs) -> Result<()> {
116116
.await
117117
.context("Failed print missing networks script")?;
118118
}
119+
CommandType::Auth => {
120+
commands::auth::run_auth()
121+
.await
122+
.context("Failed running auth flow")?;
123+
}
119124
};
120125

121126
Ok(())

0 commit comments

Comments
 (0)