feat: Add password confirmation and profile editing to user management

Frontend:
- Add password confirmation field when creating sub-users
- Add password mismatch validation
- Add 'Meine Stammdaten' form for primary users to edit their profile
- Extend session context with updateUser() function
- Disable autocomplete for sensitive fields
This commit is contained in:
2026-03-17 17:04:45 +01:00
parent 91f67f7dfc
commit e315160975
2 changed files with 230 additions and 5 deletions

View File

@@ -14,12 +14,14 @@ interface SessionContextValue {
user: UserOption | null; user: UserOption | null;
ready: boolean; ready: boolean;
setSession: (session: SessionResponse | null) => void; setSession: (session: SessionResponse | null) => void;
updateUser: (user: UserOption) => void;
} }
const SessionContext = createContext<SessionContextValue>({ const SessionContext = createContext<SessionContextValue>({
user: null, user: null,
ready: false, ready: false,
setSession: () => undefined, setSession: () => undefined,
updateUser: () => undefined,
}); });
function loadStoredUser(): UserOption | null { function loadStoredUser(): UserOption | null {
@@ -91,11 +93,16 @@ export function SessionProvider({ children }: PropsWithChildren) {
setReady(true); setReady(true);
} }
function updateUser(updatedUser: UserOption) {
setUserState(updatedUser);
}
const value = useMemo( const value = useMemo(
() => ({ () => ({
user, user,
ready, ready,
setSession, setSession,
updateUser,
}), }),
[ready, user], [ready, user],
); );

View File

@@ -12,7 +12,7 @@ interface SubUserRow {
} }
export default function UserManagementPage() { export default function UserManagementPage() {
const { user } = useSession(); const { user, updateUser } = useSession();
const [users, setUsers] = useState<SubUserRow[]>([]); const [users, setUsers] = useState<SubUserRow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
@@ -25,8 +25,39 @@ export default function UserManagementPage() {
const [newUserName, setNewUserName] = useState(""); const [newUserName, setNewUserName] = useState("");
const [newUserEmail, setNewUserEmail] = useState(""); const [newUserEmail, setNewUserEmail] = useState("");
const [newUserPassword, setNewUserPassword] = useState(""); const [newUserPassword, setNewUserPassword] = useState("");
const [newUserPasswordConfirm, setNewUserPasswordConfirm] = useState("");
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
// Form state for editing own profile
const [showProfileForm, setShowProfileForm] = useState(false);
const [profileData, setProfileData] = useState({
displayName: "",
companyName: "",
street: "",
houseNumber: "",
postalCode: "",
city: "",
email: "",
phoneNumber: "",
});
const [savingProfile, setSavingProfile] = useState(false);
// Initialize profile data from session user
useEffect(() => {
if (user) {
setProfileData({
displayName: user.displayName || "",
companyName: user.companyName || "",
street: user.street || "",
houseNumber: user.houseNumber || "",
postalCode: user.postalCode || "",
city: user.city || "",
email: user.email || "",
phoneNumber: user.phoneNumber || "",
});
}
}, [user]);
useEffect(() => { useEffect(() => {
async function loadUsers() { async function loadUsers() {
try { try {
@@ -98,6 +129,10 @@ export default function UserManagementPage() {
setMessage("Bitte alle Felder ausfüllen."); setMessage("Bitte alle Felder ausfüllen.");
return; return;
} }
if (newUserPassword !== newUserPasswordConfirm) {
setMessage("Die Passwörter stimmen nicht überein.");
return;
}
setCreating(true); setCreating(true);
try { try {
@@ -124,6 +159,7 @@ export default function UserManagementPage() {
setNewUserName(""); setNewUserName("");
setNewUserEmail(""); setNewUserEmail("");
setNewUserPassword(""); setNewUserPassword("");
setNewUserPasswordConfirm("");
setShowCreateForm(false); setShowCreateForm(false);
setMessage(`Benutzer "${newUserName}" wurde erstellt.`); setMessage(`Benutzer "${newUserName}" wurde erstellt.`);
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
@@ -134,6 +170,52 @@ export default function UserManagementPage() {
} }
} }
async function handleSaveProfile(e: React.FormEvent) {
e.preventDefault();
if (!profileData.displayName.trim()) {
setMessage("Name ist erforderlich.");
return;
}
setSavingProfile(true);
try {
const response = await apiPost<UserRow>("/portal/users", {
id: user?.id,
displayName: profileData.displayName.trim(),
companyName: profileData.companyName.trim() || null,
street: profileData.street.trim() || null,
houseNumber: profileData.houseNumber.trim() || null,
postalCode: profileData.postalCode.trim() || null,
city: profileData.city.trim() || null,
email: profileData.email.trim() || null,
phoneNumber: profileData.phoneNumber.trim() || null,
});
// Update session user
if (updateUser && user) {
updateUser({
...user,
displayName: response.displayName,
companyName: response.companyName,
street: response.street,
houseNumber: response.houseNumber,
postalCode: response.postalCode,
city: response.city,
email: response.email,
phoneNumber: response.phoneNumber,
});
}
setShowProfileForm(false);
setMessage("Ihre Stammdaten wurden aktualisiert.");
setTimeout(() => setMessage(null), 3000);
} catch (error) {
setMessage((error as Error).message);
} finally {
setSavingProfile(false);
}
}
function formatDate(value: string) { function formatDate(value: string) {
return new Intl.DateTimeFormat("de-DE", { return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium", dateStyle: "medium",
@@ -174,7 +256,8 @@ export default function UserManagementPage() {
className={ className={
message.includes("freigegeben") || message.includes("freigegeben") ||
message.includes("gesperrt") || message.includes("gesperrt") ||
message.includes("erstellt") message.includes("erstellt") ||
message.includes("aktualisiert")
? "alert alert--success" ? "alert alert--success"
: "alert alert--error" : "alert alert--error"
} }
@@ -183,6 +266,127 @@ export default function UserManagementPage() {
</div> </div>
) : null} ) : null}
{/* Own Profile Form (nur für Hauptbenutzer) */}
{isPrimaryUser && !isAdmin && (
<section className="section-card">
{!showProfileForm ? (
<div className="section-card__header">
<div>
<p className="eyebrow">Meine Stammdaten</p>
<h3>{user?.displayName}</h3>
</div>
<button
type="button"
className="accent-button"
onClick={() => setShowProfileForm(true)}
>
Bearbeiten
</button>
</div>
) : (
<>
<div className="section-card__header">
<div>
<p className="eyebrow">Meine Stammdaten</p>
<h3>Stammdaten bearbeiten</h3>
</div>
<button
type="button"
className="ghost-button"
onClick={() => setShowProfileForm(false)}
>
Abbrechen
</button>
</div>
<form onSubmit={handleSaveProfile} className="field-grid field-grid--2col">
<label className="field">
<span>Name *</span>
<input
type="text"
value={profileData.displayName}
onChange={(e) => setProfileData({ ...profileData, displayName: e.target.value })}
placeholder="Ihr Name"
required
/>
</label>
<label className="field">
<span>Firma</span>
<input
type="text"
value={profileData.companyName}
onChange={(e) => setProfileData({ ...profileData, companyName: e.target.value })}
placeholder="Firmenname"
/>
</label>
<label className="field">
<span>Straße</span>
<input
type="text"
value={profileData.street}
onChange={(e) => setProfileData({ ...profileData, street: e.target.value })}
placeholder="Straße"
/>
</label>
<label className="field">
<span>Hausnummer</span>
<input
type="text"
value={profileData.houseNumber}
onChange={(e) => setProfileData({ ...profileData, houseNumber: e.target.value })}
placeholder="Hausnummer"
/>
</label>
<label className="field">
<span>PLZ</span>
<input
type="text"
value={profileData.postalCode}
onChange={(e) => setProfileData({ ...profileData, postalCode: e.target.value })}
placeholder="Postleitzahl"
/>
</label>
<label className="field">
<span>Ort</span>
<input
type="text"
value={profileData.city}
onChange={(e) => setProfileData({ ...profileData, city: e.target.value })}
placeholder="Ort"
/>
</label>
<label className="field">
<span>E-Mail</span>
<input
type="email"
value={profileData.email}
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
placeholder="email@beispiel.de"
/>
</label>
<label className="field">
<span>Telefon</span>
<input
type="tel"
value={profileData.phoneNumber}
onChange={(e) => setProfileData({ ...profileData, phoneNumber: e.target.value })}
placeholder="Telefonnummer"
/>
</label>
<div className="field" style={{ gridColumn: "1 / -1" }}>
<button
type="submit"
className="accent-button"
disabled={savingProfile}
>
{savingProfile ? "Wird gespeichert..." : "Stammdaten speichern"}
</button>
</div>
</form>
</>
)}
</section>
)}
{/* Create Sub-user Form (nur für Hauptnutzer) */} {/* Create Sub-user Form (nur für Hauptnutzer) */}
{isPrimaryUser && !isAdmin && ( {isPrimaryUser && !isAdmin && (
<section className="section-card"> <section className="section-card">
@@ -217,32 +421,46 @@ export default function UserManagementPage() {
</div> </div>
<form onSubmit={handleCreateUser} className="field-grid field-grid--2col"> <form onSubmit={handleCreateUser} className="field-grid field-grid--2col">
<label className="field"> <label className="field">
<span>Name</span> <span>Name *</span>
<input <input
type="text" type="text"
value={newUserName} value={newUserName}
onChange={(e) => setNewUserName(e.target.value)} onChange={(e) => setNewUserName(e.target.value)}
placeholder="Name des Benutzers" placeholder="Name des Benutzers"
autoComplete="off"
required required
/> />
</label> </label>
<label className="field"> <label className="field">
<span>E-Mail</span> <span>E-Mail *</span>
<input <input
type="email" type="email"
value={newUserEmail} value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)} onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="email@beispiel.de" placeholder="email@beispiel.de"
autoComplete="off"
required required
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Passwort</span> <span>Passwort *</span>
<input <input
type="password" type="password"
value={newUserPassword} value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)} onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Passwort" placeholder="Passwort"
autoComplete="new-password"
required
/>
</label>
<label className="field">
<span>Passwort wiederholen *</span>
<input
type="password"
value={newUserPasswordConfirm}
onChange={(e) => setNewUserPasswordConfirm(e.target.value)}
placeholder="Passwort wiederholen"
autoComplete="new-password"
required required
/> />
</label> </label>