Rust URL Shortener - A fast, secure URL shortening service built with Rust and Actix-web. Supports two deployment modes: standalone with built-in auth, or SaaS for integration with a parent application.
- JWT Authentication - Secure user registration and login with Argon2id password hashing
- SQLite Persistence - Reliable data storage with SQLite (bundled, zero setup)
- Click Tracking - Per-click history with configurable retention and analytics
- QR Code Generation - Generate QR codes for shortened URLs (PNG and SVG)
- Custom Names - Give your shortened URLs memorable names
- URL Management - Create, rename, delete, and monitor URLs
- Admin Panel - User management and abuse report review
- Abuse Reporting - Public abuse reporting for malicious URLs
- Account Security - Login attempt tracking with configurable lockout
- Rate Limiting - Built-in request rate limiting via actix-governor
- Refresh Tokens - Seamless token refresh without re-login
- Dual Build Modes - Standalone or SaaS deployment
- Docker Support - Multi-stage Dockerfile with dependency caching
Full-featured URL shortener with built-in user management:
- User registration and login with JWT authentication
- Password hashing with Argon2id (with automatic bcrypt migration)
- Admin user management
- Account lockout protection
- Refresh token rotation
cargo build --release --features standaloneLightweight version for integration with a parent application:
- No built-in user management (uses external auth via
access_tokencookie) - User identity extracted from parent app's JWT
- No registration/login routes
- Dashboard redirects to parent app if no valid session
cargo build --release --no-default-features --features saas- Rust 1.93 or higher (or use Docker)
- Clone the repository:
git clone https://github.com/joshrandall8478/rus.git
cd rus- Copy and edit the environment file:
cp .env.standalone .env
# Edit .env and set JWT_SECRET- Build and run:
cargo build --release
cargo run --releaseThe application starts on http://localhost:4001.
Each developer gets their own HTTPS instance at https://{USER}-rus.a8n.run (where {USER} is your OS username), routed via Traefik:
just dev # Start standalone instance
just dev saas # Start SaaS mode instance
just dev-stop # Stop instanceFor local development without Traefik (cargo-watch with hot-reload on localhost:4001):
just dev-local # Start local dev server
just dev-local-stop # Stop local dev server- Sign Up - Create an account at
/signup.html - Log In - Authenticate at
/login.html - Dashboard - Manage your URLs at
/dashboard.html:- Shorten new URLs
- View click statistics and history
- Generate QR codes
- Rename URLs with custom names
- Copy short URLs to clipboard
- Delete URLs
- Admin - Manage users and abuse reports at
/admin.html(admin only) - Report - Report abusive URLs at
/report.html
| Method | Path | Description |
|---|---|---|
POST |
/api/register |
Register a new user (standalone only) |
POST |
/api/login |
Login, returns JWT + refresh token (standalone only) |
POST |
/api/token/refresh |
Refresh an expired JWT (standalone only) |
GET |
/{short_code} |
Redirect to original URL |
POST |
/api/report |
Report an abusive URL |
| Method | Path | Description |
|---|---|---|
POST |
/api/shorten |
Shorten a URL |
GET |
/api/urls |
List user's URLs |
GET |
/api/stats/{code} |
Get URL statistics |
GET |
/api/stats/{code}/clicks |
Get click history |
DELETE |
/api/urls/{code} |
Delete a URL |
PATCH |
/api/urls/{code}/name |
Rename a URL |
GET |
/api/qr/{code} |
Generate QR code (PNG) |
GET |
/api/qr/{code}/svg |
Generate QR code (SVG) |
GET |
/api/config |
Get public configuration |
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users |
List all users |
DELETE |
/api/admin/users/{id} |
Delete a user |
PATCH |
/api/admin/users/{id}/admin |
Toggle admin status |
GET |
/api/admin/reports |
List abuse reports |
PATCH |
/api/admin/reports/{id} |
Resolve an abuse report |
Register:
curl -X POST http://localhost:4001/api/register \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"password123"}'Login and save token:
TOKEN=$(curl -s -X POST http://localhost:4001/api/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"password123"}' | jq -r '.token')Shorten a URL:
curl -X POST http://localhost:4001/api/shorten \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"url":"https://github.com/joshrandall8478/rus"}'Get your URLs:
curl http://localhost:4001/api/urls \
-H "Authorization: Bearer $TOKEN"rus/
├── src/
│ ├── main.rs # Entry point, route configuration
│ ├── config.rs # Environment-based configuration
│ ├── db.rs # Database connection and schema
│ ├── models.rs # Data models and request/response types
│ ├── security.rs # Password validation, account lockout
│ ├── auth/
│ │ ├── mod.rs
│ │ ├── jwt.rs # JWT creation and validation
│ │ └── middleware.rs # Auth middleware
│ ├── handlers/
│ │ ├── mod.rs
│ │ ├── auth.rs # Registration, login (standalone)
│ │ ├── admin.rs # User management (standalone)
│ │ ├── abuse.rs # Abuse reporting
│ │ ├── pages.rs # Static page serving
│ │ ├── saas_auth.rs # Cookie-based auth (saas)
│ │ └── urls.rs # URL CRUD, redirect, statistics
│ └── url/
│ ├── mod.rs
│ ├── shortener.rs # Short code generation
│ └── qr.rs # QR code generation
├── static/
│ ├── index.html # Landing page
│ ├── login.html # Login page
│ ├── signup.html # Registration page
│ ├── dashboard.html # URL management dashboard
│ ├── admin.html # Admin panel
│ ├── report.html # Abuse report form
│ ├── setup.html # Initial setup page
│ ├── 404.html # Custom 404 error page
│ ├── styles.css # Global styles
│ └── auth.js # Authentication utilities
├── oci-build/
│ ├── setup.nu # Nushell build script
│ └── get-tags.nu # Image tag derivation from git describe
├── data/
│ └── rus.db # SQLite database (auto-created)
├── Cargo.toml
├── Dockerfile
├── compose.yml
├── compose.dev.yml # Per-developer Traefik compose
├── justfile # Task runner recipes
├── .env.standalone # Env template for standalone mode
└── .env.saas # Env template for saas mode
| Variable | Description | Default |
|---|---|---|
DB_PATH |
Path to SQLite database file | ./data/rus.db |
HOST |
Server bind address | 0.0.0.0 |
APP_PORT |
Server port | 4001 |
HOST_URL |
Public URL for shortened links | http://localhost:4001 |
MAX_URL_LENGTH |
Maximum URL length | 2048 |
CLICK_RETENTION_DAYS |
Days to retain click history | 30 |
RUST_LOG |
Log level | info |
| Variable | Description | Default |
|---|---|---|
JWT_SECRET |
Base64-encoded 32-byte secret for JWT signing | Required |
JWT_EXPIRY |
JWT expiry in hours | 1 |
REFRESH_TOKEN_EXPIRY |
Refresh token expiry in days | 7 |
ACCOUNT_LOCKOUT_ATTEMPTS |
Failed attempts before lockout | 5 |
ACCOUNT_LOCKOUT_DURATION |
Lockout duration in minutes | 30 |
ALLOW_REGISTRATION |
Allow public signups | true |
| Variable | Description | Default |
|---|---|---|
SAAS_JWT_SECRET |
JWT secret for validating parent app tokens | Required |
userID- Primary keyusername- Unique usernamepassword- Argon2id hashed password (legacy bcrypt hashes migrated on login)is_admin- Admin flag (0/1)created_at- Account creation timestamp
id- Primary keyuser_id- Foreign key to usersoriginal_url- The original long URLshort_code- Unique 6-character code (indexed)name- Optional custom nameclicks- Click countercreated_at- URL creation timestamp
id- Primary keyurl_id- Foreign key to urlsclicked_at- Click timestamp
id- Primary keyuser_id- Foreign key to userstoken- Unique refresh tokenexpires_at- Expiry timestamp
id- Primary keyusername- Attempted usernameattempted_at- Attempt timestampsuccess- Whether login succeeded (0/1)
id- Primary keyshort_code- Reported URL codereporter_email- Optional reporter emailreason- Report reasondescription- Optional descriptionstatus- Report status (pending/resolved)created_at,resolved_at,resolved_by
- Actix-web - High-performance web framework
- SQLite - Embedded database via rusqlite (bundled)
- jsonwebtoken - JWT authentication
- Argon2id - Password hashing (standalone)
- actix-governor - Rate limiting
- qrcode - QR code generation
- Serde - Serialization/deserialization
- Tokio - Async runtime
just dev # Traefik-routed instance (standalone)
just dev saas # Traefik-routed instance (saas)
just dev-local # Local dev with hot-reload
just test # Run tests (standalone)
just test-saas # Run tests (saas)
just lint # Clippy
just fmt # Format- 6-character alphanumeric codes (A-Z, a-z, 0-9)
- 62^6 = ~56.8 billion possible combinations
- Collision detection ensures unique codes
- JWT-based authentication with short-lived tokens
- Argon2id password hashing (with transparent bcrypt migration on login)
- Refresh token rotation
- Account lockout after configurable failed attempts
- Rate limiting on API endpoints
- Protected API endpoints with user-scoped access
- SQL injection prevention via parameterized queries
- Foreign key enforcement enabled
Contributions are welcome! Feel free to:
- Report bugs
- Suggest features
- Submit pull requests
This project is open source and available under the MIT License.
Made with Rust
