feat: password management for tenant portal
- POST /api/tenant/password — change own Logto password
- POST /api/tenant/team/{userId}/password — reset team member password
- Settings page: "Change Password" card with confirm field
- Team page: "Reset Password" button per member with inline form
- LogtoManagementClient.updateUserPassword() via Logto Management API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
useTenantTeam,
|
||||
useInviteTeamMember,
|
||||
useRemoveTeamMember,
|
||||
useResetTeamMemberPassword,
|
||||
} from '../../api/tenant-hooks';
|
||||
import styles from '../../styles/platform.module.css';
|
||||
|
||||
@@ -57,6 +58,7 @@ export function TeamPage() {
|
||||
const { data: rawTeam, isLoading, isError } = useTenantTeam();
|
||||
const inviteMember = useInviteTeamMember();
|
||||
const removeMember = useRemoveTeamMember();
|
||||
const resetPassword = useResetTeamMemberPassword();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [showInvite, setShowInvite] = useState(false);
|
||||
@@ -64,6 +66,8 @@ export function TeamPage() {
|
||||
const [inviteRole, setInviteRole] = useState('viewer');
|
||||
|
||||
const [removeTarget, setRemoveTarget] = useState<TeamMember | null>(null);
|
||||
const [pwTarget, setPwTarget] = useState<TeamMember | null>(null);
|
||||
const [pwValue, setPwValue] = useState('');
|
||||
|
||||
const team: TeamMember[] = (rawTeam ?? []).map(toMember).filter((m) => m.id !== '');
|
||||
|
||||
@@ -89,12 +93,20 @@ export function TeamPage() {
|
||||
key: 'id',
|
||||
header: 'Actions',
|
||||
render: (_v, row) => (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={(e) => { e.stopPropagation(); setRemoveTarget(row); }}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={(e) => { e.stopPropagation(); setPwTarget(row); setPwValue(''); }}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={(e) => { e.stopPropagation(); setRemoveTarget(row); }}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -231,6 +243,50 @@ export function TeamPage() {
|
||||
variant="danger"
|
||||
loading={removeMember.isPending}
|
||||
/>
|
||||
|
||||
{/* Reset password inline form */}
|
||||
{pwTarget && (
|
||||
<Card title={`Reset password for ${pwTarget.name}`}>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (pwValue.length < 8) {
|
||||
toast({ title: 'Password must be at least 8 characters', variant: 'error' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await resetPassword.mutateAsync({ userId: pwTarget.id, password: pwValue });
|
||||
toast({ title: `Password reset for ${pwTarget.name}`, variant: 'success' });
|
||||
setPwTarget(null);
|
||||
setPwValue('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Reset failed', description: String(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
|
||||
>
|
||||
<FormField label="New password" htmlFor="reset-pw">
|
||||
<Input
|
||||
id="reset-pw"
|
||||
type="password"
|
||||
placeholder="Min. 8 characters"
|
||||
value={pwValue}
|
||||
onChange={(e) => setPwValue(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
/>
|
||||
</FormField>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button type="submit" variant="primary" loading={resetPassword.isPending}>
|
||||
Reset Password
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => setPwTarget(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user