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]
lto = true
#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
list-users = Liste der Benutzer
welcome-user = Herzlich willkommen {$username}
welcome = Herzlich willkommen
logout = Abmelden
login = Login
yes = Ja
no = Nein
not-found = Dieser Link existiert nicht, oder wurde gelöscht.
@ -17,6 +20,7 @@ link-code = Link Code
shortlink = Shortlink
qr-code = QR-code
search-placeholder = Filtern nach...
really-delete = Wollen Sie {$code} wirklich löschen?
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.

View File

@ -3,8 +3,11 @@ add-link = Add a new link
invite-user = Invite a new user
list-users = List of existing users
welcome-user = Welcome {$username}
welcome = Welcome
logout = Logout
login = Login
yes = Ja
no = Nein
not-found = This Link has not been found or has been deleted
@ -16,6 +19,7 @@ link-target = Link target
link-code = Link code
shortlink = Shortlink
search-placeholder = Filter according to...
really-delete = Do you really want to delete {$code}?
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.

View File

@ -5,6 +5,7 @@ pub mod pages;
use pages::list_links;
use pages::list_users;
use seed::{div, log, prelude::*, App, Url, C};
use shared::datatypes::User;
use crate::i18n::{I18n, Lang};
@ -14,6 +15,7 @@ use crate::i18n::{I18n, Lang};
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::UrlChanged);
orders.send_msg(Msg::GetLoggedUser);
log!(url);
@ -24,6 +26,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
base_url: Url::new().add_path_part("app"),
page: Page::init(url, orders, lang.clone()),
i18n: lang,
user: None,
}
}
@ -37,6 +40,7 @@ struct Model {
base_url: Url,
page: Page,
i18n: i18n::I18n,
user: Option<User>,
}
#[derive(Debug)]
@ -76,6 +80,8 @@ pub enum Msg {
UrlChanged(subs::UrlChanged),
ListLinks(list_links::Msg),
ListUsers(list_users::Msg),
GetLoggedUser,
UserReceived(User),
NoMessage,
}
@ -97,6 +103,29 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
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")
}
#[must_use]
pub fn create_link(self) -> Url {
self.list_links().add_path_part("create_link")
}
#[must_use]
pub fn list_users(self) -> Url {
self.base_url().add_path_part("list_users")
}
@ -156,7 +189,7 @@ impl<'a> Urls<'a> {
fn view(model: &Model) -> Node<Msg> {
div![
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 File

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

View File

@ -1,25 +1,50 @@
use std::cell::RefCell;
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::{
apirequests::general::Ordering,
apirequests::{
general::Operation,
links::{LinkOverviewColumns, LinkRequestForm},
general::{EditMode, Message, Operation, Status},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
},
datatypes::FullLink,
};
use crate::i18n::I18n;
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
orders.send_msg(Msg::Fetch);
/// Unwrap a result and return it's content, or return from the function with another expression.
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 {
links: Vec::new(),
i18n,
formconfig: LinkRequestForm::default(),
inputs: EnumMap::default(),
edit_link,
last_message: None,
question: None,
}
}
@ -29,6 +54,9 @@ pub struct Model {
i18n: I18n,
formconfig: LinkRequestForm,
inputs: EnumMap<LinkOverviewColumns, FilterInput>,
edit_link: Option<RefCell<LinkDelta>>,
last_message: Option<Status>,
question: Option<EditMsg>,
}
#[derive(Default, Debug, Clone)]
@ -38,6 +66,14 @@ struct FilterInput {
#[derive(Clone)]
pub enum Msg {
Query(QueryMsg),
Edit(EditMsg),
ClearAll,
SetMessage(String),
}
#[derive(Clone)]
pub enum QueryMsg {
Fetch,
OrderBy(LinkOverviewColumns),
Received(Vec<FullLink>),
@ -46,36 +82,51 @@ pub enum Msg {
TargetFilterChanged(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
/// Sould only panic on bugs.
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Fetch => {
orders.skip(); // No need to rerender
let data = model.formconfig.clone(); // complicated way to move into the closure
orders.perform_cmd(async {
let data = data;
let response = fetch(
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::Query(msg) => process_query_messages(msg, model, orders),
Msg::Edit(msg) => process_edit_messages(msg, model, orders),
Msg::ClearAll => {
model.edit_link = None;
model.last_message = None;
model.question = None;
}
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(
|| {
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 {
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;
}
Msg::CodeFilterChanged(s) => {
QueryMsg::CodeFilterChanged(s) => {
log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
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);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
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);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
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);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
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]
/// # Panics
/// Sould only panic on bugs.
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);
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![
// Add the headlines
view_link_table_head(&t),
@ -160,34 +398,51 @@ pub fn view(model: &Model) -> Node<Msg> {
// Add all the content lines
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> {
tr![
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Code)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Code
))),
t("link-code")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Description
)),
))),
t("link-description")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Target)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Target
))),
t("link-target")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Author)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Author
))),
t("username")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Statistics)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::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::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),
]],
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::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),
]],
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::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),
]],
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::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),
]],
td![]
td![],
td![],
]
}
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![
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![&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::{
apirequests::general::{Operation, Ordering},
apirequests::{
general::{EditMode, SuccessMessage},
general::{EditMode, Status},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::User,
@ -37,7 +37,7 @@ pub struct Model {
formconfig: UserRequestForm,
inputs: EnumMap<UserOverviewColumns, FilterInput>,
user_edit: Option<RefCell<UserDelta>>,
last_message: Option<SuccessMessage>,
last_message: Option<Status>,
}
#[derive(Default, Debug, Clone)]
@ -68,7 +68,7 @@ pub enum UserQueryMsg {
pub enum UserEditMsg {
EditUserSelected(UserDelta),
CreateNewUser,
UserCreated(SuccessMessage),
UserCreated(Status),
EditUsernameChanged(String),
EditEmailChanged(String),
EditPasswordChanged(String),
@ -236,7 +236,7 @@ pub fn process_user_edit_messages(
Err(_) => return Msg::Edit(UserEditMsg::FailedToCreateUser),
};
let message: SuccessMessage = response
let message: Status = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
@ -268,7 +268,6 @@ pub fn view(model: &Model) -> Node<Msg> {
keyboard_ev(Ev::KeyDown, |keyboard_event| {
IF!(keyboard_event.key() == "Escape" => Msg::ClearAll)
}),
h1!("List Users Page from list_users"),
if let Some(message) = &model.last_message {
div![
C!["message", "center"],
@ -277,7 +276,11 @@ pub fn view(model: &Model) -> Node<Msg> {
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
&message.message
match message {
Status::Success(m) | Status::Error(m) => {
&m.message
}
}
]
} else {
section![]

View File

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

View File

@ -11,6 +11,7 @@ use actix_web::HttpResponse;
use actix_web::{web, App, HttpServer};
use fluent_templates::{static_loader, FluentLoader};
use qrcode::types::QrError;
use shared::datatypes::Secret;
use sqlx::{Pool, Sqlite};
use std::{fmt::Display, path::PathBuf, str::FromStr};
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)]
pub struct ServerConfig {
pub secret: Secret,
@ -209,7 +186,13 @@ impl ServerConfig {
"# If it is changed all existing passwords are invalid.\n"
)
.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(
web::scope("/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(
"/create_user/",
@ -378,6 +373,10 @@ pub async fn webservice(
.route(
"/update_user/",
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

View File

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

View File

@ -5,10 +5,10 @@ use serde::Serialize;
use shared::{
apirequests::{
general::{EditMode, Filter, Operation, Ordering},
links::{LinkOverviewColumns, LinkRequestForm},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::{Count, FullLink, Link, User},
datatypes::{Count, FullLink, Link, Secret, User},
};
use sqlx::Row;
use tracing::{info, instrument, warn};
@ -127,7 +127,7 @@ pub async fn list_all_allowed(
id: v.get("usid"),
username: v.get("usern"),
email: v.get("uemail"),
password: "invalid".to_owned(),
password: Secret::new("invalid".to_string()),
role: v.get("urole"),
language: v.get("ulang"),
},
@ -240,7 +240,7 @@ pub async fn list_users(
id: v.get("id"),
username: v.get("username"),
email: v.get("email"),
password: "invalid".to_owned(),
password: Secret::new("".to_string()),
role: v.get("role"),
language: v.get("language"),
})
@ -458,7 +458,9 @@ pub async fn update_user_json(
Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: ");
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,
};
let new_user = User {
@ -510,7 +512,10 @@ pub async fn update_user(
Role::Admin { .. } | Role::Regular { .. } => {
info!("Updating userinfo: ");
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 {
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**
///
/// # 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 queries::{authenticate, Role};
use shared::apirequests::{
general::SuccessMessage,
links::LinkRequestForm,
general::{Message, Status},
links::{LinkDelta, LinkRequestForm},
users::{UserDelta, UserRequestForm},
};
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))]
pub async fn view_link_empty(
tera: web::Data<Tera>,
@ -358,9 +371,9 @@ pub async fn process_create_user_json(
) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api");
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),
})),
}))),
Err(e) => Err(e),
}
}
@ -373,9 +386,9 @@ pub async fn process_update_user_json(
) -> Result<HttpResponse, ServerError> {
info!("Listing Users to Json api");
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),
})),
}))),
Err(e) => Err(e),
}
}
@ -447,20 +460,26 @@ pub async fn process_login(
match user {
Ok(u) => {
let secret = &config.secret;
let valid = Verifier::default()
.with_hash(&u.password)
.with_password(&data.password)
.with_secret_key(&secret.secret)
.verify()?;
if let Some(hash) = u.password.secret {
let secret = &config.secret;
let valid = Verifier::default()
.with_hash(hash)
.with_password(&data.password)
.with_secret_key(secret.secret.as_ref().expect("No secret available"))
.verify()?;
if valid {
info!("Log-in of user: {}", &u.username);
let session_token = u.username;
id.remember(session_token);
Ok(redirect_builder("/admin/index/"))
if valid {
info!("Log-in of user: {}", &u.username);
let session_token = u.username;
id.remember(session_token);
Ok(redirect_builder("/admin/index/"))
} else {
Ok(redirect_builder("/admin/login/"))
}
} else {
Ok(redirect_builder("/admin/login/"))
Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message {
message: "Failed to Login".to_string(),
})))
}
}
Err(e) => {
@ -515,6 +534,36 @@ pub async fn redirect_empty(
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))]
pub async fn create_link(
tera: web::Data<Tera>,
@ -591,3 +640,14 @@ pub async fn process_link_delete(
queries::delete_link(&id, &link_code.0, &config).await?;
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 {
border-collapse: collapse;
width: 100%;
margin-bottom: 10px;
}
th,
@ -185,3 +186,11 @@ div.message {
border: 5px solid rgb(90, 90, 90);
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]
fn test_help_of_command_for_breaking_changes() {
let output = test_bin::get_test_bin("pslink")
@ -208,7 +210,7 @@ async fn run_server() {
);
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_pool,
public_url: "localhost:8080".to_string(),

View File

@ -39,6 +39,12 @@ impl Default for EditMode {
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct SuccessMessage {
pub struct Message {
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 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
#[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)]
pub enum LinkOverviewColumns {
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
#[derive(Clone, Deserialize, Serialize)]
pub struct ListWithOwner<T> {
@ -19,7 +19,7 @@ pub struct User {
pub id: i64,
pub username: String,
pub email: String,
pub password: String,
pub password: Secret,
pub role: i64,
pub language: String,
}
@ -44,3 +44,45 @@ pub struct Click {
pub link: i64,
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*****")
}
}