privacy/security: add PM privacy levels and improve session visibility
All checks were successful
CI / test (push) Successful in 24s

This commit is contained in:
2026-03-08 14:26:19 +03:00
parent 528778238b
commit 76cc5e0f12
17 changed files with 229 additions and 24 deletions

View File

@@ -26,7 +26,7 @@ export function SettingsPanel({ open, onClose }: Props) {
const [prefs, setPrefs] = useState<AppPreferences>(() => getAppPreferences());
const [blockedCount, setBlockedCount] = useState(0);
const [savingPrivacy, setSavingPrivacy] = useState(false);
const [allowPrivateMessages, setAllowPrivateMessages] = useState(true);
const [privacyPrivateMessages, setPrivacyPrivateMessages] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [sessions, setSessions] = useState<AuthSession[]>([]);
const [sessionsLoading, setSessionsLoading] = useState(false);
const [twofaCode, setTwofaCode] = useState("");
@@ -52,7 +52,7 @@ export function SettingsPanel({ open, onClose }: Props) {
if (!me) {
return;
}
setAllowPrivateMessages(me.allow_private_messages);
setPrivacyPrivateMessages(me.privacy_private_messages || (me.allow_private_messages ? "everyone" : "nobody"));
setPrivacyLastSeen(me.privacy_last_seen || "everyone");
setPrivacyAvatar(me.privacy_avatar || "everyone");
setPrivacyGroupInvites(me.privacy_group_invites || "everyone");
@@ -314,7 +314,13 @@ export function SettingsPanel({ open, onClose }: Props) {
/>
<SettingsRow
label="Privacy and Security"
value={allowPrivateMessages ? "Everybody can message" : "Messages disabled"}
value={
privacyPrivateMessages === "everyone"
? "Everybody can message"
: privacyPrivateMessages === "contacts"
? "Only contacts can message"
: "Messages disabled"
}
onClick={() => setPage("privacy")}
/>
</section>
@@ -417,6 +423,18 @@ export function SettingsPanel({ open, onClose }: Props) {
<section className="rounded border border-slate-700/70 bg-slate-800/50 p-3">
<p className="mb-2 text-xs uppercase tracking-wide text-slate-400">Privacy</p>
<SettingsTextRow label="Blocked Users" value={String(blockedCount)} />
<div className="mb-2 rounded bg-slate-900/50 px-2 py-2">
<p className="mb-1 text-xs text-slate-300">Who can send me private messages?</p>
<select
className="w-full rounded bg-slate-800 px-2 py-1 text-xs"
value={privacyPrivateMessages}
onChange={(e) => setPrivacyPrivateMessages(e.target.value as "everyone" | "contacts" | "nobody")}
>
<option value="everyone">Everybody</option>
<option value="contacts">My contacts</option>
<option value="nobody">Nobody</option>
</select>
</div>
<div className="mb-2 rounded bg-slate-900/50 px-2 py-2">
<p className="mb-1 text-xs text-slate-300">Who can see my profile photo?</p>
<select
@@ -458,7 +476,8 @@ export function SettingsPanel({ open, onClose }: Props) {
setSavingPrivacy(true);
try {
const updated = await updateMyProfile({
allow_private_messages: allowPrivateMessages,
allow_private_messages: privacyPrivateMessages !== "nobody",
privacy_private_messages: privacyPrivateMessages,
privacy_last_seen: privacyLastSeen,
privacy_avatar: privacyAvatar,
privacy_group_invites: privacyGroupInvites,
@@ -586,19 +605,27 @@ export function SettingsPanel({ open, onClose }: Props) {
<div className="space-y-2">
{sessions.map((session) => (
<div className="rounded bg-slate-900/50 px-2 py-2" key={session.jti}>
<p className="truncate text-xs text-slate-300">{session.user_agent || "Unknown device"}</p>
<p className="truncate text-xs text-slate-300">
{session.user_agent || "Unknown device"}
{session.current ? " (current)" : ""}
</p>
<p className="truncate text-[11px] text-slate-500">Token: {session.token_type || "refresh"}</p>
<p className="truncate text-[11px] text-slate-400">{session.ip_address || "Unknown IP"}</p>
<p className="text-[11px] text-slate-500">{new Date(session.created_at).toLocaleString()}</p>
<button
className="mt-1 rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={async () => {
await revokeSession(session.jti);
setSessions((prev) => prev.filter((item) => item.jti !== session.jti));
}}
type="button"
>
Revoke
</button>
{session.token_type === "refresh" ? (
<button
className="mt-1 rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={async () => {
await revokeSession(session.jti);
setSessions((prev) => prev.filter((item) => item.jti !== session.jti));
}}
type="button"
>
Revoke
</button>
) : (
<p className="mt-1 text-[11px] text-slate-500">Use Revoke all to invalidate active access token.</p>
)}
</div>
))}
</div>