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}