A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1use actix_cors::Cors; 2use actix_web::{web, App, HttpResponse, HttpServer}; 3use anyhow::Result; 4use rust_embed::RustEmbed; 5use simplelink::check_and_generate_admin_token; 6use simplelink::{create_db_pool, run_migrations}; 7use simplelink::{handlers, AppState}; 8use sqlx::{Postgres, Sqlite}; 9use tracing::{error, info}; 10 11#[derive(Parser, Debug)] 12#[command(author, version, about, long_about = None)] 13#[derive(RustEmbed)] 14#[folder = "static/"] 15struct Asset; 16 17async fn serve_static_file(path: &str) -> HttpResponse { 18 match Asset::get(path) { 19 Some(content) => { 20 let mime = mime_guess::from_path(path).first_or_octet_stream(); 21 HttpResponse::Ok() 22 .content_type(mime.as_ref()) 23 .body(content.data.into_owned()) 24 } 25 None => HttpResponse::NotFound().body("404 Not Found"), 26 } 27} 28 29#[actix_web::main] 30async fn main() -> Result<()> { 31 // Load environment variables from .env file 32 dotenv::dotenv().ok(); 33 34 // Initialize logging 35 tracing_subscriber::fmt::init(); 36 37 // Create database connection pool 38 let pool = create_db_pool().await?; 39 run_migrations(&pool).await?; 40 41 // First check if admin credentials are provided in environment variables 42 let admin_credentials = match ( 43 std::env::var("SIMPLELINK_USER"), 44 std::env::var("SIMPLELINK_PASS"), 45 ) { 46 (Ok(user), Ok(pass)) => Some((user, pass)), 47 _ => None, 48 }; 49 50 if let Some((email, password)) = admin_credentials { 51 // Now check for existing users 52 let user_count = match &pool { 53 DatabasePool::Postgres(pool) => { 54 let mut tx = pool.begin().await?; 55 let count = 56 sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users") 57 .fetch_one(&mut *tx) 58 .await? 59 .0; 60 tx.commit().await?; 61 count 62 } 63 DatabasePool::Sqlite(pool) => { 64 let mut tx = pool.begin().await?; 65 let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users") 66 .fetch_one(&mut *tx) 67 .await? 68 .0; 69 tx.commit().await?; 70 count 71 } 72 }; 73 74 if user_count == 0 { 75 info!("No users found, creating admin user: {}", email); 76 match create_admin_user(&pool, &email, &password).await { 77 Ok(_) => info!("Successfully created admin user"), 78 Err(e) => { 79 error!("Failed to create admin user: {}", e); 80 return Err(anyhow::anyhow!("Failed to create admin user: {}", e)); 81 } 82 } 83 } 84 } else { 85 info!( 86 "No admin credentials provided in environment variables, skipping admin user creation" 87 ); 88 } 89 90 // Create initial links from environment variables 91 create_initial_links(&pool).await?; 92 93 let admin_token = check_and_generate_admin_token(&pool).await?; 94 95 let state = AppState { 96 db: pool, 97 admin_token, 98 }; 99 100 let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 101 let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); 102 info!("Starting server at http://{}:{}", host, port); 103 104 // Start HTTP server 105 HttpServer::new(move || { 106 let cors = Cors::default() 107 .allow_any_origin() 108 .allow_any_method() 109 .allow_any_header() 110 .max_age(3600); 111 112 App::new() 113 .wrap(cors) 114 .app_data(web::Data::new(state.clone())) 115 .service( 116 web::scope("/api") 117 .route("/shorten", web::post().to(handlers::create_short_url)) 118 .route("/links", web::get().to(handlers::get_all_links)) 119 .route("/links/{id}", web::delete().to(handlers::delete_link)) 120 .route( 121 "/links/{id}/clicks", 122 web::get().to(handlers::get_link_clicks), 123 ) 124 .route( 125 "/links/{id}/sources", 126 web::get().to(handlers::get_link_sources), 127 ) 128 .route("/links/{id}", web::patch().to(handlers::edit_link)) 129 .route("/auth/register", web::post().to(handlers::register)) 130 .route("/auth/login", web::post().to(handlers::login)) 131 .route( 132 "/auth/check-first-user", 133 web::get().to(handlers::check_first_user), 134 ) 135 .route("/health", web::get().to(handlers::health_check)), 136 ) 137 .service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url))) 138 .default_service(web::route().to(|req: actix_web::HttpRequest| async move { 139 let path = req.path().trim_start_matches('/'); 140 let path = if path.is_empty() { "index.html" } else { path }; 141 serve_static_file(path).await 142 })) 143 }) 144 .workers(2) 145 .backlog(10_000) 146 .bind(format!("{}:{}", host, port))? 147 .run() 148 .await?; 149 150 Ok(()) 151}