Add QR codes, link expiry, password protection, previews, audit trail and bulk operations

This commit is contained in:
thakares committed 2026-06-12 19:57:53 +05:30
1 parent 157aa81252
commit bcbcc90d98
88 files changed
+5354 -815

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
use crate::db::Db;
use crate::models::ApiKey;
use crate::error::AppError;
use crate::models::ApiKey;
pub fn create_api_key(
db: &Db,
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::db::Db;
use crate::models::AuditLog;
use crate::error::AppError;
use crate::models::AuditLog;
pub fn log_action(
db: &Db,
+49
View File
@@ -0,0 +1,49 @@
use crate::models::Url;
use crate::services::qr::{generate_qr_png, generate_qr_svg};
use std::io::Write;
use zip::write::FileOptions;
use zip::ZipWriter;
/// Export QR codes for the given URLs as a ZIP file.
/// `format` can be "png" or "svg".
/// `base_url` is used to build the full short URL encoded in the QR.
pub fn export_qr_zip(
urls: &[Url],
format: &str,
base_url: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buf = Vec::new();
{
let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf));
let options =
FileOptions::<()>::default().compression_method(zip::CompressionMethod::Deflated);
let mut csv_content = String::from("code,destination,qr_filename\n");
for url in urls {
let full_url = format!("{}/{}", base_url.trim_end_matches('/'), url.code);
let ext = if format == "svg" { "svg" } else { "png" };
let filename = format!("{}.{}", url.code, ext);
let qr_data = if format == "svg" {
generate_qr_svg(&full_url)?.into_bytes()
} else {
generate_qr_png(&full_url, 256)?
};
zip.start_file(&filename, options)?;
zip.write_all(&qr_data)?;
// Escape quotes in destination for CSV formatting
let escaped_dest = url.destination.replace('"', "\"\"");
csv_content.push_str(&format!("{},\"{}\",{}\n", url.code, escaped_dest, filename));
}
zip.start_file("manifest.csv", options)?;
zip.write_all(csv_content.as_bytes())?;
zip.finish()?;
}
Ok(buf)
}
+3 -2
View File
@@ -1,6 +1,6 @@
use crate::db::Db;
use crate::models::LandingPage;
use crate::error::AppError;
use crate::models::LandingPage;
pub fn create_landing_page(
db: &Db,
@@ -11,7 +11,8 @@ pub fn create_landing_page(
state: &str,
) -> Result<LandingPage, AppError> {
let conn = db.content.lock().unwrap();
let page = crate::db::content::create_landing_page(&conn, code, slug, title, html_content, state)?;
let page =
crate::db::content::create_landing_page(&conn, code, slug, title, html_content, state)?;
Ok(page)
}
+4 -2
View File
@@ -1,4 +1,6 @@
pub mod shortener;
pub mod landing_pages;
pub mod api_keys;
pub mod audit;
pub mod bulk;
pub mod landing_pages;
pub mod qr;
pub mod shortener;
+58
View File
@@ -0,0 +1,58 @@
//! QR code generation service.
//!
//! Generates QR codes on-demand as PNG or SVG. No files are stored on disk.
use image::Luma;
use qrcode::QrCode;
use std::io::Cursor;
/// Generate a QR code as a PNG byte vector.
///
/// The `url` is encoded into the QR matrix. The `size` parameter controls
/// the pixel dimensions of the output image (default: 256).
pub fn generate_qr_png(url: &str, size: u32) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let code = QrCode::new(url.as_bytes())?;
let image = code.render::<Luma<u8>>().min_dimensions(size, size).build();
let mut buf = Vec::new();
let mut cursor = Cursor::new(&mut buf);
image.write_to(&mut cursor, image::ImageFormat::Png)?;
Ok(buf)
}
/// Generate a QR code as an SVG string.
pub fn generate_qr_svg(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let code = QrCode::new(url.as_bytes())?;
let svg = code
.render::<qrcode::render::svg::Color>()
.min_dimensions(256, 256)
.build();
Ok(svg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qr_png_generation() {
let png = generate_qr_png("https://bzo.in/abc123", 256).unwrap();
assert!(!png.is_empty());
// PNG magic bytes
assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
}
#[test]
fn test_qr_svg_generation() {
let svg = generate_qr_svg("https://bzo.in/abc123").unwrap();
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
}
#[test]
fn test_qr_long_url() {
let long_url = format!("https://example.com/{}", "a".repeat(500));
let png = generate_qr_png(&long_url, 512).unwrap();
assert!(!png.is_empty());
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::db::Db;
use crate::models::Url;
use crate::error::AppError;
use crate::models::Url;
pub fn create_url(
db: &Db,