Make secret more logsafe, implement add and edit for links

This commit is contained in:
Dietrich 2021-05-20 11:40:44 +02:00 committed by Franz Dietrich
parent d503d49917
commit b782d97920
18 changed files with 909 additions and 158 deletions

View File

@ -8,4 +8,4 @@ members = [
[profile.release] [profile.release]
lto = true lto = true
#codegen-units = 1 #codegen-units = 1
opt-level = 'z' #opt-level = 'z' # Z is not supported by argonautica

View File

@ -3,8 +3,11 @@ add-link = Link hinzufügen
invite-user = Benutzer einladen invite-user = Benutzer einladen
list-users = Liste der Benutzer list-users = Liste der Benutzer
welcome-user = Herzlich willkommen {$username} welcome-user = Herzlich willkommen {$username}
welcome = Herzlich willkommen
logout = Abmelden logout = Abmelden
login = Login login = Login
yes = Ja
no = Nein
not-found = Dieser Link existiert nicht, oder wurde gelöscht. not-found = Dieser Link existiert nicht, oder wurde gelöscht.
@ -17,6 +20,7 @@ link-code = Link Code
shortlink = Shortlink shortlink = Shortlink
qr-code = QR-code qr-code = QR-code
search-placeholder = Filtern nach... search-placeholder = Filtern nach...
really-delete = Wollen Sie {$code} wirklich löschen?
danger-zone = Achtung! danger-zone = Achtung!
danger-zone-text = Verändern Sie den Code von bereits veröffentlichten Links nicht. Sollte es dennoch geschehen werden veröffentlichte links unbenutzbar. Wird das Linkziel verändert, so zeigen auch die bereits veröffentlichten Links auf das neue Ziel. danger-zone-text = Verändern Sie den Code von bereits veröffentlichten Links nicht. Sollte es dennoch geschehen werden veröffentlichte links unbenutzbar. Wird das Linkziel verändert, so zeigen auch die bereits veröffentlichten Links auf das neue Ziel.

View File

@ -3,8 +3,11 @@ add-link = Add a new link
invite-user = Invite a new user invite-user = Invite a new user
list-users = List of existing users list-users = List of existing users
welcome-user = Welcome {$username} welcome-user = Welcome {$username}
welcome = Welcome
logout = Logout logout = Logout
login = Login login = Login
yes = Ja
no = Nein
not-found = This Link has not been found or has been deleted not-found = This Link has not been found or has been deleted
@ -16,6 +19,7 @@ link-target = Link target
link-code = Link code link-code = Link code
shortlink = Shortlink shortlink = Shortlink
search-placeholder = Filter according to... search-placeholder = Filter according to...
really-delete = Do you really want to delete {$code}?
danger-zone = Danger Zone! danger-zone = Danger Zone!
danger-zone-text = Do not change the code of links that are published. If you do so the published links will become invalid! If you change the target the published links will point to the new target. danger-zone-text = Do not change the code of links that are published. If you do so the published links will become invalid! If you change the target the published links will point to the new target.

View File

@ -5,6 +5,7 @@ pub mod pages;
use pages::list_links; use pages::list_links;
use pages::list_users; use pages::list_users;
use seed::{div, log, prelude::*, App, Url, C}; use seed::{div, log, prelude::*, App, Url, C};
use shared::datatypes::User;
use crate::i18n::{I18n, Lang}; use crate::i18n::{I18n, Lang};
@ -14,6 +15,7 @@ use crate::i18n::{I18n, Lang};
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model { fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::UrlChanged); orders.subscribe(Msg::UrlChanged);
orders.send_msg(Msg::GetLoggedUser);
log!(url); log!(url);
@ -24,6 +26,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
base_url: Url::new().add_path_part("app"), base_url: Url::new().add_path_part("app"),
page: Page::init(url, orders, lang.clone()), page: Page::init(url, orders, lang.clone()),
i18n: lang, i18n: lang,
user: None,
} }
} }
@ -37,6 +40,7 @@ struct Model {
base_url: Url, base_url: Url,
page: Page, page: Page,
i18n: i18n::I18n, i18n: i18n::I18n,
user: Option<User>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -76,6 +80,8 @@ pub enum Msg {
UrlChanged(subs::UrlChanged), UrlChanged(subs::UrlChanged),
ListLinks(list_links::Msg), ListLinks(list_links::Msg),
ListUsers(list_users::Msg), ListUsers(list_users::Msg),
GetLoggedUser,
UserReceived(User),
NoMessage, NoMessage,
} }
@ -97,6 +103,29 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
} }
Msg::NoMessage => (), Msg::NoMessage => (),
Msg::GetLoggedUser => {
orders.skip(); // No need to rerender/ complicated way to move into the closure
orders.perform_cmd(async {
let response = fetch(
Request::new("/admin/json/get_logged_user/")
.method(Method::Post)
.json(&())
.expect("serialization failed"),
)
.await
.expect("HTTP request failed");
let user: User = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
.await
.expect("deserialization failed");
Msg::UserReceived(user)
});
}
Msg::UserReceived(user) => model.user = Some(user),
} }
} }
@ -140,6 +169,10 @@ impl<'a> Urls<'a> {
self.base_url().add_path_part("list_links") self.base_url().add_path_part("list_links")
} }
#[must_use] #[must_use]
pub fn create_link(self) -> Url {
self.list_links().add_path_part("create_link")
}
#[must_use]
pub fn list_users(self) -> Url { pub fn list_users(self) -> Url {
self.base_url().add_path_part("list_users") self.base_url().add_path_part("list_users")
} }
@ -156,7 +189,7 @@ impl<'a> Urls<'a> {
fn view(model: &Model) -> Node<Msg> { fn view(model: &Model) -> Node<Msg> {
div![ div![
C!["page"], C!["page"],
navigation::navigation(&model.i18n, &model.base_url,), navigation::navigation(&model.i18n, &model.base_url, &model.user),
view_content(&model.page, &model.base_url), view_content(&model.page, &model.base_url),
] ]
} }

View File

@ -1,29 +1,34 @@
use fluent::fluent_args; use fluent::fluent_args;
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url}; use seed::{a, attrs, div, li, nav, ol, prelude::*, Url};
use shared::datatypes::User;
use crate::{i18n::I18n, Msg}; use crate::{i18n::I18n, Msg};
#[must_use] #[must_use]
pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> { pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option<User>) -> Node<Msg> {
let username = fluent_args![ "username" => "enaut"]; let t = move |key: &str| i18n.translate(key, None);
macro_rules! t { let welcome = if let Some(user) = user {
{ $key:expr } => { i18n.translate(
{ "welcome-user",
i18n.translate($key, None) Some(&fluent_args![ "username" => user.username.clone()]),
} )
} else {
t("welcome")
}; };
{ $key:expr, $args:expr } => {
{
i18n.translate($key, Some(&$args))
}
};
}
nav![ nav![
ol![ ol![
li![a![ li![a![
attrs! {At::Href => crate::Urls::new(base_url).list_links()}, attrs! {At::Href => crate::Urls::new(base_url).list_links()},
t!("list-links"), t("list-links"),
],],
li![a![
attrs! {At::Href => crate::Urls::new(base_url).create_link()},
ev(Ev::Click, |_| Msg::ListLinks(
super::pages::list_links::Msg::Edit(
super::pages::list_links::EditMsg::CreateNewLink
)
)),
t("add-link"),
],], ],],
li![a![ev(Ev::Click, |_| Msg::NoMessage), t!("add-link"),],],
li![a![ li![a![
attrs! {At::Href => crate::Urls::new(base_url).create_user()}, attrs! {At::Href => crate::Urls::new(base_url).create_user()},
ev(Ev::Click, |_| Msg::ListUsers( ev(Ev::Click, |_| Msg::ListUsers(
@ -31,19 +36,19 @@ pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> {
super::pages::list_users::UserEditMsg::CreateNewUser super::pages::list_users::UserEditMsg::CreateNewUser
) )
)), )),
t!("invite-user"), t("invite-user"),
],], ],],
li![a![ li![a![
attrs! {At::Href => crate::Urls::new(base_url).list_users()}, attrs! {At::Href => crate::Urls::new(base_url).list_users()},
t!("list-users"), t("list-users"),
],], ],],
], ],
ol![ ol![
li![div![t!("welcome-user", username)]], li![div![welcome]],
li![a![ li![a![
attrs! {At::Href => "#"}, attrs! {At::Href => "#"},
ev(Ev::Click, |_| Msg::NoMessage), ev(Ev::Click, |_| Msg::NoMessage),
t!("logout"), t("logout"),
]] ]]
] ]
] ]

View File

@ -1,25 +1,50 @@
use std::cell::RefCell;
use enum_map::EnumMap; use enum_map::EnumMap;
use seed::{a, attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C}; use fluent::fluent_args;
use seed::{
a, attrs, button, div, h1, img, input, log, prelude::*, section, span, table, td, th, tr, Url,
C,
};
use shared::{ use shared::{
apirequests::general::Ordering, apirequests::general::Ordering,
apirequests::{ apirequests::{
general::Operation, general::{EditMode, Message, Operation, Status},
links::{LinkOverviewColumns, LinkRequestForm}, links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
}, },
datatypes::FullLink, datatypes::FullLink,
}; };
use crate::i18n::I18n; use crate::i18n::I18n;
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model { /// Unwrap a result and return it's content, or return from the function with another expression.
orders.send_msg(Msg::Fetch); macro_rules! unwrap_or_return {
( $e:expr, $result:expr) => {
match $e {
Ok(x) => x,
Err(_) => return $result,
}
};
}
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
log!(url);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
let edit_link = match url.next_path_part() {
Some("create_link") => Some(RefCell::new(LinkDelta::default())),
None | Some(_) => None,
};
log!(edit_link);
Model { Model {
links: Vec::new(), links: Vec::new(),
i18n, i18n,
formconfig: LinkRequestForm::default(), formconfig: LinkRequestForm::default(),
inputs: EnumMap::default(), inputs: EnumMap::default(),
edit_link,
last_message: None,
question: None,
} }
} }
@ -29,6 +54,9 @@ pub struct Model {
i18n: I18n, i18n: I18n,
formconfig: LinkRequestForm, formconfig: LinkRequestForm,
inputs: EnumMap<LinkOverviewColumns, FilterInput>, inputs: EnumMap<LinkOverviewColumns, FilterInput>,
edit_link: Option<RefCell<LinkDelta>>,
last_message: Option<Status>,
question: Option<EditMsg>,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@ -38,6 +66,14 @@ struct FilterInput {
#[derive(Clone)] #[derive(Clone)]
pub enum Msg { pub enum Msg {
Query(QueryMsg),
Edit(EditMsg),
ClearAll,
SetMessage(String),
}
#[derive(Clone)]
pub enum QueryMsg {
Fetch, Fetch,
OrderBy(LinkOverviewColumns), OrderBy(LinkOverviewColumns),
Received(Vec<FullLink>), Received(Vec<FullLink>),
@ -46,36 +82,51 @@ pub enum Msg {
TargetFilterChanged(String), TargetFilterChanged(String),
AuthorFilterChanged(String), AuthorFilterChanged(String),
} }
/// All the messages on link editing
#[derive(Clone, Debug)]
pub enum EditMsg {
EditSelected(LinkDelta),
CreateNewLink,
Created(Status),
EditCodeChanged(String),
EditDescriptionChanged(String),
EditTargetChanged(String),
MayDeleteSelected(LinkDelta),
DeleteSelected(LinkDelta),
SaveLink,
FailedToCreateLink,
FailedToDeleteLink,
DeletedLink(Status),
}
/// # Panics /// # Panics
/// Sould only panic on bugs. /// Sould only panic on bugs.
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::Fetch => { Msg::Query(msg) => process_query_messages(msg, model, orders),
orders.skip(); // No need to rerender Msg::Edit(msg) => process_edit_messages(msg, model, orders),
let data = model.formconfig.clone(); // complicated way to move into the closure Msg::ClearAll => {
orders.perform_cmd(async { model.edit_link = None;
let data = data; model.last_message = None;
let response = fetch( model.question = None;
Request::new("/admin/json/list_links/")
.method(Method::Post)
.json(&data)
.expect("serialization failed"),
)
.await
.expect("HTTP request failed");
let user: Vec<FullLink> = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
.await
.expect("deserialization failed");
Msg::Received(user)
});
} }
Msg::OrderBy(column) => { Msg::SetMessage(msg) => {
model.edit_link = None;
model.question = None;
model.last_message = Some(Status::Error(Message { message: msg }));
}
}
}
/// # Panics
/// Sould only panic on bugs.
pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
QueryMsg::Fetch => {
orders.skip(); // No need to rerender
load_links(model, orders)
}
QueryMsg::OrderBy(column) => {
model.formconfig.order = model.formconfig.order.as_ref().map_or_else( model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|| { || {
Some(Operation { Some(Operation {
@ -94,7 +145,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}) })
}, },
); );
orders.send_msg(Msg::Fetch); orders.send_msg(Msg::Query(QueryMsg::Fetch));
model.links.sort_by(match column { model.links.sort_by(match column {
LinkOverviewColumns::Code => { LinkOverviewColumns::Code => {
@ -114,44 +165,231 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
}) })
} }
Msg::Received(response) => { QueryMsg::Received(response) => {
model.links = response; model.links = response;
} }
Msg::CodeFilterChanged(s) => { QueryMsg::CodeFilterChanged(s) => {
log!("Filter is: ", &s); log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect(); let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Code].sieve = sanit; model.formconfig.filter[LinkOverviewColumns::Code].sieve = sanit;
orders.send_msg(Msg::Fetch); orders.send_msg(Msg::Query(QueryMsg::Fetch));
} }
Msg::DescriptionFilterChanged(s) => { QueryMsg::DescriptionFilterChanged(s) => {
log!("Filter is: ", &s); log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect(); let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Description].sieve = sanit; model.formconfig.filter[LinkOverviewColumns::Description].sieve = sanit;
orders.send_msg(Msg::Fetch); orders.send_msg(Msg::Query(QueryMsg::Fetch));
} }
Msg::TargetFilterChanged(s) => { QueryMsg::TargetFilterChanged(s) => {
log!("Filter is: ", &s); log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect(); let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Target].sieve = sanit; model.formconfig.filter[LinkOverviewColumns::Target].sieve = sanit;
orders.send_msg(Msg::Fetch); orders.send_msg(Msg::Query(QueryMsg::Fetch));
} }
Msg::AuthorFilterChanged(s) => { QueryMsg::AuthorFilterChanged(s) => {
log!("Filter is: ", &s); log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect(); let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit; model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit;
orders.send_msg(Msg::Fetch); orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
}
}
fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
let data = model.formconfig.clone(); // complicated way to move into the closure
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new("/admin/json/list_links/")
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to parse data".to_string())
);
let response = unwrap_or_return!(
fetch(request).await,
Msg::SetMessage("Failed to send data".to_string())
);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let links: Vec<FullLink> = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response".to_string())
);
Msg::Query(QueryMsg::Received(links))
});
}
pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
EditMsg::EditSelected(link) => {
log!("Editing link: ", link);
model.last_message = None;
model.edit_link = Some(RefCell::new(link))
}
EditMsg::CreateNewLink => {
log!("Create new link!");
model.edit_link = Some(RefCell::new(LinkDelta::default()))
}
EditMsg::Created(success_msg) => {
model.last_message = Some(success_msg);
model.edit_link = None;
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
EditMsg::EditCodeChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").code = s;
}
}
EditMsg::EditDescriptionChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").title = s;
}
}
EditMsg::EditTargetChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut()
.expect("Failed to borrow mutably")
.target = s;
}
}
EditMsg::SaveLink => {
save_link(model, orders);
}
EditMsg::FailedToCreateLink => {
log!("Failed to create Link");
}
link @ EditMsg::MayDeleteSelected(..) => {
log!("Deleting link: ", link);
model.last_message = None;
model.edit_link = None;
model.question = Some(link)
}
EditMsg::DeleteSelected(link) => {
orders.perform_cmd(async {
let data = link;
let response = unwrap_or_return!(
fetch(
Request::new("/admin/json/delete_link/")
.method(Method::Post)
.json(&data)
.expect("serialization failed"),
)
.await,
Msg::Edit(EditMsg::FailedToDeleteLink)
);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code!".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage(
"Failed to parse the response the link might be deleted however!"
.to_string()
)
);
Msg::Edit(EditMsg::DeletedLink(message))
});
}
EditMsg::FailedToDeleteLink => {
log!("Failed to delete Link");
}
EditMsg::DeletedLink(message) => {
log!("Deleted link", message);
} }
} }
} }
fn save_link(model: &Model, orders: &mut impl Orders<Msg>) {
let data = model
.edit_link
.as_ref()
.expect("should exist!")
.borrow()
.clone();
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new(match data.edit {
EditMode::Create => "/admin/json/create_link/",
EditMode::Edit => "/admin/json/edit_link/",
})
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to encode the link!".to_string())
);
let response =
unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToCreateLink));
log!(response);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response!".to_string())
);
Msg::Edit(EditMsg::Created(message))
});
}
#[must_use] #[must_use]
/// # Panics
/// Sould only panic on bugs.
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let lang = model.i18n.clone(); let lang = &model.i18n.clone();
let t = move |key: &str| lang.translate(key, None); let t = move |key: &str| lang.translate(key, None);
section![ section![
h1!("List Links Page from list_links"), if let Some(message) = &model.last_message {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
match message {
Status::Success(m) | Status::Error(m) => &m.message,
}
]
} else {
section![]
},
if let Some(question) = &model.question {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
if let EditMsg::MayDeleteSelected(l) = question.clone() {
div![
lang.translate(
"really-delete",
Some(&fluent_args!["code" => l.code.clone()])
),
a![t("no"), C!["button"], ev(Ev::Click, |_| Msg::ClearAll)],
a![
t("yes"),
C!["button"],
ev(Ev::Click, move |_| Msg::Edit(EditMsg::DeleteSelected(l)))
]
]
} else {
span!()
}
]
} else {
section![]
},
table![ table![
// Add the headlines // Add the headlines
view_link_table_head(&t), view_link_table_head(&t),
@ -160,34 +398,51 @@ pub fn view(model: &Model) -> Node<Msg> {
// Add all the content lines // Add all the content lines
model.links.iter().map(view_link) model.links.iter().map(view_link)
], ],
button![ev(Ev::Click, |_| Msg::Fetch), "Fetch links"] button![
ev(Ev::Click, |_| Msg::Query(QueryMsg::Fetch)),
"Fetch links"
],
if let Some(l) = &model.edit_link {
edit_or_create_link(l, t)
} else {
section!()
}
] ]
} }
fn view_link_table_head<F: Fn(&str) -> String>(t: F) -> Node<Msg> { fn view_link_table_head<F: Fn(&str) -> String>(t: F) -> Node<Msg> {
tr![ tr![
th![ th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Code)), ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Code
))),
t("link-code") t("link-code")
], ],
th![ th![
ev(Ev::Click, |_| Msg::OrderBy( ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Description LinkOverviewColumns::Description
)), ))),
t("link-description") t("link-description")
], ],
th![ th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Target)), ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Target
))),
t("link-target") t("link-target")
], ],
th![ th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Author)), ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Author
))),
t("username") t("username")
], ],
th![ th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Statistics)), ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Statistics
))),
t("statistics") t("statistics")
] ],
th![]
] ]
} }
@ -200,7 +455,7 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search", At::Type => "search",
At::Placeholder => t("search-placeholder") At::Placeholder => t("search-placeholder")
}, },
input_ev(Ev::Input, Msg::CodeFilterChanged), input_ev(Ev::Input, |s| Msg::Query(QueryMsg::CodeFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Code].filter_input), el_ref(&model.inputs[LinkOverviewColumns::Code].filter_input),
]], ]],
td![input![ td![input![
@ -210,7 +465,9 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search", At::Type => "search",
At::Placeholder => t("search-placeholder") At::Placeholder => t("search-placeholder")
}, },
input_ev(Ev::Input, Msg::DescriptionFilterChanged), input_ev(Ev::Input, |s| Msg::Query(
QueryMsg::DescriptionFilterChanged(s)
)),
el_ref(&model.inputs[LinkOverviewColumns::Description].filter_input), el_ref(&model.inputs[LinkOverviewColumns::Description].filter_input),
]], ]],
td![input![ td![input![
@ -220,7 +477,7 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search", At::Type => "search",
At::Placeholder => t("search-placeholder") At::Placeholder => t("search-placeholder")
}, },
input_ev(Ev::Input, Msg::TargetFilterChanged), input_ev(Ev::Input, |s| Msg::Query(QueryMsg::TargetFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Target].filter_input), el_ref(&model.inputs[LinkOverviewColumns::Target].filter_input),
]], ]],
td![input![ td![input![
@ -230,19 +487,114 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search", At::Type => "search",
At::Placeholder => t("search-placeholder") At::Placeholder => t("search-placeholder")
}, },
input_ev(Ev::Input, Msg::AuthorFilterChanged), input_ev(Ev::Input, |s| Msg::Query(QueryMsg::AuthorFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input), el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input),
]], ]],
td![] td![],
td![],
] ]
} }
fn view_link(l: &FullLink) -> Node<Msg> { fn view_link(l: &FullLink) -> Node<Msg> {
// Ugly hack
let link = LinkDelta::from(l.clone());
let link2 = LinkDelta::from(l.clone());
let link3 = LinkDelta::from(l.clone());
let link4 = LinkDelta::from(l.clone());
let link5 = LinkDelta::from(l.clone());
tr![ tr![
td![&l.link.code], {
td![&l.link.title], td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link))),
&l.link.code
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link2))),
&l.link.title
]
},
td![a![attrs![At::Href => &l.link.target], &l.link.target]], td![a![attrs![At::Href => &l.link.target], &l.link.target]],
td![&l.user.username], {
td![&l.clicks.number] td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link3))),
&l.user.username
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link4))),
&l.clicks.number
]
},
{
td![img![
ev(Ev::Click, |_| Msg::Edit(EditMsg::MayDeleteSelected(link5))),
C!["trashicon"],
attrs!(At::Src => "/static/trash.svg")
]]
},
]
}
fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> Node<Msg> {
let link = l.borrow();
div![
C!["editdialog", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
h1![match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
}],
table![
tr![
th![t("link-description")],
td![input![
attrs! {
At::Value => &link.title,
At::Type => "text",
At::Placeholder => t("link-description")
},
input_ev(Ev::Input, |s| {
Msg::Edit(EditMsg::EditDescriptionChanged(s))
}),
]]
],
tr![
th![t("link-target")],
td![input![
attrs! {
At::Value => &link.target,
At::Type => "text",
At::Placeholder => t("link-target")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditTargetChanged(s)) }),
]]
],
tr![
th![t("link-code")],
td![input![
attrs! {
At::Value => &link.code,
At::Type => "text",
At::Placeholder => t("password")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditCodeChanged(s)) }),
],]
]
],
a![
match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
},
C!["button"],
ev(Ev::Click, |_| Msg::Edit(EditMsg::SaveLink))
]
] ]
} }

View File

@ -7,7 +7,7 @@ use seed::{
use shared::{ use shared::{
apirequests::general::{Operation, Ordering}, apirequests::general::{Operation, Ordering},
apirequests::{ apirequests::{
general::{EditMode, SuccessMessage}, general::{EditMode, Status},
users::{UserDelta, UserOverviewColumns, UserRequestForm}, users::{UserDelta, UserOverviewColumns, UserRequestForm},
}, },
datatypes::User, datatypes::User,
@ -37,7 +37,7 @@ pub struct Model {
formconfig: UserRequestForm, formconfig: UserRequestForm,
inputs: EnumMap<UserOverviewColumns, FilterInput>, inputs: EnumMap<UserOverviewColumns, FilterInput>,
user_edit: Option<RefCell<UserDelta>>, user_edit: Option<RefCell<UserDelta>>,
last_message: Option<SuccessMessage>, last_message: Option<Status>,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@ -68,7 +68,7 @@ pub enum UserQueryMsg {
pub enum UserEditMsg { pub enum UserEditMsg {
EditUserSelected(UserDelta), EditUserSelected(UserDelta),
CreateNewUser, CreateNewUser,
UserCreated(SuccessMessage), UserCreated(Status),
EditUsernameChanged(String), EditUsernameChanged(String),
EditEmailChanged(String), EditEmailChanged(String),
EditPasswordChanged(String), EditPasswordChanged(String),
@ -236,7 +236,7 @@ pub fn process_user_edit_messages(
Err(_) => return Msg::Edit(UserEditMsg::FailedToCreateUser), Err(_) => return Msg::Edit(UserEditMsg::FailedToCreateUser),
}; };
let message: SuccessMessage = response let message: Status = response
.check_status() // ensure we've got 2xx status .check_status() // ensure we've got 2xx status
.expect("status check failed") .expect("status check failed")
.json() .json()
@ -268,7 +268,6 @@ pub fn view(model: &Model) -> Node<Msg> {
keyboard_ev(Ev::KeyDown, |keyboard_event| { keyboard_ev(Ev::KeyDown, |keyboard_event| {
IF!(keyboard_event.key() == "Escape" => Msg::ClearAll) IF!(keyboard_event.key() == "Escape" => Msg::ClearAll)
}), }),
h1!("List Users Page from list_users"),
if let Some(message) = &model.last_message { if let Some(message) = &model.last_message {
div![ div![
C!["message", "center"], C!["message", "center"],
@ -277,7 +276,11 @@ pub fn view(model: &Model) -> Node<Msg> {
a!["\u{d7}"], a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll) ev(Ev::Click, |_| Msg::ClearAll)
], ],
&message.message match message {
Status::Success(m) | Status::Error(m) => {
&m.message
}
}
] ]
} else { } else {
section![] section![]

View File

@ -3,7 +3,7 @@ use clap::{
ArgMatches, SubCommand, ArgMatches, SubCommand,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use shared::datatypes::User; use shared::datatypes::{Secret, User};
use sqlx::{migrate::Migrator, Pool, Sqlite}; use sqlx::{migrate::Migrator, Pool, Sqlite};
use std::{ use std::{
fs::File, fs::File,
@ -151,7 +151,7 @@ async fn parse_args_to_config(config: ArgMatches<'_>) -> ServerConfig {
} else { } else {
secret secret
}; };
let secret = pslink::Secret::new(secret); let secret = Secret::new(secret);
let db = config let db = config
.value_of("database") .value_of("database")
.expect(concat!( .expect(concat!(

View File

@ -11,6 +11,7 @@ use actix_web::HttpResponse;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use fluent_templates::{static_loader, FluentLoader}; use fluent_templates::{static_loader, FluentLoader};
use qrcode::types::QrError; use qrcode::types::QrError;
use shared::datatypes::Secret;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::{fmt::Display, path::PathBuf, str::FromStr}; use std::{fmt::Display, path::PathBuf, str::FromStr};
use tera::Tera; use tera::Tera;
@ -156,30 +157,6 @@ impl FromStr for Protocol {
} }
} }
#[derive(Clone)]
pub struct Secret {
secret: String,
}
impl Secret {
#[must_use]
pub const fn new(secret: String) -> Self {
Self { secret }
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServerConfig { pub struct ServerConfig {
pub secret: Secret, pub secret: Secret,
@ -209,7 +186,13 @@ impl ServerConfig {
"# If it is changed all existing passwords are invalid.\n" "# If it is changed all existing passwords are invalid.\n"
) )
.to_owned(), .to_owned(),
format!("PSLINK_SECRET=\"{}\"\n", self.secret.secret), format!(
"PSLINK_SECRET=\"{}\"\n",
self.secret
.secret
.as_ref()
.expect("A Secret was not specified!")
),
] ]
} }
} }
@ -370,6 +353,18 @@ pub async fn webservice(
.service( .service(
web::scope("/json") web::scope("/json")
.route("/list_links/", web::post().to(views::index_json)) .route("/list_links/", web::post().to(views::index_json))
.route(
"/create_link/",
web::post().to(views::process_create_link_json),
)
.route(
"/edit_link/",
web::post().to(views::process_update_link_json),
)
.route(
"/delete_link/",
web::post().to(views::process_delete_link_json),
)
.route("/list_users/", web::post().to(views::index_users_json)) .route("/list_users/", web::post().to(views::index_users_json))
.route( .route(
"/create_user/", "/create_user/",
@ -378,6 +373,10 @@ pub async fn webservice(
.route( .route(
"/update_user/", "/update_user/",
web::post().to(views::process_update_user_json), web::post().to(views::process_update_user_json),
)
.route(
"/get_logged_user/",
web::post().to(views::get_logged_user_json),
), ),
) )
// login to the admin area // login to the admin area

View File

@ -5,7 +5,12 @@ use async_trait::async_trait;
use dotenv::dotenv; use dotenv::dotenv;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use shared::datatypes::{Count, Link, User}; use shared::{
apirequests::links::LinkDelta,
datatypes::{Count, Link, User},
};
use sqlx::Row;
use tracing::{error, info, instrument};
#[async_trait] #[async_trait]
pub trait UserDbOperations<T> { pub trait UserDbOperations<T> {
@ -24,10 +29,19 @@ pub trait UserDbOperations<T> {
#[async_trait] #[async_trait]
impl UserDbOperations<Self> for User { impl UserDbOperations<Self> for User {
#[instrument()]
async fn get_user(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> { async fn get_user(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users where id = ? ", id) let user = sqlx::query!("Select * from users where id = ? ", id)
.fetch_one(&server_config.db_pool) .fetch_one(&server_config.db_pool)
.await; .await
.map(|row| Self {
id: row.id,
username: row.username,
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
});
user.map_err(ServerError::Database) user.map_err(ServerError::Database)
} }
@ -35,23 +49,46 @@ impl UserDbOperations<Self> for User {
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the user does not exist or the database cannot be acessed. /// fails with [`ServerError`] if the user does not exist or the database cannot be acessed.
#[instrument()]
async fn get_user_by_name( async fn get_user_by_name(
name: &str, name: &str,
server_config: &ServerConfig, server_config: &ServerConfig,
) -> Result<Self, ServerError> { ) -> Result<Self, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users where username = ? ", name) let user = sqlx::query!("Select * from users where username = ? ", name)
.fetch_one(&server_config.db_pool) .fetch_one(&server_config.db_pool)
.await; .await
.map(|row| Self {
id: row.id,
username: row.username,
email: row.email,
password: Secret::new(row.password),
role: row.role,
language: row.language,
});
user.map_err(ServerError::Database) user.map_err(ServerError::Database)
} }
#[instrument()]
async fn get_all_users(server_config: &ServerConfig) -> Result<Vec<Self>, ServerError> { async fn get_all_users(server_config: &ServerConfig) -> Result<Vec<Self>, ServerError> {
let user = sqlx::query_as!(Self, "Select * from users") let user = sqlx::query("Select * from users")
.fetch_all(&server_config.db_pool) .fetch_all(&server_config.db_pool)
.await; .await
.map(|row| {
row.into_iter()
.map(|r| Self {
id: r.get("id"),
username: r.get("username"),
email: r.get("email"),
password: Secret::new(r.get("password")),
role: r.get("role"),
language: r.get("language"),
})
.collect()
});
user.map_err(ServerError::Database) user.map_err(ServerError::Database)
} }
#[instrument()]
async fn update_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> { async fn update_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!( sqlx::query!(
"UPDATE users SET "UPDATE users SET
@ -61,7 +98,7 @@ impl UserDbOperations<Self> for User {
role = ? where id = ?", role = ? where id = ?",
self.username, self.username,
self.email, self.email,
self.password, self.password.secret,
self.role, self.role,
self.id self.id
) )
@ -69,10 +106,12 @@ impl UserDbOperations<Self> for User {
.await?; .await?;
Ok(()) Ok(())
} }
/// Change an admin user to normal user and a normal user to admin /// Change an admin user to normal user and a normal user to admin
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the database cannot be acessed. (the user should exist) /// fails with [`ServerError`] if the database cannot be acessed. (the user should exist)
#[instrument()]
async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> { async fn toggle_admin(self, server_config: &ServerConfig) -> Result<(), ServerError> {
let new_role = 2 - (self.role + 1) % 2; let new_role = 2 - (self.role + 1) % 2;
sqlx::query!("UPDATE users SET role = ? where id = ?", new_role, self.id) sqlx::query!("UPDATE users SET role = ? where id = ?", new_role, self.id)
@ -81,6 +120,7 @@ impl UserDbOperations<Self> for User {
Ok(()) Ok(())
} }
#[instrument()]
async fn set_language( async fn set_language(
self, self,
server_config: &ServerConfig, server_config: &ServerConfig,
@ -102,6 +142,7 @@ impl UserDbOperations<Self> for User {
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the database cannot be acessed. /// fails with [`ServerError`] if the database cannot be acessed.
#[instrument()]
async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> { async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> {
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2") let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&server_config.db_pool) .fetch_one(&server_config.db_pool)
@ -122,6 +163,7 @@ impl NewUser {
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the password could not be encrypted. /// fails with [`ServerError`] if the password could not be encrypted.
#[instrument()]
pub fn new( pub fn new(
username: String, username: String,
email: String, email: String,
@ -137,12 +179,13 @@ impl NewUser {
}) })
} }
#[instrument()]
pub(crate) fn hash_password(password: &str, secret: &Secret) -> Result<String, ServerError> { pub(crate) fn hash_password(password: &str, secret: &Secret) -> Result<String, ServerError> {
dotenv().ok(); dotenv().ok();
let hash = Hasher::default() let hash = Hasher::default()
.with_password(password) .with_password(password)
.with_secret_key(&secret.secret) .with_secret_key(secret.secret.as_ref().expect("A secret key was not given"))
.hash()?; .hash()?;
Ok(hash) Ok(hash)
@ -152,6 +195,7 @@ impl NewUser {
/// ///
/// # Errors /// # Errors
/// fails with [`ServerError`] if the database cannot be acessed. /// fails with [`ServerError`] if the database cannot be acessed.
#[instrument()]
pub async fn insert_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> { pub async fn insert_user(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!( sqlx::query!(
"Insert into users ( "Insert into users (
@ -178,6 +222,7 @@ pub struct LoginUser {
#[async_trait] #[async_trait]
pub trait LinkDbOperations<T> { pub trait LinkDbOperations<T> {
async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>; async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result<T, ServerError>;
async fn get_link_by_id(id: i64, server_config: &ServerConfig) -> Result<T, ServerError>;
async fn delete_link_by_code( async fn delete_link_by_code(
code: &str, code: &str,
server_config: &ServerConfig, server_config: &ServerConfig,
@ -187,6 +232,7 @@ pub trait LinkDbOperations<T> {
#[async_trait] #[async_trait]
impl LinkDbOperations<Self> for Link { impl LinkDbOperations<Self> for Link {
#[instrument()]
async fn get_link_by_code( async fn get_link_by_code(
code: &str, code: &str,
server_config: &ServerConfig, server_config: &ServerConfig,
@ -197,7 +243,16 @@ impl LinkDbOperations<Self> for Link {
tracing::info!("Found link: {:?}", &link); tracing::info!("Found link: {:?}", &link);
link.map_err(ServerError::Database) link.map_err(ServerError::Database)
} }
#[instrument()]
async fn get_link_by_id(id: i64, server_config: &ServerConfig) -> Result<Self, ServerError> {
let link = sqlx::query_as!(Self, "Select * from links where id = ? ", id)
.fetch_one(&server_config.db_pool)
.await;
tracing::info!("Found link: {:?}", &link);
link.map_err(ServerError::Database)
}
#[instrument()]
async fn delete_link_by_code( async fn delete_link_by_code(
code: &str, code: &str,
server_config: &ServerConfig, server_config: &ServerConfig,
@ -207,8 +262,11 @@ impl LinkDbOperations<Self> for Link {
.await?; .await?;
Ok(()) Ok(())
} }
#[instrument()]
async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError> { async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!( info!("{:?}", self);
let qry = sqlx::query!(
"UPDATE links SET "UPDATE links SET
title = ?, title = ?,
target = ?, target = ?,
@ -221,10 +279,15 @@ impl LinkDbOperations<Self> for Link {
self.author, self.author,
self.created_at, self.created_at,
self.id self.id
) );
.execute(&server_config.db_pool) match qry.execute(&server_config.db_pool).await {
.await?; Ok(_) => Ok(()),
Ok(()) Err(e) => {
//error!("{}", qry);
error!("{}", e);
Err(e.into())
}
}
} }
} }
@ -247,6 +310,15 @@ impl NewLink {
created_at: chrono::Local::now().naive_utc(), created_at: chrono::Local::now().naive_utc(),
} }
} }
pub(crate) fn from_link_delta(link: LinkDelta, uid: i64) -> Self {
Self {
title: link.title,
target: link.target,
code: link.code,
author: uid,
created_at: chrono::Local::now().naive_utc(),
}
}
pub(crate) async fn insert(self, server_config: &ServerConfig) -> Result<(), ServerError> { pub(crate) async fn insert(self, server_config: &ServerConfig) -> Result<(), ServerError> {
sqlx::query!( sqlx::query!(

View File

@ -5,10 +5,10 @@ use serde::Serialize;
use shared::{ use shared::{
apirequests::{ apirequests::{
general::{EditMode, Filter, Operation, Ordering}, general::{EditMode, Filter, Operation, Ordering},
links::{LinkOverviewColumns, LinkRequestForm}, links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
users::{UserDelta, UserOverviewColumns, UserRequestForm}, users::{UserDelta, UserOverviewColumns, UserRequestForm},
}, },
datatypes::{Count, FullLink, Link, User}, datatypes::{Count, FullLink, Link, Secret, User},
}; };
use sqlx::Row; use sqlx::Row;
use tracing::{info, instrument, warn}; use tracing::{info, instrument, warn};
@ -127,7 +127,7 @@ pub async fn list_all_allowed(
id: v.get("usid"), id: v.get("usid"),
username: v.get("usern"), username: v.get("usern"),
email: v.get("uemail"), email: v.get("uemail"),
password: "invalid".to_owned(), password: Secret::new("invalid".to_string()),
role: v.get("urole"), role: v.get("urole"),
language: v.get("ulang"), language: v.get("ulang"),
}, },
@ -240,7 +240,7 @@ pub async fn list_users(
id: v.get("id"), id: v.get("id"),
username: v.get("username"), username: v.get("username"),
email: v.get("email"), email: v.get("email"),
password: "invalid".to_owned(), password: Secret::new("".to_string()),
role: v.get("role"), role: v.get("role"),
language: v.get("language"), language: v.get("language"),
}) })
@ -458,7 +458,9 @@ pub async fn update_user_json(
Role::Admin { .. } | Role::Regular { .. } => { Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: "); info!("Updating userinfo: ");
let password = match &data.password { let password = match &data.password {
Some(password) => NewUser::hash_password(password, &server_config.secret)?, Some(password) => {
Secret::new(NewUser::hash_password(password, &server_config.secret)?)
}
None => unmodified_user.password, None => unmodified_user.password,
}; };
let new_user = User { let new_user = User {
@ -510,7 +512,10 @@ pub async fn update_user(
Role::Admin { .. } | Role::Regular { .. } => { Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: "); info!("Updating userinfo: ");
let password = if data.password.len() > 3 { let password = if data.password.len() > 3 {
NewUser::hash_password(&data.password, &server_config.secret)? Secret::new(NewUser::hash_password(
&data.password,
&server_config.secret,
)?)
} else { } else {
unmodified_user.password unmodified_user.password
}; };
@ -629,6 +634,28 @@ pub async fn get_link(
} }
} }
/// Get one link if permissions are accordingly.
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(id))]
pub async fn get_link_by_id(
id: &Identity,
lid: i64,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
match authenticate(id, server_config).await? {
Role::Admin { user } | Role::Regular { user } => {
let link = Link::get_link_by_id(lid, server_config).await?;
Ok(Item { user, item: link })
}
Role::Disabled | Role::NotAuthenticated => {
warn!("User could not be authenticated!");
Err(ServerError::User("Not Allowed".to_owned()))
}
}
}
/// Get link **without authentication** /// Get link **without authentication**
/// ///
/// # Errors /// # Errors
@ -743,3 +770,73 @@ pub async fn create_link(
} }
} }
} }
/// Create a new link
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(id))]
pub async fn create_link_json(
id: &Identity,
data: web::Json<LinkDelta>,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
let auth = authenticate(id, server_config).await?;
match auth {
Role::Admin { user } | Role::Regular { user } => {
let code = data.code.clone();
info!("Creating link for: {}", &code);
let new_link = NewLink::from_link_delta(data.into_inner(), user.id);
info!("Creating link for: {:?}", &new_link);
new_link.insert(server_config).await?;
let new_link: Link = get_link_simple(&code, server_config).await?;
Ok(Item {
user,
item: new_link,
})
}
Role::Disabled | Role::NotAuthenticated => {
Err(ServerError::User("Permission denied!".to_owned()))
}
}
}
/// Update a link if the user is admin or it is its own link.
///
/// # Errors
/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions.
#[instrument(skip(ident))]
pub async fn update_link_json(
ident: &Identity,
data: web::Json<LinkDelta>,
server_config: &ServerConfig,
) -> Result<Item<Link>, ServerError> {
let auth = authenticate(ident, server_config).await?;
match auth {
Role::Admin { .. } | Role::Regular { .. } => {
if let Some(id) = data.id {
let query: Item<Link> = get_link_by_id(ident, id, server_config).await?;
if auth.admin_or_self(query.item.author) {
let mut link = query.item;
let LinkDelta {
title,
target,
code,
..
} = data.into_inner();
link.code = code.clone();
link.target = target;
link.title = title;
link.update_link(server_config).await?;
get_link(ident, &code, server_config).await
} else {
Err(ServerError::User("Invalid Request".to_owned()))
}
} else {
Err(ServerError::User("Not Allowed".to_owned()))
}
}
Role::Disabled | Role::NotAuthenticated => Err(ServerError::User("Not Allowed".to_owned())),
}
}

View File

@ -15,8 +15,8 @@ use image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::{render::svg, QrCode}; use qrcode::{render::svg, QrCode};
use queries::{authenticate, Role}; use queries::{authenticate, Role};
use shared::apirequests::{ use shared::apirequests::{
general::SuccessMessage, general::{Message, Status},
links::LinkRequestForm, links::{LinkDelta, LinkRequestForm},
users::{UserDelta, UserRequestForm}, users::{UserDelta, UserRequestForm},
}; };
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -171,6 +171,19 @@ pub async fn index_users_json(
} }
} }
pub async fn get_logged_user_json(
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let user = authenticate(&id, &config).await?;
match user {
Role::NotAuthenticated | Role::Disabled => {
Err(ServerError::User("User not logged in!".to_string()))
}
Role::Regular { user } | Role::Admin { user } => Ok(HttpResponse::Ok().json2(&user)),
}
}
#[instrument(skip(id, tera))] #[instrument(skip(id, tera))]
pub async fn view_link_empty( pub async fn view_link_empty(
tera: web::Data<Tera>, tera: web::Data<Tera>,
@ -358,9 +371,9 @@ pub async fn process_create_user_json(
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api"); info!("Listing Users to Json api");
match queries::create_user_json(&id, &form, &config).await { match queries::create_user_json(&id, &form, &config).await {
Ok(item) => Ok(HttpResponse::Ok().json2(&SuccessMessage { Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved user: {}", item.item.username), message: format!("Successfully saved user: {}", item.item.username),
})), }))),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
@ -373,9 +386,9 @@ pub async fn process_update_user_json(
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api"); info!("Listing Users to Json api");
match queries::update_user_json(&id, &form, &config).await { match queries::update_user_json(&id, &form, &config).await {
Ok(item) => Ok(HttpResponse::Ok().json2(&SuccessMessage { Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved user: {}", item.item.username), message: format!("Successfully saved user: {}", item.item.username),
})), }))),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
@ -447,11 +460,12 @@ pub async fn process_login(
match user { match user {
Ok(u) => { Ok(u) => {
if let Some(hash) = u.password.secret {
let secret = &config.secret; let secret = &config.secret;
let valid = Verifier::default() let valid = Verifier::default()
.with_hash(&u.password) .with_hash(hash)
.with_password(&data.password) .with_password(&data.password)
.with_secret_key(&secret.secret) .with_secret_key(secret.secret.as_ref().expect("No secret available"))
.verify()?; .verify()?;
if valid { if valid {
@ -462,6 +476,11 @@ pub async fn process_login(
} else { } else {
Ok(redirect_builder("/admin/login/")) Ok(redirect_builder("/admin/login/"))
} }
} else {
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
} }
Err(e) => { Err(e) => {
info!("Failed to login: {}", e); info!("Failed to login: {}", e);
@ -515,6 +534,36 @@ pub async fn redirect_empty(
Ok(redirect_builder(&config.empty_forward_url)) Ok(redirect_builder(&config.empty_forward_url))
} }
#[instrument(skip(id))]
pub async fn process_create_link_json(
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let new_link = queries::create_link_json(&id, data, &config).await;
match new_link {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully saved link: {}", item.item.code),
}))),
Err(e) => Err(e),
}
}
#[instrument(skip(id))]
pub async fn process_update_link_json(
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
let new_link = queries::update_link_json(&id, data, &config).await;
match new_link {
Ok(item) => Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully updated link: {}", item.item.code),
}))),
Err(e) => Err(e),
}
}
#[instrument(skip(id))] #[instrument(skip(id))]
pub async fn create_link( pub async fn create_link(
tera: web::Data<Tera>, tera: web::Data<Tera>,
@ -591,3 +640,14 @@ pub async fn process_link_delete(
queries::delete_link(&id, &link_code.0, &config).await?; queries::delete_link(&id, &link_code.0, &config).await?;
Ok(redirect_builder("/admin/login/")) Ok(redirect_builder("/admin/login/"))
} }
#[instrument(skip(id))]
pub async fn process_delete_link_json(
id: Identity,
config: web::Data<crate::ServerConfig>,
data: web::Json<LinkDelta>,
) -> Result<HttpResponse, ServerError> {
queries::delete_link(&id, &data.code, &config).await?;
Ok(HttpResponse::Ok().json2(&Status::Success(Message {
message: format!("Successfully deleted link: {}", &data.code),
})))
}

View File

@ -43,6 +43,7 @@ form {
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin-bottom: 10px;
} }
th, th,
@ -185,3 +186,11 @@ div.message {
border: 5px solid rgb(90, 90, 90); border: 5px solid rgb(90, 90, 90);
height: auto; height: auto;
} }
a {
cursor: pointer
}
img.trashicon {
width: 0.5cm;
}

19
pslink/static/trash.svg Normal file
View File

@ -0,0 +1,19 @@
<svg width="99.857mm" height="134.86mm" version="1.1" viewBox="0 0 99.857 134.86" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-58.021 -74.624)">
<path d="m65.101 94.716h84.426l-10.99 112.77h-62.431z" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:4;stroke:#000"/>
<path d="m107.57 113.88v74.637" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m86.154 113.62 5.4485 74.438" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m129.73 114.36-5.5378 74.432" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
<path d="m59.882 87.653h96.136" style="fill:none;stroke-linecap:round;stroke-width:3.721;stroke:#000"/>
<path d="m93.588 87.521c13.139-19.351 20.218-10.383 27.029-0.1697" style="fill:none;stroke-linecap:round;stroke-width:3;stroke:#000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,3 +1,5 @@
use shared::datatypes::Secret;
#[test] #[test]
fn test_help_of_command_for_breaking_changes() { fn test_help_of_command_for_breaking_changes() {
let output = test_bin::get_test_bin("pslink") let output = test_bin::get_test_bin("pslink")
@ -208,7 +210,7 @@ async fn run_server() {
); );
let server_config = pslink::ServerConfig { let server_config = pslink::ServerConfig {
secret: pslink::Secret::new("abcdefghijklmnopqrstuvw".to_string()), secret: Secret::new("abcdefghijklmnopqrstuvw".to_string()),
db: std::path::PathBuf::from("links.db"), db: std::path::PathBuf::from("links.db"),
db_pool, db_pool,
public_url: "localhost:8080".to_string(), public_url: "localhost:8080".to_string(),

View File

@ -39,6 +39,12 @@ impl Default for EditMode {
} }
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]
pub struct SuccessMessage { pub struct Message {
pub message: String, pub message: String,
} }
#[derive(Clone, Deserialize, Serialize, Debug)]
pub enum Status {
Success(Message),
Error(Message),
}

View File

@ -1,7 +1,9 @@
use enum_map::{Enum, EnumMap}; use enum_map::{Enum, EnumMap};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::general::{Filter, Operation, Ordering}; use crate::datatypes::{FullLink, Link};
use super::general::{EditMode, Filter, Operation, Ordering};
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users /// A generic list returntype containing the User and a Vec containing e.g. Links or Users
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]
@ -21,6 +23,48 @@ impl Default for LinkRequestForm {
} }
} }
/// The Struct that is responsible for creating and editing users.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct LinkDelta {
pub edit: EditMode,
pub id: Option<i64>,
pub title: String,
pub target: String,
pub code: String,
pub author: i64,
pub created_at: Option<chrono::NaiveDateTime>,
}
impl From<Link> for LinkDelta {
/// Automatically create a `UserDelta` from a User.
fn from(l: Link) -> Self {
Self {
edit: EditMode::Edit,
id: Some(l.id),
title: l.title,
target: l.target,
code: l.code,
author: l.author,
created_at: Some(l.created_at),
}
}
}
impl From<FullLink> for LinkDelta {
/// Automatically create a `UserDelta` from a User.
fn from(l: FullLink) -> Self {
Self {
edit: EditMode::Edit,
id: Some(l.link.id),
title: l.link.title,
target: l.link.target,
code: l.link.code,
author: l.link.author,
created_at: Some(l.link.created_at),
}
}
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Enum)] #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Enum)]
pub enum LinkOverviewColumns { pub enum LinkOverviewColumns {
Code, Code,

View File

@ -1,4 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize, Serializer};
/// A generic list returntype containing the User and a Vec containing e.g. Links or Users /// A generic list returntype containing the User and a Vec containing e.g. Links or Users
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
pub struct ListWithOwner<T> { pub struct ListWithOwner<T> {
@ -19,7 +19,7 @@ pub struct User {
pub id: i64, pub id: i64,
pub username: String, pub username: String,
pub email: String, pub email: String,
pub password: String, pub password: Secret,
pub role: i64, pub role: i64,
pub language: String, pub language: String,
} }
@ -44,3 +44,45 @@ pub struct Click {
pub link: i64, pub link: i64,
pub created_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime,
} }
#[derive(PartialEq, Clone, Deserialize)]
#[serde(from = "String")]
pub struct Secret {
pub secret: Option<String>,
}
impl From<String> for Secret {
fn from(_: String) -> Self {
Self { secret: None }
}
}
impl Serialize for Secret {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("*****SECRET*****")
}
}
impl Secret {
#[must_use]
pub const fn new(secret: String) -> Self {
Self {
secret: Some(secret),
}
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}
impl std::fmt::Display for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("*****SECRET*****")
}
}