mod db;
mod handlers;
use std::path::PathBuf;
use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;
use clap::{Parser, Subcommand};
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
#[derive(Parser)]
#[command(name = "archergate-license-server")]
#[command(about = "License validation server for indie VST plugin developers")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Serve {
#[arg(short, long, default_value = "3100")]
port: u16,
#[arg(short, long, default_value = "./archergate-licenses.db")]
db: PathBuf,
},
CreateKey {
#[arg(short, long)]
email: String,
#[arg(short, long, default_value = "./archergate-licenses.db")]
db: PathBuf,
},
CreateLicense {
#[arg(short, long)]
plugin: String,
#[arg(short, long)]
email: Option<String>,
#[arg(short, long, default_value = "3")]
max_machines: i32,
#[arg(long)]
expires: Option<String>,
#[arg(short, long)]
api_key_id: String,
#[arg(short, long, default_value = "./archergate-licenses.db")]
db: PathBuf,
},
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "archergate_license_server=info,tower_http=info".into()),
)
.init();
let cli = Cli::parse();
match cli.command {
Commands::Serve { port, db: db_path } => {
let database = db::Db::open(&db_path).expect("Failed to open database");
let state: handlers::AppState = Arc::new(database);
let app = Router::new()
.route("/validate", post(handlers::validate))
.route("/activate", post(handlers::activate))
.route("/licenses", post(handlers::create_license))
.route("/health", get(handlers::health))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.with_state(state);
let addr = format!("0.0.0.0:{port}");
tracing::info!("Archergate License Server listening on {addr}");
tracing::info!("Database: {}", db_path.display());
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Commands::CreateKey { email, db: db_path } => {
let database = db::Db::open(&db_path).expect("Failed to open database");
let (raw_key, record) = database.create_api_key(&email).unwrap();
println!("API Key created:");
println!(" Key: {raw_key}");
println!(" ID: {}", record.id);
println!(" Email: {email}");
println!();
println!("Save this key — it cannot be retrieved later.");
}
Commands::CreateLicense {
plugin,
email,
max_machines,
expires,
api_key_id,
db: db_path,
} => {
let database = db::Db::open(&db_path).expect("Failed to open database");
let license = database
.create_license(
&plugin,
email.as_deref(),
expires.as_deref(),
max_machines,
&api_key_id,
)
.unwrap();
println!("License created:");
println!(" Key: {}", license.license_key);
println!(" Plugin: {plugin}");
println!(" Max machines: {max_machines}");
if let Some(exp) = expires {
println!(" Expires: {exp}");
} else {
println!(" Expires: never (perpetual)");
}
}
}
}