mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: trigger browser password save prompt on password change
Safari, Chrome, and Firefox only offer to save passwords when they see a real form submission with page navigation, not fetch() + redirect. After the change-password API call succeeds, dynamically create a form with the username and new password (autocomplete=username + new-password), POST it to "/" causing a real navigation. The browser detects the form submission with credential fields and prompts to save. Also make the username field visible (read-only) on the change-password page since Safari ignores hidden inputs for password detection, and add autocomplete attributes to the login page fields.
This commit is contained in:
@@ -1,4 +1,42 @@
|
|||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger the browser's "Save Password" prompt by submitting a real form
|
||||||
|
* with the new credentials and causing a page navigation.
|
||||||
|
*
|
||||||
|
* Safari (and most browsers) only offer to save passwords when they detect:
|
||||||
|
* 1. A real HTMLFormElement.submit() call (not fetch / XHR)
|
||||||
|
* 2. Visible input fields with autocomplete="username" + "new-password"
|
||||||
|
* 3. An actual page navigation following the submission
|
||||||
|
*
|
||||||
|
* We POST to "/" which the SPA serves as index.html. The browser sees the
|
||||||
|
* form submission + navigation and prompts to save.
|
||||||
|
*/
|
||||||
|
function triggerBrowserPasswordSave(username: string, password: string) {
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.method = "POST";
|
||||||
|
form.action = "/";
|
||||||
|
form.style.position = "fixed";
|
||||||
|
form.style.top = "-9999px";
|
||||||
|
|
||||||
|
const uField = document.createElement("input");
|
||||||
|
uField.type = "text";
|
||||||
|
uField.name = "username";
|
||||||
|
uField.autocomplete = "username";
|
||||||
|
uField.value = username;
|
||||||
|
form.appendChild(uField);
|
||||||
|
|
||||||
|
const pField = document.createElement("input");
|
||||||
|
pField.type = "password";
|
||||||
|
pField.name = "password";
|
||||||
|
pField.autocomplete = "new-password";
|
||||||
|
pField.value = password;
|
||||||
|
form.appendChild(pField);
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
// The form.submit() causes a full page navigation to "/", so no cleanup needed.
|
||||||
|
}
|
||||||
|
|
||||||
function generatePassword(): string {
|
function generatePassword(): string {
|
||||||
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
@@ -63,8 +101,10 @@ export function ChangePasswordPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password changed, reload to re-check auth state
|
// Trigger browser password save prompt via real form submission + navigation
|
||||||
window.location.href = "/";
|
const username = localStorage.getItem("stirling-username") || "admin";
|
||||||
|
triggerBrowserPasswordSave(username, newPassword);
|
||||||
|
return; // navigation happens inside triggerBrowserPasswordSave
|
||||||
} catch {
|
} catch {
|
||||||
setError("Connection error");
|
setError("Connection error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -87,13 +127,20 @@ export function ChangePasswordPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Hidden username so the browser associates saved credentials correctly */}
|
<div>
|
||||||
<input
|
<label htmlFor="username" className="block text-sm font-medium mb-1 text-foreground">
|
||||||
type="hidden"
|
Username
|
||||||
name="username"
|
</label>
|
||||||
autoComplete="username"
|
<input
|
||||||
value={localStorage.getItem("stirling-username") || "admin"}
|
id="username"
|
||||||
/>
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
|
value={localStorage.getItem("stirling-username") || "admin"}
|
||||||
|
readOnly
|
||||||
|
className="w-full px-4 py-3 rounded-lg border border-border bg-muted text-muted-foreground cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="current-password"
|
htmlFor="current-password"
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export function LoginPage() {
|
|||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
type="text"
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="Enter username"
|
placeholder="Enter username"
|
||||||
@@ -71,6 +73,8 @@ export function LoginPage() {
|
|||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
name="password"
|
||||||
|
autoComplete="current-password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
|
|||||||
Reference in New Issue
Block a user