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