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
parent
2b6104027c
commit
7947b9058b
|
@ -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 {
|
||||
|
|
|
@ -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)?))
|
||||
|
|
|
@ -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
|
||||
)))
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue