web: add messages list in session

Add messages list for showing notifications (success, error, warning,
info) to users after actions like saving and/or submitting forms.
grcov
Manos Pitsidianakis 2023-04-13 18:47:08 +03:00
parent 2b6104027c
commit 7947b9058b
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
6 changed files with 279 additions and 9 deletions

View File

@ -69,6 +69,12 @@ pub async fn ssh_signin(
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
if auth.current_user.is_some() {
if let Err(err) = session.add_message(Message {
message: "You are already logged in.".into(),
level: Level::Info,
}) {
return err.into_response();
}
return Redirect::to("/settings/").into_response();
}
@ -120,6 +126,7 @@ pub async fn ssh_signin(
ssh_challenge => token,
timeout_left => timeout_left,
current_user => auth.current_user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Html(
@ -133,12 +140,16 @@ pub async fn ssh_signin(
}
pub async fn ssh_signin_post(
session: ReadableSession,
mut session: WritableSession,
mut auth: AuthContext,
Form(payload): Form<AuthFormPayload>,
state: Arc<AppState>,
) -> Result<Redirect, ResponseError> {
if auth.current_user.as_ref().is_some() {
session.add_message(Message {
message: "You are already logged in.".into(),
level: Level::Info,
})?;
return Ok(Redirect::to("/settings/"));
}
@ -147,13 +158,19 @@ pub async fn ssh_signin_post(
let (prev_token, _) =
if let Some(tok @ (_, timestamp)) = session.get::<(String, i64)>(TOKEN_KEY) {
if !(timestamp < now && now - timestamp < EXPIRY_IN_SECS) {
// expired
session.add_message(Message {
message: "The token has expired. Please retry.".into(),
level: Level::Error,
})?;
return Ok(Redirect::to("/login/"));
} else {
tok
}
} else {
// expired
session.add_message(Message {
message: "The token has expired. Please retry.".into(),
level: Level::Error,
})?;
return Ok(Redirect::to("/login/"));
};
@ -178,7 +195,6 @@ pub async fn ssh_signin_post(
namespace: "lists.mailpot.rs".into(),
token: prev_token,
};
// FIXME: show failure to user
ssh_keygen(sig).await?;
let user = User {

View File

@ -63,7 +63,7 @@ async fn main() {
"/settings/",
get({
let shared_state = Arc::clone(&shared_state);
move |user| settings(user, shared_state)
move |session, user| settings(session, user, shared_state)
}
.layer(RequireAuth::login())),
)
@ -73,8 +73,8 @@ async fn main() {
.layer(RequireAuth::login_with_role(Role::User..))
.post({
let shared_state = Arc::clone(&shared_state);
move |path, user, body| {
user_list_subscription_post(path, user, body, shared_state)
move |session, path, user, body| {
user_list_subscription_post(session, path, user, body, shared_state)
}
})
.layer(RequireAuth::login_with_role(Role::User..)),
@ -91,6 +91,7 @@ async fn main() {
}
async fn root(
mut session: WritableSession,
auth: AuthContext,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, ResponseError> {
@ -123,12 +124,14 @@ async fn root(
lists => &lists,
root_url_prefix => &root_url_prefix,
current_user => auth.current_user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(TEMPLATES.get_template("lists.html")?.render(context)?))
}
async fn list(
mut session: WritableSession,
Path(id): Path<i64>,
auth: AuthContext,
State(state): State<Arc<AppState>>,
@ -197,6 +200,7 @@ async fn list(
root_url_prefix => "",
list => Value::from_object(MailingList::from(list)),
current_user => auth.current_user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(TEMPLATES.get_template("list.html")?.render(context)?))
@ -205,6 +209,7 @@ async fn list(
async fn list_edit(Path(_): Path<i64>, State(_): State<Arc<AppState>>) {}
async fn help(
mut session: WritableSession,
auth: AuthContext,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, ResponseError> {
@ -224,6 +229,7 @@ async fn help(
description => "",
root_url_prefix => root_url_prefix,
current_user => auth.current_user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(TEMPLATES.get_template("help.html")?.render(context)?))

View File

@ -21,6 +21,7 @@ use super::*;
use mailpot::models::changesets::ListMembershipChangeset;
pub async fn settings(
mut session: WritableSession,
Extension(user): Extension<User>,
state: Arc<AppState>,
) -> Result<Html<String>, ResponseError> {
@ -53,6 +54,7 @@ pub async fn settings(
user => user,
subscriptions => subscriptions,
current_user => user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(
@ -61,6 +63,7 @@ pub async fn settings(
}
pub async fn user_list_subscription(
mut session: WritableSession,
Extension(user): Extension<User>,
Path(id): Path<i64>,
State(state): State<Arc<AppState>>,
@ -118,6 +121,7 @@ pub async fn user_list_subscription(
list => list,
subscription => subscription,
current_user => user,
messages => session.drain_messages(),
crumbs => crumbs,
};
Ok(Html(
@ -142,11 +146,12 @@ pub struct SubscriptionFormPayload {
}
pub async fn user_list_subscription_post(
mut session: WritableSession,
Path(id): Path<i64>,
Extension(user): Extension<User>,
Form(payload): Form<SubscriptionFormPayload>,
state: Arc<AppState>,
) -> Result<impl IntoResponse, ResponseError> {
) -> Result<Redirect, ResponseError> {
let mut db = Connection::open_db(state.conf.clone())?;
let _list = db.list(id).with_status(StatusCode::NOT_FOUND)?;
@ -193,5 +198,13 @@ pub async fn user_list_subscription_post(
db.update_member(cset)
.with_status(StatusCode::BAD_REQUEST)?;
Ok(Redirect::to(&format!("{}/settings/list/{id}/", &state.root_url_prefix)).into_response())
session.add_message(Message {
message: "Settings saved successfully.".into(),
level: Level::Success,
})?;
Ok(Redirect::to(&format!(
"{}/settings/list/{id}/",
&state.root_url_prefix
)))
}

View File

@ -91,6 +91,152 @@
isolation: isolate;
}
@media (prefers-color-scheme: light) {
:root {
--text-primary: #1b1b1b;
--text-secondary: #4e4e4e;
--text-inactive: #9e9e9ea6;
--text-link: #0069c2;
--text-invert: #fff;
--background-primary: #fff;
--background-secondary: #f9f9fb;
--background-tertiary: #fff;
--background-toc-active: #ebeaea;
--background-mark-yellow: #c7b70066;
--background-mark-green: #00d06166;
--background-information: #0085f21a;
--background-warning: #ff2a511a;
--background-critical: #d300381a;
--background-success: #0079361a;
--border-primary: #cdcdcd;
--border-secondary: #cdcdcd;
--button-primary-default: #1b1b1b;
--button-primary-hover: #696969;
--button-primary-active: #9e9e9e;
--button-primary-inactive: #1b1b1b;
--button-secondary-default: #fff;
--button-secondary-hover: #cdcdcd;
--button-secondary-active: #cdcdcd;
--button-secondary-inactive: #f9f9fb;
--button-secondary-border-focus: #0085f2;
--button-secondary-border-red: #ff97a0;
--button-secondary-border-red-focus: #ffd9dc;
--icon-primary: #696969;
--icon-secondary: #b3b3b3;
--icon-information: #0085f2;
--icon-warning: #ff2a51;
--icon-critical: #d30038;
--icon-success: #007936;
--accent-primary: #0085f2;
--accent-primary-engage: #0085f21a;
--accent-secondary: #0085f2;
--accent-tertiary: #0085f21a;
--shadow-01: 0 1px 2px rgba(43,42,51,.05);
--shadow-02: 0 1px 6px rgba(43,42,51,.1);
--focus-01: 0 0 0 3px rgba(0,144,237,.4);
--field-focus-border: #0085f2;
--code-token-tag: #0069c2;
--code-token-punctuation: #858585;
--code-token-attribute-name: #d30038;
--code-token-attribute-value: #007936;
--code-token-comment: #858585;
--code-token-default: #1b1b1b;
--code-token-selector: #872bff;
--code-background-inline: #f2f1f1;
--code-background-block: #f2f1f1;
--notecard-link-color: #343434;
--scrollbar-bg: transparent;
--scrollbar-color: #00000040;
--category-color: #0085f2;
--category-color-background: #0085f210;
--code-color: #5e9eff;
--mark-color: #dce2f2;
--blend-color: #fff80;
--text-primary-red: #d30038;
--text-primary-green: #007936;
--text-primary-blue: #0069c2;
--text-primary-yellow: #746a00;
--form-invalid-color: #d30038;
--form-invalid-focus-color: #ff2a51;
--form-invalid-focus-effect-color: #ff2a5133;
color-scheme: light;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #fff;
--text-secondary: #cdcdcd;
--text-inactive: #cdcdcda6;
--text-link: #8cb4ff;
--text-invert: #1b1b1b;
--background-primary: #1b1b1b;
--background-secondary: #343434;
--background-tertiary: #4e4e4e;
--background-toc-active: #343434;
--background-mark-yellow: #c7b70066;
--background-mark-green: #00d06166;
--background-information: #0085f21a;
--background-warning: #ff2a511a;
--background-critical: #d300381a;
--background-success: #0079361a;
--border-primary: #858585;
--border-secondary: #696969;
--button-primary-default: #fff;
--button-primary-hover: #cdcdcd;
--button-primary-active: #9e9e9e;
--button-primary-inactive: #fff;
--button-secondary-default: #4e4e4e;
--button-secondary-hover: #858585;
--button-secondary-active: #9e9e9e;
--button-secondary-inactive: #4e4e4e;
--button-secondary-border-focus: #0085f2;
--button-secondary-border-red: #ff97a0;
--button-secondary-border-red-focus: #ffd9dc;
--icon-primary: #fff;
--icon-secondary: #b3b3b3;
--icon-information: #5e9eff;
--icon-warning: #afa100;
--icon-critical: #ff707f;
--icon-success: #00b755;
--accent-primary: #5e9eff;
--accent-primary-engage: #5e9eff1a;
--accent-secondary: #5e9eff;
--accent-tertiary: #0085f21a;
--shadow-01: 0 1px 2px rgba(251,251,254,.2);
--shadow-02: 0 1px 6px rgba(251,251,254,.2);
--focus-01: 0 0 0 3px rgba(251,251,254,.5);
--field-focus-border: #fff;
--notecard-link-color: #e2e2e2;
--scrollbar-bg: transparent;
--scrollbar-color: #ffffff40;
--category-color: #8cb4ff;
--category-color-background: #8cb4ff70;
--code-color: #c1cff1;
--mark-color: #004d92;
--blend-color: #00080;
--text-primary-red: #ff97a0;
--text-primary-green: #00d061;
--text-primary-blue: #8cb4ff;
--text-primary-yellow: #c7b700;
--collections-link: #ff97a0;
--collections-header: #40000a;
--collections-mandala: #9e0027;
--collections-icon: #d30038;
--updates-link: #8cb4ff;
--updates-header: #000;
--updates-mandala: #c1cff1;
--updates-icon: #8cb4ff;
--form-limit-color: #9e9e9e;
--form-limit-color-emphasis: #b3b3b3;
--form-invalid-color: #ff97a0;
--form-invalid-focus-color: #ff707f;
--form-invalid-focus-effect-color: #ff707f33;
color-scheme: dark;
}
}
body>main.layout {
width: 100%;
@ -177,6 +323,46 @@
margin-left: -0.4rem;
}
ul.messagelist {
list-style-type: none;
margin: 0;
padding: 0;
background: var(--background-secondary);
}
ul.messagelist:not(:empty) {
margin-block-end: 0.5rem;
}
ul.messagelist>li {
padding: 0.5rem 1rem;
background: var(--message-background);
border: .1rem solid var(--border-secondary);
border-radius: 0.2rem;
font-weight: medium;
margin-block-end: 1.0rem;
}
ul.messagelist>li>span.label {
text-transform: capitalize;
font-weight: bolder;
}
ul.messagelist>li.error {
--message-background: var(--background-critical);
}
ul.messagelist>li.success {
--message-background: var(--background-success);
}
ul.messagelist>li.warning {
--message-background: var(--background-warning);
}
ul.messagelist>li.info {
--message-background: var(--background-information);
}
div.preamble {
border-left: 0.2rem solid GrayText;
padding-left: 0.5rem;

View File

@ -13,4 +13,13 @@
<p class="description">{{ description }}</p>
{% endif %}
{% include "menu.html" %}
{% if messages %}
<ul class="messagelist">
{% for message in messages %}
<li class="{{ message.level|lower }}">
<span class="label">{{ message.level }}: </span>{{ message.message }}
</li>
{% endfor %}
</ul>
{% endif %}
</div>

View File

@ -186,3 +186,43 @@ pub struct Crumb {
pub label: Cow<'static, str>,
pub url: Cow<'static, str>,
}
#[derive(Debug, Default, Hash, Copy, Clone, serde::Deserialize, serde::Serialize)]
pub enum Level {
Success,
#[default]
Info,
Warning,
Error,
}
#[derive(Debug, Hash, Clone, serde::Deserialize, serde::Serialize)]
pub struct Message {
pub message: Cow<'static, str>,
#[serde(default)]
pub level: Level,
}
impl Message {
const MESSAGE_KEY: &str = "session-message";
}
pub trait SessionMessages {
fn drain_messages(&mut self) -> Vec<Message>;
fn add_message(&mut self, _: Message) -> Result<(), ResponseError>;
}
impl SessionMessages for WritableSession {
fn drain_messages(&mut self) -> Vec<Message> {
let ret = self.get(Message::MESSAGE_KEY).unwrap_or_default();
self.remove(Message::MESSAGE_KEY);
ret
}
fn add_message(&mut self, message: Message) -> Result<(), ResponseError> {
let mut messages: Vec<Message> = self.get(Message::MESSAGE_KEY).unwrap_or_default();
messages.push(message);
self.insert(Message::MESSAGE_KEY, messages)?;
Ok(())
}
}