diff --git a/Cargo.lock b/Cargo.lock index 10c1601..1537f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,7 +538,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.52", @@ -845,7 +845,7 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.52", @@ -1567,6 +1567,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2015,6 +2021,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -2043,6 +2063,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2069,6 +2098,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -2192,7 +2233,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -2806,7 +2847,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -2864,7 +2905,7 @@ version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be16d30795cc707c355d1c0ba704db085d5bd507a858509a0d784189b8fe31b" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "sea-bae", @@ -2929,7 +2970,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.52", @@ -2953,7 +2994,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f686050f76bffc4f635cda8aea6df5548666b830b52387e8bc7de11056d11e" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -3108,10 +3149,12 @@ dependencies = [ "dotenvy", "entity", "envy", - "heck", + "heck 0.5.0", + "ipnetwork", "itertools", "lazy_static", "migration", + "num", "once_cell", "password-auth", "proctitle", @@ -3320,7 +3363,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 44b9bae..9905c6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,9 +63,11 @@ clap = { version = "4.5.2", features = ["cargo", "derive", "unicode"] } csrf = "0.4.1" dotenvy = "0.15.7" envy = "0.4.2" -heck = { version = "0.4.1", features = ["unicode"] } +heck = "0.5.0" +ipnetwork = "0.20.0" itertools = "0.12.1" lazy_static = "1.4.0" +num = "0.4.1" once_cell = { version = "1.19.0", features = ["parking_lot"] } password-auth = "1.0.0" proctitle = "0.1.1" @@ -77,7 +79,7 @@ sea-orm = { version = "1.0.0-rc.1", features = ["macros", "runtime-tokio-native- sea-query = { version = "0.31.0-rc.4", features = ["thread-safe", "with-time"] } serde = { version = "1.0.197", features = ["derive"] } thiserror = "1.0.58" -time = "0.3.34" +time = { version = "0.3.34", features = ["local-offset"] } tokio = { version = "1.36.0", features = ["macros", "parking_lot", "rt-multi-thread", "signal", "time"] } tracing = { version = "0.1.40", features = ["async-await", "log"] } tracing-subscriber = { version = "0.3.18", features = ["local-time", "parking_lot", "time"] } diff --git a/entity/src/cidr_ban.rs b/entity/src/cidr_ban.rs index fbe64b6..6035e4e 100644 --- a/entity/src/cidr_ban.rs +++ b/entity/src/cidr_ban.rs @@ -12,7 +12,7 @@ pub struct Model { #[sea_orm(column_type = "Binary(16)", unique)] pub range_end: Vec, pub reason: Option, - pub created_at: DateTime, + pub created_at: TimeDateTimeWithTimeZone, pub user_created_id: Option, } diff --git a/entity/src/url.rs b/entity/src/url.rs index 21b7fe0..6f46653 100644 --- a/entity/src/url.rs +++ b/entity/src/url.rs @@ -9,7 +9,7 @@ pub struct Model { pub id: i64, pub url: String, pub shady: String, - pub created_at: DateTime, + pub created_at: TimeDateTimeWithTimeZone, pub ip: Option, } diff --git a/entity/src/url_filter.rs b/entity/src/url_filter.rs index ef2cd93..7ea4f12 100644 --- a/entity/src/url_filter.rs +++ b/entity/src/url_filter.rs @@ -10,7 +10,7 @@ pub struct Model { #[sea_orm(unique)] pub filter: String, pub reason: Option, - pub created_at: DateTime, + pub created_at: TimeDateTimeWithTimeZone, pub user_created_id: Option, } diff --git a/env_example b/env_example index 2a1ca0f..8e7428c 100644 --- a/env_example +++ b/env_example @@ -53,7 +53,8 @@ IP_SOURCE="RightmostXForwardedFor" # # Be sure to run from the source dir: # . ./.env && sea-orm-cli migrate -u "$DATABASE_URL" up -DATABASE_URL="sqlite:/tmp/test.db" +DATABASE_URL="postgresql://localhost/shadyurl" +#DATABASE_URL="sqlite:/tmp/test.db" # Redis URL, must start with redis:// as below. REDIS_URL="redis://127.0.0.1" diff --git a/src/util.rs b/src/util.rs index 75fa950..b75d8b1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -12,7 +12,115 @@ * work. If not, see . */ -pub mod macros { +pub(crate) mod bits { + use num::PrimInt; + + pub(crate) fn count_bits(mut n: T) -> u32 { + let mut count = 0; + while n != T::zero() { + count += 1; + n = n >> 1; + } + count + } +} + +pub(crate) mod math { + use num::Float; + + pub(crate) fn is_close(a: T, b: T) -> bool { + let abs_tol = T::from(0.0).unwrap(); + let rel_tol = T::from(1e-05).unwrap(); + + if a == b { + return true; + } + + if a.is_infinite() || b.is_infinite() { + return false; + } + + let diff = (b - a).abs(); + + ((diff <= (rel_tol * b).abs()) || (diff <= (rel_tol * a).abs())) || (diff <= abs_tol) + } +} + +pub(crate) mod format { + use time::{ + convert::{Day, Hour, Microsecond, Millisecond, Minute, Nanosecond, Second, Week}, + Duration, + }; + + use super::math::is_close; + + // This implementation is heavily modified from the time crate. + pub(crate) fn humanize_duration(duration: Duration) -> String { + let suffix = if duration.is_positive() { + "ago" + } else { + "from now" + }; + + // Concise, rounded representation. + + if duration.is_zero() { + return "now".to_string(); + } + + /// Format the first item that produces a value greater than 1 and then break. + macro_rules! item { + ($singular:literal, $plural:literal, $value:expr) => { + let value = $value; + if is_close(value.round(), 1.0) { + return format!("{} {suffix}", $singular); + } else if value > 1.0 { + return format!("{value:.0} {} {suffix}", $plural); + } + }; + } + + const AVERAGE_YEAR: f64 = 365.2425; + const AVERAGE_MONTH: f64 = AVERAGE_YEAR / 12.0; + + // Even if this produces a de-normal float, because we're rounding we don't really care. + let seconds = duration.unsigned_abs().as_secs_f64(); + + item!( + "a year", + "years", + seconds / (Second::per(Day) as f64 * AVERAGE_YEAR) + ); + item!( + "a month", + "months", + seconds / (Second::per(Day) as f64 * AVERAGE_MONTH) + ); + item!("a week", "weeks", seconds / Second::per(Week) as f64); + item!("a day", "days", seconds / Second::per(Day) as f64); + item!("an hour", "hours", seconds / Second::per(Hour) as f64); + item!("a minute", "minutes", seconds / Second::per(Minute) as f64); + item!("a second", "seconds", seconds); + item!( + "a millisecond", + "milliseconds", + seconds * Millisecond::per(Second) as f64 + ); + item!( + "a microsecond", + "microseconds", + seconds * Microsecond::per(Second) as f64 + ); + item!( + "a nanosecond", + "nanoseconds", + seconds * Nanosecond::per(Second) as f64 + ); + format!("an instant {suffix}") + } +} + +pub(crate) mod macros { #[macro_export] macro_rules! arr { ( @@ -26,3 +134,79 @@ pub mod macros { pub(crate) use arr; } + +pub(crate) mod net { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use ipnetwork::{IpNetwork, IpNetworkError}; + + use super::bits::count_bits; + + #[derive(Debug, thiserror::Error)] + pub(crate) enum NetworkPrefixError { + #[error(transparent)] + IpNetwork(#[from] IpNetworkError), + + #[error("IP address types are mismatched")] + IpTypeMismatch, + } + + pub(crate) fn find_networks( + start: IpAddr, + end: IpAddr, + ) -> Result, NetworkPrefixError> { + let res = match (start, end) { + (IpAddr::V4(start_ip), IpAddr::V4(end_ip)) => { + let mut start_int: u32 = start_ip.into(); + let end_int: u32 = end_ip.into(); + + let mut res = Vec::new(); + + while start_int <= end_int { + // SAFETY: safe cast, we can never have > 255 + let nbits = start_int + .trailing_zeros() + .min(count_bits(end_int - start_int + 1) - 1) + as u8; + res.push(IpNetwork::new( + IpAddr::V4(Ipv4Addr::from(start_int)), + 32 - nbits, + )?); + start_int += 1 << nbits; + if start_int - 1 == u32::MAX { + break; + } + } + + res + } + (IpAddr::V6(start_ip), IpAddr::V6(end_ip)) => { + let mut start_int: u128 = start_ip.into(); + let end_int: u128 = end_ip.into(); + + let mut res = Vec::new(); + + while start_int <= end_int { + // SAFETY: safe cast, we can never have > 255 + let nbits = start_int + .trailing_zeros() + .min(count_bits(end_int - start_int + 1) - 1) + as u8; + res.push(IpNetwork::new( + IpAddr::V6(Ipv6Addr::from(start_int)), + 128 - nbits, + )?); + start_int += 1 << nbits; + if start_int - 1 == u128::MAX { + break; + } + } + + res + } + _ => return Err(NetworkPrefixError::IpTypeMismatch), + }; + + Ok(res) + } +} diff --git a/src/web/admin.rs b/src/web/admin.rs index 1f85634..c733d07 100644 --- a/src/web/admin.rs +++ b/src/web/admin.rs @@ -13,6 +13,7 @@ */ pub mod auth; +pub mod cidr_ban; pub mod delete; pub mod index; pub mod url_filter; diff --git a/src/web/admin/cidr_ban.rs b/src/web/admin/cidr_ban.rs new file mode 100644 index 0000000..5646e92 --- /dev/null +++ b/src/web/admin/cidr_ban.rs @@ -0,0 +1,247 @@ +/* SPDX-License-Identifier: CC0-1.0 + * + * src/web/admin/cidr_ban.rs + * + * This file is a component of ShadyURL by Elizabeth Myers. + * + * To the extent possible under law, the person who associated CC0 with + * ShadyURL has waived all copyright and related or neighboring rights + * to ShadyURL. + * + * You should have received a copy of the CC0 legalcode along with this + * work. If not, see . + */ + +use std::{convert::TryInto, net::IpAddr, str::FromStr}; + +use askama_axum::Template; +use axum::{ + extract::State, + response::{IntoResponse, Redirect, Response}, + routing::{get, post}, + Form, Router, +}; +use axum_messages::{Message, Messages}; +use csrf::CsrfProtection; +use ipnetwork::{IpNetwork, IpNetworkError}; +use itertools::Itertools; +use serde::Deserialize; +use time::OffsetDateTime; +use tower_sessions::Session; + +use entity::{cidr_ban, user}; +use service::{Mutation, Query}; + +use crate::{ + auth::AuthSession, + csrf as csrf_crate, + error_response::AppError, + state::AppState, + util::{ + format, + net::{find_networks, NetworkPrefixError}, + }, +}; + +#[derive(Template)] +#[template(path = "admin/cidr_ban.html")] +struct CidrBansTemplate<'a> { + authenticity_token: &'a str, + messages: Vec, + sitename: &'a str, + cidr_bans: Vec<(cidr_ban::Model, Option)>, +} + +#[derive(Debug, Clone, Deserialize)] +struct SubmitBanForm { + authenticity_token: String, + range: String, + reason: Option, +} + +#[derive(Debug, Clone, Deserialize)] +struct DeleteForm { + authenticity_token: String, + id: i64, +} + +pub fn router() -> Router { + Router::new() + .route("/admin/cidr_bans", get(self::get::cidr_bans)) + .route("/admin/cidr_bans", post(self::post::cidr_bans)) + .route("/admin/cidr_bans/delete", post(self::post::delete)) +} + +mod render { + use super::{find_networks, IpAddr, IpNetworkError, Itertools, NetworkPrefixError, TryInto}; + + pub(super) fn range_to_display( + start: Vec, + end: Vec, + ) -> Result, NetworkPrefixError> { + let Some((start, end)) = match (start.len(), end.len()) { + (4, 4) => [ + // These should never fail + IpAddr::from( + TryInto::<[u8; 4]>::try_into(start).expect("Failed to convert start IP"), + ), + IpAddr::from(TryInto::<[u8; 4]>::try_into(end).expect("Failed to convert end IP")), + ], + (16, 16) => [ + // These should never fail + IpAddr::from( + TryInto::<[u8; 16]>::try_into(start).expect("Failed to convert start IP"), + ), + IpAddr::from(TryInto::<[u8; 16]>::try_into(end).expect("Failed to convert end IP")), + ], + _ => { + return Err(NetworkPrefixError::IpNetwork(IpNetworkError::InvalidAddr( + "Invalid range".to_string(), + ))) + } + } + .into_iter() + .map(|v| v.to_canonical()) + .collect_tuple() else { + unreachable!(); + }; + + let nets = find_networks(start.clone(), end.clone())?; + Ok(nets.into_iter().map(|v| format!("{v}")).collect()) + } +} + +mod post { + use super::{ + csrf_crate, AppError, AppState, AuthSession, DeleteForm, Form, FromStr, IntoResponse, + IpAddr, IpNetwork, Messages, Mutation, Redirect, Response, Session, State, SubmitBanForm, + }; + + pub(super) async fn cidr_bans( + session: Session, + auth_session: AuthSession, + messages: Messages, + State(state): State, + Form(submit_ban_form): Form, + ) -> Result { + csrf_crate::verify( + &session, + &submit_ban_form.authenticity_token, + &state.protect, + ) + .await?; + + let Some(user) = auth_session.user else { + return Err(AppError::Unauthorized); + }; + + if submit_ban_form.range.is_empty() { + messages.error("Range cannot be empty"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + } + + let range = submit_ban_form.range.clone(); + let network = match range.rsplit_once("/") { + Some((address, prefix)) => { + let Ok(addr) = IpAddr::from_str(address) else { + messages.error("Invalid IP range"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + }; + + let Ok(prefix) = prefix.parse::() else { + messages.error("Invalid network prefix"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + }; + + match IpNetwork::new(addr, prefix) { + Ok(i) => i, + Err(_) => { + messages.error("Invalid IP range"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + } + } + } + None => { + let Ok(addr) = IpAddr::from_str(&range) else { + messages.error("Invalid IP range"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + }; + + let prefix = match addr { + IpAddr::V4(_) => 32, + IpAddr::V6(_) => 128, + }; + + match IpNetwork::new(addr, prefix) { + Ok(i) => i, + Err(_) => { + messages.error("Invalid IP range"); + return Ok(Redirect::to("/admin/cidr_bans").into_response()); + } + } + } + }; + + Mutation::create_cidr_ban(&state.db, network, submit_ban_form.reason, &user.0).await?; + + messages.success(format!( + "Added CIDR ban {} successfullly", + submit_ban_form.range + )); + Ok(Redirect::to("/admin/cidr_bans").into_response()) + } + + pub(super) async fn delete( + session: Session, + auth_session: AuthSession, + messages: Messages, + State(state): State, + Form(delete_form): Form, + ) -> Result { + csrf_crate::verify(&session, &delete_form.authenticity_token, &state.protect).await?; + + if auth_session.user.is_none() { + return Err(AppError::Unauthorized); + }; + + Mutation::delete_cidr_ban(&state.db, delete_form.id).await?; + + messages.success(format!("Deleted CIDR ban #{} successfully", delete_form.id)); + Ok(Redirect::to("/admin/cidr_bans").into_response()) + } +} + +mod get { + use super::{ + AppError, AppState, AuthSession, CidrBansTemplate, CsrfProtection, IntoResponse, Messages, + Query, Response, Session, State, + }; + + pub(super) async fn cidr_bans( + session: Session, + auth_session: AuthSession, + messages: Messages, + State(state): State, + ) -> Result { + if auth_session.user.is_none() { + return Err(AppError::Unauthorized); + } + + let (authenticity_token, session_token) = state.protect.generate_token_pair(None, 300)?; + + let authenticity_token = authenticity_token.b64_string(); + let session_token = session_token.b64_string(); + + session.insert("authenticity_token", &session_token).await?; + + let cidr_bans = Query::fetch_all_cidr_bans(&state.db).await?; + + Ok(CidrBansTemplate { + authenticity_token: &authenticity_token, + messages: messages.into_iter().collect(), + sitename: &state.env.sitename, + cidr_bans, + } + .into_response()) + } +} diff --git a/src/web/admin/delete.rs b/src/web/admin/delete.rs index a03f2b3..ae4bfb7 100644 --- a/src/web/admin/delete.rs +++ b/src/web/admin/delete.rs @@ -22,12 +22,15 @@ use axum::{ use axum_messages::{Message, Messages}; use csrf::CsrfProtection; use serde::Deserialize; +use time::OffsetDateTime; use tower_sessions::Session; use entity::url; use service::{Mutation, Query}; -use crate::{auth::AuthSession, csrf as csrf_crate, error_response::AppError, state::AppState}; +use crate::{ + auth::AuthSession, csrf as csrf_crate, error_response::AppError, state::AppState, util::format, +}; #[derive(Template)] #[template(path = "admin/urls.html")] diff --git a/src/web/admin/url_filter.rs b/src/web/admin/url_filter.rs index f09fde4..9c150c1 100644 --- a/src/web/admin/url_filter.rs +++ b/src/web/admin/url_filter.rs @@ -23,12 +23,15 @@ use axum_messages::{Message, Messages}; use csrf::CsrfProtection; use regex::Regex; use serde::Deserialize; +use time::OffsetDateTime; use tower_sessions::Session; use entity::{url_filter, user}; use service::{Mutation, Query}; -use crate::{auth::AuthSession, csrf as csrf_crate, error_response::AppError, state::AppState}; +use crate::{ + auth::AuthSession, csrf as csrf_crate, error_response::AppError, state::AppState, util::format, +}; #[derive(Template)] #[template(path = "admin/url_filter.html")] diff --git a/src/web/app.rs b/src/web/app.rs index f715206..0ec55b2 100644 --- a/src/web/app.rs +++ b/src/web/app.rs @@ -33,7 +33,7 @@ use crate::{ env::Vars, state::AppState, web::{ - admin::{auth, delete, index, url_filter}, + admin::{auth, cidr_ban, delete, index, url_filter}, fallback, files, submission, url, }, }; @@ -97,10 +97,11 @@ impl App { let app = Router::new() .merge(auth::router()) + .merge(cidr_ban::router()) + .merge(delete::router()) .merge(files::router()) - .merge(submission::router()) .merge(index::router()) - .merge(delete::router()) + .merge(submission::router()) .merge(url::router()) .merge(url_filter::router()) .merge(fallback::router()) diff --git a/static/assets/style.css b/static/assets/style.css index 027c939..b2b15e0 100644 --- a/static/assets/style.css +++ b/static/assets/style.css @@ -40,27 +40,47 @@ tr#admin-list-heading th { th#admin-list-id-heading { text-align: left; - width: 2%; + min-width: 5%; } th#admin-list-url-heading { text-align: left; - width: 20%; + min-width: 20%; } th#admin-list-redirect-heading { text-align: left; - width: 20%; + min-width: 25%; +} + +th#admin-list-filter-heading { + text-align: left; + min-width: 10%; } th#admin-list-created-heading { text-align: right; - width: 8%; + min-width: 10%; +} + +th#admin-list-note-heading { + text-align: left; + min-width: 20%; +} + +th#admin-list-network-heading { + text-align: left; + min-width: 5%; } th#admin-list-ip-heading { text-align: right; - width: 10%; + min-width: 5%; +} + +th#admin-list-admin-heading { + text-align: right; + min-width: 10%; } tr#admin-list-item td { @@ -84,13 +104,34 @@ td#admin-list-redirect-item { padding-right: 0.5em; } +td#admin-list-filter-item { + text-align: left; + padding-right: 0.5em; +} + td#admin-list-created-item { text-align: right; padding-right: 0.5em; } +td#admin-list-note-item { + text-align: left; + padding-left: 0.5em; +} + +td#admin-list-network-item { + text-align: left; + padding-left: 0.5em; + white-space: nowrap; +} + td#admin-list-ip-item { text-align: right; + white-space: nowrap; +} + +td#admin-list-admin-item { + text-align: right; } tr#admin-list-item #inline { diff --git a/templates/admin/admin_base.html b/templates/admin/admin_base.html new file mode 100644 index 0000000..2986fff --- /dev/null +++ b/templates/admin/admin_base.html @@ -0,0 +1,23 @@ +{# SPDX-License-Identifier: CC0-1.0 + # + # templates/admin/admin_base.html + # + # This file is a component of ShadyURL by Elizabeth Myers. + # + # To the extent possible under law, the person who associated CC0 with + # ShadyURL has waived all copyright and related or neighboring rights + # to ShadyURL. + # + # You should have received a copy of the CC0 legalcode along with this + # work. If not, see . + #} +{% extends "base.html" %} +{% block head_addition %}{% endblock %} +{% block title %}{{ sitename }} — Admin{% endblock %} +{% block header %} +

ShadyUrl

+

Admin portal

+Admin main page +
+Logout +{% endblock %} diff --git a/templates/admin/cidr_ban.html b/templates/admin/cidr_ban.html new file mode 100644 index 0000000..88defa2 --- /dev/null +++ b/templates/admin/cidr_ban.html @@ -0,0 +1,65 @@ +{# SPDX-License-Identifier: CC0-1.0 + # + # templates/admin/cidr_ban.html + # + # This file is a component of ShadyURL by Elizabeth Myers. + # + # To the extent possible under law, the person who associated CC0 with + # ShadyURL has waived all copyright and related or neighboring rights + # to ShadyURL. + # + # You should have received a copy of the CC0 legalcode along with this + # work. If not, see . + #} +{% extends "admin/admin_base.html" %} +{% block path %}admin/cidr_bans{% endblock %} +{% block title %}{{ sitename }} — Admin — CIDR bans{% endblock %} +{% block content %} +
+
+ + + + + + +
+
+ + + + + + + + +{% for (entry, user) in cidr_bans %} + + + + + + + +{% endfor %} +
IDRangeNoteCreatedAdmin
+
+ + + +
+
+ {% match render::range_to_display(entry.range_begin.clone(), entry.range_end.clone()) %} + {% when Ok with (val) %} + {{ val|join("
") }} + {% when Err with (e) %} + Error fetching IP: {{ e }} + {% endmatch %} +
{% if entry.reason.is_some() %}{{ entry.reason.as_ref().unwrap() }}{% else %}—{% endif %} + + {{ format::humanize_duration(OffsetDateTime::now_utc() - entry.created_at.clone()) }} + + {% if user.is_some() %}{{ user.as_ref().unwrap().username }}{% else %}–{% endif %}
+{% endblock %} diff --git a/templates/admin/index.html b/templates/admin/index.html index eefea84..5f4cc1e 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -11,17 +11,15 @@ # You should have received a copy of the CC0 legalcode along with this # work. If not, see . #} -{% extends "base.html" %} -{% block path %}admin/urls{% endblock %} -{% block head_addition %}{% endblock %} -{% block title %}Admin – {{ sitename }} — Admin{% endblock %} -{% block header %} -

Admin – {{ sitename }}

-Logout -{% endblock %} +{% extends "admin/admin_base.html" %} +{% block path %}admin{% endblock %} +{% block title %}A{{ sitename }} — Admin{% endblock %} {% block content %}

Manage URLs +

Manage URL filters +

+Manage CIDR bans

{% endblock %} diff --git a/templates/admin/url_filter.html b/templates/admin/url_filter.html index ded8cfa..9c255f0 100644 --- a/templates/admin/url_filter.html +++ b/templates/admin/url_filter.html @@ -11,14 +11,9 @@ # You should have received a copy of the CC0 legalcode along with this # work. If not, see . #} -{% extends "base.html" %} +{% extends "admin/admin_base.html" %} {% block path %}admin/url_filters{% endblock %} -{% block head_addition %}{% endblock %} -{% block title %}Admin – {{ sitename }} — URL filters{% endblock %} -{% block header %} -

Admin – {{ sitename }}

-Logout -{% endblock %} +{% block title %}{{ sitename }} — Admin — URL filters{% endblock %} {% block content %}
@@ -33,10 +28,10 @@

Admin – {{ sitename }}

- - + + - + {% for (entry, user) in url_filters %} @@ -49,10 +44,14 @@

Admin – {{ sitename }}

- - - - + + + + {% endfor %}
IDFilterNoteFilterNote CreatedAdminAdmin
{{ entry.filter }}{% if entry.reason.is_some() %}{{ entry.reason.as_ref().unwrap() }}{% else %}—{% endif %}{{ entry.created_at }}{% if user.is_some() %}{{ user.as_ref().unwrap().username }}{% else %}–{% endif %}{{ entry.filter }}{% if entry.reason.is_some() %}{{ entry.reason.as_ref().unwrap() }}{% else %}—{% endif %} + + {{ format::humanize_duration(OffsetDateTime::now_utc() - entry.created_at.clone()) }} + + {% if user.is_some() %}{{ user.as_ref().unwrap().username }}{% else %}–{% endif %}
diff --git a/templates/admin/urls.html b/templates/admin/urls.html index 3a5fa48..52e3257 100644 --- a/templates/admin/urls.html +++ b/templates/admin/urls.html @@ -11,14 +11,9 @@ # You should have received a copy of the CC0 legalcode along with this # work. If not, see . #} -{% extends "base.html" %} +{% extends "admin/admin_base.html" %} {% block path %}admin/urls{% endblock %} -{% block head_addition %}{% endblock %} -{% block title %}Admin – {{ sitename }} — URLs{% endblock %} -{% block header %} -

Admin – {{ sitename }}

-Logout -{% endblock %} +{% block title %}{{ sitename }} — Admin — URLs{% endblock %} {% block content %}
@@ -42,7 +37,11 @@

Admin – {{ sitename }}

- + {% endfor %}
{{ entry.url }} {{ entry.shady }}{{ entry.created_at }} + + {{ format::humanize_duration(OffsetDateTime::now_utc() - entry.created_at.clone()) }} + + {% if entry.ip.as_ref().is_some() %}{{ entry.ip.as_ref().unwrap() }}{% else %}–{% endif %}