A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.

fixes to db stuff

+35
.sqlx/query-2adc9fa303079a3e9c28bbf0565c1ac60eea3a5c37e34fc6a0cb6e151c325382.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int4" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "email", 14 + "type_info": "Varchar" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "password_hash", 19 + "type_info": "Text" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Varchar", 25 + "Text" 26 + ] 27 + }, 28 + "nullable": [ 29 + false, 30 + false, 31 + false 32 + ] 33 + }, 34 + "hash": "2adc9fa303079a3e9c28bbf0565c1ac60eea3a5c37e34fc6a0cb6e151c325382" 35 + }
+22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT id FROM users WHERE email = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int4" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + false 19 + ] 20 + }, 21 + "hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068" 22 + }
+34
.sqlx/query-f3f58600e971f1be6cbe206bba24f77769f54c6230e28f5b3dc719b869d9cb3f.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT * FROM users WHERE email = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int4" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "email", 14 + "type_info": "Varchar" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "password_hash", 19 + "type_info": "Text" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + false, 30 + false 31 + ] 32 + }, 33 + "hash": "f3f58600e971f1be6cbe206bba24f77769f54c6230e28f5b3dc719b869d9cb3f" 34 + }
+4
Cargo.toml
··· 3 3 version = "0.1.0" 4 4 edition = "2021" 5 5 6 + [lib] 7 + name = "simple_link" 8 + path = "src/lib.rs" 9 + 6 10 [dependencies] 7 11 jsonwebtoken = "9" 8 12 actix-web = "4.4"
+2 -2
src/handlers.rs
··· 1 1 use actix_web::{web, HttpResponse, Responder, HttpRequest}; 2 - use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation, errors::Error as JwtError};use crate::{error::AppError, models::{AuthResponse, Claims, CreateLink, Link, LoginRequest, RegisterRequest, User, UserResponse}, AppState}; 2 + use jsonwebtoken::{encode, Header, EncodingKey};use crate::{error::AppError, models::{AuthResponse, Claims, CreateLink, Link, LoginRequest, RegisterRequest, User, UserResponse}, AppState}; 3 3 use regex::Regex; 4 4 use argon2::{password_hash::{rand_core::OsRng, SaltString}, PasswordVerifier}; 5 5 use lazy_static::lazy_static; 6 6 use argon2::{Argon2, PasswordHash, PasswordHasher}; 7 - use crate::auth::{AuthenticatedUser}; 7 + use crate::auth::AuthenticatedUser; 8 8 9 9 lazy_static! { 10 10 static ref VALID_CODE_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]{1,32}$").unwrap();
+11
src/lib.rs
··· 1 + use sqlx::PgPool; 2 + 3 + pub mod auth; 4 + pub mod error; 5 + pub mod handlers; 6 + pub mod models; 7 + 8 + #[derive(Clone)] 9 + pub struct AppState { 10 + pub db: PgPool, 11 + }
+6 -19
src/main.rs
··· 1 1 use actix_web::{web, App, HttpServer}; 2 2 use actix_cors::Cors; 3 3 use anyhow::Result; 4 - use sqlx::PgPool; 4 + use sqlx::postgres::PgPoolOptions; 5 + use simple_link::{AppState, handlers}; 5 6 use tracing::info; 6 - 7 - mod error; 8 - mod handlers; 9 - mod models; 10 - mod auth; 11 - 12 - #[derive(Clone)] 13 - pub struct AppState { 14 - db: PgPool, 15 - } 16 7 17 8 #[actix_web::main] 18 9 async fn main() -> Result<()> { ··· 26 17 let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); 27 18 28 19 // Create database connection pool 29 - use sqlx::postgres::PgPoolOptions; 30 - 31 - // In main(), replace the PgPool::connect with: 32 20 let pool = PgPoolOptions::new() 33 21 .max_connections(5) 34 22 .acquire_timeout(std::time::Duration::from_secs(3)) ··· 36 24 .await?; 37 25 38 26 // Run database migrations 39 - //sqlx::migrate!("./migrations").run(&pool).await?; 27 + sqlx::migrate!("./migrations").run(&pool).await?; 40 28 41 29 let state = AppState { db: pool }; 42 30 ··· 58 46 .route("/shorten", web::post().to(handlers::create_short_url)) 59 47 .route("/links", web::get().to(handlers::get_all_links)) 60 48 .route("/auth/register", web::post().to(handlers::register)) 61 - .route("/auth/login", web::post().to(handlers::login)), 62 - 49 + .route("/auth/login", web::post().to(handlers::login)) 50 + .route("/health", web::get().to(handlers::health_check)), 63 51 ) 64 52 .service( 65 53 web::resource("/{short_code}") 66 54 .route(web::get().to(handlers::redirect_to_url)) 67 55 ) 68 - .service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url))) 69 56 }) 70 - .workers(2) // Limit worker threads 57 + .workers(2) 71 58 .backlog(10_000) 72 59 .bind("127.0.0.1:8080")? 73 60 .run()
+9 -2
src/migrations/2025125_initial.sql migrations/20250125000000_init.sql
··· 1 + -- Add Migration Version 2 + CREATE TABLE IF NOT EXISTS _sqlx_migrations ( 3 + version BIGINT PRIMARY KEY, 4 + description TEXT NOT NULL, 5 + installed_on TIMESTAMPTZ NOT NULL DEFAULT NOW() 6 + ); 7 + 1 8 -- Create users table 2 9 CREATE TABLE users ( 3 10 id SERIAL PRIMARY KEY, ··· 5 12 password_hash TEXT NOT NULL 6 13 ); 7 14 8 - -- Create links table with user_id from the start 15 + -- Create links table 9 16 CREATE TABLE links ( 10 17 id SERIAL PRIMARY KEY, 11 18 original_url TEXT NOT NULL, ··· 15 22 user_id INTEGER REFERENCES users(id) 16 23 ); 17 24 18 - -- Create clicks table for tracking 25 + -- Create clicks table 19 26 CREATE TABLE clicks ( 20 27 id SERIAL PRIMARY KEY, 21 28 link_id INTEGER REFERENCES links(id),