A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.
1use anyhow::Result; 2use chrono::NaiveDate; 3use futures::future::BoxFuture; 4use serde::{Deserialize, Serialize}; 5use sqlx::postgres::PgRow; 6use sqlx::sqlite::SqliteRow; 7use sqlx::FromRow; 8use sqlx::Pool; 9use sqlx::Postgres; 10use sqlx::Sqlite; 11use sqlx::Transaction; 12use std::time::{SystemTime, UNIX_EPOCH}; 13 14#[derive(Clone)] 15pub enum DatabasePool { 16 Postgres(Pool<Postgres>), 17 Sqlite(Pool<Sqlite>), 18} 19 20impl DatabasePool { 21 pub async fn begin(&self) -> Result<Box<dyn std::any::Any + Send>> { 22 match self { 23 DatabasePool::Postgres(pool) => Ok(Box::new(pool.begin().await?)), 24 DatabasePool::Sqlite(pool) => Ok(Box::new(pool.begin().await?)), 25 } 26 } 27 28 pub async fn fetch_optional<T>(&self, pg_query: &str, sqlite_query: &str) -> Result<Option<T>> 29 where 30 T: for<'r> FromRow<'r, PgRow> + for<'r> FromRow<'r, SqliteRow> + Send + Sync + Unpin, 31 { 32 match self { 33 DatabasePool::Postgres(pool) => { 34 Ok(sqlx::query_as(pg_query).fetch_optional(pool).await?) 35 } 36 DatabasePool::Sqlite(pool) => { 37 Ok(sqlx::query_as(sqlite_query).fetch_optional(pool).await?) 38 } 39 } 40 } 41 42 pub async fn execute(&self, pg_query: &str, sqlite_query: &str) -> Result<()> { 43 match self { 44 DatabasePool::Postgres(pool) => { 45 sqlx::query(pg_query).execute(pool).await?; 46 Ok(()) 47 } 48 DatabasePool::Sqlite(pool) => { 49 sqlx::query(sqlite_query).execute(pool).await?; 50 Ok(()) 51 } 52 } 53 } 54 55 pub async fn transaction<'a, F, R>(&'a self, f: F) -> Result<R> 56 where 57 F: for<'c> Fn(&'c mut Transaction<'_, Postgres>) -> BoxFuture<'c, Result<R>> 58 + for<'c> Fn(&'c mut Transaction<'_, Sqlite>) -> BoxFuture<'c, Result<R>> 59 + Copy, 60 R: Send + 'static, 61 { 62 match self { 63 DatabasePool::Postgres(pool) => { 64 let mut tx = pool.begin().await?; 65 let result = f(&mut tx).await?; 66 tx.commit().await?; 67 Ok(result) 68 } 69 DatabasePool::Sqlite(pool) => { 70 let mut tx = pool.begin().await?; 71 let result = f(&mut tx).await?; 72 tx.commit().await?; 73 Ok(result) 74 } 75 } 76 } 77} 78 79#[derive(Debug, Serialize, Deserialize)] 80pub struct Claims { 81 pub sub: i32, // user id 82 pub exp: usize, 83} 84 85impl Claims { 86 pub fn new(user_id: i32) -> Self { 87 let exp = SystemTime::now() 88 .duration_since(UNIX_EPOCH) 89 .unwrap() 90 .as_secs() as usize 91 + 24 * 60 * 60; // 24 hours from now 92 93 Self { sub: user_id, exp } 94 } 95} 96 97#[derive(Deserialize)] 98pub struct CreateLink { 99 pub url: String, 100 pub source: Option<String>, 101 pub custom_code: Option<String>, 102} 103 104#[derive(Serialize, FromRow)] 105pub struct Link { 106 pub id: i32, 107 pub user_id: Option<i32>, 108 pub original_url: String, 109 pub short_code: String, 110 pub created_at: chrono::DateTime<chrono::Utc>, 111 pub clicks: i64, 112} 113 114#[derive(Deserialize)] 115pub struct LoginRequest { 116 pub email: String, 117 pub password: String, 118} 119 120#[derive(Deserialize)] 121pub struct RegisterRequest { 122 pub email: String, 123 pub password: String, 124 pub admin_token: Option<String>, 125} 126 127#[derive(Serialize)] 128pub struct AuthResponse { 129 pub token: String, 130 pub user: UserResponse, 131} 132 133#[derive(Serialize)] 134pub struct UserResponse { 135 pub id: i32, 136 pub email: String, 137} 138 139#[derive(FromRow)] 140pub struct User { 141 pub id: i32, 142 pub email: String, 143 pub password_hash: String, 144} 145 146#[derive(sqlx::FromRow, Serialize)] 147pub struct ClickStats { 148 pub date: NaiveDate, 149 pub clicks: i64, 150} 151 152#[derive(sqlx::FromRow, Serialize)] 153pub struct SourceStats { 154 pub source: String, 155 pub count: i64, 156}