feat: implement CSRF protection across forms and routes
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
<input type="hidden" name="redirect_after" value="/?offset={{ current_offset }}">
|
<input type="hidden" name="redirect_after" value="/?offset={{ current_offset }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
{% if let Some(uid) = ctx.user_id %}
|
{% if let Some(uid) = ctx.user_id %}
|
||||||
{% if *uid == entry.review().user_id().value() %}
|
{% if *uid == entry.review().user_id().value() %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
||||||
<form method="POST" action="/users/{{ user_id }}/followers/remove" style="display:inline">
|
<form method="POST" action="/users/{{ user_id }}/followers/remove" style="display:inline">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Remove</button>
|
<button type="submit">Remove</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
||||||
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
|
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Unfollow</button>
|
<button type="submit">Unfollow</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
Password<br>
|
Password<br>
|
||||||
<input type="password" name="password" required autocomplete="current-password">
|
<input type="password" name="password" required autocomplete="current-password">
|
||||||
</label>
|
</label>
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Login</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
Comment<br>
|
Comment<br>
|
||||||
<textarea name="comment"></textarea>
|
<textarea name="comment"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Log Review</button>
|
<button type="submit">Log Review</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<h3>Follow remote user</h3>
|
<h3>Follow remote user</h3>
|
||||||
<form method="POST" action="/users/{{ profile_user_id }}/follow">
|
<form method="POST" action="/users/{{ profile_user_id }}/follow">
|
||||||
<input type="text" name="handle" placeholder="user@instance.example.com" required>
|
<input type="text" name="handle" placeholder="user@instance.example.com" required>
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Follow</button>
|
<button type="submit">Follow</button>
|
||||||
</form>
|
</form>
|
||||||
{% if let Some(err) = error %}
|
{% if let Some(err) = error %}
|
||||||
@@ -47,10 +48,12 @@
|
|||||||
<a href="{{ actor.url }}" class="pending-url" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
<a href="{{ actor.url }}" class="pending-url" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
||||||
<form method="POST" action="/users/{{ profile_user_id }}/followers/accept" class="inline-form">
|
<form method="POST" action="/users/{{ profile_user_id }}/followers/accept" class="inline-form">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit" class="btn-accept">Accept</button>
|
<button type="submit" class="btn-accept">Accept</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="/users/{{ profile_user_id }}/followers/reject" class="inline-form">
|
<form method="POST" action="/users/{{ profile_user_id }}/followers/reject" class="inline-form">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit" class="btn-reject">Reject</button>
|
<button type="submit" class="btn-reject">Reject</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
@@ -183,6 +186,7 @@
|
|||||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?view={{ view }}&offset={{ current_offset }}">
|
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?view={{ view }}&offset={{ current_offset }}">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
Password<br>
|
Password<br>
|
||||||
<input type="password" name="password" required autocomplete="new-password">
|
<input type="password" name="password" required autocomplete="new-password">
|
||||||
</label>
|
</label>
|
||||||
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Register</button>
|
<button type="submit">Register</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub struct HtmlPageContext {
|
|||||||
pub rss_url: String,
|
pub rss_url: String,
|
||||||
pub page_title: String,
|
pub page_title: String,
|
||||||
pub canonical_url: String,
|
pub canonical_url: String,
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HtmlPageContext {
|
impl HtmlPageContext {
|
||||||
|
|||||||
58
crates/presentation/src/csrf.rs
Normal file
58
crates/presentation/src/csrf.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::Request,
|
||||||
|
http::{HeaderValue, header},
|
||||||
|
middleware::Next,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CsrfToken(pub String);
|
||||||
|
|
||||||
|
pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
|
||||||
|
headers
|
||||||
|
.get(header::COOKIE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.and_then(|cookies| {
|
||||||
|
cookies
|
||||||
|
.split(';')
|
||||||
|
.find_map(|c| c.trim().strip_prefix("csrf=").map(str::to_string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secure_flag() -> &'static str {
|
||||||
|
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
|
||||||
|
"; Secure"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn csrf_middleware(mut req: Request, next: Next) -> Response {
|
||||||
|
let existing = extract_from_cookie(req.headers());
|
||||||
|
let (token, needs_set) = match existing {
|
||||||
|
Some(t) => (t, false),
|
||||||
|
None => (uuid::Uuid::new_v4().to_string(), true),
|
||||||
|
};
|
||||||
|
|
||||||
|
req.extensions_mut().insert(CsrfToken(token.clone()));
|
||||||
|
|
||||||
|
let mut response = next.run(req).await;
|
||||||
|
|
||||||
|
if needs_set {
|
||||||
|
let cookie = format!(
|
||||||
|
"csrf={}; HttpOnly; Path=/; SameSite=Strict{}",
|
||||||
|
token,
|
||||||
|
secure_flag()
|
||||||
|
);
|
||||||
|
if let Ok(val) = HeaderValue::from_str(&cookie) {
|
||||||
|
response.headers_mut().append(header::SET_COOKIE, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the form token does not match the cookie token.
|
||||||
|
pub fn mismatch(token: &CsrfToken, form_value: &str) -> bool {
|
||||||
|
token.0 != form_value || form_value.is_empty()
|
||||||
|
}
|
||||||
@@ -41,12 +41,16 @@ pub struct LogReviewForm {
|
|||||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
pub watched_at: String,
|
pub watched_at: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LoginForm {
|
pub struct LoginForm {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -54,6 +58,8 @@ pub struct RegisterForm {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -65,6 +71,8 @@ pub struct ErrorQuery {
|
|||||||
pub struct DeleteRedirectForm {
|
pub struct DeleteRedirectForm {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub redirect_after: Option<String>,
|
pub redirect_after: Option<String>,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
@@ -240,16 +248,22 @@ impl From<DiaryQueryParams> for GetDiaryQuery {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct FollowForm {
|
pub struct FollowForm {
|
||||||
pub handle: String,
|
pub handle: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UnfollowForm {
|
pub struct UnfollowForm {
|
||||||
pub actor_url: String,
|
pub actor_url: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct FollowerActionForm {
|
pub struct FollowerActionForm {
|
||||||
pub actor_url: String,
|
pub actor_url: String,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Default)]
|
#[derive(serde::Deserialize, Default)]
|
||||||
@@ -410,6 +424,7 @@ mod tests {
|
|||||||
rating: 4,
|
rating: 4,
|
||||||
comment: None,
|
comment: None,
|
||||||
watched_at: watched_at.to_string(),
|
watched_at: watched_at.to_string(),
|
||||||
|
csrf_token: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pub mod html {
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Form,
|
Form,
|
||||||
extract::{Path, Query, State},
|
extract::{Extension, Path, Query, State},
|
||||||
http::{HeaderValue, StatusCode, header::SET_COOKIE},
|
http::{HeaderValue, StatusCode, header::SET_COOKIE},
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
@@ -28,6 +28,7 @@ pub mod html {
|
|||||||
use domain::{errors::DomainError, value_objects::UserId};
|
use domain::{errors::DomainError, value_objects::UserId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
csrf::CsrfToken,
|
||||||
dtos::{
|
dtos::{
|
||||||
DiaryQueryParams, ErrorQuery, FollowForm, FollowerActionForm, LogReviewData,
|
DiaryQueryParams, ErrorQuery, FollowForm, FollowerActionForm, LogReviewData,
|
||||||
LogReviewForm, LoginForm, RegisterForm, UnfollowForm,
|
LogReviewForm, LoginForm, RegisterForm, UnfollowForm,
|
||||||
@@ -36,7 +37,11 @@ pub mod html {
|
|||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn build_page_context(state: &AppState, user_id: Option<UserId>) -> HtmlPageContext {
|
async fn build_page_context(
|
||||||
|
state: &AppState,
|
||||||
|
user_id: Option<UserId>,
|
||||||
|
csrf_token: String,
|
||||||
|
) -> HtmlPageContext {
|
||||||
let uuid = user_id.as_ref().map(|u| u.value());
|
let uuid = user_id.as_ref().map(|u| u.value());
|
||||||
let user_email = if let Some(ref id) = user_id {
|
let user_email = if let Some(ref id) = user_id {
|
||||||
state
|
state
|
||||||
@@ -57,6 +62,7 @@ pub mod html {
|
|||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Movies Diary".to_string(),
|
page_title: "Movies Diary".to_string(),
|
||||||
canonical_url: state.app_ctx.config.base_url.clone(),
|
canonical_url: state.app_ctx.config.base_url.clone(),
|
||||||
|
csrf_token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +95,7 @@ pub mod html {
|
|||||||
pub async fn get_login_page(
|
pub async fn get_login_page(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<ErrorQuery>,
|
Query(params): Query<ErrorQuery>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let ctx = HtmlPageContext {
|
let ctx = HtmlPageContext {
|
||||||
user_email: None,
|
user_email: None,
|
||||||
@@ -97,6 +104,7 @@ pub mod html {
|
|||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Login — Movies Diary".to_string(),
|
page_title: "Login — Movies Diary".to_string(),
|
||||||
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
||||||
|
csrf_token: csrf.0,
|
||||||
};
|
};
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
@@ -110,8 +118,12 @@ pub mod html {
|
|||||||
|
|
||||||
pub async fn post_login(
|
pub async fn post_login(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<LoginForm>,
|
Form(form): Form<LoginForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match login_uc::execute(
|
match login_uc::execute(
|
||||||
&state.app_ctx,
|
&state.app_ctx,
|
||||||
LoginCommand {
|
LoginCommand {
|
||||||
@@ -145,6 +157,7 @@ pub mod html {
|
|||||||
pub async fn get_register_page(
|
pub async fn get_register_page(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<ErrorQuery>,
|
Query(params): Query<ErrorQuery>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if !state.app_ctx.config.allow_registration {
|
if !state.app_ctx.config.allow_registration {
|
||||||
return Redirect::to("/").into_response();
|
return Redirect::to("/").into_response();
|
||||||
@@ -156,6 +169,7 @@ pub mod html {
|
|||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Register — Movies Diary".to_string(),
|
page_title: "Register — Movies Diary".to_string(),
|
||||||
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
||||||
|
csrf_token: csrf.0,
|
||||||
};
|
};
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
@@ -169,11 +183,15 @@ pub mod html {
|
|||||||
|
|
||||||
pub async fn post_register(
|
pub async fn post_register(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<RegisterForm>,
|
Form(form): Form<RegisterForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if !state.app_ctx.config.allow_registration {
|
if !state.app_ctx.config.allow_registration {
|
||||||
return Redirect::to("/").into_response();
|
return Redirect::to("/").into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
let email = form.email.clone();
|
let email = form.email.clone();
|
||||||
let password = form.password.clone();
|
let password = form.password.clone();
|
||||||
match register_uc::execute(
|
match register_uc::execute(
|
||||||
@@ -205,8 +223,9 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<ErrorQuery>,
|
Query(params): Query<ErrorQuery>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, Some(user_id)).await;
|
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
||||||
ctx.page_title = "Log a Review — Movies Diary".to_string();
|
ctx.page_title = "Log a Review — Movies Diary".to_string();
|
||||||
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
|
||||||
let html = state
|
let html = state
|
||||||
@@ -222,8 +241,12 @@ pub mod html {
|
|||||||
pub async fn post_review(
|
pub async fn post_review(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<LogReviewForm>,
|
Form(form): Form<LogReviewForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
let data = match LogReviewData::try_from(form) {
|
let data = match LogReviewData::try_from(form) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -243,9 +266,13 @@ pub mod html {
|
|||||||
pub async fn post_delete_review(
|
pub async fn post_delete_review(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Path(review_id): Path<Uuid>,
|
Path(review_id): Path<Uuid>,
|
||||||
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
let cmd = DeleteReviewCommand {
|
let cmd = DeleteReviewCommand {
|
||||||
review_id,
|
review_id,
|
||||||
requesting_user_id: user_id.value(),
|
requesting_user_id: user_id.value(),
|
||||||
@@ -312,8 +339,9 @@ pub mod html {
|
|||||||
OptionalCookieUser(user_id): OptionalCookieUser,
|
OptionalCookieUser(user_id): OptionalCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<DiaryQueryParams>,
|
Query(params): Query<DiaryQueryParams>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let ctx = build_page_context(&state, user_id).await;
|
let ctx = build_page_context(&state, user_id, csrf.0).await;
|
||||||
let query = application::queries::GetActivityFeedQuery {
|
let query = application::queries::GetActivityFeedQuery {
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
offset: params.offset,
|
offset: params.offset,
|
||||||
@@ -342,8 +370,9 @@ pub mod html {
|
|||||||
pub async fn get_users_list(
|
pub async fn get_users_list(
|
||||||
OptionalCookieUser(user_id): OptionalCookieUser,
|
OptionalCookieUser(user_id): OptionalCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, user_id).await;
|
let mut ctx = build_page_context(&state, user_id, csrf.0).await;
|
||||||
ctx.page_title = "Members — Movies Diary".to_string();
|
ctx.page_title = "Members — Movies Diary".to_string();
|
||||||
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||||
match application::use_cases::get_users::execute(
|
match application::use_cases::get_users::execute(
|
||||||
@@ -369,6 +398,7 @@ pub mod html {
|
|||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
Query(params): Query<crate::dtos::ProfileQueryParams>,
|
Query(params): Query<crate::dtos::ProfileQueryParams>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Content negotiation: AP clients request application/activity+json
|
// Content negotiation: AP clients request application/activity+json
|
||||||
let accept = headers
|
let accept = headers
|
||||||
@@ -393,7 +423,7 @@ pub mod html {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ctx = build_page_context(&state, user_id.clone()).await;
|
let mut ctx = build_page_context(&state, user_id.clone(), csrf.0).await;
|
||||||
let view_str = params.view.as_deref().unwrap_or("recent");
|
let view_str = params.view.as_deref().unwrap_or("recent");
|
||||||
let profile_view = match application::queries::ProfileView::from_str(view_str) {
|
let profile_view = match application::queries::ProfileView::from_str(view_str) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -520,11 +550,15 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<FollowForm>,
|
Form(form): Form<FollowForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match state.ap_service.follow(user_id.value(), &form.handle).await {
|
match state.ap_service.follow(user_id.value(), &form.handle).await {
|
||||||
Ok(()) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
Ok(()) => Redirect::to(&format!("/users/{}", profile_user_uuid)).into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -539,11 +573,15 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<UnfollowForm>,
|
Form(form): Form<UnfollowForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match state
|
match state
|
||||||
.ap_service
|
.ap_service
|
||||||
.unfollow(user_id.value(), &form.actor_url)
|
.unfollow(user_id.value(), &form.actor_url)
|
||||||
@@ -566,11 +604,15 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<FollowerActionForm>,
|
Form(form): Form<FollowerActionForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match state
|
match state
|
||||||
.ap_service
|
.ap_service
|
||||||
.accept_follower(user_id.value(), &form.actor_url)
|
.accept_follower(user_id.value(), &form.actor_url)
|
||||||
@@ -588,11 +630,15 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<FollowerActionForm>,
|
Form(form): Form<FollowerActionForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match state
|
match state
|
||||||
.ap_service
|
.ap_service
|
||||||
.reject_follower(user_id.value(), &form.actor_url)
|
.reject_follower(user_id.value(), &form.actor_url)
|
||||||
@@ -611,11 +657,12 @@ pub mod html {
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
Query(params): Query<crate::dtos::ErrorQuery>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone())).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Following — Movies Diary".to_string();
|
ctx.page_title = "Following — Movies Diary".to_string();
|
||||||
ctx.canonical_url = format!(
|
ctx.canonical_url = format!(
|
||||||
"{}/users/{}/following-list",
|
"{}/users/{}/following-list",
|
||||||
@@ -658,11 +705,12 @@ pub mod html {
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
Query(params): Query<crate::dtos::ErrorQuery>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone())).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Followers — Movies Diary".to_string();
|
ctx.page_title = "Followers — Movies Diary".to_string();
|
||||||
ctx.canonical_url = format!(
|
ctx.canonical_url = format!(
|
||||||
"{}/users/{}/followers-list",
|
"{}/users/{}/followers-list",
|
||||||
@@ -708,11 +756,15 @@ pub mod html {
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Form(form): Form<FollowerActionForm>,
|
Form(form): Form<FollowerActionForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
|
}
|
||||||
match state
|
match state
|
||||||
.ap_service
|
.ap_service
|
||||||
.remove_follower(user_id.value(), &form.actor_url)
|
.remove_follower(user_id.value(), &form.actor_url)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod csrf;
|
||||||
pub mod dtos;
|
pub mod dtos;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod event_handlers;
|
pub mod event_handlers;
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
|||||||
"/users/{id}/feed.rss",
|
"/users/{id}/feed.rss",
|
||||||
routing::get(handlers::rss::get_user_feed),
|
routing::get(handlers::rss::get_user_feed),
|
||||||
)
|
)
|
||||||
|
.layer(middleware::from_fn(crate::csrf::csrf_middleware))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_routes(rate_limit: u64) -> Router<AppState> {
|
fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||||
|
|||||||
Reference in New Issue
Block a user