#174 better banning

This commit is contained in:
Avris 2021-06-16 16:08:38 +02:00
parent 35fb535955
commit fb689e2f6c
16 changed files with 182 additions and 11 deletions

View File

@ -1,5 +1,6 @@
<template> <template>
<header v-if="config.header" class="mb-4"> <div v-if="config.header" class="mb-4">
<header>
<div class="d-none d-lg-flex justify-content-between align-items-center flex-row nav-custom btn-group mb-0"> <div class="d-none d-lg-flex justify-content-between align-items-center flex-row nav-custom btn-group mb-0">
<nuxt-link v-for="link in links" :key="link.link" :to="link.link" :class="`nav-item btn btn-sm ${isActiveRoute(link) ? 'active' : ''} ${link.header ? 'flex-grow-0' : ''}`"> <nuxt-link v-for="link in links" :key="link.link" :to="link.link" :class="`nav-item btn btn-sm ${isActiveRoute(link) ? 'active' : ''} ${link.header ? 'flex-grow-0' : ''}`">
<h1 v-if="link.header" class="text-nowrap"> <h1 v-if="link.header" class="text-nowrap">
@ -48,18 +49,39 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="locales[config.locale].published === false" class="alert alert-warning mt-3"> </header>
<div v-if="locales[config.locale].published === false" class="alert alert-warning mb-0">
<Icon v="exclamation-triangle"/> <Icon v="exclamation-triangle"/>
This language version is still under construction! This language version is still under construction!
</div> </div>
<div v-show="showCensus" class="alert alert-info mt-3"> <div v-show="showCensus" class="alert alert-info mb-0">
<a href="#" class="float-end" @click.prevent="dismissCensus"> <a href="#" class="float-end" @click.prevent="dismissCensus">
<Icon v="times"/> <Icon v="times"/>
</a> </a>
<Icon v="user-chart" size="2" class="d-inline-block float-start me-3 mt-2"/> <Icon v="user-chart" size="2" class="d-inline-block float-start me-3 mt-2"/>
<T silent>census.banner</T> <T silent>census.banner</T>
</div> </div>
</header> <div v-if="$user() && $user().bannedReason" class="alert alert-danger mb-0 container">
<p class="h4 mb-2">
<Icon v="ban"/>
<T>ban.header</T>
</p>
<p >
<T>ban.reason</T>:
{{$user().bannedReason}}
</p>
<p>
<T>ban.termsIntro</T>
</p>
<blockquote class="small">
It is forbidden to post on the Service any Content that might break the law or violate social norms,
including but not limited to:
propagation of totalitarian regimes, hate speech, racism, xenophobia, homophobia, transphobia, queerphobia,
misogyny, harassment, impersonation, child pornography, unlawful conduct, misinformation,
sharing of someone else's personal data, spam, advertisement, copyright or trademark violations.
</blockquote>
</div>
</div>
<header v-else class="mb-4"> <header v-else class="mb-4">
<h1 class="text-nowrap"> <h1 class="text-nowrap">
<nuxt-link to="/"> <nuxt-link to="/">

View File

@ -496,6 +496,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terms=Terms of Service}:'
flags: flags:
Abrosexual: 'Abrosexuell' Abrosexual: 'Abrosexuell'
Achillean: 'Achillean' Achillean: 'Achillean'

View File

@ -571,3 +571,11 @@ error:
mode: mode:
dark: 'Dark mode' dark: 'Dark mode'
light: 'Light mode' light: 'Light mode'
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terms=Terms of Service}:'

View File

@ -506,6 +506,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:'
flags: flags:
Abroromantic: 'Abrorrománti{inflection_c}' Abroromantic: 'Abrorrománti{inflection_c}'
Abrosexual: 'Abrosexual' Abrosexual: 'Abrosexual'

View File

@ -487,6 +487,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terms=Terms of Service}:'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -500,6 +500,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/voorwaarden=Terms of Service}:'
flags: flags:
Abroromantic: 'Abroromantisch' Abroromantic: 'Abroromantisch'
Abrosexual: 'Abroseksueel' Abrosexual: 'Abroseksueel'

View File

@ -1105,6 +1105,14 @@ mode:
dark: 'Tryb nocny' dark: 'Tryb nocny'
light: 'Tryb dzienny' light: 'Tryb dzienny'
ban:
reason: 'Powód blokady'
action: 'Zablokuj tę osobę'
confirm: 'Czy na pewno chcesz zbanować @%username%?'
header: 'Twoje konto jest zablokowane. Twoje profile nie są widoczne publicznie.'
banned: 'Zbanowanx'
termsIntro: 'Zgodnie z naszym {/regulamin=Regulaminem}:'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -504,6 +504,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/termos=Terms of Service}:'
flags: flags:
Abroromantic: 'Abrorromânti{inflection_c}' Abroromantic: 'Abrorromânti{inflection_c}'
Abrosexual: 'Abrossexual' Abrosexual: 'Abrossexual'

View File

@ -1076,6 +1076,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -517,6 +517,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:'
flags: flags:
Abroromantic: 'אַבראָראָמאַנטיש' Abroromantic: 'אַבראָראָמאַנטיש'
Abrosexual: 'אַבראָסעקסועל' Abrosexual: 'אַבראָסעקסועל'

View File

@ -485,6 +485,15 @@ mode:
dark: 'Dark mode' # TODO dark: 'Dark mode' # TODO
light: 'Light mode' light: 'Light mode'
# TODO
ban:
reason: 'Ban reason'
action: 'Ban this person'
confirm: 'Are you sure you want to ban @%username%?'
header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:'
flags: flags:
Abrosexual: '嫩性戀' Abrosexual: '嫩性戀'
Abroromantic: '嫩浪漫傾向' Abroromantic: '嫩浪漫傾向'

5
migrations/027-ban.sql Normal file
View File

@ -0,0 +1,5 @@
-- Up
ALTER TABLE users ADD COLUMN bannedReason TEXT NULL;
-- Down

View File

@ -31,6 +31,16 @@
</div> </div>
</div> </div>
<section v-if="$isGranted('users') && profile.bannedReason">
<div class="alert alert-warning">
<p class="h4">
<Icon v="ban"/>
{{$t('ban.banned')}}
</p>
<p class="mb-0">{{profile.bannedReason}}</p>
</div>
</section>
<section v-if="profile.age ||profile.description.trim().length"> <section v-if="profile.age ||profile.description.trim().length">
<p v-for="line in profile.description.split('\n')" class="mb-1"> <p v-for="line in profile.description.split('\n')" class="mb-1">
<Spelling escape :text="line"/> <Spelling escape :text="line"/>
@ -111,6 +121,16 @@
<OpinionLegend/> <OpinionLegend/>
</section> </section>
<section v-if="$isGranted('users')">
<div class="alert alert-warning">
<textarea v-model="profile.bannedReason" class="form-control" rows="3" :placeholder="$t('ban.reason')" :disabled="saving"></textarea>
<button class="btn btn-danger d-block w-100 mt-2" :disabled="saving" @click="ban">
<Icon v="ban"/>
{{$t('ban.action')}}
</button>
</div>
</section>
<Separator icon="heart"/> <Separator icon="heart"/>
<Support/> <Support/>
<section> <section>
@ -137,16 +157,17 @@
</template> </template>
<script> <script>
import { head } from "../src/helpers"; import {head, listToDict} from "../src/helpers";
import { pronouns } from "~/src/data"; import { pronouns } from "~/src/data";
import { buildPronoun } from "../src/buildPronoun"; import { buildPronoun } from "../src/buildPronoun";
export default { export default {
data() { data() {
return { return {
profiles: {}, profiles: {},
glue: ' ' + this.$t('pronouns.or') + ' ', glue: ' ' + this.$t('pronouns.or') + ' ',
allFlags: process.env.FLAGS, allFlags: process.env.FLAGS,
saving: false,
} }
}, },
async asyncData({ app, route }) { async asyncData({ app, route }) {
@ -238,6 +259,20 @@
return mainPronoun; return mainPronoun;
}, },
}, },
methods: {
async ban() {
await this.$confirm(this.$t('ban.confirm', {username: this.username}), 'danger');
this.saving = true;
try {
await this.$post(`/admin/ban/${encodeURIComponent(this.username)}`, {
reason: this.profile.bannedReason,
});
window.location.reload();
} finally {
this.saving = false;
}
}
},
head() { head() {
return head({ return head({
title: `@${this.username}`, title: `@${this.username}`,

View File

@ -117,4 +117,20 @@ router.get('/admin/stats', handleErrorAsync(async (req, res) => {
return res.json(stats); return res.json(stats);
})); }));
const normalise = s => s.trim().toLowerCase();
router.post('/admin/ban/:username', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'});
}
await req.db.get(SQL`
UPDATE users
SET bannedReason = ${req.body.reason || null}
WHERE lower(trim(replace(replace(replace(replace(replace(replace(replace(replace(replace(username, 'Ą', 'ą'), 'Ć', 'ć'), 'Ę', 'ę'), 'Ł', 'ł'), 'Ń', 'ń'), 'Ó', 'ó'), 'Ś', 'ś'), 'Ż', 'ż'), 'Ź', 'ż'))) = ${normalise(req.params.username)}
`);
return res.json(true);
}));
export default router; export default router;

View File

@ -24,9 +24,9 @@ const calcAge = birthday => {
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25)); return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
} }
const fetchProfiles = async (db, username, self) => { const fetchProfiles = async (db, username, self, isAdmin) => {
const profiles = await db.all(SQL` const profiles = await db.all(SQL`
SELECT profiles.*, users.id, users.username, users.email, users.avatarSource FROM profiles LEFT JOIN users on users.id == profiles.userId SELECT profiles.*, users.id, users.username, users.email, users.avatarSource, users.bannedReason FROM profiles LEFT JOIN users on users.id == profiles.userId
WHERE lower(trim(replace(replace(replace(replace(replace(replace(replace(replace(replace(username, 'Ą', 'ą'), 'Ć', 'ć'), 'Ę', 'ę'), 'Ł', 'ł'), 'Ń', 'ń'), 'Ó', 'ó'), 'Ś', 'ś'), 'Ż', 'ż'), 'Ź', 'ż'))) = ${normalise(username)} WHERE lower(trim(replace(replace(replace(replace(replace(replace(replace(replace(replace(username, 'Ą', 'ą'), 'Ć', 'ć'), 'Ę', 'ę'), 'Ł', 'ł'), 'Ń', 'ń'), 'Ó', 'ó'), 'Ś', 'ś'), 'Ż', 'ż'), 'Ź', 'ż'))) = ${normalise(username)}
AND profiles.active = 1 AND profiles.active = 1
ORDER BY profiles.locale ORDER BY profiles.locale
@ -34,6 +34,9 @@ const fetchProfiles = async (db, username, self) => {
const p = {} const p = {}
for (let profile of profiles) { for (let profile of profiles) {
if (profile.bannedReason !== null && !isAdmin && !self) {
return {};
}
p[profile.locale] = { p[profile.locale] = {
id: profile.id, id: profile.id,
userId: profile.userId, userId: profile.userId,
@ -52,6 +55,7 @@ const fetchProfiles = async (db, username, self) => {
teamName: profile.teamName, teamName: profile.teamName,
footerName: profile.footerName, footerName: profile.footerName,
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [], footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
bannedReason: profile.bannedReason,
}; };
} }
return p; return p;
@ -60,7 +64,7 @@ const fetchProfiles = async (db, username, self) => {
const router = Router(); const router = Router();
router.get('/profile/get/:username', handleErrorAsync(async (req, res) => { router.get('/profile/get/:username', handleErrorAsync(async (req, res) => {
return res.json(await fetchProfiles(req.db, req.params.username, req.user && req.user.username === req.params.username)) return res.json(await fetchProfiles(req.db, req.params.username, req.user && req.user.username === req.params.username, req.isGranted('users')))
})); }));
router.post('/profile/save', handleErrorAsync(async (req, res) => { router.post('/profile/save', handleErrorAsync(async (req, res) => {

View File

@ -157,6 +157,7 @@ const reloadUser = async (req, res, next) => {
|| req.user.email !== dbUser.email || req.user.email !== dbUser.email
|| req.user.roles !== dbUser.roles || req.user.roles !== dbUser.roles
|| req.user.avatarSource !== dbUser.avatarSource || req.user.avatarSource !== dbUser.avatarSource
|| req.user.bannedReason !== dbUser.bannedReason
) { ) {
const newUser = { const newUser = {
...dbUser, ...dbUser,