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

add admin setup token i love admin setup token

+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
···
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "SELECT COUNT(*) as count FROM users",
+
"describe": {
+
"columns": [
+
{
+
"ordinal": 0,
+
"name": "count",
+
"type_info": "Int8"
+
}
+
],
+
"parameters": {
+
"Left": []
+
},
+
"nullable": [
+
null
+
]
+
},
+
"hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538"
+
}
+1
Cargo.lock
···
"dotenv",
"jsonwebtoken",
"lazy_static",
"regex",
"serde",
"serde_json",
···
"dotenv",
"jsonwebtoken",
"lazy_static",
+
"rand",
"regex",
"serde",
"serde_json",
+1
Cargo.toml
···
regex = "1.10"
lazy_static = "1.4"
argon2 = "0.5.3"
···
regex = "1.10"
lazy_static = "1.4"
argon2 = "0.5.3"
+
rand = { version = "0.8", features = ["std"] }
+1
admin-setup-token.txt
···
···
+
fqfO6awRz3mkc2Kxunkp1uTQcXaSfGD9
+3 -3
docker-compose.yml
···
context: .
dockerfile: Dockerfile
args:
-
- API_URL=${API_URL:-http://localhost:8080}
container_name: shortener-app
ports:
-
- "8080:8080"
environment:
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
- SERVER_HOST=0.0.0.0
-
- SERVER_PORT=8080
depends_on:
db:
condition: service_healthy
···
context: .
dockerfile: Dockerfile
args:
+
- API_URL=${API_URL:-http://localhost:3000}
container_name: shortener-app
ports:
+
- "3000:3000"
environment:
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
- SERVER_HOST=0.0.0.0
+
- SERVER_PORT=3000
depends_on:
db:
condition: service_healthy
+2 -1
frontend/src/api/client.ts
···
return response.data;
};
-
export const register = async (email: string, password: string) => {
const response = await api.post<AuthResponse>('/auth/register', {
email,
password,
});
return response.data;
};
···
return response.data;
};
+
export const register = async (email: string, password: string, adminToken: string) => {
const response = await api.post<AuthResponse>('/auth/register', {
email,
password,
+
admin_token: adminToken,
});
return response.data;
};
+21 -3
frontend/src/components/AuthForms.tsx
···
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters long'),
})
type FormValues = z.infer<typeof formSchema>
···
defaultValues: {
email: '',
password: '',
},
})
···
if (activeTab === 'login') {
await login(values.email, values.password)
} else {
-
await register(values.email, values.password)
}
form.reset()
} catch (err: any) {
toast({
variant: 'destructive',
title: 'Error',
-
description: err.response?.data?.error || 'An error occurred',
})
}
}
···
)}
/>
<Button type="submit" className="w-full">
{activeTab === 'login' ? 'Sign in' : 'Create account'}
</Button>
···
</Tabs>
</Card>
)
-
}
···
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters long'),
+
adminToken: z.string(),
})
type FormValues = z.infer<typeof formSchema>
···
defaultValues: {
email: '',
password: '',
+
adminToken: '',
},
})
···
if (activeTab === 'login') {
await login(values.email, values.password)
} else {
+
await register(values.email, values.password, values.adminToken)
}
form.reset()
} catch (err: any) {
toast({
variant: 'destructive',
title: 'Error',
+
description: err.response?.data || 'An error occurred',
})
}
}
···
)}
/>
+
{activeTab === 'register' && (
+
<FormField
+
control={form.control}
+
name="adminToken"
+
render={({ field }) => (
+
<FormItem>
+
<FormLabel>Admin Setup Token</FormLabel>
+
<FormControl>
+
<Input type="text" {...field} />
+
</FormControl>
+
<FormMessage />
+
</FormItem>
+
)}
+
/>
+
)}
+
<Button type="submit" className="w-full">
{activeTab === 'login' ? 'Sign in' : 'Create account'}
</Button>
···
</Tabs>
</Card>
)
+
}
+4 -2
frontend/src/components/LinkList.tsx
···
}
const handleCopy = (shortCode: string) => {
-
navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`)
toast({
-
description: "Link copied to clipboard",
})
}
···
}
const handleCopy = (shortCode: string) => {
+
// Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin
+
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin
+
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
toast({
+
description: "Link copied to clipboard",
})
}
+3 -3
frontend/src/context/AuthContext.tsx
···
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
-
register: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
···
setUser(user);
};
-
const register = async (email: string, password: string) => {
-
const response = await api.register(email, password);
const { token, user } = response;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
···
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
+
register: (email: string, password: string, adminToken: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
···
setUser(user);
};
+
const register = async (email: string, password: string, adminToken: string) => {
+
const response = await api.register(email, password, adminToken);
const { token, user } = response;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
+6
frontend/src/types/api.ts
···
source: string;
count: number;
}
···
source: string;
count: number;
}
+
+
export interface RegisterRequest {
+
email: string;
+
password: string;
+
admin_token: string;
+
}
+4 -8
frontend/vite.config.ts
···
import tailwindcss from '@tailwindcss/vite'
import path from "path"
-
export default defineConfig({
-
plugins: [
-
react(),
-
tailwindcss(),
-
],
server: {
proxy: {
'/api': {
-
target: 'http://localhost:8080',
changeOrigin: true,
},
},
···
"@": path.resolve(__dirname, "./src"),
},
},
-
})
-
···
import tailwindcss from '@tailwindcss/vite'
import path from "path"
+
export default defineConfig(() => ({
+
plugins: [react(), tailwindcss()],
server: {
proxy: {
'/api': {
+
target: process.env.VITE_API_URL || 'http://localhost:8080',
changeOrigin: true,
},
},
···
"@": path.resolve(__dirname, "./src"),
},
},
+
}))
+21
src/handlers.rs
···
state: web::Data<AppState>,
payload: web::Json<RegisterRequest>,
) -> Result<impl Responder, AppError> {
let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email)
.fetch_optional(&state.db)
.await?;
···
state: web::Data<AppState>,
payload: web::Json<RegisterRequest>,
) -> Result<impl Responder, AppError> {
+
// Check if any users exist
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
+
.fetch_one(&state.db)
+
.await?
+
.count
+
.unwrap_or(0);
+
+
// If users exist, registration is closed - no exceptions
+
if user_count > 0 {
+
return Err(AppError::Auth("Registration is closed".to_string()));
+
}
+
+
// Verify admin token for first user
+
match (&state.admin_token, &payload.admin_token) {
+
(Some(stored_token), Some(provided_token)) if stored_token == provided_token => {
+
// Token matches, proceed with registration
+
}
+
_ => return Err(AppError::Auth("Invalid admin setup token".to_string())),
+
}
+
+
// Check if email already exists
let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email)
.fetch_optional(&state.db)
.await?;
+41
src/lib.rs
···
use sqlx::PgPool;
pub mod auth;
pub mod error;
···
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
}
···
+
use rand::Rng;
use sqlx::PgPool;
+
use std::fs::File;
+
use std::io::Write;
+
use tracing::info;
pub mod auth;
pub mod error;
···
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
+
pub admin_token: Option<String>,
+
}
+
+
pub async fn check_and_generate_admin_token(pool: &sqlx::PgPool) -> anyhow::Result<Option<String>> {
+
// Check if any users exist
+
let user_count = sqlx::query!("SELECT COUNT(*) as count FROM users")
+
.fetch_one(pool)
+
.await?
+
.count
+
.unwrap_or(0);
+
+
if user_count == 0 {
+
// Generate a random token using simple characters
+
let token: String = (0..32)
+
.map(|_| {
+
let idx = rand::thread_rng().gen_range(0..62);
+
match idx {
+
0..=9 => (b'0' + idx as u8) as char,
+
10..=35 => (b'a' + (idx - 10) as u8) as char,
+
_ => (b'A' + (idx - 36) as u8) as char,
+
}
+
})
+
.collect();
+
+
// Save token to file
+
let mut file = File::create("admin-setup-token.txt")?;
+
writeln!(file, "{}", token)?;
+
+
info!("No users found - generated admin setup token");
+
info!("Token has been saved to admin-setup-token.txt");
+
info!("Use this token to create the admin user");
+
info!("Admin setup token: {}", token);
+
+
Ok(Some(token))
+
} else {
+
Ok(None)
+
}
}
+7 -1
src/main.rs
···
use actix_files as fs;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
use simplelink::{handlers, AppState};
use sqlx::postgres::PgPoolOptions;
use tracing::info;
···
// Run database migrations
sqlx::migrate!("./migrations").run(&pool).await?;
-
let state = AppState { db: pool };
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
···
use actix_files as fs;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
+
use simplelink::check_and_generate_admin_token;
use simplelink::{handlers, AppState};
use sqlx::postgres::PgPoolOptions;
use tracing::info;
···
// Run database migrations
sqlx::migrate!("./migrations").run(&pool).await?;
+
let admin_token = check_and_generate_admin_token(&pool).await?;
+
+
let state = AppState {
+
db: pool,
+
admin_token,
+
};
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
+1
src/models.rs
···
pub struct RegisterRequest {
pub email: String,
pub password: String,
}
#[derive(Serialize)]
···
pub struct RegisterRequest {
pub email: String,
pub password: String,
+
pub admin_token: Option<String>,
}
#[derive(Serialize)]