7 changed files with 626 additions and 180 deletions

No files matched your search

+17 -12
View File
@@ -1,7 +1,6 @@
# ==========================================
# Stage 1: Build
# ==========================================
# FROM rust:1.82-slim-bookworm AS builder
FROM rust:1.89-bookworm AS builder
WORKDIR /app
@@ -13,45 +12,51 @@ RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
# Copy configuration files
# COPY Cargo.toml ./
# Copy Cargo metadata
COPY Cargo.toml Cargo.lock ./
# Pre-build dependencies to cache them
# Pre-build dependencies for layer caching
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# Copy source and templates
# Copy application source
COPY src ./src
COPY templates ./templates
COPY www ./www
# Trigger rebuilding with actual source
# Build application
RUN touch src/main.rs
RUN cargo build --release
# ==========================================
# Stage 2: Runner
# Stage 2: Runtime
# ==========================================
FROM debian:bookworm-slim
WORKDIR /app
# Install runtime dependencies
# Runtime dependencies
RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy binary from builder
# Application binary
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
# Create non-root user and data directory
# Runtime assets
COPY templates ./templates
COPY www ./www
# Create non-root user
RUN groupadd -g 1000 bzod && \
useradd -u 1000 -g bzod -m -s /bin/bash bzod
RUN mkdir -p /app/data && chown -R bzod:bzod /app/data
# Create writable data directory
RUN mkdir -p /app/data && \
chown -R bzod:bzod /app
USER bzod
@@ -66,4 +71,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD curl -f http://localhost:$${PORT:-8654}/status || exit 1
ENTRYPOINT ["bzod"]
CMD ["serve"]
CMD ["serve"]
+2 -1
View File
@@ -368,8 +368,9 @@ cargo run -- create-admin
cargo test
```
---
## Development & Testing
See [docs/TESTING.md](docs/TESTING.md) for comprehensive testing, validation, backup, restore, disaster recovery, and release procedures.
## Project Structure
```text
+404
View File
@@ -0,0 +1,404 @@
# TESTING.md
# BZOD Test Procedures
This document describes the official verification procedures for BZOD.
The objective is not merely to confirm that code compiles, but to ensure that the complete platform can be built, deployed, backed up, restored, migrated, and recovered successfully.
---
# Philosophy
BZOD prioritizes:
1. Data Integrity
2. Operational Simplicity
3. Recovery Capability
4. Deployment Reproducibility
5. Functional Correctness
A passing unit test suite alone is insufficient.
A release is considered valid only if backup, restore, migration, and recovery procedures have been verified.
---
# Test Categories
## 1. Build Verification
Verify the application compiles successfully.
```bash
cargo check
cargo build
cargo build --release
```
Expected Result:
* No compiler errors
* No panics during startup
* Release binary generated successfully
---
## 2. Static Analysis
```bash
cargo fmt --check
cargo clippy --all-targets
```
Expected Result:
* Formatting passes
* No significant Clippy warnings
---
## 3. Unit Tests
```bash
cargo test
```
Expected Result:
* All tests pass
* No ignored critical tests
---
## 4. Database Initialization
Create a clean environment.
```bash
rm -rf data
./bzod stats
```
Expected Result:
* Databases are automatically created
* Migrations applied successfully
Verify:
```bash
./bzod doctor
```
Expected Result:
```text
Overall status: HEALTHY
```
---
## 5. Migration Verification
Run migrations repeatedly.
```bash
./bzod migrate
./bzod migrate
./bzod migrate
```
Expected Result:
* No duplicate migrations
* No errors
* Schema remains stable
---
## 6. Administrator Creation
Create an administrator account.
```bash
./bzod create-admin
```
Expected Result:
* User created successfully
* Authentication works
Attempt duplicate creation:
```bash
./bzod create-admin
```
Expected Result:
* Duplicate username rejected
---
## 7. Backup Verification
Create backup archive.
```bash
./bzod backup
```
Expected Result:
* Backup archive generated
* Archive contains all databases
Verify:
```bash
tar -tzf backup-*.tar.gz
```
Expected Result:
```text
admin.db
content.db
analytics.db
system.db
```
---
## 8. Restore Verification
Create sample data.
Generate:
* Administrator
* URL records
* Landing pages
* Analytics records
Create backup:
```bash
./bzod backup
```
Delete databases:
```bash
rm -rf data
```
Restore:
```bash
./bzod restore --file backup.tar.gz
```
Expected Result:
* Restore completes successfully
* All records preserved
Verify:
```bash
./bzod doctor
./bzod stats
```
Expected Result:
```text
Overall status: HEALTHY
```
and original record counts preserved.
---
## 9. Disaster Recovery Test
This is the most important test.
Procedure:
1. Backup system.
2. Delete entire data directory.
3. Restore backup.
4. Start server.
5. Login to Admin UI.
Commands:
```bash
./bzod backup
rm -rf data
./bzod restore --file backup.tar.gz
./bzod serve
```
Expected Result:
* System fully operational
* No manual database repair required
---
## 10. Database Health Verification
Run:
```bash
./bzod doctor
```
Expected Result:
For every database:
```text
Integrity: ok
Foreign keys: enabled
Journal mode: wal
```
Final result:
```text
Overall status: HEALTHY
```
---
## 11. SQLite Integrity Checks
Manual verification.
```bash
sqlite3 data/admin.db "PRAGMA integrity_check;"
sqlite3 data/content.db "PRAGMA integrity_check;"
sqlite3 data/analytics.db "PRAGMA integrity_check;"
sqlite3 data/system.db "PRAGMA integrity_check;"
```
Expected Result:
```text
ok
```
for all databases.
---
## 12. Web Interface Verification
Start server.
```bash
./bzod serve
```
Verify:
* Homepage loads
* Redirects function
* Landing pages render
* Admin login works
* Dashboard loads
* API endpoints respond
---
## 13. Docker Verification
Build image.
```bash
docker compose build --no-cache
```
Start service.
```bash
docker compose up -d
```
Verify:
```bash
docker compose logs -f
```
Expected Result:
```text
Listening for requests
```
Verify:
```bash
./bzod doctor
```
inside container.
---
## 14. Upgrade Verification
1. Create backup.
2. Upgrade binary.
3. Run migration.
4. Start service.
```bash
./bzod backup
./bzod migrate
./bzod serve
```
Expected Result:
* Existing data preserved
* No migration failures
---
# Release Acceptance Criteria
A release is considered production-ready only if:
* Build verification passes
* Static analysis passes
* Unit tests pass
* Backup verification passes
* Restore verification passes
* Disaster recovery verification passes
* Doctor reports HEALTHY
* Docker deployment succeeds
* Web UI functions correctly
Failure of backup, restore, or disaster recovery tests is considered a release blocker.
---
# Guiding Principle
A successful release is not merely one that starts.
A successful release is one that can be recovered.
+44
View File
@@ -77,3 +77,47 @@ pub async fn resolve_page(
None => (StatusCode::NOT_FOUND, "Landing page not found").into_response(),
}
}
// GET /
// Serve static root landing page from www/index.html
pub async fn root_landing() -> Response {
let mut target_path = std::path::PathBuf::from("www/index.html");
if !target_path.exists() {
// Search relative to executable
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let path1 = exe_dir.join("www/index.html");
if path1.exists() {
target_path = path1;
} else if let Some(parent1) = exe_dir.parent() {
let path2 = parent1.join("www/index.html");
if path2.exists() {
target_path = path2;
} else if let Some(parent2) = parent1.parent() {
let path3 = parent2.join("www/index.html");
if path3.exists() {
target_path = path3;
}
}
}
}
}
}
if !target_path.exists() {
// Search in CARGO_MANIFEST_DIR
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
let path = std::path::PathBuf::from(manifest_dir).join("www/index.html");
if path.exists() {
target_path = path;
}
}
}
match std::fs::read_to_string(&target_path) {
Ok(content) => Html(content).into_response(),
Err(_) => (StatusCode::NOT_FOUND, "Not Found").into_response(),
}
}
+2
View File
@@ -7,6 +7,8 @@ use axum::{
pub fn create_router(state: AppState) -> Router {
Router::new()
// --- Root Landing Page ---
.route("/", get(pages::root_landing))
// --- Public Redirection Routes ---
.route("/:code", get(redirect::resolve_redirect))
.route("/p/:code", get(pages::resolve_page))
+43
View File
@@ -0,0 +1,43 @@
use axum::http::StatusCode;
use bzod::web::pages::root_landing;
use std::fs;
#[tokio::test]
async fn test_root_landing_page() {
// --- Test case 1: Successful serving ---
// Read expected content
let expected_content = fs::read_to_string("www/index.html").expect("www/index.html must exist for test");
let response = root_landing().await;
assert_eq!(response.status(), StatusCode::OK);
assert!(
response.headers().get("content-type").unwrap().to_str().unwrap().contains("text/html")
);
// Convert response body to bytes using axum::body::to_bytes
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
assert_eq!(body_str, expected_content);
// --- Test case 2: File Not Found fallback ---
// Temporarily rename www/index.html to simulate file not found
let orig_path = "www/index.html";
let temp_path = "www/index.html.tmp_test_bak";
fs::rename(orig_path, temp_path).unwrap();
let response_res = tokio::spawn(async move {
root_landing().await
}).await;
// Restore index.html immediately in case of panic/error
let rename_res = fs::rename(temp_path, orig_path);
// Now verify the response status
let response = response_res.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
rename_res.unwrap();
}
+114 -167
View File
@@ -1,182 +1,129 @@
<!DOCTYPE html>
<html lang="en" data-lt-installed="true"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bzo.in - Simple &amp; Private URL Shortener</title>
<title>BZOD — Private • Fast • Beautiful URL Shortener</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root {
--bg: #0f1321;
--card: #1a2337;
--accent: #8b5cf6;
--text: #e0e7ff;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
header {
background: rgba(26, 35, 55, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid #2a3a5a;
padding: 1.25rem 0;
}
.nav {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 1.75rem; font-weight: 700; color: var(--accent); }
.main { padding: 6rem 2rem 4rem; text-align: center; }
.beta {
display: inline-block;
background: #eab308;
color: #1e2937;
font-size: 0.85rem;
padding: 5px 14px;
border-radius: 9999px;
font-weight: 600;
margin-bottom: 1rem;
}
h1 {
font-size: 3.2rem;
line-height: 1.1;
margin-bottom: 1rem;
background: linear-gradient(90deg, #c4b5fd, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.tagline {
font-size: 1.35rem;
color: #94a3c0;
max-width: 700px;
margin: 0 auto 3rem;
}
.shorten-box {
background: var(--card);
border-radius: 16px;
padding: 1.25rem;
max-width: 620px;
margin: 0 auto 4rem;
border: 1px solid #3b4a6b;
display: flex;
gap: 12px;
}
input {
flex: 1;
padding: 16px 20px;
font-size: 1.1rem;
background: #111827;
border: 1px solid #475569;
border-radius: 12px;
color: white;
}
button {
padding: 16px 36px;
background: var(--accent);
color: white;
border: none;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
}
button:hover { background: #a78bfa; }
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
max-width: 1100px;
margin: 0 auto;
}
.feature-card {
background: var(--card);
padding: 2rem 1.75rem;
border-radius: 16px;
border: 1px solid #3b4a6b;
text-align: left;
}
footer {
text-align: center;
padding: 3rem 1rem 2rem;
color: #64748b;
font-size: 0.95rem;
body { font-family: 'Inter', system-ui, sans-serif; }
.hero-bg {
background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1e 100%);
}
</style>
</head>
<body>
<header>
<div class="nav">
<div class="logo">BZOD • bzo.in</div>
<a href="https://bzo.in/admin/dashboard" style="color:#a5b4fc; text-decoration:none;">Admin</a>
</div>
</header>
<body class="bg-zinc-950 text-zinc-200">
<div class="main">
<div class="beta">BETA</div>
<h1>Short. Clean.<br>Privacy-First.</h1>
<p class="tagline">Lightning-fast URL shortener with built-in analytics, custom landing pages, and full self-hosting control.</p>
<!-- Hero -->
<section class="hero-bg py-24">
<div class="max-w-5xl mx-auto px-6 text-center">
<div class="inline-flex items-center gap-2 bg-zinc-900 border border-zinc-700 rounded-full px-4 py-1.5 mb-6">
<span class="text-emerald-400">●</span>
<span class="text-sm font-medium">Self-hosted • Rust • Single Binary</span>
</div>
<h1 class="text-6xl md:text-7xl font-bold tracking-tighter mb-6">
Short links.<br>
<span class="bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent">Your domain.</span>
</h1>
<p class="text-2xl text-zinc-400 max-w-2xl mx-auto mb-10">
Beautiful, private, and powerful URL shortener with rich landing pages, QR codes, analytics, and full CLI control.
</p>
<div class="shorten-box">
<input type="url" placeholder="Paste your long URL here..." id="urlInput">
<button onclick="shorten()">Shorten Now</button>
</div>
<div class="flex flex-wrap justify-center gap-4">
<a href="https://github.com/thakares/nx9-url-shortener"
target="_blank"
class="bg-white text-black px-8 py-4 rounded-2xl font-semibold flex items-center gap-3 hover:scale-105 transition">
<i class="fab fa-github text-xl"></i>
View on GitHub
</a>
<a href="/admin"
class="bg-violet-600 hover:bg-violet-700 px-8 py-4 rounded-2xl font-semibold transition">
Admin Dashboard →
</a>
</div>
<div class="features">
<div class="feature-card">
<h3>🔗 Compact Hex Codes</h3>
<p>Short, memorable 4-6 character links that look professional.</p>
</div>
<div class="feature-card">
<h3>📊 Real-time Analytics</h3>
<p>Track clicks, countries, referrers with beautiful charts.</p>
</div>
<div class="feature-card">
<h3>🎨 Custom Landing Pages</h3>
<p>Beautiful branded pages before redirect (like this one!).</p>
</div>
<div class="feature-card">
<h3>🔒 Self-Hosted &amp; Private</h3>
<p>Your data. Your server. No third-party tracking.</p>
<div class="mt-16 text-sm text-zinc-500">
Powered by <span class="font-mono text-emerald-400">bzo.in</span>
</div>
</div>
</div>
</section>
<footer>
<p>Made with ❤️ using <strong>nx9-url-shortener</strong> •
<a href="https://github.com/thakares/nx9-url-shortener" style="color:#a78bfa">Star on GitHub</a></p>
<!-- Features -->
<section class="py-20 bg-zinc-900">
<div class="max-w-5xl mx-auto px-6">
<h2 class="text-4xl font-bold text-center mb-16">Why people love BZOD</h2>
<div class="grid md:grid-cols-3 gap-8">
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">⚡</div>
<h3 class="text-2xl font-semibold mb-3">Lightning Fast</h3>
<p class="text-zinc-400">~18 MB single Rust binary. Starts instantly. Uses SQLite with WAL mode. Minimal resource usage.</p>
</div>
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">🔒</div>
<h3 class="text-2xl font-semibold mb-3">Private by Design</h3>
<p class="text-zinc-400">No telemetry. No third-party services. Everything runs on your server. Strong password hashing & audit logs.</p>
</div>
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">🎨</div>
<h3 class="text-2xl font-semibold mb-3">Beautiful Links</h3>
<p class="text-zinc-400">Rich custom landing pages with title, description, OG metadata, and branded preview.</p>
</div>
</div>
<div class="grid md:grid-cols-3 gap-8 mt-8">
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">📱</div>
<h3 class="text-2xl font-semibold mb-3">QR Codes Built-in</h3>
<p class="text-zinc-400">Generate PNG & SVG QR codes. Track scans separately in analytics.</p>
</div>
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">🛠️</div>
<h3 class="text-2xl font-semibold mb-3">CLI First</h3>
<p class="text-zinc-400">backup, restore, doctor, stats, validate, create-admin — everything from terminal.</p>
</div>
<div class="bg-zinc-950 border border-zinc-800 rounded-3xl p-8">
<div class="text-4xl mb-6">🔑</div>
<h3 class="text-2xl font-semibold mb-3">Powerful Features</h3>
<p class="text-zinc-400">Password protection • Expiry • Access limits • Tags • REST API • Bulk QR export</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="py-20 bg-black border-t border-zinc-800">
<div class="max-w-2xl mx-auto text-center px-6">
<h2 class="text-4xl font-bold mb-6">Ready to own your short links?</h2>
<p class="text-zinc-400 mb-10">Self-host BZOD in under 5 minutes on any VPS, homelab, or even a Raspberry Pi.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="https://github.com/thakares/nx9-url-shortener"
target="_blank"
class="bg-white text-black px-10 py-4 rounded-2xl font-semibold text-lg">
Get BZOD Now
</a>
<a href="/admin"
class="border border-zinc-700 hover:bg-zinc-900 px-10 py-4 rounded-2xl font-semibold text-lg transition">
Open Dashboard
</a>
</div>
</div>
</section>
<footer class="bg-zinc-950 py-12 border-t border-zinc-800">
<div class="max-w-5xl mx-auto px-6 text-center text-zinc-500 text-sm">
© 2026 BZOD • Made with ❤️ in Rust • Running on <span class="font-mono">bzo.in</span>
</div>
</footer>
</body>
</html>
<script>
function shorten() {
const url = document.getElementById('urlInput').value.trim();
if (url) {
window.location.href = `/?url=${encodeURIComponent(url)}`;
}
}
</script>
</body></html>