mirror of
https://github.com/SinTan1729/chhoto-url.git
synced 2025-12-06 05:24:25 -08:00
chg: Some code cleanup and semantic changes (#100)
* new: Periodically do wal checkpoints during cleanup * docs: Added spdx header in quadlet * chg: More sensible names for auth functions * chg: Use Results in edit, add, and delete routes This would increase reabability and make more semantic sense. * chg: Moved is_api_ok to auth It makes more sense to keep it there * fix: Changed comments and changed a function name to make semantic sense * chg: find_url and find_and_add_hit now use Result * chg: Some reorganizing * fix: Do not use expect unless absolutely necessary Basically, unless there's some unrecoverable error, or something that's guaranteed not to happen, do not use expect.
This commit is contained in:
@@ -6,12 +6,74 @@ use actix_web::HttpRequest;
|
|||||||
use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier};
|
use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier};
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use passwords::PasswordGenerator;
|
use passwords::PasswordGenerator;
|
||||||
|
use serde::Serialize;
|
||||||
use std::{rc::Rc, time::SystemTime};
|
use std::{rc::Rc, time::SystemTime};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
// Define JSON struct for error response
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct APIResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub error: bool,
|
||||||
|
reason: String,
|
||||||
|
pass: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the api_key environment variable exists
|
||||||
|
pub fn is_api_ok(http: HttpRequest, config: &Config) -> APIResponse {
|
||||||
|
// If the api_key environment variable exists
|
||||||
|
if config.api_key.is_some() {
|
||||||
|
// If the header exists
|
||||||
|
if let Some(header) = get_api_header(&http) {
|
||||||
|
// If the header is correct
|
||||||
|
if is_key_valid(header, config) {
|
||||||
|
APIResponse {
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
reason: "Correct API key".to_string(),
|
||||||
|
pass: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
APIResponse {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason: "Incorrect API key".to_string(),
|
||||||
|
pass: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The header may not exist when the user logs in through the web interface, so allow a request with no header.
|
||||||
|
// Further authentication checks will be conducted in services.rs
|
||||||
|
} else {
|
||||||
|
// Due to the implementation of this result in services.rs, this JSON object will not be outputted.
|
||||||
|
APIResponse {
|
||||||
|
success: false,
|
||||||
|
error: false,
|
||||||
|
reason: "No valid authentication was found".to_string(),
|
||||||
|
pass: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the API key isn't set, but an API Key header is provided
|
||||||
|
if get_api_header(&http).is_some() {
|
||||||
|
APIResponse {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason: "An API key was provided, but the 'api_key' environment variable is not configured in the Chhoto URL instance".to_string(),
|
||||||
|
pass: false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
APIResponse {
|
||||||
|
success: false,
|
||||||
|
error: false,
|
||||||
|
reason: "".to_string(),
|
||||||
|
pass: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Validate API key
|
// Validate API key
|
||||||
pub fn validate_key(key: &str, config: &Config) -> bool {
|
pub fn is_key_valid(key: &str, config: &Config) -> bool {
|
||||||
if let Some(api_key) = &config.api_key {
|
if let Some(api_key) = &config.api_key {
|
||||||
// Check if API Key is hashed using Argon2. More algorithms maybe added later.
|
// Check if API Key is hashed using Argon2. More algorithms maybe added later.
|
||||||
let authorized = if config.hash_algorithm.is_some() {
|
let authorized = if config.hash_algorithm.is_some() {
|
||||||
@@ -54,26 +116,26 @@ pub fn gen_key() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the API key header exists
|
// Check if the API key header exists
|
||||||
pub fn api_header(req: &HttpRequest) -> Option<&str> {
|
pub fn get_api_header(req: &HttpRequest) -> Option<&str> {
|
||||||
req.headers().get("X-API-Key")?.to_str().ok()
|
req.headers().get("X-API-Key")?.to_str().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a session
|
// Validate a session
|
||||||
pub fn validate(session: Session, config: &Config) -> bool {
|
pub fn is_session_valid(session: Session, config: &Config) -> bool {
|
||||||
// If there's no password provided, just return true
|
// If there's no password provided, just return true
|
||||||
if config.password.is_none() {
|
if config.password.is_none() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(token) = session.get::<String>("chhoto-url-auth") {
|
if let Ok(token) = session.get::<String>("chhoto-url-auth") {
|
||||||
check(token.as_deref())
|
is_token_valid(token.as_deref())
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check a token cryptographically
|
// Check a token cryptographically
|
||||||
fn check(token: Option<&str>) -> bool {
|
fn is_token_valid(token: Option<&str>) -> bool {
|
||||||
if let Some(token_body) = token {
|
if let Some(token_body) = token {
|
||||||
let token_parts: Rc<[&str]> = token_body.split(';').collect();
|
let token_parts: Rc<[&str]> = token_body.split(';').collect();
|
||||||
if token_parts.len() < 2 {
|
if token_parts.len() < 2 {
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ pub fn read() -> Config {
|
|||||||
if use_wal_mode {
|
if use_wal_mode {
|
||||||
info!("Using WAL journaling mode for database.");
|
info!("Using WAL journaling mode for database.");
|
||||||
} else {
|
} else {
|
||||||
warn!("Using DELETE journaling mode for database. WAL mode is recommended. (Please read the docs.)");
|
warn!("Using DELETE journaling mode for database. WAL mode is recommended.");
|
||||||
}
|
}
|
||||||
let ensure_acid = !var("ensure_acid").is_ok_and(|s| s.trim() == "False");
|
let ensure_acid = !var("ensure_acid").is_ok_and(|s| s.trim() == "False");
|
||||||
if ensure_acid {
|
if ensure_acid {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use log::info;
|
use log::{error, info};
|
||||||
use rusqlite::{fallible_iterator::FallibleIterator, Connection, Error};
|
use rusqlite::{fallible_iterator::FallibleIterator, Connection};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::services::ChhotoError::{self, ClientError, ServerError};
|
||||||
|
|
||||||
// Struct for encoding a DB row
|
// Struct for encoding a DB row
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct DBRow {
|
pub struct DBRow {
|
||||||
@@ -16,23 +18,27 @@ pub struct DBRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find a single URL for /api/expand
|
// Find a single URL for /api/expand
|
||||||
pub fn find_url(shortlink: &str, db: &Connection) -> (Option<String>, Option<i64>, Option<i64>) {
|
pub fn find_url(shortlink: &str, db: &Connection) -> Result<(String, i64, i64), ChhotoError> {
|
||||||
// Long link, hits, expiry time
|
// Long link, hits, expiry time
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
let query = "SELECT long_url, hits, expiry_time FROM urls
|
let query = "SELECT long_url, hits, expiry_time FROM urls
|
||||||
WHERE short_url = ?1
|
WHERE short_url = ?1
|
||||||
AND (expiry_time = 0 OR expiry_time > ?2)";
|
AND (expiry_time = 0 OR expiry_time > ?2)";
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached(query) else {
|
||||||
.prepare_cached(query)
|
error!("Error preparing SQL statement for find_url.");
|
||||||
.expect("Error preparing SQL statement for find_url.");
|
return Err(ServerError);
|
||||||
|
};
|
||||||
statement
|
statement
|
||||||
.query_row((shortlink, now), |row| {
|
.query_row((shortlink, now), |row| {
|
||||||
let longlink = row.get("long_url").ok();
|
Ok((
|
||||||
let hits = row.get("hits").ok();
|
row.get("long_url")?,
|
||||||
let expiry_time = row.get("expiry_time").ok();
|
row.get("hits")?,
|
||||||
Ok((longlink, hits, expiry_time))
|
row.get("expiry_time")?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.map_err(|_| ChhotoError::ClientError {
|
||||||
|
reason: "The shortlink does not exist on the server!".to_string(),
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all URLs in DB
|
// Get all URLs in DB
|
||||||
@@ -67,28 +73,26 @@ pub fn getall(
|
|||||||
FROM urls WHERE expiry_time = 0 OR expiry_time > ?1
|
FROM urls WHERE expiry_time = 0 OR expiry_time > ?1
|
||||||
ORDER BY id ASC"
|
ORDER BY id ASC"
|
||||||
};
|
};
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached(query) else {
|
||||||
.prepare_cached(query)
|
error!("Error preparing SQL statement for getall.");
|
||||||
.expect("Error preparing SQL statement for getall.");
|
return [].into();
|
||||||
|
};
|
||||||
|
|
||||||
let data = if let Some(pos) = page_after {
|
let raw_data = if let Some(pos) = page_after {
|
||||||
let size = page_size.unwrap_or(10);
|
let size = page_size.unwrap_or(10);
|
||||||
statement
|
statement.query((pos, now, size))
|
||||||
.query((pos, now, size))
|
|
||||||
.expect("Error executing query for getall: curson pagination.")
|
|
||||||
} else if let Some(num) = page_no {
|
} else if let Some(num) = page_no {
|
||||||
let size = page_size.unwrap_or(10);
|
let size = page_size.unwrap_or(10);
|
||||||
statement
|
statement.query((now, size, (num - 1) * size))
|
||||||
.query((now, size, (num - 1) * size))
|
|
||||||
.expect("Error executing query for getall: offset pagination.")
|
|
||||||
} else if let Some(size) = page_size {
|
} else if let Some(size) = page_size {
|
||||||
statement
|
statement.query((now, size))
|
||||||
.query((now, size))
|
|
||||||
.expect("Error executing query for getall: offset pagination (default).")
|
|
||||||
} else {
|
} else {
|
||||||
statement
|
statement.query([now])
|
||||||
.query([now])
|
};
|
||||||
.expect("Error executing query for getall: no pagination.")
|
|
||||||
|
let Ok(data) = raw_data else {
|
||||||
|
error!("Error running SQL statement for getall: {query}");
|
||||||
|
return [].into();
|
||||||
};
|
};
|
||||||
|
|
||||||
let links: Rc<[DBRow]> = data
|
let links: Rc<[DBRow]> = data
|
||||||
@@ -102,25 +106,29 @@ pub fn getall(
|
|||||||
Ok(row_struct)
|
Ok(row_struct)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
.expect("Error procecssing fetched row.");
|
.unwrap_or({
|
||||||
|
error!("Error processing fetched rows.");
|
||||||
|
[].into()
|
||||||
|
});
|
||||||
|
|
||||||
links
|
links
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a hit when site is visited during link resolution
|
// Add a hit when site is visited during link resolution
|
||||||
pub fn find_and_add_hit(shortlink: &str, db: &Connection) -> Option<String> {
|
pub fn find_and_add_hit(shortlink: &str, db: &Connection) -> Result<String, ()> {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached(
|
||||||
.prepare_cached(
|
"UPDATE urls
|
||||||
"UPDATE urls
|
|
||||||
SET hits = hits + 1
|
SET hits = hits + 1
|
||||||
WHERE short_url = ?1 AND (expiry_time = 0 OR expiry_time > ?2)
|
WHERE short_url = ?1 AND (expiry_time = 0 OR expiry_time > ?2)
|
||||||
RETURNING long_url",
|
RETURNING long_url",
|
||||||
)
|
) else {
|
||||||
.expect("Error preparing SQL statement for add_hit.");
|
error!("Error preparing SQL statement for add_hit.");
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
statement
|
statement
|
||||||
.query_one((shortlink, now), |row| row.get("long_url"))
|
.query_one((shortlink, now), |row| row.get("long_url"))
|
||||||
.ok()
|
.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a new link
|
// Insert a new link
|
||||||
@@ -129,7 +137,7 @@ pub fn add_link(
|
|||||||
longlink: &str,
|
longlink: &str,
|
||||||
expiry_delay: i64,
|
expiry_delay: i64,
|
||||||
db: &Connection,
|
db: &Connection,
|
||||||
) -> Option<i64> {
|
) -> Result<i64, ChhotoError> {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
let expiry_time = if expiry_delay == 0 {
|
let expiry_time = if expiry_delay == 0 {
|
||||||
0
|
0
|
||||||
@@ -137,23 +145,26 @@ pub fn add_link(
|
|||||||
now + expiry_delay
|
now + expiry_delay
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached(
|
||||||
.prepare_cached(
|
"INSERT INTO urls
|
||||||
"INSERT INTO urls
|
|
||||||
(long_url, short_url, hits, expiry_time)
|
(long_url, short_url, hits, expiry_time)
|
||||||
VALUES (?1, ?2, 0, ?3)
|
VALUES (?1, ?2, 0, ?3)
|
||||||
ON CONFLICT(short_url) DO UPDATE
|
ON CONFLICT(short_url) DO UPDATE
|
||||||
SET long_url = ?1, hits = 0, expiry_time = ?3
|
SET long_url = ?1, hits = 0, expiry_time = ?3
|
||||||
WHERE short_url = ?2 AND expiry_time <= ?4 AND expiry_time > 0",
|
WHERE short_url = ?2 AND expiry_time <= ?4 AND expiry_time > 0",
|
||||||
)
|
) else {
|
||||||
.expect("Error preparing SQL statement for add_link.");
|
error!("Error preparing SQL statement for add_link.");
|
||||||
let delta = statement
|
return Err(ServerError);
|
||||||
.execute((longlink, shortlink, expiry_time, now))
|
};
|
||||||
.expect("There was an unexpected error while inserting link.");
|
match statement.execute((longlink, shortlink, expiry_time, now)) {
|
||||||
if delta == 1 {
|
Ok(1) => Ok(expiry_time),
|
||||||
Some(expiry_time)
|
Ok(_) => Err(ClientError {
|
||||||
} else {
|
reason: "Short URL is already in use!".to_string(),
|
||||||
None
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
error!("There was some error while adding the link ({shortlink}, {longlink}, {expiry_delay}): {e}");
|
||||||
|
Err(ServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +174,7 @@ pub fn edit_link(
|
|||||||
longlink: &str,
|
longlink: &str,
|
||||||
reset_hits: bool,
|
reset_hits: bool,
|
||||||
db: &Connection,
|
db: &Connection,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, ()> {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
let query = if reset_hits {
|
let query = if reset_hits {
|
||||||
"UPDATE urls
|
"UPDATE urls
|
||||||
@@ -174,14 +185,23 @@ pub fn edit_link(
|
|||||||
SET long_url = ?1
|
SET long_url = ?1
|
||||||
WHERE short_url = ?2 AND (expiry_time = 0 OR expiry_time > ?3)"
|
WHERE short_url = ?2 AND (expiry_time = 0 OR expiry_time > ?3)"
|
||||||
};
|
};
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached(query) else {
|
||||||
.prepare_cached(query)
|
error!("Error preparing SQL statement for edit_link.");
|
||||||
.expect("Error preparing SQL statement for edit_link.");
|
return Err(());
|
||||||
statement.execute((longlink, shortlink, now))
|
};
|
||||||
|
|
||||||
|
statement
|
||||||
|
.execute((longlink, shortlink, now))
|
||||||
|
.inspect_err(|err| {
|
||||||
|
error!(
|
||||||
|
"Got an error while editing link ({shortlink}, {longlink}, {reset_hits}): {err}"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean expired links
|
// Clean expired links
|
||||||
pub fn cleanup(db: &Connection) {
|
pub fn cleanup(db: &Connection, use_wal_mode: bool) {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let now = chrono::Utc::now().timestamp();
|
||||||
info!("Starting database cleanup.");
|
info!("Starting database cleanup.");
|
||||||
|
|
||||||
@@ -197,24 +217,36 @@ pub fn cleanup(db: &Connection) {
|
|||||||
})
|
})
|
||||||
.expect("Error cleaning expired links.");
|
.expect("Error cleaning expired links.");
|
||||||
|
|
||||||
|
if use_wal_mode {
|
||||||
|
let mut pragma_statement = db
|
||||||
|
.prepare_cached("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
.expect("Error preparing SQL statement for pragma: wal_checkpoint.");
|
||||||
|
pragma_statement
|
||||||
|
.query_one([], |row| row.get::<usize, isize>(1))
|
||||||
|
.ok()
|
||||||
|
.filter(|&v| v != -1)
|
||||||
|
.expect("Unable to create WAL checkpoint.");
|
||||||
|
}
|
||||||
let mut pragma_statement = db
|
let mut pragma_statement = db
|
||||||
.prepare_cached("PRAGMA optimize")
|
.prepare_cached("PRAGMA optimize")
|
||||||
.expect("Error preparing SQL statement for pragma optimize.");
|
.expect("Error preparing SQL statement for pragma: optimize.");
|
||||||
pragma_statement
|
pragma_statement
|
||||||
.execute([])
|
.execute([])
|
||||||
.expect("Unable to optimize database");
|
.expect("Unable to optimize database.");
|
||||||
info!("Optimized database.")
|
info!("Optimized database.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete an existing link
|
// Delete an existing link
|
||||||
pub fn delete_link(shortlink: &str, db: &Connection) -> bool {
|
pub fn delete_link(shortlink: &str, db: &Connection) -> Result<(), ChhotoError> {
|
||||||
let mut statement = db
|
let Ok(mut statement) = db.prepare_cached("DELETE FROM urls WHERE short_url = ?1") else {
|
||||||
.prepare_cached("DELETE FROM urls WHERE short_url = ?1")
|
error!("Error preparing SQL statement for delete_link.");
|
||||||
.expect("Error preparing SQL statement for delete_link.");
|
return Err(ServerError);
|
||||||
if let Ok(delta) = statement.execute([shortlink]) {
|
};
|
||||||
delta > 0
|
match statement.execute([shortlink]) {
|
||||||
} else {
|
Ok(delta) if delta > 0 => Ok(()),
|
||||||
false
|
_ => Err(ClientError {
|
||||||
|
reason: "The shortlink was not found, and could not be deleted.".to_string(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut interval = time::interval(time::Duration::from_secs(3600));
|
let mut interval = time::interval(time::Duration::from_secs(3600));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
database::cleanup(&db);
|
database::cleanup(&db, conf.use_wal_mode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,18 @@ use std::env;
|
|||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::{auth, database};
|
use crate::{auth, database};
|
||||||
use crate::{auth::validate, utils};
|
use crate::{auth::is_session_valid, utils};
|
||||||
|
use ChhotoError::{ClientError, ServerError};
|
||||||
|
|
||||||
// Store the version number
|
// Store the version number
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
pub enum ChhotoError {
|
||||||
|
ServerError,
|
||||||
|
ClientError { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
// Define JSON struct for returning success/error data
|
// Define JSON struct for returning success/error data
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Response {
|
struct Response {
|
||||||
@@ -82,53 +89,64 @@ pub async fn add_link(
|
|||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
// Call is_api_ok() function, pass HttpRequest
|
// Call is_api_ok() function, pass HttpRequest
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
// If success, add new link
|
// If success, add new link
|
||||||
if result.success {
|
if result.success {
|
||||||
let (success, reply, expiry_time) = utils::add_link(&req, &data.db, config, false);
|
match utils::add_link(&req, &data.db, config, false) {
|
||||||
if success {
|
Ok((shorturl, expiry_time)) => {
|
||||||
let site_url = config.site_url.clone();
|
let site_url = config.site_url.clone();
|
||||||
let shorturl = if let Some(url) = site_url {
|
let shorturl = if let Some(url) = site_url {
|
||||||
format!("{url}/{reply}")
|
format!("{url}/{shorturl}")
|
||||||
} else {
|
|
||||||
let protocol = if config.port == 443 { "https" } else { "http" };
|
|
||||||
let port_text = if [80, 443].contains(&config.port) {
|
|
||||||
String::new()
|
|
||||||
} else {
|
} else {
|
||||||
format!(":{}", config.port)
|
let protocol = if config.port == 443 { "https" } else { "http" };
|
||||||
|
let port_text = if [80, 443].contains(&config.port) {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(":{}", config.port)
|
||||||
|
};
|
||||||
|
format!("{protocol}://localhost{port_text}/{shorturl}")
|
||||||
};
|
};
|
||||||
format!("{protocol}://localhost{port_text}/{reply}")
|
let response = CreatedURL {
|
||||||
};
|
success: true,
|
||||||
let response = CreatedURL {
|
error: false,
|
||||||
success: true,
|
shorturl,
|
||||||
error: false,
|
expiry_time,
|
||||||
shorturl,
|
};
|
||||||
expiry_time,
|
HttpResponse::Created().json(response)
|
||||||
};
|
}
|
||||||
HttpResponse::Created().json(response)
|
Err(ServerError) => {
|
||||||
} else {
|
let response = Response {
|
||||||
let response = Response {
|
success: false,
|
||||||
success: false,
|
error: true,
|
||||||
error: true,
|
reason: "Something went wrong when adding the link.".to_string(),
|
||||||
reason: reply,
|
};
|
||||||
};
|
HttpResponse::InternalServerError().json(response)
|
||||||
HttpResponse::Conflict().json(response)
|
}
|
||||||
|
Err(ClientError { reason }) => {
|
||||||
|
let response = Response {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
HttpResponse::Conflict().json(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if result.error {
|
} else if result.error {
|
||||||
HttpResponse::Unauthorized().json(result)
|
HttpResponse::Unauthorized().json(result)
|
||||||
// If password authentication or public mode is used - keeps backwards compatibility
|
// If password authentication or public mode is used - keeps backwards compatibility
|
||||||
} else {
|
} else {
|
||||||
let (success, reply, _) = if auth::validate(session, config) {
|
let result = if auth::is_session_valid(session, config) {
|
||||||
utils::add_link(&req, &data.db, config, false)
|
utils::add_link(&req, &data.db, config, false)
|
||||||
} else if config.public_mode {
|
} else if config.public_mode {
|
||||||
utils::add_link(&req, &data.db, config, true)
|
utils::add_link(&req, &data.db, config, true)
|
||||||
} else {
|
} else {
|
||||||
return HttpResponse::Unauthorized().body("Not logged in!");
|
return HttpResponse::Unauthorized().body("Not logged in!");
|
||||||
};
|
};
|
||||||
if success {
|
match result {
|
||||||
HttpResponse::Created().body(reply)
|
Ok((shorturl, _)) => HttpResponse::Created().body(shorturl),
|
||||||
} else {
|
Err(ServerError) => HttpResponse::InternalServerError()
|
||||||
HttpResponse::Conflict().body(reply)
|
.body("Something went wrong when adding the link.".to_string()),
|
||||||
|
Err(ClientError { reason }) => HttpResponse::Conflict().body(reason),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,14 +161,14 @@ pub async fn getall(
|
|||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
// Call is_api_ok() function, pass HttpRequest
|
// Call is_api_ok() function, pass HttpRequest
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
// If success, return all links
|
// If success, return all links
|
||||||
if result.success {
|
if result.success {
|
||||||
HttpResponse::Ok().body(utils::getall(&data.db, params.into_inner()))
|
HttpResponse::Ok().body(utils::getall(&data.db, params.into_inner()))
|
||||||
} else if result.error {
|
} else if result.error {
|
||||||
HttpResponse::Unauthorized().json(result)
|
HttpResponse::Unauthorized().json(result)
|
||||||
// If password authentication is used - keeps backwards compatibility
|
// If password authentication is used - keeps backwards compatibility
|
||||||
} else if auth::validate(session, config) {
|
} else if auth::is_session_valid(session, config) {
|
||||||
HttpResponse::Ok().body(utils::getall(&data.db, params.into_inner()))
|
HttpResponse::Ok().body(utils::getall(&data.db, params.into_inner()))
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Unauthorized().body("Not logged in!")
|
HttpResponse::Unauthorized().body("Not logged in!")
|
||||||
@@ -160,26 +178,35 @@ pub async fn getall(
|
|||||||
// Get information about a single shortlink
|
// Get information about a single shortlink
|
||||||
#[post("/api/expand")]
|
#[post("/api/expand")]
|
||||||
pub async fn expand(req: String, data: web::Data<AppState>, http: HttpRequest) -> HttpResponse {
|
pub async fn expand(req: String, data: web::Data<AppState>, http: HttpRequest) -> HttpResponse {
|
||||||
let result = utils::is_api_ok(http, &data.config);
|
let result = auth::is_api_ok(http, &data.config);
|
||||||
if result.success {
|
if result.success {
|
||||||
let (longurl, hits, expiry_time) = database::find_url(&req, &data.db);
|
match database::find_url(&req, &data.db) {
|
||||||
if let Some(longlink) = longurl {
|
Ok((longurl, hits, expiry_time)) => {
|
||||||
let body = LinkInfo {
|
let body = LinkInfo {
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
longurl: longlink,
|
longurl,
|
||||||
hits: hits.expect("Error getting hit count for existing shortlink."),
|
hits,
|
||||||
expiry_time: expiry_time
|
expiry_time,
|
||||||
.expect("Error getting expiry time for existing shortlink."),
|
};
|
||||||
};
|
HttpResponse::Ok().json(body)
|
||||||
HttpResponse::Ok().json(body)
|
}
|
||||||
} else {
|
Err(ServerError) => {
|
||||||
let body = Response {
|
let body = Response {
|
||||||
success: false,
|
success: false,
|
||||||
error: true,
|
error: true,
|
||||||
reason: "The shortlink does not exist on the server.".to_string(),
|
reason: "Something went wrong when finding the link.".to_string(),
|
||||||
};
|
};
|
||||||
HttpResponse::BadRequest().json(body)
|
HttpResponse::BadRequest().json(body)
|
||||||
|
}
|
||||||
|
Err(ClientError { reason }) => {
|
||||||
|
let body = Response {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
HttpResponse::BadRequest().json(body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Unauthorized().json(result)
|
HttpResponse::Unauthorized().json(result)
|
||||||
@@ -195,26 +222,33 @@ pub async fn edit_link(
|
|||||||
http: HttpRequest,
|
http: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
if result.success || validate(session, config) {
|
if result.success || is_session_valid(session, config) {
|
||||||
if let Some((server_error, error_msg)) = utils::edit_link(&req, &data.db, config) {
|
match utils::edit_link(&req, &data.db, config) {
|
||||||
let body = Response {
|
Ok(()) => {
|
||||||
success: false,
|
let body = Response {
|
||||||
error: true,
|
success: true,
|
||||||
reason: error_msg,
|
error: false,
|
||||||
};
|
reason: String::from("Edit was successful."),
|
||||||
if server_error {
|
};
|
||||||
|
HttpResponse::Created().json(body)
|
||||||
|
}
|
||||||
|
Err(ServerError) => {
|
||||||
|
let body = Response {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason: "Something went wrong when editing the link.".to_string(),
|
||||||
|
};
|
||||||
HttpResponse::InternalServerError().json(body)
|
HttpResponse::InternalServerError().json(body)
|
||||||
} else {
|
}
|
||||||
|
Err(ClientError { reason }) => {
|
||||||
|
let body = Response {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
HttpResponse::BadRequest().json(body)
|
HttpResponse::BadRequest().json(body)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let body = Response {
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
reason: String::from("Edit was successful."),
|
|
||||||
};
|
|
||||||
HttpResponse::Created().json(body)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Unauthorized().json(result)
|
HttpResponse::Unauthorized().json(result)
|
||||||
@@ -249,8 +283,8 @@ pub async fn whoami(
|
|||||||
http: HttpRequest,
|
http: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
let acting_user = if result.success || validate(session, config) {
|
let acting_user = if result.success || is_session_valid(session, config) {
|
||||||
"admin"
|
"admin"
|
||||||
} else if config.public_mode {
|
} else if config.public_mode {
|
||||||
"public"
|
"public"
|
||||||
@@ -268,8 +302,8 @@ pub async fn getconfig(
|
|||||||
http: HttpRequest,
|
http: HttpRequest,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
if result.success || validate(session, config) || data.config.public_mode {
|
if result.success || is_session_valid(session, config) || data.config.public_mode {
|
||||||
let backend_config = BackendConfig {
|
let backend_config = BackendConfig {
|
||||||
version: VERSION.to_string(),
|
version: VERSION.to_string(),
|
||||||
allow_capital_letters: config.allow_capital_letters,
|
allow_capital_letters: config.allow_capital_letters,
|
||||||
@@ -301,7 +335,7 @@ pub async fn link_handler(
|
|||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let shortlink_str = shortlink.as_str();
|
let shortlink_str = shortlink.as_str();
|
||||||
if let Some(longlink) = database::find_and_add_hit(shortlink_str, &data.db) {
|
if let Ok(longlink) = database::find_and_add_hit(shortlink_str, &data.db) {
|
||||||
if data.config.use_temp_redirect {
|
if data.config.use_temp_redirect {
|
||||||
Either::Left(Redirect::to(longlink))
|
Either::Left(Redirect::to(longlink))
|
||||||
} else {
|
} else {
|
||||||
@@ -403,29 +437,40 @@ pub async fn delete_link(
|
|||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let config = &data.config;
|
let config = &data.config;
|
||||||
// Call is_api_ok() function, pass HttpRequest
|
// Call is_api_ok() function, pass HttpRequest
|
||||||
let result = utils::is_api_ok(http, config);
|
let result = auth::is_api_ok(http, config);
|
||||||
// If success, delete shortlink
|
// If success, delete shortlink
|
||||||
if result.success {
|
if result.success {
|
||||||
if utils::delete_link(&shortlink, &data.db, data.config.allow_capital_letters) {
|
match utils::delete_link(&shortlink, &data.db, data.config.allow_capital_letters) {
|
||||||
let response = Response {
|
Ok(()) => {
|
||||||
success: true,
|
let response = Response {
|
||||||
error: false,
|
success: true,
|
||||||
reason: format!("Deleted {shortlink}"),
|
error: false,
|
||||||
};
|
reason: format!("Deleted {shortlink}"),
|
||||||
HttpResponse::Ok().json(response)
|
};
|
||||||
} else {
|
HttpResponse::Ok().json(response)
|
||||||
let response = Response {
|
}
|
||||||
success: false,
|
Err(ServerError) => {
|
||||||
error: true,
|
let response = Response {
|
||||||
reason: "The short link was not found, and could not be deleted.".to_string(),
|
success: false,
|
||||||
};
|
error: true,
|
||||||
HttpResponse::NotFound().json(response)
|
reason: "Something went wrong when deleting the link.".to_string(),
|
||||||
|
};
|
||||||
|
HttpResponse::InternalServerError().json(response)
|
||||||
|
}
|
||||||
|
Err(ClientError { reason }) => {
|
||||||
|
let response = Response {
|
||||||
|
success: false,
|
||||||
|
error: true,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
HttpResponse::NotFound().json(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if result.error {
|
} else if result.error {
|
||||||
HttpResponse::Unauthorized().json(result)
|
HttpResponse::Unauthorized().json(result)
|
||||||
// If "pass" is true - keeps backwards compatibility
|
// If using password - keeps backwards compatibility
|
||||||
} else if auth::validate(session, config) {
|
} else if auth::is_session_valid(session, config) {
|
||||||
if utils::delete_link(&shortlink, &data.db, data.config.allow_capital_letters) {
|
if utils::delete_link(&shortlink, &data.db, data.config.allow_capital_letters).is_ok() {
|
||||||
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::NotFound().body("Not found!")
|
HttpResponse::NotFound().body("Not found!")
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use actix_web::HttpRequest;
|
use log::error;
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{auth, config::Config, database, services::GetReqParams};
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
database,
|
||||||
|
services::{
|
||||||
|
ChhotoError::{self, ClientError, ServerError},
|
||||||
|
GetReqParams,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Struct for reading link pairs sent during API call for new link
|
// Struct for reading link pairs sent during API call for new link
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -28,70 +35,8 @@ struct EditURLRequest {
|
|||||||
reset_hits: bool,
|
reset_hits: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define JSON struct for error response
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub(crate) success: bool,
|
|
||||||
pub(crate) error: bool,
|
|
||||||
reason: String,
|
|
||||||
pass: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the api_key environment variable exists
|
|
||||||
pub fn is_api_ok(http: HttpRequest, config: &Config) -> Response {
|
|
||||||
// If the api_key environment variable exists
|
|
||||||
if config.api_key.is_some() {
|
|
||||||
// If the header exists
|
|
||||||
if let Some(header) = auth::api_header(&http) {
|
|
||||||
// If the header is correct
|
|
||||||
if auth::validate_key(header, config) {
|
|
||||||
Response {
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
reason: "Correct API key".to_string(),
|
|
||||||
pass: false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Response {
|
|
||||||
success: false,
|
|
||||||
error: true,
|
|
||||||
reason: "Incorrect API key".to_string(),
|
|
||||||
pass: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The header may not exist when the user logs in through the web interface, so allow a request with no header.
|
|
||||||
// Further authentication checks will be conducted in services.rs
|
|
||||||
} else {
|
|
||||||
// Due to the implementation of this result in services.rs, this JSON object will not be outputted.
|
|
||||||
Response {
|
|
||||||
success: false,
|
|
||||||
error: false,
|
|
||||||
reason: "No valid authentication was found".to_string(),
|
|
||||||
pass: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the API key isn't set, but an API Key header is provided
|
|
||||||
if auth::api_header(&http).is_some() {
|
|
||||||
Response {
|
|
||||||
success: false,
|
|
||||||
error: true,
|
|
||||||
reason: "An API key was provided, but the 'api_key' environment variable is not configured in the Chhoto URL instance".to_string(),
|
|
||||||
pass: false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Response {
|
|
||||||
success: false,
|
|
||||||
error: false,
|
|
||||||
reason: "".to_string(),
|
|
||||||
pass: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only have a-z, 0-9, - and _ as valid characters in a shortlink
|
// Only have a-z, 0-9, - and _ as valid characters in a shortlink
|
||||||
fn validate_link(link: &str, allow_capital_letters: bool) -> bool {
|
fn is_link_valid(link: &str, allow_capital_letters: bool) -> bool {
|
||||||
let re = if allow_capital_letters {
|
let re = if allow_capital_letters {
|
||||||
Regex::new("^[A-Za-z0-9-_]+$").expect("Regex generation failed.")
|
Regex::new("^[A-Za-z0-9-_]+$").expect("Regex generation failed.")
|
||||||
} else {
|
} else {
|
||||||
@@ -115,13 +60,15 @@ pub fn add_link(
|
|||||||
db: &Connection,
|
db: &Connection,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
using_public_mode: bool,
|
using_public_mode: bool,
|
||||||
) -> (bool, String, i64) {
|
) -> Result<(String, i64), ChhotoError> {
|
||||||
// Success status, response string, expiry time
|
// Ok : shortlink, expiry_time
|
||||||
let mut chunks: NewURLRequest;
|
let mut chunks: NewURLRequest;
|
||||||
if let Ok(json) = serde_json::from_str(req) {
|
if let Ok(json) = serde_json::from_str(req) {
|
||||||
chunks = json;
|
chunks = json;
|
||||||
} else {
|
} else {
|
||||||
return (false, String::from("Invalid request!"), 0);
|
return Err(ClientError {
|
||||||
|
reason: "Invalid request!".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = &config.slug_style;
|
let style = &config.slug_style;
|
||||||
@@ -147,65 +94,75 @@ pub fn add_link(
|
|||||||
chunks.expiry_delay = chunks.expiry_delay.min(157784760);
|
chunks.expiry_delay = chunks.expiry_delay.min(157784760);
|
||||||
chunks.expiry_delay = chunks.expiry_delay.max(0);
|
chunks.expiry_delay = chunks.expiry_delay.max(0);
|
||||||
|
|
||||||
if validate_link(chunks.shortlink.as_str(), allow_capital_letters) {
|
if is_link_valid(chunks.shortlink.as_str(), allow_capital_letters) {
|
||||||
if let Some(expiry_time) =
|
match database::add_link(&chunks.shortlink, &chunks.longlink, chunks.expiry_delay, db) {
|
||||||
database::add_link(&chunks.shortlink, &chunks.longlink, chunks.expiry_delay, db)
|
Ok(expiry_time) => Ok((chunks.shortlink, expiry_time)),
|
||||||
{
|
Err(ClientError { reason }) => {
|
||||||
(true, chunks.shortlink, expiry_time)
|
if shortlink_provided {
|
||||||
} else if shortlink_provided {
|
Err(ClientError { reason })
|
||||||
(false, String::from("Short URL is already in use!"), 0)
|
} else if config.slug_style == "UID" && config.try_longer_slug {
|
||||||
} else if config.slug_style == "UID" && config.try_longer_slug {
|
// Optionally, retry with a longer slug length
|
||||||
// Optionally, retry with a longer slug length
|
chunks.shortlink = gen_link(style, len + 4, allow_capital_letters);
|
||||||
chunks.shortlink = gen_link(style, len + 4, allow_capital_letters);
|
match database::add_link(
|
||||||
if let Some(expiry_time) =
|
&chunks.shortlink,
|
||||||
database::add_link(&chunks.shortlink, &chunks.longlink, chunks.expiry_delay, db)
|
&chunks.longlink,
|
||||||
{
|
chunks.expiry_delay,
|
||||||
(true, chunks.shortlink, expiry_time)
|
db,
|
||||||
} else {
|
) {
|
||||||
(false, String::from("Something went very wrong!"), 0)
|
Ok(expiry_time) => Ok((chunks.shortlink, expiry_time)),
|
||||||
|
Err(_) => Err(ServerError),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Something went wrong while adding a link: {reason}");
|
||||||
|
Err(ServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
Err(ServerError) => Err(ServerError),
|
||||||
(false, String::from("Something went wrong!"), 0)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(false, String::from("Short URL is not valid!"), 0)
|
Err(ClientError {
|
||||||
|
reason: "Short URL is not valid!".to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make checks and then request the DB to edit an URL entry
|
// Make checks and then request the DB to edit an URL entry
|
||||||
pub fn edit_link(req: &str, db: &Connection, config: &Config) -> Option<(bool, String)> {
|
pub fn edit_link(req: &str, db: &Connection, config: &Config) -> Result<(), ChhotoError> {
|
||||||
// None means success
|
|
||||||
// The boolean is true when it's a server error and false when it's a client error
|
|
||||||
// The string is the error message
|
|
||||||
|
|
||||||
let chunks: EditURLRequest;
|
let chunks: EditURLRequest;
|
||||||
if let Ok(json) = serde_json::from_str(req) {
|
if let Ok(json) = serde_json::from_str(req) {
|
||||||
chunks = json;
|
chunks = json;
|
||||||
} else {
|
} else {
|
||||||
return Some((false, String::from("Malformed request!")));
|
return Err(ClientError {
|
||||||
|
reason: "Malformed request!".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if !validate_link(&chunks.shortlink, config.allow_capital_letters) {
|
if !is_link_valid(&chunks.shortlink, config.allow_capital_letters) {
|
||||||
return Some((false, String::from("Invalid shortlink!")));
|
return Err(ClientError {
|
||||||
|
reason: "Invalid shortlink!".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let result = database::edit_link(&chunks.shortlink, &chunks.longlink, chunks.reset_hits, db);
|
let result = database::edit_link(&chunks.shortlink, &chunks.longlink, chunks.reset_hits, db);
|
||||||
if Ok(0) == result {
|
match result {
|
||||||
// Zero rows returned means no updates
|
// Zero rows returned means no updates
|
||||||
Some((
|
Ok(0) => Err(ClientError {
|
||||||
false,
|
reason: "The shortlink was not found, and could not be edited.".to_string(),
|
||||||
"The short link was not found, and could not be edited.".to_string(),
|
}),
|
||||||
))
|
Ok(_) => Ok(()),
|
||||||
} else if result.is_ok() {
|
Err(()) => Err(ServerError),
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((true, String::from("Something went wrong!"))) // Should not really happen
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if link, and request DB to delete it if exists
|
// Check if link, and request DB to delete it if exists
|
||||||
pub fn delete_link(shortlink: &str, db: &Connection, allow_capital_letters: bool) -> bool {
|
pub fn delete_link(
|
||||||
if validate_link(shortlink, allow_capital_letters) {
|
shortlink: &str,
|
||||||
|
db: &Connection,
|
||||||
|
allow_capital_letters: bool,
|
||||||
|
) -> Result<(), ChhotoError> {
|
||||||
|
if is_link_valid(shortlink, allow_capital_letters) {
|
||||||
database::delete_link(shortlink, db)
|
database::delete_link(shortlink, db)
|
||||||
} else {
|
} else {
|
||||||
false
|
Err(ClientError {
|
||||||
|
reason: "The shortlink is invalid.".to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 Sayantan Santra <sayantan.santra689@gmail.com>
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
# chhoto-url.container
|
# chhoto-url.container
|
||||||
|
#
|
||||||
# To be used with rootless quadlets. Put inside your $XDG_CONFIG_HOME/containers/systemd/
|
# To be used with rootless quadlets. Put inside your $XDG_CONFIG_HOME/containers/systemd/
|
||||||
# Take a look at README for the explanation of the configs.
|
# Take a look at README for the explanation of the configs.
|
||||||
# The commented out configs are optional.
|
# The commented out configs are optional.
|
||||||
|
|||||||
Reference in New Issue
Block a user