2.8 KiB · text History 487bb12
//! CLI to bootstrap and inspect invite tokens — useful for creating the very
//! first invite before any user exists (there's no one to generate one from
//! the web UI yet).
use clap::{Parser, Subcommand};
use sea_orm::*;
use std::sync::Arc;

use bismuth::auth;
use bismuth::config::{create_db_connection, Config};
use bismuth::entities::registration_tokens;

#[derive(Parser)]
#[command(name = "registration-token", about = "Manage invite (registration) tokens")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Create a new invite token
    Create {
        /// Restrict the token to this email (optional)
        #[arg(short, long)]
        email: Option<String>,
        /// Days until the token expires
        #[arg(short, long, default_value = "7")]
        days: i64,
    },
    /// List all invite tokens
    List,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();
    let cli = Cli::parse();
    let config = Config::from_env();
    let db = Arc::new(create_db_connection(&config.database_url).await?);

    match cli.command {
        Commands::Create { email, days } => {
            let token = auth::create_registration_token(&db, None, email.clone(), days).await?;
            println!("Invite token created");
            println!("  token:   {}", token.token);
            println!("  email:   {}", email.unwrap_or_else(|| "any".to_string()));
            println!("  expires: {}", token.expires_at);
            println!();
            println!("Registration URL:");
            println!("  {}/register?token={}", public_base(&config), token.token);
        }
        Commands::List => {
            let tokens = registration_tokens::Entity::find()
                .order_by_desc(registration_tokens::Column::CreatedAt)
                .all(db.as_ref())
                .await?;
            if tokens.is_empty() {
                println!("No invite tokens.");
                return Ok(());
            }
            for t in tokens {
                let status = if t.is_used {
                    "used"
                } else if t.expires_at < chrono::Utc::now() {
                    "expired"
                } else {
                    "active"
                };
                println!(
                    "{:8}  {:36}  {}",
                    status,
                    t.token,
                    t.email.unwrap_or_else(|| "any".to_string())
                );
            }
        }
    }
    Ok(())
}

/// Best-effort public base for the printed registration URL. Override with
/// `PUBLIC_URL`; otherwise derived from the bind address.
fn public_base(config: &Config) -> String {
    std::env::var("PUBLIC_URL").unwrap_or_else(|_| format!("http://{}", config.bind_address))
}