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:
@@ -14,12 +14,14 @@ interface SessionContextValue {
|
||||
user: UserOption | null;
|
||||
ready: boolean;
|
||||
setSession: (session: SessionResponse | null) => void;
|
||||
updateUser: (user: UserOption) => void;
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue>({
|
||||
user: null,
|
||||
ready: false,
|
||||
setSession: () => undefined,
|
||||
updateUser: () => undefined,
|
||||
});
|
||||
|
||||
function loadStoredUser(): UserOption | null {
|
||||
@@ -91,11 +93,16 @@ export function SessionProvider({ children }: PropsWithChildren) {
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
function updateUser(updatedUser: UserOption) {
|
||||
setUserState(updatedUser);
|
||||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
ready,
|
||||
setSession,
|
||||
updateUser,
|
||||
}),
|
||||
[ready, user],
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SubUserRow {
|
||||
}
|
||||
|
||||
export default function UserManagementPage() {
|
||||
const { user } = useSession();
|
||||
const { user, updateUser } = useSession();
|
||||
const [users, setUsers] = useState<SubUserRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
@@ -25,8 +25,39 @@ export default function UserManagementPage() {
|
||||
const [newUserName, setNewUserName] = useState("");
|
||||
const [newUserEmail, setNewUserEmail] = useState("");
|
||||
const [newUserPassword, setNewUserPassword] = useState("");
|
||||
const [newUserPasswordConfirm, setNewUserPasswordConfirm] = useState("");
|
||||
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(() => {
|
||||
async function loadUsers() {
|
||||
try {
|
||||
@@ -98,6 +129,10 @@ export default function UserManagementPage() {
|
||||
setMessage("Bitte alle Felder ausfüllen.");
|
||||
return;
|
||||
}
|
||||
if (newUserPassword !== newUserPasswordConfirm) {
|
||||
setMessage("Die Passwörter stimmen nicht überein.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCreating(true);
|
||||
try {
|
||||
@@ -124,6 +159,7 @@ export default function UserManagementPage() {
|
||||
setNewUserName("");
|
||||
setNewUserEmail("");
|
||||
setNewUserPassword("");
|
||||
setNewUserPasswordConfirm("");
|
||||
setShowCreateForm(false);
|
||||
setMessage(`Benutzer "${newUserName}" wurde erstellt.`);
|
||||
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) {
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "medium",
|
||||
@@ -174,7 +256,8 @@ export default function UserManagementPage() {
|
||||
className={
|
||||
message.includes("freigegeben") ||
|
||||
message.includes("gesperrt") ||
|
||||
message.includes("erstellt")
|
||||
message.includes("erstellt") ||
|
||||
message.includes("aktualisiert")
|
||||
? "alert alert--success"
|
||||
: "alert alert--error"
|
||||
}
|
||||
@@ -183,6 +266,127 @@ export default function UserManagementPage() {
|
||||
</div>
|
||||
) : 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) */}
|
||||
{isPrimaryUser && !isAdmin && (
|
||||
<section className="section-card">
|
||||
@@ -217,32 +421,46 @@ export default function UserManagementPage() {
|
||||
</div>
|
||||
<form onSubmit={handleCreateUser} className="field-grid field-grid--2col">
|
||||
<label className="field">
|
||||
<span>Name</span>
|
||||
<span>Name *</span>
|
||||
<input
|
||||
type="text"
|
||||
value={newUserName}
|
||||
onChange={(e) => setNewUserName(e.target.value)}
|
||||
placeholder="Name des Benutzers"
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>E-Mail</span>
|
||||
<span>E-Mail *</span>
|
||||
<input
|
||||
type="email"
|
||||
value={newUserEmail}
|
||||
onChange={(e) => setNewUserEmail(e.target.value)}
|
||||
placeholder="email@beispiel.de"
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Passwort</span>
|
||||
<span>Passwort *</span>
|
||||
<input
|
||||
type="password"
|
||||
value={newUserPassword}
|
||||
onChange={(e) => setNewUserPassword(e.target.value)}
|
||||
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
|
||||
/>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user