mirror of
https://github.com/SinTan1729/chhoto-url
synced 2025-04-20 03:40:00 -05:00
Compare commits
7 commits
f27984a63f
...
231fd3c8ca
Author | SHA1 | Date | |
---|---|---|---|
231fd3c8ca | |||
86ec787d96 | |||
30c0b8b50a | |||
62ae71f4ca | |||
e9bb9d0b65 | |||
ca14c02e70 | |||
0469f9b933 |
7 changed files with 91 additions and 48 deletions
14
actix/Cargo.lock
generated
14
actix/Cargo.lock
generated
|
@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.57",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -217,7 +217,7 @@ dependencies = [
|
|||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.57",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -475,7 +475,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "chhoto-url"
|
||||
version = "5.2.2"
|
||||
version = "5.2.4"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-session",
|
||||
|
@ -1239,7 +1239,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.57",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1340,9 +1340,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.57"
|
||||
version = "2.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
|
||||
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1682,7 +1682,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.57",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "chhoto-url"
|
||||
version = "5.2.2"
|
||||
version = "5.2.4"
|
||||
edition = "2021"
|
||||
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
|
||||
license = "mit"
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
use actix_session::Session;
|
||||
use std::{env, time::SystemTime};
|
||||
|
||||
// Validate a given password
|
||||
pub fn validate(session: Session) -> bool {
|
||||
// If there's no password provided, just return true
|
||||
if env::var("password").is_err() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let token = session.get::<String>("session-token");
|
||||
token.is_ok() && check(token.unwrap())
|
||||
if let Ok(token) = session.get::<String>("chhoto-url-auth") {
|
||||
check(token)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Check a token cryptographically
|
||||
fn check(token: Option<String>) -> bool {
|
||||
if let Some(token_body) = token {
|
||||
let token_parts: Vec<&str> = token_body.split(';').collect();
|
||||
|
@ -23,15 +28,16 @@ fn check(token: Option<String>) -> bool {
|
|||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Time went backwards!")
|
||||
.as_secs();
|
||||
token_text == "session-token" && time_now < token_time + 1209600 // There are 1209600 seconds in 14 days
|
||||
token_text == "chhoto-url-auth" && time_now < token_time + 1209600 // There are 1209600 seconds in 14 days
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new cryptographic token
|
||||
pub fn gen_token() -> String {
|
||||
let token_text = String::from("session-token");
|
||||
let token_text = String::from("chhoto-url-auth");
|
||||
let time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Time went backwards!")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
// Struct for encoding a DB row
|
||||
#[derive(Serialize)]
|
||||
pub struct DBRow {
|
||||
shortlink: String,
|
||||
|
@ -8,27 +9,37 @@ pub struct DBRow {
|
|||
hits: i64,
|
||||
}
|
||||
|
||||
// Find a single URL
|
||||
pub fn find_url(shortlink: &str, db: &Connection) -> Option<String> {
|
||||
let mut statement = db
|
||||
.prepare_cached("SELECT long_url FROM urls WHERE short_url = ?1")
|
||||
.unwrap();
|
||||
.expect("Error preparing SQL statement for find_url.");
|
||||
|
||||
statement
|
||||
.query_row([shortlink], |row| row.get("long_url"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
// Get all URLs in DB
|
||||
pub fn getall(db: &Connection) -> Vec<DBRow> {
|
||||
let mut statement = db.prepare_cached("SELECT * FROM urls").unwrap();
|
||||
let mut statement = db
|
||||
.prepare_cached("SELECT * FROM urls")
|
||||
.expect("Error preparing SQL statement for getall.");
|
||||
|
||||
let mut data = statement.query([]).unwrap();
|
||||
let mut data = statement
|
||||
.query([])
|
||||
.expect("Error executing query for getall.");
|
||||
|
||||
let mut links: Vec<DBRow> = Vec::new();
|
||||
while let Some(row) = data.next().unwrap() {
|
||||
while let Some(row) = data.next().expect("Error reading fetched rows.") {
|
||||
let row_struct = DBRow {
|
||||
shortlink: row.get("short_url").unwrap(),
|
||||
longlink: row.get("long_url").unwrap(),
|
||||
hits: row.get("hits").unwrap(),
|
||||
shortlink: row
|
||||
.get("short_url")
|
||||
.expect("Error reading shortlink from row."),
|
||||
longlink: row
|
||||
.get("long_url")
|
||||
.expect("Error reading shortlink from row."),
|
||||
hits: row.get("hits").expect("Error reading shortlink from row."),
|
||||
};
|
||||
links.push(row_struct);
|
||||
}
|
||||
|
@ -36,14 +47,16 @@ pub fn getall(db: &Connection) -> Vec<DBRow> {
|
|||
links
|
||||
}
|
||||
|
||||
// Add a hit when site is visited
|
||||
pub fn add_hit(shortlink: &str, db: &Connection) {
|
||||
db.execute(
|
||||
"UPDATE urls SET hits = hits + 1 WHERE short_url = ?1",
|
||||
[shortlink],
|
||||
)
|
||||
.unwrap();
|
||||
.expect("Error updating hit count.");
|
||||
}
|
||||
|
||||
// Insert a new link
|
||||
pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
|
||||
db.execute(
|
||||
"INSERT INTO urls (long_url, short_url, hits) VALUES (?1, ?2, ?3)",
|
||||
|
@ -52,11 +65,16 @@ pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
|
|||
.is_ok()
|
||||
}
|
||||
|
||||
// Delete and existing link
|
||||
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
|
||||
let out = db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink]);
|
||||
out.is_ok() && (out.unwrap() > 0)
|
||||
if let Ok(delta) = db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink]) {
|
||||
delta > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Open the DB, and create schema if missing
|
||||
pub fn open_db(path: String) -> Connection {
|
||||
let db = Connection::open(path).expect("Unable to open database!");
|
||||
// Create table if it doesn't exist
|
||||
|
@ -69,6 +87,6 @@ pub fn open_db(path: String) -> Connection {
|
|||
)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
.expect("Unable to initialize empty database.");
|
||||
db
|
||||
}
|
||||
|
|
|
@ -100,15 +100,18 @@ async fn link_handler(shortlink: web::Path<String>, data: web::Data<AppState>) -
|
|||
// Handle login
|
||||
#[post("/api/login")]
|
||||
async fn login(req: String, session: Session) -> HttpResponse {
|
||||
if req == env::var("password").unwrap_or(req.clone()) {
|
||||
// If no password was provided, match any password
|
||||
session.insert("session-token", auth::gen_token()).unwrap();
|
||||
HttpResponse::Ok().body("Correct password!")
|
||||
} else {
|
||||
if let Ok(password) = env::var("password") {
|
||||
if password != req {
|
||||
eprintln!("Failed login attempt!");
|
||||
HttpResponse::Forbidden().body("Wrong password!")
|
||||
return HttpResponse::Forbidden().body("Wrong password!");
|
||||
}
|
||||
}
|
||||
// Return Ok if no password was set on the server side
|
||||
session
|
||||
.insert("chhoto-url-auth", auth::gen_token())
|
||||
.expect("Error inserting auth token.");
|
||||
HttpResponse::Ok().body("Correct password!")
|
||||
}
|
||||
|
||||
// Delete a given shortlink
|
||||
#[delete("/api/del/{shortlink}")]
|
||||
|
@ -145,6 +148,7 @@ async fn main() -> std::io::Result<()> {
|
|||
App::new()
|
||||
.wrap(
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||
.cookie_same_site(actix_web::cookie::SameSite::Strict)
|
||||
.cookie_secure(false)
|
||||
.build(),
|
||||
)
|
||||
|
|
|
@ -7,12 +7,14 @@ use std::env;
|
|||
|
||||
use crate::database;
|
||||
|
||||
#[derive(Deserialize, Default, PartialEq)]
|
||||
// Struct for reading link pairs sent during API call
|
||||
#[derive(Deserialize)]
|
||||
struct URLPair {
|
||||
shortlink: String,
|
||||
longlink: String,
|
||||
}
|
||||
|
||||
// Request the DB for searching an URL
|
||||
pub fn get_longurl(shortlink: String, db: &Connection) -> Option<String> {
|
||||
if validate_link(&shortlink) {
|
||||
database::find_url(shortlink.as_str(), db)
|
||||
|
@ -21,26 +23,33 @@ pub fn get_longurl(shortlink: String, db: &Connection) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
// Only have a-z, 0-9, - and _ as valid characters in a shortlink
|
||||
fn validate_link(link: &str) -> bool {
|
||||
let re = Regex::new("^[a-z0-9-_]+$").unwrap();
|
||||
let re = Regex::new("^[a-z0-9-_]+$").expect("Regex generation failed.");
|
||||
re.is_match(link)
|
||||
}
|
||||
|
||||
// Request the DB for all URLs
|
||||
pub fn getall(db: &Connection) -> String {
|
||||
let links = database::getall(db);
|
||||
serde_json::to_string(&links).unwrap()
|
||||
serde_json::to_string(&links).expect("Failure during creation of json from db.")
|
||||
}
|
||||
|
||||
// Make checks and then request the DB to add a new URL entry
|
||||
pub fn add_link(req: String, db: &Connection) -> (bool, String) {
|
||||
let mut chunks: URLPair = serde_json::from_str(&req).unwrap_or_default();
|
||||
|
||||
if chunks == URLPair::default() {
|
||||
let mut chunks: URLPair;
|
||||
if let Ok(json) = serde_json::from_str(&req) {
|
||||
chunks = json;
|
||||
} else {
|
||||
// shorturl should always be supplied, even if empty
|
||||
return (false, String::from("Invalid request!"));
|
||||
}
|
||||
|
||||
let style = env::var("slug_style").unwrap_or(String::from("Pair"));
|
||||
let len_str = env::var("slug_length").unwrap_or(String::from("8"));
|
||||
let mut len: usize = len_str.parse().unwrap_or(8);
|
||||
let mut len = env::var("slug_style")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(8);
|
||||
if len < 4 {
|
||||
len = 4;
|
||||
}
|
||||
|
@ -61,6 +70,7 @@ pub fn add_link(req: String, db: &Connection) -> (bool, String) {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if link, and request DB to delete it if exists
|
||||
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
|
||||
if validate_link(shortlink.as_str()) {
|
||||
database::delete_link(shortlink, db)
|
||||
|
@ -69,6 +79,7 @@ pub fn delete_link(shortlink: String, db: &Connection) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Generate a random link using either adjective-name pair (default) of a slug or a-z, 0-9
|
||||
fn gen_link(style: String, len: usize) -> String {
|
||||
#[rustfmt::skip]
|
||||
static ADJECTIVES: [&str; 108] = ["admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful",
|
||||
|
@ -110,8 +121,12 @@ fn gen_link(style: String, len: usize) -> String {
|
|||
} else {
|
||||
format!(
|
||||
"{0}-{1}",
|
||||
ADJECTIVES.choose(&mut rand::thread_rng()).unwrap(),
|
||||
NAMES.choose(&mut rand::thread_rng()).unwrap()
|
||||
ADJECTIVES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("Error choosing random adjective."),
|
||||
NAMES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.expect("Error choosing random name.")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const getSiteUrl = async () => {
|
|||
return window.location.host.replace(/\/$/, '');
|
||||
}
|
||||
else {
|
||||
return text.replace(/\/$/, '').replace(/^"/, '').replace(/"$/, '');
|
||||
return url.replace(/\/$/, '').replace(/^"/, '').replace(/"$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,6 +173,7 @@ const submitForm = () => {
|
|||
};
|
||||
|
||||
const url = prepSubdir("/api/new");
|
||||
let ok = false;
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
|
@ -182,15 +183,14 @@ const submitForm = () => {
|
|||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
showAlert(res.text(), "red");
|
||||
return "error";
|
||||
ok = res.ok;
|
||||
return res.text();
|
||||
})
|
||||
.then(text => {
|
||||
if (!ok) {
|
||||
showAlert(text, "red");
|
||||
}
|
||||
else {
|
||||
return res.text();
|
||||
}
|
||||
}).then(text => {
|
||||
if (text != "error") {
|
||||
copyShortUrl(text);
|
||||
longUrl.value = "";
|
||||
shortUrl.value = "";
|
||||
|
|
Loading…
Reference in a new issue