paths in mail

This commit is contained in:
Mateusz Gruszczyński
2026-07-28 10:13:41 +02:00
parent 351dd081b5
commit d38b5b0b51
6 changed files with 60 additions and 23 deletions
Generated
+1 -1
View File
@@ -2581,7 +2581,7 @@ dependencies = [
[[package]]
name = "rustpad"
version = "0.1.12"
version = "0.1.13"
dependencies = [
"argon2",
"aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustpad"
version = "0.1.12"
version = "0.1.13"
edition = "2024"
rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
+3
View File
@@ -43,6 +43,9 @@ pub fn router(
Router::new()
.route("/", get(home))
.route("/auth/confirm/{token}", get(home))
.route("/auth/reset-password/{token}", get(home))
.route("/auth/account-action/{token}", get(home))
.route("/p/{slug}", get(pad))
.route("/s/{token}", get(public_page))
.route("/w/{workspace_slug}", get(workspace))
+3 -3
View File
@@ -883,7 +883,7 @@ async fn send_account_action(
token: &str,
) -> Result<(), AuthError> {
let site = smtp.public_url.trim_end_matches('/');
let url = format!("{site}/?account_action_token={token}");
let url = format!("{site}/auth/account-action/{token}");
let sender = smtp
.from
.parse::<Mailbox>()
@@ -1954,7 +1954,7 @@ async fn send_registration_email(
let message = match token {
Some(token) => {
let confirmation_url = format!("{site}/?confirm_token={token}");
let confirmation_url = format!("{site}/auth/confirm/{token}");
let subject = "Confirm your RustPad account";
let text_body = format!(
"Hello {},\n\nYour RustPad account has been created.\nNickname: {}\nSite: {}\n\nConfirm the account within 24 hours by opening this link:\n{}\n",
@@ -2142,7 +2142,7 @@ async fn send_message(smtp: &SmtpConfig, message: Message, label: &str) -> Resul
async fn send_reset(smtp: &SmtpConfig, user: &User, token: &str) -> Result<(), AuthError> {
let site = smtp.public_url.trim_end_matches('/');
let reset_url = format!("{site}/?reset_token={token}");
let reset_url = format!("{site}/auth/reset-password/{token}");
let sender = smtp
.from
.parse::<Mailbox>()
+50 -13
View File
@@ -1,6 +1,6 @@
import { api } from "@rustpad/api";
import * as sessionStore from "@rustpad/session";
import { askInput, showMessage } from "@rustpad/modal";
import { askConfirm, askInput, showMessage } from "@rustpad/modal";
const { getAuthToken, setAuthSession, setNickname } = sessionStore;
const clearAuthSession = sessionStore.clearAuthSession || (() => {
@@ -319,28 +319,44 @@ export async function logoutCurrentSession() {
}
export async function handleAccountConfirmationToken() {
function mailActionToken(pathPrefix, legacyQueryName) {
const url = new URL(location.href);
const prefix = `${pathPrefix}/`;
let token = null;
if (url.pathname.startsWith(prefix)) {
const encodedToken = url.pathname.slice(prefix.length).split("/", 1)[0];
try { token = decodeURIComponent(encodedToken); } catch { token = null; }
}
if (!token) token = url.searchParams.get(legacyQueryName);
return token?.trim() || null;
}
function clearMailActionUrl() {
const url = new URL(location.href);
const token = url.searchParams.get("confirm_token");
if (!token) return;
url.searchParams.delete("confirm_token");
history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
url.searchParams.delete("reset_token");
url.searchParams.delete("account_action_token");
history.replaceState({}, "", `/${url.search}${url.hash}`);
}
export async function handleAccountConfirmationToken() {
const token = mailActionToken("/auth/confirm", "confirm_token");
if (!token) return false;
clearMailActionUrl();
try {
const result = await api("/api/auth/confirm-account", { method: "POST", body: JSON.stringify({ token }) });
await showMessage(result.message, { title: "Account confirmed" });
} catch (error) {
await showMessage(error.message, { title: "Account confirmation failed" });
}
return true;
}
export async function handleResetToken() {
const url = new URL(location.href);
const token = url.searchParams.get("reset_token");
if (!token) return;
// Remove the token immediately. Refreshing or navigating back must not reopen the reset dialog.
url.searchParams.delete("reset_token");
history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
const token = mailActionToken("/auth/reset-password", "reset_token");
if (!token) return false;
clearMailActionUrl();
const password = await askInput({
title: "Set a new password",
@@ -352,11 +368,32 @@ export async function handleResetToken() {
confirmText: "Change password",
bitwardenIgnore: true,
});
if (!password) return;
if (!password) return true;
try {
await api("/api/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, password }) });
await showMessage("Password changed. The reset link has been used and cannot be opened again.", { title: "Password changed" });
} catch (error) {
await showMessage(error.message, { title: "Password reset failed" });
}
return true;
}
export async function handleAccountActionToken() {
const token = mailActionToken("/auth/account-action", "account_action_token");
if (!token) return false;
clearMailActionUrl();
const confirmed = await askConfirm(
"Confirm the requested account action. If you did not request it, cancel and ignore the e-mail.",
{ title: "Confirm account action", confirmText: "Confirm action", danger: true },
);
if (!confirmed) return true;
try {
const result = await api("/api/auth/account-action/confirm", { method: "POST", body: JSON.stringify({ token }) });
await showMessage(result.message, { title: "Account action confirmed" });
} catch (error) {
await showMessage(error.message, { title: "Account action failed" });
}
return true;
}
+2 -5
View File
@@ -1,7 +1,7 @@
import { installGlobalDiagnostics, logInfo } from "@rustpad/logger";
installGlobalDiagnostics();
import { bindIdentityDialog, handleAccountConfirmationToken, handleResetToken, logoutCurrentSession, validateCurrentSession } from "@rustpad/auth-ui";
import { bindIdentityDialog, handleAccountActionToken, handleAccountConfirmationToken, handleResetToken, logoutCurrentSession, validateCurrentSession } from "@rustpad/auth-ui";
import { getAuthToken, setAccessToken } from "@rustpad/session";
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
@@ -83,10 +83,7 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
}
});
handleAccountConfirmationToken();
handleResetToken();
{ const params=new URLSearchParams(location.search); const token=params.get("account_action_token"); if(token){ api("/api/auth/account-action/confirm",{method:"POST",body:JSON.stringify({token})}).then(r=>toast(r.message)).catch(e=>toast(e.message)).finally(()=>{params.delete("account_action_token");history.replaceState({},"",`${location.pathname}${params.size?`?${params}`:""}${location.hash}`);}); } }
await handleAccountConfirmationToken() || await handleResetToken() || await handleAccountActionToken();
const identityDialog = document.querySelector("#identity-dialog");
const guestAccount = document.querySelector("#footer-account-guest");