mailpot/web/src/settings.rs

415 lines
13 KiB
Rust

/*
* This file is part of mailpot
*
* Copyright 2020 - Manos Pitsidianakis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use mailpot::models::{
changesets::{AccountChangeset, ListSubscriptionChangeset},
ListSubscription,
};
use super::*;
pub async fn settings(
_: SettingsPath,
mut session: WritableSession,
Extension(user): Extension<User>,
state: Arc<AppState>,
) -> Result<Html<String>, ResponseError> {
let root_url_prefix = &state.root_url_prefix;
let crumbs = vec![
Crumb {
label: "Home".into(),
url: "/".into(),
},
Crumb {
label: "Settings".into(),
url: SettingsPath.to_crumb(),
},
];
let db = Connection::open_db(state.conf.clone())?;
let acc = db
.account_by_address(&user.address)
.with_status(StatusCode::BAD_REQUEST)?
.ok_or_else(|| {
ResponseError::new("Account not found".to_string(), StatusCode::BAD_REQUEST)
})?;
let subscriptions = db
.account_subscriptions(acc.pk())
.with_status(StatusCode::BAD_REQUEST)?
.into_iter()
.filter_map(|s| match db.list(s.list) {
Err(err) => Some(Err(err)),
Ok(Some(list)) => Some(Ok((s, list))),
Ok(None) => None,
})
.collect::<Result<
Vec<(
DbVal<mailpot::models::ListSubscription>,
DbVal<mailpot::models::MailingList>,
)>,
mailpot::Error,
>>()?;
let context = minijinja::context! {
title => state.site_title.as_ref(),
page_title => "Account settings",
description => "",
root_url_prefix => &root_url_prefix,
user => user,
subscriptions => subscriptions,
current_user => user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(
TEMPLATES.get_template("settings.html")?.render(context)?,
))
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ChangeSetting {
Subscribe { list_pk: IntPOST },
Unsubscribe { list_pk: IntPOST },
ChangePassword { new: String },
ChangePublicKey { new: String },
// RemovePassword,
RemovePublicKey,
ChangeName { new: String },
}
pub async fn settings_post(
_: SettingsPath,
mut session: WritableSession,
Extension(user): Extension<User>,
Form(payload): Form<ChangeSetting>,
state: Arc<AppState>,
) -> Result<Redirect, ResponseError> {
let mut db = Connection::open_db(state.conf.clone())?;
let acc = db
.account_by_address(&user.address)
.with_status(StatusCode::BAD_REQUEST)?
.ok_or_else(|| {
ResponseError::new("Account not found".to_string(), StatusCode::BAD_REQUEST)
})?;
match payload {
ChangeSetting::Subscribe {
list_pk: IntPOST(list_pk),
} => {
let subscriptions = db
.account_subscriptions(acc.pk())
.with_status(StatusCode::BAD_REQUEST)?;
if subscriptions.iter().any(|s| s.list == list_pk) {
session.add_message(Message {
message: "You are already subscribed to this list.".into(),
level: Level::Info,
})?;
} else {
db.add_subscription(
list_pk,
ListSubscription {
pk: 0,
list: list_pk,
account: Some(acc.pk()),
address: acc.address.clone(),
name: acc.name.clone(),
digest: false,
enabled: true,
verified: true,
hide_address: false,
receive_duplicates: false,
receive_own_posts: false,
receive_confirmation: false,
},
)?;
session.add_message(Message {
message: "You have subscribed to this list.".into(),
level: Level::Success,
})?;
}
}
ChangeSetting::Unsubscribe {
list_pk: IntPOST(list_pk),
} => {
let subscriptions = db
.account_subscriptions(acc.pk())
.with_status(StatusCode::BAD_REQUEST)?;
if !subscriptions.iter().any(|s| s.list == list_pk) {
session.add_message(Message {
message: "You are already not subscribed to this list.".into(),
level: Level::Info,
})?;
} else {
let db = db.trusted();
db.remove_subscription(list_pk, &acc.address)?;
session.add_message(Message {
message: "You have unsubscribed from this list.".into(),
level: Level::Success,
})?;
}
}
ChangeSetting::ChangePassword { new } => {
db.update_account(AccountChangeset {
address: acc.address.clone(),
name: None,
public_key: None,
password: Some(new.clone()),
enabled: None,
})
.with_status(StatusCode::BAD_REQUEST)?;
session.add_message(Message {
message: "You have successfully updated your SSH public key.".into(),
level: Level::Success,
})?;
let mut user = user.clone();
user.password = new;
state.insert_user(acc.pk(), user).await;
}
ChangeSetting::ChangePublicKey { new } => {
db.update_account(AccountChangeset {
address: acc.address.clone(),
name: None,
public_key: Some(Some(new.clone())),
password: None,
enabled: None,
})
.with_status(StatusCode::BAD_REQUEST)?;
session.add_message(Message {
message: "You have successfully updated your PGP public key.".into(),
level: Level::Success,
})?;
let mut user = user.clone();
user.public_key = Some(new);
state.insert_user(acc.pk(), user).await;
}
ChangeSetting::RemovePublicKey => {
db.update_account(AccountChangeset {
address: acc.address.clone(),
name: None,
public_key: Some(None),
password: None,
enabled: None,
})
.with_status(StatusCode::BAD_REQUEST)?;
session.add_message(Message {
message: "You have successfully removed your PGP public key.".into(),
level: Level::Success,
})?;
let mut user = user.clone();
user.public_key = None;
state.insert_user(acc.pk(), user).await;
}
ChangeSetting::ChangeName { new } => {
let new = if new.trim().is_empty() {
None
} else {
Some(new)
};
db.update_account(AccountChangeset {
address: acc.address.clone(),
name: Some(new.clone()),
public_key: None,
password: None,
enabled: None,
})
.with_status(StatusCode::BAD_REQUEST)?;
session.add_message(Message {
message: "You have successfully updated your name.".into(),
level: Level::Success,
})?;
let mut user = user.clone();
user.name = new.clone();
state.insert_user(acc.pk(), user).await;
}
}
Ok(Redirect::to(&format!(
"{}/{}",
&state.root_url_prefix,
SettingsPath.to_uri()
)))
}
pub async fn user_list_subscription(
ListSettingsPath(id): ListSettingsPath,
mut session: WritableSession,
Extension(user): Extension<User>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, ResponseError> {
let root_url_prefix = &state.root_url_prefix;
let db = Connection::open_db(state.conf.clone())?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
}) else {
return Err(ResponseError::new(
"List not found".to_string(),
StatusCode::NOT_FOUND,
));
};
let acc = match db.account_by_address(&user.address)? {
Some(v) => v,
None => {
return Err(ResponseError::new(
"Account not found".to_string(),
StatusCode::BAD_REQUEST,
))
}
};
let mut subscriptions = db
.account_subscriptions(acc.pk())
.with_status(StatusCode::BAD_REQUEST)?;
subscriptions.retain(|s| s.list == list.pk());
let subscription = db
.list_subscription(
list.pk(),
subscriptions
.get(0)
.ok_or_else(|| {
ResponseError::new(
"Subscription not found".to_string(),
StatusCode::BAD_REQUEST,
)
})?
.pk(),
)
.with_status(StatusCode::BAD_REQUEST)?;
let crumbs = vec![
Crumb {
label: "Home".into(),
url: "/".into(),
},
Crumb {
label: "Settings".into(),
url: SettingsPath.to_crumb(),
},
Crumb {
label: "List Subscription".into(),
url: ListSettingsPath(list.pk().into()).to_crumb(),
},
];
let context = minijinja::context! {
title => state.site_title.as_ref(),
page_title => "Subscription settings",
description => "",
root_url_prefix => &root_url_prefix,
user => user,
list => list,
subscription => subscription,
current_user => user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(
TEMPLATES
.get_template("settings_subscription.html")?
.render(context)?,
))
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
pub struct SubscriptionFormPayload {
#[serde(default)]
pub digest: bool,
#[serde(default)]
pub hide_address: bool,
#[serde(default)]
pub receive_duplicates: bool,
#[serde(default)]
pub receive_own_posts: bool,
#[serde(default)]
pub receive_confirmation: bool,
}
pub async fn user_list_subscription_post(
ListSettingsPath(id): ListSettingsPath,
mut session: WritableSession,
Extension(user): Extension<User>,
Form(payload): Form<SubscriptionFormPayload>,
state: Arc<AppState>,
) -> Result<Redirect, ResponseError> {
let mut db = Connection::open_db(state.conf.clone())?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id as _)?,
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
}) else {
return Err(ResponseError::new(
"List not found".to_string(),
StatusCode::NOT_FOUND,
));
};
let acc = match db.account_by_address(&user.address)? {
Some(v) => v,
None => {
return Err(ResponseError::new(
"Account with this address was not found".to_string(),
StatusCode::BAD_REQUEST,
));
}
};
let mut subscriptions = db
.account_subscriptions(acc.pk())
.with_status(StatusCode::BAD_REQUEST)?;
subscriptions.retain(|s| s.list == list.pk());
let mut s = db
.list_subscription(list.pk(), subscriptions[0].pk())
.with_status(StatusCode::BAD_REQUEST)?;
let SubscriptionFormPayload {
digest,
hide_address,
receive_duplicates,
receive_own_posts,
receive_confirmation,
} = payload;
let cset = ListSubscriptionChangeset {
list: s.list,
address: std::mem::take(&mut s.address),
account: None,
name: None,
digest: Some(digest),
hide_address: Some(hide_address),
receive_duplicates: Some(receive_duplicates),
receive_own_posts: Some(receive_own_posts),
receive_confirmation: Some(receive_confirmation),
enabled: None,
verified: None,
};
db.update_subscription(cset)
.with_status(StatusCode::BAD_REQUEST)?;
session.add_message(Message {
message: "Settings saved successfully.".into(),
level: Level::Success,
})?;
Ok(Redirect::to(&format!(
"{}{}",
&state.root_url_prefix,
ListSettingsPath(list.id.clone().into()).to_uri()
)))
}