mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 03:57:47 -04:00
Merge branch 'blocking' into 'main'
(user) allow blocking accounts See merge request PronounsPage/PronounsPage!604
This commit is contained in:
commit
d2a166da81
@ -283,7 +283,7 @@ const addBrackets = (str: string): string => {
|
||||
</div>
|
||||
|
||||
<TabsNav
|
||||
:tabs="['general', 'cards', 'socials', 'circles', 'backup']"
|
||||
:tabs="['general', 'cards', 'socials', 'circles', 'blocks', 'backup']"
|
||||
pills
|
||||
showheaders
|
||||
navclass="mb-3 border-bottom-0"
|
||||
@ -562,6 +562,14 @@ const addBrackets = (str: string): string => {
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<template #blocks-header>
|
||||
<Icon v="ban" />
|
||||
<T>profile.blocks.header</T>
|
||||
</template>
|
||||
<template #blocks>
|
||||
<BlocksList />
|
||||
</template>
|
||||
|
||||
<template #backup-header>
|
||||
<Icon v="copy" />
|
||||
<T>profile.backup.headerShort</T>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="$user() && $user().username !== user.username">
|
||||
<section v-if="$user()">
|
||||
<section>
|
||||
<a v-if="!showReportForm" href="#" class="small" @click.prevent="showReportForm = true">
|
||||
<Icon v="spider" />
|
||||
<T>report.action</T>
|
||||
@ -37,6 +37,12 @@
|
||||
<T>report.sent</T>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<a href="#" class="small" @click.prevent="block">
|
||||
<Icon v="ban" />
|
||||
<T>profile.blocks.action</T>
|
||||
</a>
|
||||
</section>
|
||||
<section v-if="$isGranted('users') || $isGranted('community')">
|
||||
<div v-if="banSnapshot" class="my-3">
|
||||
<a
|
||||
@ -380,6 +386,11 @@ Unfortunately, I need to remove your account.
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async block() {
|
||||
await this.dialogue.confirm(this.$t('profile.blocks.confirm', { username: this.user.username }), 'danger');
|
||||
await this.dialogue.postWithAlertOnError(`/api/user/block/${this.user.id}`);
|
||||
window.location.reload();
|
||||
},
|
||||
copyProposal(proposal) {
|
||||
this.user.bannedReason = proposal.bannedReason;
|
||||
this.user.bannedTerms = proposal.bannedTerms.split(',');
|
||||
|
42
components/BlocksList.vue
Normal file
42
components/BlocksList.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { useFetch } from 'nuxt/app';
|
||||
|
||||
import useDialogue from '~/composables/useDialogue.ts';
|
||||
|
||||
const { $translator: translator } = useNuxtApp();
|
||||
const dialogue = useDialogue();
|
||||
|
||||
type Block = {
|
||||
id: string;
|
||||
to_userId: string;
|
||||
to_username: string;
|
||||
};
|
||||
|
||||
const blocks = useFetch<Block[]>(`/api/user/blocks`, { lazy: true });
|
||||
|
||||
const unblock = async (block: Block) => {
|
||||
await dialogue.confirm(translator.translate('profile.blocks.unblock.confirm', { username: block.to_username }), 'danger');
|
||||
await dialogue.postWithAlertOnError(`/api/user/unblock/${block.to_userId}`);
|
||||
await blocks.refresh();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading :value="blocks.data.value">
|
||||
<ul v-if="blocks.data.value">
|
||||
<li v-if="blocks.data.value.length === 0">
|
||||
<T>profile.blocks.empty</T>
|
||||
</li>
|
||||
<li v-for="block in blocks.data.value" :key="block.id">
|
||||
@{{ block.to_username }}
|
||||
<small class="text-muted">({{ $datetime($ulidTime(block.id)) }})</small>
|
||||
<a href="#" :aria-label="$t('profile.blocks.unblock.action')" class="btn btn-link btn-sm" @click.prevent="unblock(block)">
|
||||
<Icon v="trash" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="small mb-0">
|
||||
<Icon v="info-circle" /> <T>profile.blocks.instruction</T>
|
||||
</p>
|
||||
</Loading>
|
||||
</template>
|
@ -850,6 +850,20 @@ profile:
|
||||
action: 'Remove yourself'
|
||||
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
||||
add: 'Add people to your circle'
|
||||
blocks:
|
||||
header: 'Blocked accounts'
|
||||
action: 'Block this person'
|
||||
confirm: >
|
||||
Are you sure you want to block @%username%?
|
||||
You will not be able to see each other's cards or add each other to your circles.
|
||||
This can be reversed in your Account page.
|
||||
unblock:
|
||||
action: 'Unblock this person'
|
||||
confirm: >
|
||||
Are you sure you want to unblock @%username%?
|
||||
You will be able to see each other's cards or add each other to your circles.
|
||||
empty: 'You currently do not have any accounts blocked.'
|
||||
instruction: 'You can block a person by visiting their profile.'
|
||||
sensitive:
|
||||
header: 'Content warning'
|
||||
info: >
|
||||
|
@ -1067,6 +1067,20 @@ profile:
|
||||
action: 'Remove yourself'
|
||||
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
||||
add: 'Add people to your circle'
|
||||
blocks:
|
||||
header: 'Blocked accounts'
|
||||
action: 'Block this person'
|
||||
confirm: >
|
||||
Are you sure you want to block @%username%?
|
||||
You will not be able to see each other's cards or add each other to your circles.
|
||||
This can be reversed in your Account page.
|
||||
unblock:
|
||||
action: 'Unblock this person'
|
||||
confirm: >
|
||||
Are you sure you want to unblock @%username%?
|
||||
You will be able to see each other's cards or add each other to your circles.
|
||||
empty: 'You currently do not have any accounts blocked.'
|
||||
instruction: 'You can block a person by visiting their profile.'
|
||||
sensitive:
|
||||
header: 'Content warning'
|
||||
info: >
|
||||
|
@ -1690,6 +1690,20 @@ profile:
|
||||
action: 'Usuń się'
|
||||
confirm: 'Czy na pewno chcesz usunąć link do swojej wizytówki z kręgów osoby @%username%?'
|
||||
add: 'Dodaj osoby do swojego kręgu'
|
||||
blocks:
|
||||
header: 'Zablokowane konta'
|
||||
action: 'Zablokuj tę osobę'
|
||||
confirm: >
|
||||
Czy na pewno chcesz zablokować @%username%?
|
||||
Nie będziecie mogły widzieć swoich wizytówek ani dodawać się nawzajem do kręgów.
|
||||
Możesz cofnąć blokadę na stronie „Twoje konto”.
|
||||
unblock:
|
||||
action: 'Unblock this person'
|
||||
confirm: >
|
||||
Czy na pewno chcesz odblokować @%username%?
|
||||
Będziecie mogły znowu widzieć swoje wizytówki oraz dodawać się nawzajem do kręgów.
|
||||
empty: 'You currently do not have any accounts blocked.'
|
||||
instruction: 'You can block a person by visiting their profile.'
|
||||
sensitive:
|
||||
header: 'Ostrzeżenie o zawartości'
|
||||
info: >
|
||||
@ -2119,7 +2133,7 @@ ban:
|
||||
reason: 'Powód blokady'
|
||||
visible: '(to będzie widoczne dla osoby zbanowanej)'
|
||||
terms: 'Złamane punkty regulaminu (wymagane)'
|
||||
action: 'Zablokuj tę osobę'
|
||||
action: 'Zbanuj tę osobę'
|
||||
confirm: 'Czy na pewno chcesz zbanować @%username%?'
|
||||
header: 'Twoje konto jest zablokowane. Twoje profile nie są widoczne publicznie.'
|
||||
banned: 'Konto nieaktywne'
|
||||
|
12
migrations/089-block-connections.sql
Normal file
12
migrations/089-block-connections.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- Up
|
||||
|
||||
CREATE TABLE block_connections (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
from_userId TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
to_userId TEXT NOT NULL REFERENCES users ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "block_connections_from_userId" ON "block_connections" ("from_userId");
|
||||
CREATE INDEX "block_connections_to_userId" ON "block_connections" ("to_userId");
|
||||
|
||||
-- Down
|
@ -60,7 +60,7 @@ const username = computed(() => {
|
||||
return usernameFromRoute;
|
||||
}
|
||||
|
||||
if (userAsyncData.data.value.username !== usernameFromRoute && import.meta.client) {
|
||||
if (userAsyncData.data.value.username && userAsyncData.data.value.username !== usernameFromRoute && import.meta.client) {
|
||||
history.pushState(
|
||||
'',
|
||||
document.title,
|
||||
@ -74,9 +74,9 @@ const username = computed(() => {
|
||||
const { mainPronoun } = useMainPronoun(pronounLibrary, profile, translator);
|
||||
const flags = buildFlags(config.locale);
|
||||
useSimpleHead({
|
||||
title: `@${username.value}`,
|
||||
title: username.value ? `@${username.value}` : undefined,
|
||||
description: computed(() => profile.value ? profile.value.description ?? null : null),
|
||||
banner: `api/banner/@${username.value}.png`,
|
||||
banner: username.value ? `api/banner/@${username.value}.png` : undefined,
|
||||
noindex: true,
|
||||
keywords: computed(() => profile.value?.flags?.map((flagName) => flags[flagName])
|
||||
.filter((flag) => flag !== undefined)
|
||||
|
@ -495,14 +495,30 @@ const fetchCircles = async (
|
||||
}));
|
||||
};
|
||||
|
||||
const isUserBlocked = async (db: Database, user: any, otherUser: any): Promise<boolean> => {
|
||||
if (!user || !otherUser || user.id === otherUser.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = await db.get<{ c: number }>(SQL`
|
||||
SELECT count(*) as c
|
||||
FROM block_connections
|
||||
WHERE (from_userId = ${user.id} AND to_userId = ${otherUser.id})
|
||||
OR (from_userId = ${otherUser.id} AND to_userId = ${user.id})
|
||||
`);
|
||||
|
||||
return row!.c > 0;
|
||||
};
|
||||
|
||||
const router = Router();
|
||||
|
||||
const fetchProfilesRoute = async (req: Request, res: Response, locale: string, user: any): Promise<Response> => {
|
||||
const isSelf = !!req.user && req.user.username === req.params.username;
|
||||
const isAdmin = req.isGranted('users') || req.isGranted('community');
|
||||
const opts = new ProfileOptions(req.query, req.locales);
|
||||
const isBlocked = await isUserBlocked(req.db, req.user, user);
|
||||
|
||||
if (!user || user.bannedReason !== null && !isAdmin && !isSelf) {
|
||||
if (!user || isBlocked || user.bannedReason !== null && !isAdmin && !isSelf) {
|
||||
return res.json({
|
||||
profiles: {},
|
||||
});
|
||||
@ -573,6 +589,11 @@ router.get('/profile/get-id/:id', handleErrorAsync(async (req, res) => {
|
||||
}));
|
||||
|
||||
router.get('/profile/versions/:username', handleErrorAsync(async (req, res) => {
|
||||
const user = await req.db.get<Pick<UserRow, 'id'>>(SQL`SELECT * FROM users WHERE usernameNorm = ${normalise(req.params.username)}`);
|
||||
if (await isUserBlocked(req.db, req.user, user)) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
return res.json((await req.db.all<Pick<ProfileRow, 'locale'>>(SQL`
|
||||
SELECT
|
||||
profiles.locale
|
||||
@ -832,7 +853,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||
for (const connection of profile.circle) {
|
||||
const toUserId = usernameIdMap[normaliseWithLink(connection.username)];
|
||||
const relationship = connection.relationship.substring(0, 64).trim();
|
||||
if (toUserId === undefined || !relationship) {
|
||||
if (toUserId === undefined || !relationship || await isUserBlocked(req.db, req.user, { id: toUserId })) {
|
||||
continue;
|
||||
}
|
||||
await req.db.get(SQL`INSERT INTO user_connections (id, from_profileId, to_userId, relationship) VALUES (
|
||||
|
@ -1006,4 +1006,57 @@ router.get('/user/social-lookup/:provider/:identifier', handleErrorAsync(async (
|
||||
return res.json(row ? row.username : null);
|
||||
}));
|
||||
|
||||
router.get('/user/blocks', handleErrorAsync(async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorised' });
|
||||
}
|
||||
|
||||
const rows = await req.db.all<{ id: string; to_userId: string; to_username: string }[]>(SQL`
|
||||
SELECT b.id, b.to_userId, u.username AS to_username
|
||||
FROM block_connections b
|
||||
LEFT JOIN users u ON b.to_userId = u.id
|
||||
WHERE from_userId = ${req.user.id}
|
||||
ORDER BY b.id DESC
|
||||
`);
|
||||
|
||||
return res.json(rows);
|
||||
}));
|
||||
|
||||
router.post('/user/block/:id', handleErrorAsync(async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const blockedUser = await req.db.get<UserRow>(SQL`SELECT * FROM users WHERE id = ${id}`);
|
||||
if (!req.user || !blockedUser || req.user.id === blockedUser.id) {
|
||||
return res.status(401).json({ error: 'Unauthorised' });
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
INSERT INTO block_connections (id, from_userId, to_userId)
|
||||
VALUES (${ulid()}, ${req.user.id}, ${blockedUser.id})
|
||||
`);
|
||||
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM user_connections
|
||||
WHERE (from_profileId IN (SELECT id FROM profiles WHERE userId = ${req.user.id}) AND to_userId = ${blockedUser.id})
|
||||
OR (from_profileId IN (SELECT id FROM profiles WHERE userId = ${blockedUser.id}) AND to_userId = ${req.user.id})
|
||||
`);
|
||||
|
||||
return res.json(null);
|
||||
}));
|
||||
|
||||
router.post('/user/unblock/:id', handleErrorAsync(async (req, res) => {
|
||||
const id = req.params.id;
|
||||
const blockedUser = await req.db.get<UserRow>(SQL`SELECT * FROM users WHERE id = ${id}`);
|
||||
if (!req.user || !blockedUser || req.user.id === blockedUser.id) {
|
||||
return res.status(401).json({ error: 'Unauthorised' });
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM block_connections
|
||||
WHERE from_userId = ${req.user.id}
|
||||
AND to_userId = ${blockedUser.id}
|
||||
`);
|
||||
|
||||
return res.json(null);
|
||||
}));
|
||||
|
||||
export default router;
|
||||
|
Loading…
x
Reference in New Issue
Block a user