feat(privacy): user blocklist with private chat enforcement
Some checks failed
CI / test (push) Failing after 21s

- add blocked_users table and migration
- add users API: block, unblock, list blocked users
- prevent private chat creation and private messaging when block relation exists
- add block/unblock action in private chat info panel
This commit is contained in:
2026-03-08 02:19:37 +03:00
parent ea8a50ee05
commit 159a8ba516
9 changed files with 228 additions and 6 deletions

View File

@@ -24,3 +24,16 @@ export async function getUserById(userId: number): Promise<AuthUser> {
const { data } = await http.get<AuthUser>(`/users/${userId}`);
return data;
}
export async function listBlockedUsers(): Promise<UserSearchItem[]> {
const { data } = await http.get<UserSearchItem[]>("/users/blocked");
return data;
}
export async function blockUser(userId: number): Promise<void> {
await http.post(`/users/${userId}/block`);
}
export async function unblockUser(userId: number): Promise<void> {
await http.delete(`/users/${userId}/block`);
}

View File

@@ -11,7 +11,7 @@ import {
updateChatMemberRole,
updateChatTitle
} from "../api/chats";
import { getUserById, searchUsers } from "../api/users";
import { blockUser, getUserById, listBlockedUsers, searchUsers, unblockUser } from "../api/users";
import type { AuthUser, ChatDetail, ChatMember, UserSearchItem } from "../chat/types";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
@@ -37,6 +37,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
const [searchResults, setSearchResults] = useState<UserSearchItem[]>([]);
const [muted, setMuted] = useState(false);
const [savingMute, setSavingMute] = useState(false);
const [counterpartBlocked, setCounterpartBlocked] = useState(false);
const [savingBlock, setSavingBlock] = useState(false);
const myRole = useMemo(() => members.find((m) => m.user_id === me?.id)?.role, [members, me?.id]);
const isGroupLike = chat?.type === "group" || chat?.type === "channel";
@@ -73,6 +75,14 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
if (!cancelled) {
setMuted(notificationSettings.muted);
}
if (detail.type === "private" && !detail.is_saved && detail.counterpart_user_id) {
const blocked = await listBlockedUsers();
if (!cancelled) {
setCounterpartBlocked(blocked.some((u) => u.id === detail.counterpart_user_id));
}
} else if (!cancelled) {
setCounterpartBlocked(false);
}
await refreshMembers(chatId);
} catch {
if (!cancelled) setError("Failed to load chat info");
@@ -282,6 +292,31 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
</div>
) : null}
{chat.type === "private" && !chat.is_saved && chat.counterpart_user_id ? (
<button
className="mb-3 w-full rounded bg-slate-700 px-3 py-2 text-sm disabled:opacity-60"
disabled={savingBlock}
onClick={async () => {
setSavingBlock(true);
try {
if (counterpartBlocked) {
await unblockUser(chat.counterpart_user_id!);
setCounterpartBlocked(false);
} else {
await blockUser(chat.counterpart_user_id!);
setCounterpartBlocked(true);
}
} catch {
setError("Failed to update block status");
} finally {
setSavingBlock(false);
}
}}
>
{counterpartBlocked ? "Unblock user" : "Block user"}
</button>
) : null}
{showMembersSection && (chat.type === "group" || chat.type === "channel") ? (
<button
className="w-full rounded bg-slate-700 px-3 py-2 text-sm"