mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
123 lines
318 KiB
Rust
123 lines
318 KiB
Rust
|
|
use askama::Template;
|
||
|
|
use axum::{
|
||
|
|
Router,
|
||
|
|
extract::{Query, State},
|
||
|
|
http::StatusCode,
|
||
|
|
response::{Html, IntoResponse, Redirect, Response},
|
||
|
|
routing::get,
|
||
|
|
};
|
||
|
|
use ruma::{UserId, user_id};
|
||
|
|
use serde::Deserialize;
|
||
|
|
use validator::Validate;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
WebError, form,
|
||
|
|
pages::components::{UserCard, form::Form},
|
||
|
|
};
|
||
|
|
|
||
|
|
const TEST: &str = "data:image/png;base64,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
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
struct PasswordResetQuery {
|
||
|
|
token: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Template)]
|
||
|
|
#[template(path = "password_reset.html.j2")]
|
||
|
|
struct PasswordReset<'a> {
|
||
|
|
user_card: UserCard<'a>,
|
||
|
|
body: PasswordResetBody,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
enum PasswordResetBody {
|
||
|
|
Form(Form<'static>),
|
||
|
|
Success,
|
||
|
|
}
|
||
|
|
|
||
|
|
form! {
|
||
|
|
struct PasswordResetForm {
|
||
|
|
#[validate(length(min = 1, message = "Password cannot be empty"))]
|
||
|
|
new_password: String where {
|
||
|
|
input_type: "password",
|
||
|
|
label: "New password",
|
||
|
|
autocomplete: "new-password"
|
||
|
|
},
|
||
|
|
|
||
|
|
#[validate(must_match(other = "new_password", message = "Passwords must match"))]
|
||
|
|
confirm_new_password: String where {
|
||
|
|
input_type: "password",
|
||
|
|
label: "Confirm new password",
|
||
|
|
autocomplete: "new-password"
|
||
|
|
}
|
||
|
|
|
||
|
|
submit: "Reset Password"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub(crate) fn build() -> Router<crate::State> {
|
||
|
|
Router::new().route(
|
||
|
|
"/_continuwuity/password_reset",
|
||
|
|
get(get_password_reset).post(post_password_reset),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn password_reset_form(
|
||
|
|
services: crate::State,
|
||
|
|
query: PasswordResetQuery,
|
||
|
|
reset_form: Form<'static>,
|
||
|
|
) -> Result<impl IntoResponse, WebError> {
|
||
|
|
let Some(token) = services.password_reset.check_token(&query.token).await else {
|
||
|
|
return Err(WebError::BadRequest("Invalid reset token".to_owned()));
|
||
|
|
};
|
||
|
|
|
||
|
|
let user_card = UserCard::for_local_user(&services, &token.info.user).await;
|
||
|
|
|
||
|
|
let template = PasswordReset {
|
||
|
|
user_card,
|
||
|
|
body: PasswordResetBody::Form(reset_form),
|
||
|
|
};
|
||
|
|
|
||
|
|
Ok(Html(template.render()?))
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn get_password_reset(
|
||
|
|
State(services): State<crate::State>,
|
||
|
|
Query(query): Query<PasswordResetQuery>,
|
||
|
|
) -> Result<impl IntoResponse, WebError> {
|
||
|
|
password_reset_form(services, query, PasswordResetForm::build(None)).await
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn post_password_reset(
|
||
|
|
State(services): State<crate::State>,
|
||
|
|
Query(query): Query<PasswordResetQuery>,
|
||
|
|
axum::Form(form): axum::Form<PasswordResetForm>,
|
||
|
|
) -> Result<Response, WebError> {
|
||
|
|
match form.validate() {
|
||
|
|
| Ok(()) => {
|
||
|
|
let Some(token) = services.password_reset.check_token(&query.token).await else {
|
||
|
|
return Err(WebError::BadRequest("Invalid reset token".to_owned()));
|
||
|
|
};
|
||
|
|
let user_id = token.info.user.clone();
|
||
|
|
|
||
|
|
services
|
||
|
|
.password_reset
|
||
|
|
.consume_token(token, &form.new_password)
|
||
|
|
.await?;
|
||
|
|
|
||
|
|
let user_card = UserCard::for_local_user(&services, &user_id).await;
|
||
|
|
let template = PasswordReset {
|
||
|
|
user_card,
|
||
|
|
body: PasswordResetBody::Success,
|
||
|
|
};
|
||
|
|
|
||
|
|
Ok(Html(template.render()?).into_response())
|
||
|
|
},
|
||
|
|
| Err(err) => Ok((
|
||
|
|
StatusCode::BAD_REQUEST,
|
||
|
|
password_reset_form(services, query, PasswordResetForm::build(Some(err))).await,
|
||
|
|
)
|
||
|
|
.into_response()),
|
||
|
|
}
|
||
|
|
}
|