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