mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
refactor: Change template context to allow using a CSP nonce
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,)*
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user