Skip to content

Commit

Permalink
add in-memory shortlink logic
Browse files Browse the repository at this point in the history
  • Loading branch information
glendc committed Oct 23, 2023
1 parent 9ae7b0f commit 85b880e
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 8 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ askama_axum = "0.3"
axum = "0.6"
base64 = "0.21"
chrono = "0.4"
nanoid = "0.4"
orion = "0.17"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ using a codebase written in Rust and make use of dependencies such as
In case you have furher questions you can ping `@glendc` at
[Shuttle's Discord](https://discord.gg/YDHm6Yz3).

## Work In Progress

This project is not yet finished. Use at your own risk.

Developer todos:

- split auth:
- create magic module: MagicIdentity, MagicSender
- also store email as lower in MagicIdentity
- provide proper Identity in data.rs: basically a hash from email, this is also used by MagicIdentity
- create secret logic (dirty)
- list created links by user
- allow created links by user to be delete
- allow secrets to be deleted
- switch storage for real storage
- import blocklists for all kind of nasty domains which we want to avoid
- add l18n support using `i18n-embed-fl` and `accept-language` crates (for now only english, dutch and spanish support);
- add support for all known languages possible;
- move allowed_email_filters to db storage;
- support invites for users as long as we have less then 50;

## Contributing

🎈 Thanks for your help improving the project! We are so happy to have
Expand Down
2 changes: 2 additions & 0 deletions src/data/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod shortlink;
pub use shortlink::Shortlink;
33 changes: 33 additions & 0 deletions src/data/shortlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#[derive(Debug, Clone)]
pub struct Shortlink {
id: String,
long_url: String,
_owner: String,
}

impl Shortlink {
pub fn new(long_url: String, owner: String) -> Self {
let id = nanoid::nanoid!(8);
Self {
id,
long_url,
_owner: owner,
}
}

pub fn _owner(&self) -> &str {
&self._owner
}

pub fn long_url(&self) -> &str {
&self.long_url
}

pub fn id(&self) -> &str {
&self.id
}

pub fn short_url(&self) -> String {
format!("/{}", self.id,)
}
}
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::sync::Arc;

use shuttle_secrets::SecretStore;

mod data;
mod router;
mod services;

Expand All @@ -13,7 +14,9 @@ async fn axum(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> shuttle_
secret_store.get("SENDGRID_API_KEY").unwrap(),
));

let state = router::State { auth };
let storage = services::Storage::new();

let state = router::State { auth, storage };
let router = router::new(state);

tracing::debug!("starting axum router");
Expand Down
67 changes: 64 additions & 3 deletions src/router/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use axum::{extract::State, http::StatusCode, response::Redirect, Form};
use serde::Deserialize;
use tower_cookies::Cookies;

use crate::data::Shortlink;

#[derive(Template)]
#[template(path = "../templates/content/link.html")]
pub struct GetTemplate {
Expand Down Expand Up @@ -104,12 +106,71 @@ pub async fn post(
.into_response();
}

// TODO validate domain
// validate domains
let domain = match url.domain() {
Some(domain) => domain,
None => {
return (
StatusCode::BAD_REQUEST,
super::shared::ErrorTemplate {
title: "Invalid Long URL".to_string(),
message: "The long URL is invalid. No domain found.".to_string(),
back_path: format!("/link?long={}", long),
},
)
.into_response();
}
};
// ...only allow second level domains or higher
if domain.split('.').count() < 2 {
return (
StatusCode::BAD_REQUEST,
super::shared::ErrorTemplate {
title: "Invalid Long URL".to_string(),
message: "The long URL is invalid. Bare top level domains are not allowed."
.to_string(),
back_path: format!("/link?long={}", long),
},
)
.into_response();
}
// ...only allow domains that are not blocked
if state.storage.is_domain_blocked(domain).await {
return (
StatusCode::BAD_REQUEST,
super::shared::ErrorTemplate {
title: "Invalid Long URL".to_string(),
message: "The long URL is invalid. The domain is blocked.".to_string(),
back_path: format!("/link?long={}", long),
},
)
.into_response();
}

// create shortlink
let shortlink = Shortlink::new(url.to_string(), email.clone());

// store shortlink
if let Err(err) = state.storage.add_shortlink(&shortlink).await {
tracing::error!("Failed to store shortlink for long url {}: {}", long, err);
return (
StatusCode::INTERNAL_SERVER_ERROR,
super::shared::ErrorTemplate {
title: "Failed to store shortlink".to_string(),
message: format!(
"Failed to store shortlink for long url '{}'. Please try again later.",
long
),
back_path: format!("/link?long={}", long),
},
)
.into_response();
};

return PostOkTemplate {
email,
long,
short: "example.com".to_string(),
long: shortlink.long_url().to_string(),
short: shortlink.short_url(),
}
.into_response();
}
Expand Down
1 change: 1 addition & 0 deletions src/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod shared;
#[derive(Debug, Clone)]
pub struct State {
pub auth: Arc<crate::services::Auth>,
pub storage: crate::services::Storage,
}

fn new_root(state: State) -> Router {
Expand Down
20 changes: 17 additions & 3 deletions src/router/redirect.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
use axum::{extract::Path, response::Redirect};
use std::sync::Arc;

pub async fn get(Path(hash): Path<String>) -> Redirect {
use axum::{
extract::{Path, State},
response::Redirect,
};

pub async fn get(
State(state): State<Arc<crate::router::State>>,
Path(hash): Path<String>,
) -> Redirect {
match hash.as_str() {
"code" => Redirect::permanent("https://github.com/plabayo/bucket"),
"author" => Redirect::permanent("https://plabayo.tech"),
"og-image" => Redirect::permanent(
"https://upload.wikimedia.org/wikipedia/commons/3/3b/Sand_bucket.jpg",
),
hash => Redirect::temporary(&format!("https://{hash}")),
hash => {
if let Some(link) = state.storage.get_shortlink(hash).await {
Redirect::temporary(link.long_url())
} else {
Redirect::temporary("/")
}
}
}
}
3 changes: 3 additions & 0 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
mod auth;
pub use auth::{Auth, COOKIE_NAME};

mod storage;
pub use storage::Storage;
55 changes: 55 additions & 0 deletions src/services/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};

use crate::data::Shortlink;

#[derive(Debug)]
pub struct Storage {
inner: Arc<Mutex<InnerStorage>>,
}

impl Clone for Storage {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}

#[derive(Debug)]
pub struct InnerStorage {
shortlinks: HashMap<String, Shortlink>,
blocked_domains: HashSet<String>,
}

impl Storage {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(InnerStorage {
shortlinks: HashMap::new(),
blocked_domains: HashSet::new(),
})),
}
}

pub async fn is_domain_blocked(&self, domain: &str) -> bool {
self.inner.lock().unwrap().blocked_domains.contains(domain)
}

pub async fn add_shortlink(&self, shortlink: &Shortlink) -> Result<(), String> {
let id = shortlink.id().to_string();
self.inner
.lock()
.unwrap()
.shortlinks
.entry(id)
.or_insert(shortlink.clone());
Ok(())
}

pub async fn get_shortlink(&self, id: &str) -> Option<Shortlink> {
self.inner.lock().unwrap().shortlinks.get(id).cloned()
}
}
2 changes: 1 addition & 1 deletion templates/content/link_ok.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="box info" style="width: 100%">
<strong class="block titlebar">🔗 Link Created</strong>
<p>
<a href="{{ long }}">{{ long }}</a> can be found shortened
<a href="{{ long }}" style="overflow-wrap: anywhere;">{{ long }}</a> can be found shortened
as <a href="{{ short }}" hx-boost="false">{{ short }}</a>.
</p>
<p>
Expand Down

0 comments on commit 85b880e

Please sign in to comment.