feat(settings): harden privacy and sessions actions UX
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -38,6 +38,8 @@ export function SettingsPanel({ open, onClose }: Props) {
|
||||
const [privacyPrivateMessages, setPrivacyPrivateMessages] = useState<"everyone" | "contacts" | "nobody">("everyone");
|
||||
const [sessions, setSessions] = useState<AuthSession[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
const [revokeAllLoading, setRevokeAllLoading] = useState(false);
|
||||
const [revokingSessionIds, setRevokingSessionIds] = useState<string[]>([]);
|
||||
const [twofaCode, setTwofaCode] = useState("");
|
||||
const [twofaSecret, setTwofaSecret] = useState<string | null>(null);
|
||||
const [twofaUrl, setTwofaUrl] = useState<string | null>(null);
|
||||
@@ -521,6 +523,9 @@ export function SettingsPanel({ open, onClose }: Props) {
|
||||
privacy_group_invites: privacyGroupInvites,
|
||||
});
|
||||
useAuthStore.setState({ me: updated as AuthUser });
|
||||
showToast("Privacy settings saved");
|
||||
} catch {
|
||||
showToast("Failed to save privacy settings");
|
||||
} finally {
|
||||
setSavingPrivacy(false);
|
||||
}
|
||||
@@ -665,16 +670,30 @@ export function SettingsPanel({ open, onClose }: Props) {
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-400">Active Sessions</p>
|
||||
<button
|
||||
className="rounded bg-slate-700 px-2 py-1 text-xs"
|
||||
className="rounded bg-slate-700 px-2 py-1 text-xs disabled:opacity-60"
|
||||
onClick={async () => {
|
||||
await revokeAllSessions();
|
||||
setSessions([]);
|
||||
logout();
|
||||
onClose();
|
||||
if (revokeAllLoading) {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm("Revoke all sessions and log out on this device?")) {
|
||||
return;
|
||||
}
|
||||
setRevokeAllLoading(true);
|
||||
try {
|
||||
await revokeAllSessions();
|
||||
setSessions([]);
|
||||
logout();
|
||||
onClose();
|
||||
} catch {
|
||||
showToast("Failed to revoke sessions");
|
||||
} finally {
|
||||
setRevokeAllLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={revokeAllLoading}
|
||||
type="button"
|
||||
>
|
||||
Revoke all
|
||||
{revokeAllLoading ? "Revoking..." : "Revoke all"}
|
||||
</button>
|
||||
</div>
|
||||
{sessionsLoading ? <p className="text-xs text-slate-400">Loading sessions...</p> : null}
|
||||
@@ -691,14 +710,23 @@ export function SettingsPanel({ open, onClose }: Props) {
|
||||
<p className="text-[11px] text-slate-500">{new Date(session.created_at).toLocaleString()}</p>
|
||||
{session.token_type === "refresh" ? (
|
||||
<button
|
||||
className="mt-1 rounded bg-slate-700 px-2 py-1 text-[11px]"
|
||||
className="mt-1 rounded bg-slate-700 px-2 py-1 text-[11px] disabled:opacity-60"
|
||||
onClick={async () => {
|
||||
await revokeSession(session.jti);
|
||||
setSessions((prev) => prev.filter((item) => item.jti !== session.jti));
|
||||
const token = session.jti;
|
||||
setRevokingSessionIds((prev) => (prev.includes(token) ? prev : [...prev, token]));
|
||||
try {
|
||||
await revokeSession(session.jti);
|
||||
setSessions((prev) => prev.filter((item) => item.jti !== session.jti));
|
||||
} catch {
|
||||
showToast("Failed to revoke session");
|
||||
} finally {
|
||||
setRevokingSessionIds((prev) => prev.filter((item) => item !== token));
|
||||
}
|
||||
}}
|
||||
disabled={revokingSessionIds.includes(session.jti)}
|
||||
type="button"
|
||||
>
|
||||
Revoke
|
||||
{revokingSessionIds.includes(session.jti) ? "Revoking..." : "Revoke"}
|
||||
</button>
|
||||
) : (
|
||||
<p className="mt-1 text-[11px] text-slate-500">Use Revoke all to invalidate active access token.</p>
|
||||
|
||||
Reference in New Issue
Block a user