mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 20:54:48 -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>
|
</div>
|
||||||
|
|
||||||
<TabsNav
|
<TabsNav
|
||||||
:tabs="['general', 'cards', 'socials', 'circles', 'backup']"
|
:tabs="['general', 'cards', 'socials', 'circles', 'blocks', 'backup']"
|
||||||
pills
|
pills
|
||||||
showheaders
|
showheaders
|
||||||
navclass="mb-3 border-bottom-0"
|
navclass="mb-3 border-bottom-0"
|
||||||
@ -562,6 +562,14 @@ const addBrackets = (str: string): string => {
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #blocks-header>
|
||||||
|
<Icon v="ban" />
|
||||||
|
<T>profile.blocks.header</T>
|
||||||
|
</template>
|
||||||
|
<template #blocks>
|
||||||
|
<BlocksList />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #backup-header>
|
<template #backup-header>
|
||||||
<Icon v="copy" />
|
<Icon v="copy" />
|
||||||
<T>profile.backup.headerShort</T>
|
<T>profile.backup.headerShort</T>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="$user() && $user().username !== user.username">
|
<div v-if="$user() && $user().username !== user.username">
|
||||||
<section v-if="$user()">
|
<section>
|
||||||
<a v-if="!showReportForm" href="#" class="small" @click.prevent="showReportForm = true">
|
<a v-if="!showReportForm" href="#" class="small" @click.prevent="showReportForm = true">
|
||||||
<Icon v="spider" />
|
<Icon v="spider" />
|
||||||
<T>report.action</T>
|
<T>report.action</T>
|
||||||
@ -37,6 +37,12 @@
|
|||||||
<T>report.sent</T>
|
<T>report.sent</T>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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')">
|
<section v-if="$isGranted('users') || $isGranted('community')">
|
||||||
<div v-if="banSnapshot" class="my-3">
|
<div v-if="banSnapshot" class="my-3">
|
||||||
<a
|
<a
|
||||||
@ -380,6 +386,11 @@ Unfortunately, I need to remove your account.
|
|||||||
this.saving = false;
|
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) {
|
copyProposal(proposal) {
|
||||||
this.user.bannedReason = proposal.bannedReason;
|
this.user.bannedReason = proposal.bannedReason;
|
||||||
this.user.bannedTerms = proposal.bannedTerms.split(',');
|
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'
|
action: 'Remove yourself'
|
||||||
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
||||||
add: 'Add people to your 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:
|
sensitive:
|
||||||
header: 'Content warning'
|
header: 'Content warning'
|
||||||
info: >
|
info: >
|
||||||
|
@ -1067,6 +1067,20 @@ profile:
|
|||||||
action: 'Remove yourself'
|
action: 'Remove yourself'
|
||||||
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
|
||||||
add: 'Add people to your 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:
|
sensitive:
|
||||||
header: 'Content warning'
|
header: 'Content warning'
|
||||||
info: >
|
info: >
|
||||||
|
@ -1690,6 +1690,20 @@ profile:
|
|||||||
action: 'Usuń się'
|
action: 'Usuń się'
|
||||||
confirm: 'Czy na pewno chcesz usunąć link do swojej wizytówki z kręgów osoby @%username%?'
|
confirm: 'Czy na pewno chcesz usunąć link do swojej wizytówki z kręgów osoby @%username%?'
|
||||||
add: 'Dodaj osoby do swojego kręgu'
|
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:
|
sensitive:
|
||||||
header: 'Ostrzeżenie o zawartości'
|
header: 'Ostrzeżenie o zawartości'
|
||||||
info: >
|
info: >
|
||||||
@ -2119,7 +2133,7 @@ ban:
|
|||||||
reason: 'Powód blokady'
|
reason: 'Powód blokady'
|
||||||
visible: '(to będzie widoczne dla osoby zbanowanej)'
|
visible: '(to będzie widoczne dla osoby zbanowanej)'
|
||||||
terms: 'Złamane punkty regulaminu (wymagane)'
|
terms: 'Złamane punkty regulaminu (wymagane)'
|
||||||
action: 'Zablokuj tę osobę'
|
action: 'Zbanuj tę osobę'
|
||||||
confirm: 'Czy na pewno chcesz zbanować @%username%?'
|
confirm: 'Czy na pewno chcesz zbanować @%username%?'
|
||||||
header: 'Twoje konto jest zablokowane. Twoje profile nie są widoczne publicznie.'
|
header: 'Twoje konto jest zablokowane. Twoje profile nie są widoczne publicznie.'
|
||||||
banned: 'Konto nieaktywne'
|
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;
|
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(
|
history.pushState(
|
||||||
'',
|
'',
|
||||||
document.title,
|
document.title,
|
||||||
@ -74,9 +74,9 @@ const username = computed(() => {
|
|||||||
const { mainPronoun } = useMainPronoun(pronounLibrary, profile, translator);
|
const { mainPronoun } = useMainPronoun(pronounLibrary, profile, translator);
|
||||||
const flags = buildFlags(config.locale);
|
const flags = buildFlags(config.locale);
|
||||||
useSimpleHead({
|
useSimpleHead({
|
||||||
title: `@${username.value}`,
|
title: username.value ? `@${username.value}` : undefined,
|
||||||
description: computed(() => profile.value ? profile.value.description ?? null : null),
|
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,
|
noindex: true,
|
||||||
keywords: computed(() => profile.value?.flags?.map((flagName) => flags[flagName])
|
keywords: computed(() => profile.value?.flags?.map((flagName) => flags[flagName])
|
||||||
.filter((flag) => flag !== undefined)
|
.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 router = Router();
|
||||||
|
|
||||||
const fetchProfilesRoute = async (req: Request, res: Response, locale: string, user: any): Promise<Response> => {
|
const fetchProfilesRoute = async (req: Request, res: Response, locale: string, user: any): Promise<Response> => {
|
||||||
const isSelf = !!req.user && req.user.username === req.params.username;
|
const isSelf = !!req.user && req.user.username === req.params.username;
|
||||||
const isAdmin = req.isGranted('users') || req.isGranted('community');
|
const isAdmin = req.isGranted('users') || req.isGranted('community');
|
||||||
const opts = new ProfileOptions(req.query, req.locales);
|
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({
|
return res.json({
|
||||||
profiles: {},
|
profiles: {},
|
||||||
});
|
});
|
||||||
@ -573,6 +589,11 @@ router.get('/profile/get-id/:id', handleErrorAsync(async (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
router.get('/profile/versions/:username', 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`
|
return res.json((await req.db.all<Pick<ProfileRow, 'locale'>>(SQL`
|
||||||
SELECT
|
SELECT
|
||||||
profiles.locale
|
profiles.locale
|
||||||
@ -832,7 +853,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
|||||||
for (const connection of profile.circle) {
|
for (const connection of profile.circle) {
|
||||||
const toUserId = usernameIdMap[normaliseWithLink(connection.username)];
|
const toUserId = usernameIdMap[normaliseWithLink(connection.username)];
|
||||||
const relationship = connection.relationship.substring(0, 64).trim();
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
await req.db.get(SQL`INSERT INTO user_connections (id, from_profileId, to_userId, relationship) VALUES (
|
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);
|
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;
|
export default router;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user