refactor: Change template context to allow using a CSP nonce

This commit is contained in:
Ginger
2026-05-06 11:03:44 -04:00
parent 9bfc331a26
commit 53d51cf831
19 changed files with 304 additions and 231 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent,
},
};
use service::{mailer::messages, users::HashedPassword, uiaa::UiaaInitiator};
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{Ruma, router::ClientIdentity};
+1 -1
View File
@@ -75,8 +75,8 @@ pub(super) use report::*;
pub(super) use room::*;
pub(super) use search::*;
pub(super) use send::*;
pub(super) use session::*;
pub use session::handle_login;
pub(super) use session::*;
pub(super) use space::*;
pub(super) use state::*;
pub(super) use sync::*;
+127 -130
View File
@@ -452,147 +452,144 @@ impl Service {
))
}
},
| UiaaSessionMetadata::Legacy { identity } => {
match auth {
| AuthData::Dummy(_) => Ok(AuthType::Dummy),
| AuthData::EmailIdentity(EmailIdentity {
thirdparty_id_creds: ThirdpartyIdCredentials { client_secret, sid, .. },
..
}) => {
match self
.services
.threepid
.get_valid_session(sid, client_secret)
.await
{
| Ok(session) => {
let email = session.consume();
| UiaaSessionMetadata::Legacy { identity } => match auth {
| AuthData::Dummy(_) => Ok(AuthType::Dummy),
| AuthData::EmailIdentity(EmailIdentity {
thirdparty_id_creds: ThirdpartyIdCredentials { client_secret, sid, .. },
..
}) => {
match self
.services
.threepid
.get_valid_session(sid, client_secret)
.await
{
| Ok(session) => {
let email = session.consume();
if let Some(localpart) =
self.services.threepid.get_localpart_for_email(&email).await
{
identity.try_set_localpart(localpart)?;
}
if let Some(localpart) =
self.services.threepid.get_localpart_for_email(&email).await
{
identity.try_set_localpart(localpart)?;
}
identity.try_set_email(email)?;
Ok(AuthType::EmailIdentity)
},
| Err(message) => Err(StandardErrorBody::new(
ErrorKind::ThreepidAuthFailed,
message.into_owned(),
)),
}
},
#[allow(clippy::useless_let_if_seq)]
| AuthData::Password(Password { identifier, password, .. }) => {
let user_id_or_localpart = match identifier {
| UserIdentifier::Matrix(MatrixUserIdentifier { user, .. }) =>
user.to_owned(),
| UserIdentifier::Email(EmailUserIdentifier { address, .. }) => {
let Ok(email) = Address::try_from(address.to_owned()) else {
return Err(StandardErrorBody::new(
ErrorKind::InvalidParam,
"Email is malformed".to_owned(),
));
};
if let Some(localpart) =
self.services.threepid.get_localpart_for_email(&email).await
{
identity.try_set_email(email)?;
Ok(AuthType::EmailIdentity)
},
| Err(message) => Err(StandardErrorBody::new(
ErrorKind::ThreepidAuthFailed,
message.into_owned(),
)),
}
},
#[allow(clippy::useless_let_if_seq)]
| AuthData::Password(Password { identifier, password, .. }) => {
let user_id_or_localpart = match identifier {
| UserIdentifier::Matrix(MatrixUserIdentifier { user, .. }) =>
user.to_owned(),
| UserIdentifier::Email(EmailUserIdentifier { address, .. }) => {
let Ok(email) = Address::try_from(address.to_owned()) else {
return Err(StandardErrorBody::new(
ErrorKind::InvalidParam,
"Email is malformed".to_owned(),
));
};
if let Some(localpart) =
self.services.threepid.get_localpart_for_email(&email).await
{
identity.try_set_email(email)?;
localpart
} else {
return Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"Invalid identifier or password".to_owned(),
));
}
},
| _ =>
localpart
} else {
return Err(StandardErrorBody::new(
ErrorKind::Unrecognized,
"Identifier type not recognized".to_owned(),
)),
};
let Ok(user_id) = UserId::parse_with_server_name(
user_id_or_localpart,
self.services.globals.server_name(),
) else {
return Err(StandardErrorBody::new(
ErrorKind::InvalidParam,
"User ID is malformed".to_owned(),
));
};
if self
.services
.users
.check_password(&user_id, password)
.await
.is_ok()
{
identity.try_set_localpart(user_id.localpart().to_owned())?;
Ok(AuthType::Password)
} else {
Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"Invalid identifier or password".to_owned(),
))
}
},
| AuthData::ReCaptcha(ReCaptcha { response, .. }) => {
let Some(ref private_site_key) =
self.services.config.recaptcha_private_site_key
else {
return Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"ReCaptcha is not configured".to_owned(),
));
};
match recaptcha_verify::verify_v3(private_site_key, response, None).await
{
| Ok(()) => Ok(AuthType::ReCaptcha),
| Err(e) => {
error!("ReCaptcha verification failed: {e:?}");
Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"ReCaptcha verification failed".to_owned(),
))
},
}
},
| AuthData::RegistrationToken(RegistrationToken { token, .. }) => {
let token = token.trim().to_owned();
"Invalid identifier or password".to_owned(),
));
}
},
| _ =>
return Err(StandardErrorBody::new(
ErrorKind::Unrecognized,
"Identifier type not recognized".to_owned(),
)),
};
if let Some(valid_token) = self
.services
.registration_tokens
.validate_token(token)
.await
{
self.services
.registration_tokens
.mark_token_as_used(valid_token);
let Ok(user_id) = UserId::parse_with_server_name(
user_id_or_localpart,
self.services.globals.server_name(),
) else {
return Err(StandardErrorBody::new(
ErrorKind::InvalidParam,
"User ID is malformed".to_owned(),
));
};
Ok(AuthType::RegistrationToken)
} else {
if self
.services
.users
.check_password(&user_id, password)
.await
.is_ok()
{
identity.try_set_localpart(user_id.localpart().to_owned())?;
Ok(AuthType::Password)
} else {
Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"Invalid identifier or password".to_owned(),
))
}
},
| AuthData::ReCaptcha(ReCaptcha { response, .. }) => {
let Some(ref private_site_key) =
self.services.config.recaptcha_private_site_key
else {
return Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"ReCaptcha is not configured".to_owned(),
));
};
match recaptcha_verify::verify_v3(private_site_key, response, None).await {
| Ok(()) => Ok(AuthType::ReCaptcha),
| Err(e) => {
error!("ReCaptcha verification failed: {e:?}");
Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"Invalid registration token".to_owned(),
"ReCaptcha verification failed".to_owned(),
))
}
},
| AuthData::Terms(_) => Ok(AuthType::Terms),
| _ => Err(StandardErrorBody::new(
ErrorKind::Unrecognized,
"Unsupported stage type".into(),
)),
}
},
}
},
| AuthData::RegistrationToken(RegistrationToken { token, .. }) => {
let token = token.trim().to_owned();
if let Some(valid_token) = self
.services
.registration_tokens
.validate_token(token)
.await
{
self.services
.registration_tokens
.mark_token_as_used(valid_token);
Ok(AuthType::RegistrationToken)
} else {
Err(StandardErrorBody::new(
ErrorKind::Forbidden,
"Invalid registration token".to_owned(),
))
}
},
| AuthData::Terms(_) => Ok(AuthType::Terms),
| _ => Err(StandardErrorBody::new(
ErrorKind::Unrecognized,
"Unsupported stage type".into(),
)),
},
}?;
+5 -6
View File
@@ -4,11 +4,12 @@ use askama::Template;
use axum::{
Router,
extract::rejection::{FormRejection, PathRejection, QueryRejection},
http::{HeaderValue, StatusCode, header},
http::StatusCode,
middleware::from_fn_with_state,
response::{Html, IntoResponse, Redirect, Response},
};
use conduwuit_service::{Services, state};
use tower_http::{catch_panic::CatchPanicLayer, set_header::SetResponseHeaderLayer};
use tower_http::catch_panic::CatchPanicLayer;
use tower_sec_fetch::SecFetchLayer;
use tower_sessions::{ExpiredDeletion, SessionManagerLayer, cookie::SameSite};
@@ -95,6 +96,7 @@ impl IntoResponse for WebError {
context: TemplateContext {
// Statically set false to prevent error pages from being indexed.
allow_indexing: false,
csp_nonce: String::new(),
},
};
@@ -150,10 +152,7 @@ pub fn build(services: &Services) -> Router<state::State> {
WebError::Panic(details).into_response()
}))
.layer(SetResponseHeaderLayer::if_not_present(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_static("default-src 'self'; img-src 'self' https: data:;"),
))
.layer(from_fn_with_state(services.config.clone(), template_context_middleware))
.layer(SecFetchLayer::new(|policy| {
policy.allow_safe_methods().reject_missing_metadata();
}))
+5 -4
View File
@@ -1,9 +1,9 @@
use axum::{Router, extract::State, routing::on};
use axum::{Extension, Router, extract::State, routing::on};
use conduwuit_service::oauth::OAuthTicket;
use crate::{
extract::PostForm,
pages::{GET_POST, Result, components::UserCard},
pages::{GET_POST, Result, TemplateContext, components::UserCard},
response,
session::{LoginTarget, User},
template,
@@ -28,6 +28,7 @@ enum CrossSigningResetBody {
async fn route_cross_signing_reset(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
PostForm(form): PostForm<()>,
) -> Result {
@@ -39,8 +40,8 @@ async fn route_cross_signing_reset(
.oauth
.issue_ticket(user_id.localpart().to_owned(), OAuthTicket::CrossSigningReset);
response!(CrossSigningReset::new(&services, user_card, CrossSigningResetBody::Success))
response!(CrossSigningReset::new(context, user_card, CrossSigningResetBody::Success))
} else {
response!(CrossSigningReset::new(&services, user_card, CrossSigningResetBody::Form))
response!(CrossSigningReset::new(context, user_card, CrossSigningResetBody::Form))
}
}
+10 -6
View File
@@ -1,4 +1,4 @@
use axum::{Router, extract::State, routing::on};
use axum::{Extension, Router, extract::State, routing::on};
use conduwuit_api::client::full_user_deactivate;
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedUserId, UserId};
@@ -9,7 +9,7 @@ use crate::{
extract::PostForm,
form,
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
components::{UserCard, form::Form},
},
response,
@@ -28,6 +28,7 @@ template! {
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum DeactivateBody {
Form {
user_id: OwnedUserId,
@@ -57,6 +58,7 @@ form! {
async fn route_deactivate(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
session: Session,
PostForm(form): PostForm<DeactivateForm>,
@@ -70,7 +72,7 @@ async fn route_deactivate(
DeactivateBody::Form {
user_id,
user_card,
form: DeactivateForm::with_errors(err),
form: DeactivateForm::with_errors(context.clone(), err),
}
} else {
let all_joined_rooms: Vec<OwnedRoomId> = services
@@ -90,12 +92,12 @@ async fn route_deactivate(
DeactivateBody::Form {
user_id,
user_card,
form: DeactivateForm::build(),
form: DeactivateForm::build(context.clone()),
}
}
};
response!(Deactivate::new(&services, body))
response!(Deactivate::new(context, body))
}
async fn validate_deactivate_form(
@@ -105,7 +107,9 @@ async fn validate_deactivate_form(
) -> Result<(), ValidationErrors> {
form.validate()?;
if services.users.check_password(user_id, &form.password)
if services
.users
.check_password(user_id, &form.password)
.await
.is_err()
{
+7 -5
View File
@@ -1,5 +1,5 @@
use axum::{
Router,
Extension, Router,
extract::{Path, State},
routing::{get, on},
};
@@ -12,7 +12,7 @@ use crate::{
WebError,
extract::{Expect, PostForm},
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
components::{ClientScopes, DeviceCard, DeviceCardStyle},
},
response,
@@ -35,6 +35,7 @@ template! {
async fn get_device_info(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
Expect(Path(query)): Expect<Path<DevicePath>>,
) -> Result {
@@ -65,7 +66,7 @@ async fn get_device_info(
let device_card =
DeviceCard::for_device(&services, &user_id, device, DeviceCardStyle::Detailed).await;
response!(DeviceInfo::new(&services, device_card, client_metadata))
response!(DeviceInfo::new(context, device_card, client_metadata))
}
template! {
@@ -90,6 +91,7 @@ pub(crate) struct DevicePath {
async fn route_remove_device(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
Expect(Path(query)): Expect<Path<DevicePath>>,
PostForm(form): PostForm<()>,
@@ -110,13 +112,13 @@ async fn route_remove_device(
.remove_device(&user_id, &device.device_id)
.await;
response!(RemoveDevice::new(&services, RemoveDeviceBody::Success))
response!(RemoveDevice::new(context, RemoveDeviceBody::Success))
} else {
let device_card =
DeviceCard::for_device(&services, &user_id, device, DeviceCardStyle::Minimal).await;
let last_device = services.users.all_devices_metadata(&user_id).count().await <= 1;
response!(RemoveDevice::new(&services, RemoveDeviceBody::Form {
response!(RemoveDevice::new(context, RemoveDeviceBody::Form {
device_card: Box::new(device_card),
last_device
}))
+24 -20
View File
@@ -1,5 +1,5 @@
use axum::{
Router,
Extension, Router,
extract::{Query, State},
routing::{get, on, post},
};
@@ -14,7 +14,7 @@ use crate::{
extract::{Expect, PostForm},
form,
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
account::ThreepidQuery,
components::{UserCard, form::Form},
},
@@ -75,25 +75,24 @@ enum ChangeEmailBody {
async fn route_change_email_request(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
PostForm(form): PostForm<ChangeEmailRequestForm>,
) -> Result {
let user_id = user.expect_recent(LoginTarget::ChangeEmail)?;
let template = ChangeEmailRequest::new(
&services,
UserCard::for_local_user(&services, user_id.clone()).await,
services
.threepid
.get_email_for_localpart(user_id.localpart())
.await
.map(|address| address.to_string()),
ChangeEmailRequestForm::build(),
services.threepid.email_requirement().may_remove(),
);
let Some(form) = form else {
return response!(template);
return response!(ChangeEmailRequest::new(
context.clone(),
UserCard::for_local_user(&services, user_id.clone()).await,
services
.threepid
.get_email_for_localpart(user_id.localpart())
.await
.map(|address| address.to_string()),
ChangeEmailRequestForm::build(context),
services.threepid.email_requirement().may_remove(),
));
};
let client_secret = ClientSecret::new();
@@ -130,7 +129,7 @@ async fn route_change_email_request(
};
response!(ChangeEmail::new(
&services,
context,
UserCard::for_local_user(&services, user_id).await,
ChangeEmailBody::ValidationPending {
session_id,
@@ -148,6 +147,7 @@ struct ChangeEmailQuery {
async fn get_change_email(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
Expect(Query(ChangeEmailQuery {
threepid: ThreepidQuery { client_secret, session_id },
})): Expect<Query<ChangeEmailQuery>>,
@@ -166,7 +166,7 @@ async fn get_change_email(
.await
else {
return response!(ChangeEmail::new(
&services,
context,
user_card,
ChangeEmailBody::ValidationPending {
session_id,
@@ -186,10 +186,14 @@ async fn get_change_email(
return response!(BadRequest(err.message()));
}
response!(ChangeEmail::new(&services, user_card, ChangeEmailBody::Success))
response!(ChangeEmail::new(context, user_card, ChangeEmailBody::Success))
}
async fn post_delete_email(State(services): State<crate::State>, user: User) -> Result {
async fn post_delete_email(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
) -> Result {
let user_id = user.expect(LoginTarget::ChangeEmail)?;
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
@@ -202,5 +206,5 @@ async fn post_delete_email(State(services): State<crate::State>, user: User) ->
.disassociate_localpart_email(user_id.localpart())
.await;
response!(DeleteEmail::new(&services, user_card))
response!(DeleteEmail::new(context, user_card))
}
+8 -4
View File
@@ -1,7 +1,7 @@
use std::time::SystemTime;
use axum::{
Router,
Extension, Router,
extract::{Query, RawQuery, State},
response::{IntoResponse, Redirect},
routing::{get, on},
@@ -17,7 +17,7 @@ use tower_sessions::Session;
use crate::{
ROUTE_PREFIX, WebError,
extract::{Expect, PostForm},
pages::{GET_POST, Result, components::UserCard},
pages::{GET_POST, Result, TemplateContext, components::UserCard},
response,
session::{LoginQuery, LoginTarget, User, UserSession},
template,
@@ -55,6 +55,7 @@ struct LoginForm {
async fn route_login(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
Expect(Query(LoginQuery { next, reauthenticate })): Expect<Query<LoginQuery>>,
session_store: Session,
user: User,
@@ -78,13 +79,16 @@ async fn route_login(
},
};
let mut template = Login::new(&services, body, next != LoginTarget::Account, None);
let mut template = Login::new(context, body, next != LoginTarget::Account, None);
if let Some(form) = form {
let login_result = match (user_id, form.identifier) {
| (Some(user_id), _) => {
// The user is already authenticated, we need to check their password
services.users.check_password(&user_id, &form.password).await
services
.users
.check_password(&user_id, &form.password)
.await
},
| (None, Some(identifier)) => {
// The user isn't authenticated, we need to log them in
+8 -4
View File
@@ -1,5 +1,5 @@
use axum::{
Router,
Extension, Router,
extract::{Query, State},
response::Redirect,
routing::get,
@@ -17,7 +17,7 @@ use crate::{
WebError,
extract::Expect,
pages::{
Result,
Result, TemplateContext,
components::{DeviceCard, DeviceCardStyle, UserCard},
},
response,
@@ -62,7 +62,11 @@ template! {
}
}
async fn get_account(State(services): State<crate::State>, user: User) -> Result {
async fn get_account(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
) -> Result {
let user_id = user.expect(LoginTarget::Account)?;
let email_requirement = services.threepid.email_requirement();
@@ -105,7 +109,7 @@ async fn get_account(State(services): State<crate::State>, user: User) -> Result
.collect()
.await;
response!(Account::new(&services, user_card, email_requirement, email, device_cards))
response!(Account::new(context, user_card, email_requirement, email, device_cards))
}
#[derive(Deserialize)]
+13 -9
View File
@@ -1,4 +1,4 @@
use axum::{Router, extract::State, routing::on};
use axum::{Extension, Router, extract::State, routing::on};
use conduwuit_service::users::HashedPassword;
use ruma::UserId;
use validator::{Validate, ValidationError, ValidationErrors};
@@ -7,7 +7,7 @@ use crate::{
extract::PostForm,
form,
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
components::{UserCard, form::Form},
},
response,
@@ -61,6 +61,7 @@ form! {
async fn route_change_password(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
PostForm(form): PostForm<ChangePasswordForm>,
) -> Result {
@@ -70,13 +71,14 @@ async fn route_change_password(
let body = if let Some(form) = form {
match change_password(&services, &user_id, form).await {
| Ok(()) => ChangePasswordBody::Success,
| Err(errors) => ChangePasswordBody::Form(ChangePasswordForm::with_errors(errors)),
| Err(errors) =>
ChangePasswordBody::Form(ChangePasswordForm::with_errors(context.clone(), errors)),
}
} else {
ChangePasswordBody::Form(ChangePasswordForm::build())
ChangePasswordBody::Form(ChangePasswordForm::build(context.clone()))
};
response!(ChangePassword::new(&services, user_card, body))
response!(ChangePassword::new(context, user_card, body))
}
async fn change_password(
@@ -86,7 +88,9 @@ async fn change_password(
) -> Result<(), ValidationErrors> {
form.validate()?;
if services.users.check_password(user_id, &form.current_password)
if services
.users
.check_password(user_id, &form.current_password)
.await
.is_err()
{
@@ -100,10 +104,10 @@ async fn change_password(
}
match HashedPassword::new(&form.new_password) {
Ok(hash) => {
| Ok(hash) => {
services.users.set_password(user_id, Some(hash));
},
Err(err) => {
| Err(err) => {
let mut errors = ValidationErrors::new();
errors.add(
"new_password",
@@ -111,7 +115,7 @@ async fn change_password(
);
return Err(errors);
}
},
}
Ok(())
+20 -15
View File
@@ -1,10 +1,12 @@
use axum::{
Router,
Extension, Router,
extract::{Query, State},
routing::on,
};
use conduwuit_core::warn;
use conduwuit_service::{mailer::messages, threepid::session::ValidationSessions, users::HashedPassword};
use conduwuit_service::{
mailer::messages, threepid::session::ValidationSessions, users::HashedPassword,
};
use lettre::{Address, message::Mailbox};
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId, UserId};
use serde::{Deserialize, Serialize};
@@ -15,7 +17,7 @@ use crate::{
extract::{Expect, PostForm},
form,
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
account::ThreepidQuery,
components::{UserCard, form::Form},
},
@@ -55,12 +57,13 @@ form! {
async fn route_reset_password_request(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
PostForm(form): PostForm<ResetPasswordRequestForm>,
) -> Result {
// Check if SMTP is configured
if services.mailer.mailer().is_none() {
return response!(ResetPasswordRequest::new(
&services,
context,
ResetPasswordRequestBody::Unavailable
));
}
@@ -68,8 +71,8 @@ async fn route_reset_password_request(
let Some(form) = form else {
// For GET requests return the reset request form
return response!(ResetPasswordRequest::new(
&services,
ResetPasswordRequestBody::Form(ResetPasswordRequestForm::build())
context.clone(),
ResetPasswordRequestBody::Form(ResetPasswordRequestForm::build(context))
));
};
@@ -115,7 +118,7 @@ async fn route_reset_password_request(
ValidationSessions::generate_session_id()
});
response!(ResetPassword::new(&services, ResetPasswordBody::ValidationPending {
response!(ResetPassword::new(context, ResetPasswordBody::ValidationPending {
client_secret,
session_id,
validation_error: false
@@ -172,6 +175,7 @@ struct ResetPasswordQuery {
async fn route_reset_password(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
Expect(Query(query)): Expect<Query<ResetPasswordQuery>>,
PostForm(form): PostForm<ResetPasswordForm>,
) -> Result {
@@ -201,36 +205,37 @@ async fn route_reset_password(
if let Err(err) = form.validate() {
ResetPasswordBody::ValidationSuccess {
user_card,
form: ResetPasswordForm::with_errors(err),
form: ResetPasswordForm::with_errors(context.clone(), err),
}
} else {
match HashedPassword::new(&form.new_password) {
Ok(hash) => {
| Ok(hash) => {
let _ = session.consume();
services.users.set_password(&user_id, Some(hash));
ResetPasswordBody::ResetSuccess { user_card }
},
Err(err) => {
| Err(err) => {
let mut errors = ValidationErrors::new();
errors.add(
"new_password",
ValidationError::new("malformed").with_message(err.message().into()),
ValidationError::new("malformed")
.with_message(err.message().into()),
);
ResetPasswordBody::ValidationSuccess {
user_card,
form: ResetPasswordForm::with_errors(errors),
form: ResetPasswordForm::with_errors(context.clone(), errors),
}
}
},
}
}
} else {
ResetPasswordBody::ValidationSuccess {
user_card,
form: ResetPasswordForm::build(),
form: ResetPasswordForm::build(context.clone()),
}
}
},
@@ -241,5 +246,5 @@ async fn route_reset_password(
},
};
response!(ResetPassword::new(&services, body))
response!(ResetPassword::new(context, body))
}
+14 -4
View File
@@ -1,10 +1,13 @@
use askama::{Template, filters::HtmlSafe};
use validator::ValidationErrors;
use crate::pages::TemplateContext;
/// A reusable form component with field validation.
#[derive(Debug, Template)]
#[template(path = "_components/form.html.j2")]
pub(crate) struct Form<'a> {
context: TemplateContext,
inputs: Vec<FormInput<'a>>,
submit_label: &'a str,
slowdown: bool,
@@ -12,8 +15,14 @@ pub(crate) struct Form<'a> {
}
impl<'a> Form<'a> {
pub(crate) fn new(inputs: Vec<FormInput<'a>>, submit_label: &'a str, slowdown: bool) -> Self {
pub(crate) fn new(
context: TemplateContext,
inputs: Vec<FormInput<'a>>,
submit_label: &'a str,
slowdown: bool,
) -> Self {
Self {
context,
inputs,
submit_label,
slowdown,
@@ -100,8 +109,9 @@ macro_rules! form {
impl $struct_name {
/// Generate a [`Form`] which matches the shape of this struct.
#[allow(clippy::needless_update)]
fn build() -> $crate::pages::components::form::Form<'static> {
fn build(context: TemplateContext) -> $crate::pages::components::form::Form<'static> {
$crate::pages::components::form::Form::new(
context,
vec![
$(
$crate::pages::components::form::FormInput {
@@ -119,8 +129,8 @@ macro_rules! form {
/// Generate a [`Form`] with validation errors.
#[allow(unused)]
fn with_errors(errors: validator::ValidationErrors) -> $crate::pages::components::form::Form<'static> {
let mut form = Self::build();
fn with_errors(context: TemplateContext, errors: validator::ValidationErrors) -> $crate::pages::components::form::Form<'static> {
let mut form = Self::build(context);
form.validation_errors = Some(errors);
form
}
+7 -4
View File
@@ -1,6 +1,6 @@
use axum::{Router, extract::State, response::IntoResponse, routing::get};
use axum::{Extension, Router, extract::State, response::IntoResponse, routing::get};
use crate::{WebError, template};
use crate::{WebError, pages::TemplateContext, template};
pub(crate) fn build() -> Router<crate::State> {
Router::new()
@@ -8,7 +8,10 @@ pub(crate) fn build() -> Router<crate::State> {
.route(&format!("{}/", crate::ROUTE_PREFIX), get(index))
}
async fn index(State(services): State<crate::State>) -> Result<impl IntoResponse, WebError> {
async fn index(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
) -> Result<impl IntoResponse, WebError> {
template! {
struct Index<'a> use "index.html.j2" {
server_name: &'a str,
@@ -17,7 +20,7 @@ async fn index(State(services): State<crate::State>) -> Result<impl IntoResponse
}
Ok(Index::new(
&services,
context,
services.globals.server_name().as_str(),
services.firstrun.is_first_run(),
)
+44 -10
View File
@@ -1,4 +1,13 @@
use axum::{response::Response, routing::MethodFilter};
use std::sync::Arc;
use axum::{
extract::{Request, State},
http::{HeaderValue, header},
middleware::Next,
response::Response,
routing::MethodFilter,
};
use conduwuit_core::utils;
use crate::WebError;
@@ -14,17 +23,42 @@ type Result<T = Response, E = WebError> = std::result::Result<T, E>;
const GET_POST: MethodFilter = MethodFilter::GET.or(MethodFilter::POST);
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct TemplateContext {
pub allow_indexing: bool,
pub csp_nonce: String,
}
impl From<&crate::State> for TemplateContext {
fn from(state: &crate::State) -> Self {
Self {
allow_indexing: state.config.allow_web_indexing,
}
}
const CSP_NONCE_LENGTH: usize = 32;
pub(super) async fn template_context_middleware(
State(config): State<Arc<conduwuit_service::config::Service>>,
mut request: Request,
next: Next,
) -> Response {
let csp_nonce = utils::random_string(CSP_NONCE_LENGTH);
let context = TemplateContext {
allow_indexing: config.allow_web_indexing,
csp_nonce: csp_nonce.clone(),
};
assert!(
request.extensions_mut().insert(context).is_none(),
"template context should only be inserted once"
);
let mut response = next.run(request).await;
response.headers_mut().insert(
header::CONTENT_SECURITY_POLICY,
HeaderValue::from_str(&format!(
"default-src 'none'; style-src 'self'; img-src 'self' 'https' data:; script-src \
'nonce-{csp_nonce}';"
))
.expect("should be able to build CSP header"),
);
response
}
#[macro_export]
@@ -43,9 +77,9 @@ macro_rules! template {
impl$(<$lifetime>)? $name$(<$lifetime>)? {
#[allow(clippy::too_many_arguments)]
fn new(state: &$crate::State, $($field_name: $field_type,)*) -> Self {
fn new(context: $crate::pages::TemplateContext, $($field_name: $field_type,)*) -> Self {
Self {
context: state.into(),
context,
$($field_name,)*
}
}
+4 -3
View File
@@ -1,5 +1,5 @@
use axum::{
Router,
Extension, Router,
extract::{Query, State},
response::Redirect,
routing::on,
@@ -12,7 +12,7 @@ use crate::{
WebError,
extract::{Expect, PostForm},
pages::{
GET_POST, Result,
GET_POST, Result, TemplateContext,
components::{Avatar, AvatarType, ClientScopes},
},
response,
@@ -40,6 +40,7 @@ template! {
async fn route_authorization_code(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
user: User,
Expect(Query(query)): Expect<Query<AuthorizationCodeQuery>>,
PostForm(form): PostForm<()>,
@@ -86,7 +87,7 @@ async fn route_authorization_code(
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
response!(Grant::new(
&services,
context,
serde_urlencoded::to_string(LoginQuery {
next: Some(LoginTarget::AuthorizationCode(query)),
reauthenticate: false,
@@ -45,6 +45,6 @@
<button type="submit"{% if slowdown %} class="slowdown"{% endif %}>{{ submit_label }}</button>
{% if slowdown %}
<script src="{{ crate::ROUTE_PREFIX }}/resources/slowdown.js"></script>
<script src="{{ crate::ROUTE_PREFIX }}/resources/slowdown.js" nonce="{{ context.csp_nonce }}"></script>
{% endif %}
</form>
@@ -30,7 +30,7 @@ Reset your digital identity
<form method="post">
<button type="submit" class="slowdown">I understand, begin the reset process</button>
</form>
<script src="{{ crate::ROUTE_PREFIX }}/resources/slowdown.js"></script>
<script src="{{ crate::ROUTE_PREFIX }}/resources/slowdown.js" nonce="{{ context.csp_nonce }}"></script>
{% when CrossSigningResetBody::Success %}
<p>
The identity reset has been approved for the next ten minutes.
+4 -3
View File
@@ -1,5 +1,5 @@
use axum::{
Router,
Extension, Router,
extract::{Query, State, rejection::QueryRejection},
response::IntoResponse,
routing::get,
@@ -7,7 +7,7 @@ use axum::{
use ruma::OwnedSessionId;
use serde::Deserialize;
use crate::{WebError, template};
use crate::{WebError, pages::TemplateContext, template};
template! {
struct ThreepidValidation use "threepid_validation.html.j2" {}
@@ -25,6 +25,7 @@ struct ThreepidValidationQuery {
async fn threepid_validation(
State(services): State<crate::State>,
Extension(context): Extension<TemplateContext>,
query: Result<Query<ThreepidValidationQuery>, QueryRejection>,
) -> Result<impl IntoResponse, WebError> {
let Query(query) = query?;
@@ -35,5 +36,5 @@ async fn threepid_validation(
.await
.map_err(|message| WebError::BadRequest(message.into_owned()))?;
Ok(ThreepidValidation::new(&services))
Ok(ThreepidValidation::new(context))
}