reset/update password
This commit is contained in:
+38
@@ -92,6 +92,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/account/passkeys/{passkey_id}/delete",
|
||||
post(delete_passkey),
|
||||
)
|
||||
.route("/account/password", post(change_password))
|
||||
.route("/lists", get(lists_page).post(create_list))
|
||||
.route("/lists/{list_id}", get(list_page))
|
||||
.route("/lists/{list_id}/items", post(add_item))
|
||||
@@ -274,6 +275,13 @@ struct DeletePasskeyForm {
|
||||
csrf: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChangePasswordForm {
|
||||
csrf: String,
|
||||
new_password: String,
|
||||
confirm_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MealForm {
|
||||
name: String,
|
||||
@@ -448,9 +456,39 @@ async fn account_page(
|
||||
&user.session.user,
|
||||
&passkeys,
|
||||
&user.session.csrf_token,
|
||||
None,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn change_password(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
LoggedForm(form): LoggedForm<ChangePasswordForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
verify_csrf(&user, &form.csrf)?;
|
||||
let passkeys = state.webauthn.list_passkeys(user.session.user.id).await?;
|
||||
let render = |error: Option<&str>, success: bool| {
|
||||
html_response(views::account_page(
|
||||
&user.session.user,
|
||||
&passkeys,
|
||||
&user.session.csrf_token,
|
||||
error,
|
||||
success,
|
||||
))
|
||||
};
|
||||
|
||||
if form.new_password != form.confirm_password {
|
||||
return Ok(render(Some("New password and confirmation do not match."), false));
|
||||
}
|
||||
|
||||
state
|
||||
.auth
|
||||
.change_password(user.session.user.id, form.new_password)
|
||||
.await?;
|
||||
Ok(render(None, true))
|
||||
}
|
||||
|
||||
async fn passkey_register_start(
|
||||
State(state): State<AppState>,
|
||||
user: CurrentUser,
|
||||
|
||||
@@ -29,6 +29,12 @@ pub trait UserRepository: Send + Sync {
|
||||
txn: &mut SqliteConnection,
|
||||
user_handle: Vec<u8>,
|
||||
) -> DomainResult<Option<User>>;
|
||||
async fn update_password_hash(
|
||||
&self,
|
||||
txn: &mut SqliteConnection,
|
||||
user_id: i64,
|
||||
password_hash: String,
|
||||
) -> DomainResult<()>;
|
||||
async fn has_users(&self, txn: &mut SqliteConnection) -> DomainResult<bool>;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,22 @@ impl AuthService {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Replaces the user's password hash with a freshly hashed new password.
|
||||
/// No current-password check is performed because the account page is
|
||||
/// already authenticated and this app has no email capabilities.
|
||||
pub async fn change_password(&self, user_id: i64, new_password: String) -> DomainResult<()> {
|
||||
let users = Arc::clone(&self.users);
|
||||
let hasher = Arc::clone(&self.hasher);
|
||||
self.db
|
||||
.run(move |txn| {
|
||||
Box::pin(async move {
|
||||
let new_hash = hasher.hash(&new_password)?;
|
||||
users.update_password_hash(txn, user_id, new_hash).await
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListService {
|
||||
|
||||
@@ -209,6 +209,21 @@ impl UserRepository for SqliteUserRepository {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn update_password_hash(
|
||||
&self,
|
||||
txn: &mut SqliteConnection,
|
||||
user_id: i64,
|
||||
password_hash: String,
|
||||
) -> DomainResult<()> {
|
||||
sqlx::query("UPDATE users SET password_hash = ?1 WHERE id = ?2")
|
||||
.bind(&password_hash)
|
||||
.bind(user_id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn has_users(&self, txn: &mut SqliteConnection) -> DomainResult<bool> {
|
||||
let row = sqlx::query("SELECT EXISTS(SELECT 1 FROM users)")
|
||||
.fetch_one(&mut *txn)
|
||||
|
||||
+27
-1
@@ -82,7 +82,13 @@ pub fn registration_closed_page() -> Markup {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn account_page(user: &User, passkeys: &[Passkey], csrf_token: &str) -> Markup {
|
||||
pub fn account_page(
|
||||
user: &User,
|
||||
passkeys: &[Passkey],
|
||||
csrf_token: &str,
|
||||
password_error: Option<&str>,
|
||||
password_success: bool,
|
||||
) -> Markup {
|
||||
page(
|
||||
"Account",
|
||||
Some(user),
|
||||
@@ -121,6 +127,26 @@ pub fn account_page(user: &User, passkeys: &[Passkey], csrf_token: &str) -> Mark
|
||||
}
|
||||
button id="add-passkey" class="button button-primary" type="button" data-csrf=(csrf_token) { "Add a passkey" }
|
||||
}
|
||||
section class="panel" {
|
||||
div class="panel-heading" {
|
||||
h2 { "Password" }
|
||||
}
|
||||
p { "Set a new password for your account." }
|
||||
@if let Some(error) = password_error {
|
||||
div class="alert alert-error" role="alert" { (error) }
|
||||
}
|
||||
@if password_success {
|
||||
div class="alert alert-success" role="alert" { "Your password has been updated." }
|
||||
}
|
||||
form method="post" action="/account/password" class="stack" {
|
||||
input type="hidden" name="csrf" value=(csrf_token);
|
||||
label for="new-password" { "New password" }
|
||||
input id="new-password" name="new_password" type="password" autocomplete="new-password" required;
|
||||
label for="confirm-password" { "Confirm new password" }
|
||||
input id="confirm-password" name="confirm_password" type="password" autocomplete="new-password" required;
|
||||
button class="button button-primary" type="submit" { "Update password" }
|
||||
}
|
||||
}
|
||||
}
|
||||
script src="/static/passkey-register.js" {}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user