From d30f32ec1c3d81c41107c29e73cb5958d7d9c7fc Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 5 Mar 2024 02:36:47 +0000 Subject: [PATCH] Added delete and watching filterlists.csv --- Cargo.toml | 3 ++ build.rs | 2 +- src/app.rs | 42 ++++++++++++++--- src/domain_view.rs | 26 +++++------ src/fileserv.rs | 19 ++++---- src/ip_view.rs | 2 +- src/lib.rs | 14 ++++-- src/list_manager.rs | 110 +++++++++++++++++++++++++++++++++----------- src/list_parser.rs | 37 +++++++-------- src/list_view.rs | 57 ++++++++++++++++++++--- src/main.rs | 14 ++++-- src/rule_view.rs | 47 ++++++++----------- src/server.rs | 4 +- src/source_view.rs | 14 ++---- style/main.scss | 4 -- 15 files changed, 260 insertions(+), 135 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f520ecf..c33cee04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,8 @@ sct = { version = "0.7.1", optional = true } ct-logs = { version = "0.9.0", optional = true } rand = { version = "0.8.5", optional = true } hickory-proto = { version = "0.24.0", default-features = false } +humantime = "2.1.0" +notify = {version = "6.1.1", optional = true} [features] @@ -81,6 +83,7 @@ ssr = [ "dep:sct", "dep:ct-logs", "dep:rand", + "dep:notify", ] default = ["ssr"] diff --git a/build.rs b/build.rs index 76095938..d5068697 100644 --- a/build.rs +++ b/build.rs @@ -2,4 +2,4 @@ fn main() { // trigger recompilation when a new migration is added println!("cargo:rerun-if-changed=migrations"); -} \ No newline at end of file +} diff --git a/src/app.rs b/src/app.rs index 00f5b80f..ef5c5221 100644 --- a/src/app.rs +++ b/src/app.rs @@ -98,8 +98,8 @@ fn FilterListSummary(url: crate::FilterListUrl, record: crate::FilterListRecord) {record.author.to_string()} - {record.license.to_string()} - {format!("{:?}", record.expires)} + {record.license.to_string()} + {humantime::format_duration(record.expires).to_string()} {format!("{:?}", record.list_format)} @@ -320,10 +320,32 @@ fn HomePage() -> impl IntoView { view! {

"Welcome to Leptos!"

-

"Total Rules: "

-

"Total Domains: "

-

"Total Subdomains: "

-

"Total DNS Lookups: "

+ + + + + + + + + + + + + + + + + +
"Total Rules" + +
"Total Domains" + +
"Total Subdomains" + +
"Total DNS Lookups" + +
impl IntoView { log::info!("Displaying list"); view! { + + + + + + + + Result>) -> im #[server] async fn get_blocked_by( domain: String, -) -> Result, ServerFnError> { +) -> Result< + Vec<( + FilterListUrl, + RuleId, + SourceId, + crate::list_parser::RulePair, + )>, + ServerFnError, +> { let records = sqlx::query!( r#" WITH matching_domain_rules AS ( @@ -158,7 +167,7 @@ async fn get_blocked_by( let pair = crate::list_parser::RulePair::new(source.into(), rule); let url = record.url.clone(); Ok(( - url, + url.parse()?, RuleId(record.rule_id), SourceId(record.source_id), pair, @@ -191,19 +200,10 @@ fn BlockedBy(get_domain: Box Result>) -> impl I let rule = pair.get_rule().clone(); let rule_href = format!("/rule/{}", rule_id.0); let source_href = format!("/rule_source/{}", source_id.0); - let url_href = format!( - "/list{}", - params_map! { - "url" => url.as_str(), - } - .to_query_string(), - ); view! { } @@ -137,7 +132,7 @@ fn Sources(get_id: GetId) -> impl IntoView { } .into_view() } - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), None => view! { "Invalid URL" }.into_view(), }} @@ -188,7 +183,7 @@ fn RuleRawView( } .into_view() } - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), None => view! { "Invalid URL" }.into_view(), }} @@ -240,7 +235,7 @@ fn RuleBlockedDomainsView(get_id: Box Result>) each=move || { domains.clone() } key=|(id, _)| *id children=|(_domain_id, domain)| { - let domain_href = format!("/domain/{}", domain); + let domain_href = format!("/domain/{domain}"); view! {
"Name""Author""License""Update frequency""Format""Last Updated"
- - {url} - + diff --git a/src/fileserv.rs b/src/fileserv.rs index e8435765..f292b669 100644 --- a/src/fileserv.rs +++ b/src/fileserv.rs @@ -1,16 +1,20 @@ +use crate::app::App; +use axum::response::Response as AxumResponse; use axum::{ body::Body, extract::State, - response::IntoResponse, http::{Request, Response, StatusCode, Uri}, + response::IntoResponse, }; -use axum::response::Response as AxumResponse; +use leptos::*; use tower::ServiceExt; use tower_http::services::ServeDir; -use leptos::*; -use crate::app::App; -pub async fn file_and_error_handler(uri: Uri, State(options): State, req: Request) -> AxumResponse { +pub async fn file_and_error_handler( + uri: Uri, + State(options): State, + req: Request, +) -> AxumResponse { let root = options.site_root.clone(); let res = get_static_file(uri.clone(), &root).await.unwrap(); @@ -22,10 +26,7 @@ pub async fn file_and_error_handler(uri: Uri, State(options): State Result, (StatusCode, String)> { +async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder() .uri(uri.clone()) .body(Body::empty()) diff --git a/src/ip_view.rs b/src/ip_view.rs index 42bd0ef8..0b0ed323 100644 --- a/src/ip_view.rs +++ b/src/ip_view.rs @@ -88,7 +88,7 @@ pub fn IpView() -> impl IntoView {

"IP Address:" { - + get_ip } diff --git a/src/lib.rs b/src/lib.rs index a639c1bc..dc0e0e49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod source_view; use mimalloc::MiMalloc; use serde::*; use std::convert::From; +use std::str::FromStr; use std::sync::Arc; #[cfg(feature = "ssr")] @@ -55,9 +56,16 @@ impl std::ops::Deref for FilterListUrl { } } -impl FilterListUrl { - pub fn new(url: url::Url) -> Self { - Self { url: Arc::new(url) } +impl From for FilterListUrl { + fn from(url: url::Url) -> Self { + Self { url: url.into() } + } +} + +impl FromStr for FilterListUrl { + type Err = url::ParseError; + fn from_str(s: &str) -> Result { + Ok(url::Url::parse(s)?.into()) } } diff --git a/src/list_manager.rs b/src/list_manager.rs index a54812a0..e1e15b0c 100644 --- a/src/list_manager.rs +++ b/src/list_manager.rs @@ -12,12 +12,12 @@ struct CsvRecord { pub expires: u64, pub list_type: crate::FilterListType, } -#[cfg(feature = "ssr")] -const FILTERLISTS_PATH: &str = "filterlists.csv"; #[server] pub async fn load_filter_map() -> Result<(), ServerFnError> { - let contents = tokio::fs::read_to_string(FILTERLISTS_PATH).await?; + dotenvy::dotenv()?; + let filterlists_path: std::path::PathBuf = std::env::var("FILTERLISTS_PATH")?.parse()?; + let contents = tokio::fs::read_to_string(filterlists_path).await?; let records = csv::Reader::from_reader(contents.as_bytes()) .deserialize::() .collect::, _>>()?; @@ -61,15 +61,43 @@ pub async fn load_filter_map() -> Result<(), ServerFnError> { Ok(()) } +#[server] +pub async fn watch_filter_map() -> Result<(), ServerFnError> { + dotenvy::dotenv()?; + let filterlists_path: std::path::PathBuf = std::env::var("FILTERLISTS_PATH")?.parse()?; + use notify::Watcher; + let notify = std::sync::Arc::new(tokio::sync::Notify::new()); + let notify2 = notify.clone(); + load_filter_map().await?; + let mut watcher = notify::recommended_watcher(move |_| { + notify.notify_one(); + })?; + let task = async move { + tokio::task::spawn_blocking(move || { + Ok::<_, ServerFnError>( + watcher.watch(&filterlists_path, notify::RecursiveMode::NonRecursive)?, + ) + }) + .await? + }; + let reloaded = async { + notify2.notified().await; + load_filter_map().await?; + Ok(()) + }; + tokio::try_join!(task, reloaded)?; + Ok(()) +} + #[server] pub async fn write_filter_map() -> Result<(), ServerFnError> { use csv::Writer; + dotenvy::dotenv()?; + let filterlists_path: std::path::PathBuf = std::env::var("FILTERLISTS_PATH")?.parse()?; let pool = crate::server::get_db().await?; - let rows = sqlx::query!( - "SELECT url, name, format, expires, author, license FROM filterLists" - ) - .fetch_all(&pool) - .await?; + let rows = sqlx::query!("SELECT url, name, format, expires, author, license FROM filterLists") + .fetch_all(&pool) + .await?; let mut records = Vec::new(); for record in rows { records.push(CsvRecord { @@ -83,7 +111,7 @@ pub async fn write_filter_map() -> Result<(), ServerFnError> { } records.sort_by_key(|record| (record.name.clone(), record.url.clone())); records.reverse(); - let mut wtr = Writer::from_path(FILTERLISTS_PATH)?; + let mut wtr = Writer::from_path(filterlists_path)?; for record in records { wtr.serialize(record)?; } @@ -93,16 +121,13 @@ pub async fn write_filter_map() -> Result<(), ServerFnError> { #[server] pub async fn get_filter_map() -> Result { let pool = crate::server::get_db().await?; - let rows = sqlx::query!( - "SELECT url, name, format, expires, author, license FROM filterLists" - ) - .fetch_all(&pool) - .await?; + let rows = sqlx::query!("SELECT url, name, format, expires, author, license FROM filterLists") + .fetch_all(&pool) + .await?; let mut filter_list_map = std::collections::BTreeMap::new(); for record in rows { - let url = url::Url::parse(&record.url)?; - let url = crate::FilterListUrl::new(url); + let url = url::Url::parse(&record.url)?.into(); let record = crate::FilterListRecord { name: record.name.unwrap_or("".to_string()).into(), list_format: crate::FilterListType::from_str(&record.format)?, @@ -128,7 +153,7 @@ async fn get_last_version_data( ) -> Result, ServerFnError> { let pool = crate::server::get_db().await?; let url_str = url.as_str(); - #[allow(non_camel_case_types)] + #[allow(non_camel_case_types)] let last_version_data = sqlx::query!( r#"SELECT lastUpdated as "last_updated: chrono::NaiveDateTime", etag FROM filterLists WHERE url = $1"#, url_str @@ -136,16 +161,22 @@ async fn get_last_version_data( .fetch_one(&pool) .await .ok(); - let last_version_data = last_version_data.and_then(|row| Some(LastVersionData { - last_updated: row.last_updated?, - etag: row.etag, - })); + let last_version_data = last_version_data.and_then(|row| { + Some(LastVersionData { + last_updated: row.last_updated?, + etag: row.etag, + }) + }); Ok(last_version_data) } #[server] -pub async fn get_last_updated(url: crate::FilterListUrl) -> Result, ServerFnError> { - get_last_version_data(&url).await.map(|data| data.map(|data| data.last_updated)) +pub async fn get_last_updated( + url: crate::FilterListUrl, +) -> Result, ServerFnError> { + get_last_version_data(&url) + .await + .map(|data| data.map(|data| data.last_updated)) } #[cfg(feature = "ssr")] @@ -165,7 +196,10 @@ pub async fn update_list(url: crate::FilterListUrl) -> Result<(), ServerFnError> if let Some(last_updated) = last_updated { req = req.header( "if-modified-since", - last_updated.last_updated.format("%a, %d %b %Y %H:%M:%S GMT").to_string(), + last_updated + .last_updated + .format("%a, %d %b %Y %H:%M:%S GMT") + .to_string(), ); if let Some(etag) = last_updated.etag { req = req.header("if-none-match", etag); @@ -181,12 +215,12 @@ pub async fn update_list(url: crate::FilterListUrl) -> Result<(), ServerFnError> let headers = response.headers().clone(); let etag = headers.get("etag").and_then(|item| item.to_str().ok()); let body = response.text().await?; - log::info!("Fetched {:?}", url_str); let new_last_updated = chrono::Utc::now(); - log::info!("Updated {}", url_str); + log::info!("Updated {} size ({})", url_str, body.len()); sqlx::query!( - "INSERT INTO filterLists (url, lastUpdated, contents, etag) VALUES ($1, $2, $3, $4) - ON CONFLICT (url) DO UPDATE SET lastUpdated = $2, contents = $3, etag = $4 + "UPDATE filterLists + SET lastUpdated = $2, contents = $3, etag = $4 + WHERE url = $1 ", url_str, new_last_updated, @@ -203,3 +237,23 @@ pub async fn update_list(url: crate::FilterListUrl) -> Result<(), ServerFnError> } } } + +#[server] +pub async fn delete_list(url: crate::FilterListUrl) -> Result<(), ServerFnError> { + let pool = crate::server::get_db().await?; + let url_str = url.as_str(); + sqlx::query!( + "DELETE FROM list_rules + WHERE list_rules.list_id IN ( + SELECT id FROM filterLists WHERE url = $1 + )", + url_str + ) + .execute(&pool) + .await?; + sqlx::query!("DELETE FROM filterLists WHERE url = $1", url_str) + .execute(&pool) + .await?; + write_filter_map().await?; + Ok(()) +} diff --git a/src/list_parser.rs b/src/list_parser.rs index a5a04fd5..5a640b42 100644 --- a/src/list_parser.rs +++ b/src/list_parser.rs @@ -1,11 +1,8 @@ +use crate::FilterListType; use leptos::*; - use serde::{Deserialize, Serialize}; - use std::sync::Arc; -use crate::FilterListType; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(transparent)] pub struct Domain(Arc); @@ -342,20 +339,20 @@ fn parse_unknown_lines(contents: &str) -> Vec { parse_lines(contents, &|_| Some(Rule::Unknown)) } -pub fn parse_list_contents(contents: &str, list_format: crate::FilterListType) -> Vec { +pub fn parse_list_contents(contents: &str, list_format: FilterListType) -> Vec { match list_format { - crate::FilterListType::Adblock => parse_adblock(contents), - crate::FilterListType::DomainBlocklist => parse_domain_list(contents, false, true), - crate::FilterListType::DomainAllowlist => parse_domain_list(contents, true, false), - crate::FilterListType::IPBlocklist => parse_ip_network_list(contents, false), - crate::FilterListType::IPAllowlist => parse_ip_network_list(contents, true), - crate::FilterListType::IPNetBlocklist => parse_ip_network_list(contents, false), - crate::FilterListType::DenyHosts => parse_unknown_lines(contents), - crate::FilterListType::RegexAllowlist => parse_unknown_lines(contents), - crate::FilterListType::RegexBlocklist => parse_regex(contents), - crate::FilterListType::Hostfile => parse_domain_list(contents, false, true), - crate::FilterListType::DNSRPZ => parse_unknown_lines(contents), - crate::FilterListType::PrivacyBadger => vec![], + FilterListType::Adblock => parse_adblock(contents), + FilterListType::DomainBlocklist => parse_domain_list(contents, false, true), + FilterListType::DomainAllowlist => parse_domain_list(contents, true, false), + FilterListType::IPBlocklist => parse_ip_network_list(contents, false), + FilterListType::IPAllowlist => parse_ip_network_list(contents, true), + FilterListType::IPNetBlocklist => parse_ip_network_list(contents, false), + FilterListType::DenyHosts => parse_unknown_lines(contents), + FilterListType::RegexAllowlist => parse_unknown_lines(contents), + FilterListType::RegexBlocklist => parse_regex(contents), + FilterListType::Hostfile => parse_domain_list(contents, false, true), + FilterListType::DNSRPZ => parse_unknown_lines(contents), + FilterListType::PrivacyBadger => vec![], } } @@ -373,7 +370,11 @@ pub async fn parse_list(url: crate::FilterListUrl) -> Result<(), ServerFnError> .await?; let list_format: FilterListType = record.format.parse()?; - log::info!("Parsing {} as format {}", url.as_str(), list_format.as_str()); + log::info!( + "Parsing {} as format {}", + url.as_str(), + list_format.as_str() + ); let list_id = record.id; let rules = { let contents = record diff --git a/src/list_view.rs b/src/list_view.rs index b4cfb490..3a72faf1 100644 --- a/src/list_view.rs +++ b/src/list_view.rs @@ -1,8 +1,25 @@ +#[cfg(feature = "ssr")] use self::rule_view::RuleData; use crate::{app::Loading, rule_view::DisplayRule, *}; use leptos::*; use leptos_router::*; +#[component] +pub fn FilterListLink(url: crate::FilterListUrl) -> impl IntoView { + let href = format!( + "/list{}", + params_map! { + "url" => url.as_str(), + } + .to_query_string(), + ); + view! { + + {url.as_str().to_string()} + + } +} + #[server] async fn get_list_size(url: crate::FilterListUrl) -> Result, ServerFnError> { let pool = crate::server::get_db().await?; @@ -124,7 +141,7 @@ pub fn LastUpdated( .into_view() } Some(Ok(None)) => view! { "Never" }.into_view(), - Some(Ok(Some(ts))) => view! { {format!("{:?}", ts)} }.into_view(), + Some(Ok(Some(ts))) => view! { {format!("{}", ts)} }.into_view(), }} @@ -312,6 +329,8 @@ fn FilterListInner(url: crate::FilterListUrl, page: Option) -> impl IntoV

+ + {if let Some(page) = page { view! {

"Page: " {page}

} } else { @@ -367,15 +386,41 @@ enum ViewListError { impl ViewListParams { fn parse(&self) -> Result { - let url = url::Url::parse(&self.url)?; - Ok(crate::FilterListUrl::new(url)) + Ok(url::Url::parse(&self.url)?.into()) + } +} + +#[component] +fn DeleteList(url: FilterListUrl) -> impl IntoView { + let delete_list = create_action(move |url: &FilterListUrl| { + let url = url.clone(); + async move { + log::info!("Deleting {}", url.as_str()); + if let Err(err) = list_manager::delete_list(url).await { + log::error!("Error deleting list: {:?}", err); + } + } + }); + view! { + } } #[component] pub fn FilterListPage() -> impl IntoView { let params = use_query::(); - let url = move || { + let get_url = move || { params.with(|param| { param .as_ref() @@ -386,9 +431,9 @@ pub fn FilterListPage() -> impl IntoView { view! {
- {move || match url() { + {move || match get_url() { None => view! {

"No URL"

}.into_view(), - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), Some(Ok((url, page))) => view! { }.into_view(), }} diff --git a/src/main.rs b/src/main.rs index 12e9be6e..22468914 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ #[cfg(feature = "ssr")] #[tokio::main] async fn main() { - env_logger::init(); use axum::Router; + use blockconvert::app::App; + use blockconvert::fileserv::file_and_error_handler; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; - use blockconvert::app::*; - use blockconvert::fileserv::file_and_error_handler; + env_logger::init(); // Setting get_configuration(None) means we'll be using cargo-leptos's env values // For deployment these variables are: @@ -27,10 +27,14 @@ async fn main() { let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); logging::log!("listening on http://{}", &addr); tokio::spawn(async { - blockconvert::list_manager::load_filter_map().await.unwrap(); + blockconvert::list_manager::watch_filter_map() + .await + .unwrap(); }); tokio::spawn(async { - blockconvert::server::parse_missing_subdomains().await.unwrap(); + blockconvert::server::parse_missing_subdomains() + .await + .unwrap(); }); tokio::spawn(async { blockconvert::server::check_missing_dns().await.unwrap(); diff --git a/src/rule_view.rs b/src/rule_view.rs index 331c8265..6ae69f01 100644 --- a/src/rule_view.rs +++ b/src/rule_view.rs @@ -1,6 +1,8 @@ use crate::app::Loading; use crate::list_parser::DomainRule; use crate::list_parser::Rule; +use crate::list_view::FilterListLink; +use crate::FilterListUrl; use crate::{DomainId, ListId, RuleId, SourceId}; use leptos::*; use leptos_router::*; @@ -54,7 +56,9 @@ pub async fn get_rule(id: RuleId) -> Result { type GetId = Box Result>; #[server] -async fn get_sources(id: RuleId) -> Result, ServerFnError> { +async fn get_sources( + id: RuleId, +) -> Result, ServerFnError> { let sources = sqlx::query!( "SELECT rule_source.id as source_id, source, filterLists.id as list_id, filterLists.url FROM rule_source INNER JOIN list_rules ON rule_source.id = list_rules.source_id @@ -66,17 +70,17 @@ async fn get_sources(id: RuleId) -> Result impl IntoView { key=|(id, _, _, _)| *id children=|(source_id, source, _list_id, url)| { let source_href = format!("/rule_source/{}", source_id.0); - let url_href = format!( - "/list{}", - params_map! { - "url" => url.as_str(), - } - .to_query_string(), - ); view! {

@@ -121,9 +118,7 @@ fn Sources(get_id: GetId) -> impl IntoView { - - {url} - +
@@ -260,7 +255,7 @@ fn RuleBlockedDomainsView(get_id: Box Result>) } .into_view() } - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), None => view! { "Invalid URL" }.into_view(), }} @@ -322,14 +317,8 @@ pub fn RuleViewPage() -> impl IntoView { Ok::<_, ServerFnError>(rule) }); view! { - {move || { - let id = get_id(); - view! { -

"Id: " {format!("{:?}", id)}

-

"Rule: "

- - - } - }} +

"Rule: "

+ + } } diff --git a/src/server.rs b/src/server.rs index 5de4f62e..84fab493 100644 --- a/src/server.rs +++ b/src/server.rs @@ -53,7 +53,7 @@ pub async fn parse_missing_subdomains() -> Result<(), ServerFnError> { let mut all_domains = Vec::new(); let mut all_parents = Vec::new(); - for record in records.into_iter() { + for record in records{ checked_domains.push(record.domain.clone()); let parents = record .domain @@ -70,7 +70,7 @@ pub async fn parse_missing_subdomains() -> Result<(), ServerFnError> { .iter() .cloned() .collect::>(); - for domain in all_domains.iter() { + for domain in &all_domains { parent_set.remove(domain); } let parent_set = parent_set.into_iter().collect::>(); diff --git a/src/source_view.rs b/src/source_view.rs index d900be0d..9a1386d3 100644 --- a/src/source_view.rs +++ b/src/source_view.rs @@ -70,7 +70,7 @@ fn Lists(get_id: GetId) -> impl IntoView { } .into_view() } - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), None => view! { "Invalid URL" }.into_view(), }} @@ -95,7 +95,7 @@ fn SourceRawView( } .into_view() } - Some(Err(err)) => view! {

"Error: " {format!("{:?}", err)}

}.into_view(), + Some(Err(err)) => view! {

"Error: " {format!("{}", err)}

}.into_view(), None => view! { "Invalid URL" }.into_view(), }} @@ -119,13 +119,7 @@ pub fn SourceViewPage() -> impl IntoView { Ok::<_, ServerFnError>(rule) }); view! { - {move || { - let id = get_id(); - view! { -

"Id: " {format!("{:?}", id)}

- - - } - }} + + } } diff --git a/style/main.scss b/style/main.scss index e4538e15..e69de29b 100644 --- a/style/main.scss +++ b/style/main.scss @@ -1,4 +0,0 @@ -body { - font-family: sans-serif; - text-align: center; -} \ No newline at end of file