#227 allow banning people without cards

This commit is contained in:
Avris 2021-07-24 14:54:15 +02:00
parent b0162a61e2
commit 617cf70641
19 changed files with 170 additions and 101 deletions

View File

@ -143,7 +143,7 @@
}
},
async mounted() {
this.profiles = await this.$axios.$get(`/profile/get/${this.$user().username}`);
this.profiles = (await this.$axios.$get(`/profile/get/${this.$user().username}`)).profiles;
this.socialConnections = await this.$axios.$get(`/user/social-connections`);
if (process.client) {

43
components/Ban.vue Normal file
View File

@ -0,0 +1,43 @@
<template>
<client-only v-if="config.profile.editorEnabled">
<section v-if="$isGranted('users')">
<div class="alert alert-warning">
<textarea v-model="user.bannedReason" class="form-control" rows="3" :placeholder="$t('ban.reason') + ' ' + $t('ban.visible')" :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>
</client-only>
</template>
<script>
import ClientOnly from 'vue-client-only'
export default {
components: { ClientOnly },
props: {
user: { required: true },
},
data() {
return {
saving: false,
}
},
methods: {
async ban() {
await this.$confirm(this.$t('ban.confirm', {username: this.user.username}), 'danger');
this.saving = true;
try {
await this.$post(`/admin/ban/${encodeURIComponent(this.user.username)}`, {
reason: this.user.bannedReason,
});
window.location.reload();
} finally {
this.saving = false;
}
}
},
}
</script>

View File

@ -98,12 +98,14 @@
</div>
</div>
<header v-else class="mb-4">
<h1 class="text-nowrap p-4">
<nuxt-link to="/">
<Icon v="tags"/>
<T>title</T>
</nuxt-link>
</h1>
<div class="container">
<h1 class="text-nowrap p-4">
<nuxt-link to="/">
<Icon v="tags"/>
<T>title</T>
</nuxt-link>
</h1>
</div>
</header>
</template>

View File

@ -2,8 +2,8 @@
<div>
<div class="mb-3 d-flex justify-content-between flex-column flex-md-row">
<h2 class="text-nowrap">
<Avatar :user="profile"/>
@{{profile.username}}
<Avatar :user="user"/>
@{{user.username}}
</h2>
<div class="flex-grow-1 text-lg-end">
<slot></slot>
@ -110,6 +110,7 @@
export default {
props: {
user: { required: true },
profile: { required: true },
terms: { 'default': null },
},

View File

@ -16,3 +16,6 @@ confirm:
table:
scrollUp: 'Scroll to the top'
profile:
empty: 'This person hasn''t created any cards yet.'

View File

@ -407,15 +407,16 @@ profile:
meh: 'Okay'
no: 'Nope'
banner: >
You can also use our website to create a card, {/@andrea=like this one},
containing your names, pronouns, pride flags, liked words, etc.
Then you can link to it in your bio or email footer.
Just create an account {/account=here}.
bannerButton: 'Create a card'
card:
link: 'Card picture'
generating: 'Generation in progress…'
banner: >
You can also use our website to create a card, {/@andrea=like this one},
containing your names, pronouns, pride flags, liked words, etc.
Then you can link to it in your bio or email footer.
Just create an account {/account=here}.
bannerButton: 'Create a card'
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.'
share: 'Teilen'

View File

@ -514,6 +514,7 @@ profile:
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.'
share: 'Share'

View File

@ -77,7 +77,7 @@ sources:
Series: 'Series'
Song: 'Música'
Poetry: 'Poesía'
Game: 'Juegos'
Game: 'Juegos'
Other: 'Otros'
submit:
header: 'Enviar un ejemplo para que sea añadido'
@ -307,7 +307,7 @@ links:
headerLong: 'Enlaces externos'
recommended: 'Recomendamos'
blog: 'Blog'
blog: 'Blog'
media: 'Pronouns.page en los medios'
social: 'Redes sociales'
@ -328,9 +328,9 @@ contact:
{https://pronouns.page=Pronouns.page} and related initiatives
are created by the “Neutral Language Council” collective.
logo: 'Logo of the collective is a combination of the transgender symbol and a speech bubble that symbolises language.' # TODO
members: 'Miembros actuales'
member: 'Miembro de la cooperativa'
upcoming: 'Próximas versiones en otras lenguas'
members: 'Miembros actuales'
member: 'Miembro de la cooperativa'
upcoming: 'Próximas versiones en otras lenguas'
support:
header: 'Apóyanos'
@ -395,10 +395,10 @@ profile:
birthdayInfo: 'No publicamos la fecha de tu cumpleaños, sólo la edad calculada.'
flags: 'Banderas'
flagsInfo: 'Arrastra tus banderas de orgullo a este marco.'
flagsCustom: 'Sube una bandera personalizada'
flagsCustomWarning: 'Esta bandera fue subida por un usuario. El equipo de pronouns.page no es responsable de ella.'
flagsCustom: 'Sube una bandera personalizada'
flagsCustomWarning: 'Esta bandera fue subida por un usuario. El equipo de pronouns.page no es responsable de ella.'
links: 'Enlaces'
linksCake: 'We recommend linking to'
linksCake: 'We recommend linking to'
column: 'Columna'
list: 'Tus tarjetas'
@ -426,6 +426,7 @@ profile:
card:
link: 'Imagen de la tarjeta'
generating: 'Generando…'
empty: 'This person hasn''t created any cards yet.' # TODO
share: 'Compartir'
@ -440,7 +441,7 @@ crud:
filterLong: 'Filtrar la lista…'
search: 'Buscar…'
author: 'Añadido por'
saved: 'Los cambios se guardaron exitosamente!'
saved: 'Los cambios se guardaron exitosamente!'
footer:
# TODO source: 'El código fuente está {https://gitlab.com/Avris/Zaimki=publicado} bajo la licencia {https://mit.avris.it=MIT}.'
@ -509,7 +510,7 @@ error:
generic: 'Algo salió mal. Por favor, vuelve a intentarlo…'
mode:
dark: 'Modo oscuro'
dark: 'Modo oscuro'
light: 'Modo claro'
# TODO

View File

@ -403,6 +403,17 @@ profile:
jokingly: 'En plaisantant'
meh: 'D''accord'
no: 'Non'
# TODO
banner: >
You can also use our website to create a card, {/@andrea=like this one},
containing your names, pronouns, pride flags, liked words, etc.
Then you can link to it in your bio or email footer.
Just create an account {/account=here}.
bannerButton: 'Create a card'
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.'
share: 'Partager'

View File

@ -421,6 +421,7 @@ profile:
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.' # TODO
share: 'Deel'

View File

@ -980,6 +980,7 @@ profile:
card:
link: 'Obrazek'
generating: 'Trwa generowanie obrazka…'
empty: 'Ta osoba nie stworzyła jeszcze żadnych wizytówek.'
census:
header: 'Spis'

View File

@ -50,7 +50,7 @@ pronouns:
button: 'Gerar uma ligação à formas intercambiáveis'
header: 'Formas intercambiáveis'
raw: 'intercambiável'
generated: 'Estes pronomes foram criados com o gerador. A equipe do pronouns.page não é responsável por eles.'
generated: 'Estes pronomes foram criados com o gerador. A equipe do pronouns.page não é responsável por eles.'
any:
header: 'Qualquer pronome'
short: 'qualquer'
@ -77,7 +77,7 @@ sources:
Series: 'Séries'
Song: 'Música'
Poetry: 'Poesia'
Game: 'Jogos'
Game: 'Jogos'
Other: 'Outros'
submit:
header: 'Enviar um exemplo para que seja adicionado'
@ -97,9 +97,9 @@ sources:
moderation: 'Os envios devem ser aprovados antes de serem publicados.'
key: 'Key' # TODO
keyInfo: 'Identifier for linking sources between language versions and linking with the dictionary' # TODO
images: 'Imagens'
otherVersions: 'Em outros idiomas'
referenced: 'Exemplos de uso'
images: 'Imagens'
otherVersions: 'Em outros idiomas'
referenced: 'Exemplos de uso'
nouns:
header: 'Dicionário'
@ -304,7 +304,7 @@ links:
headerLong: 'Links externos'
recommended: 'Recomendamos'
blog: 'Blog'
blog: 'Blog'
media: 'Pronouns.page nas mídias'
social: 'Redes sociais'
@ -320,15 +320,15 @@ contact:
authors: 'Autores do site'
team:
name: 'O coletivo "Conselho da Língua Neutra"'
description:
description:
- >
{https://pronouns.page=Pronouns.page} e iniciativas relacionadas
são criadas pelo o coletivo “Conselho da Língua Neutra”.
logo: 'A logo do coletivo é uma combinação do símbolo transgênero e um balão de fala que simboliza a língua.'
members: 'Membres atuais'
logo: 'A logo do coletivo é uma combinação do símbolo transgênero e um balão de fala que simboliza a língua.'
members: 'Membres atuais'
member: 'Member of the collective' # TODO
blog: 'Blog'
upcoming: 'Próximas versões'
blog: 'Blog'
upcoming: 'Próximas versões'
support:
header: 'Apoie-nos'
@ -353,7 +353,7 @@ user:
Se não solicitou este código, simplesmente ignore esta mensagem.
why: >
Registrar-se te permite dirigir os cartões ({/@andrea=como esta}).
passwordless: 'O site não grava qualquer senha. {https://avris.it/blog/passwords-are-passé=More info.}'
passwordless: 'O site não grava qualquer senha. {https://avris.it/blog/passwords-are-passé=More info.}'
code:
action: 'Validar'
invalid: 'Código inválido.'
@ -424,6 +424,7 @@ profile:
card:
link: 'Imagem do cartão'
generating: 'Gerando a imagem…'
empty: 'This person hasn''t created any cards yet.' # TODO
share: 'Compartilhar'

View File

@ -960,6 +960,7 @@ profile:
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.' # TODO
census:
header: 'Spis'

View File

@ -439,6 +439,7 @@ profile:
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.' # TODO
share: 'Share'

View File

@ -406,6 +406,7 @@ profile:
card:
link: 'Card picture'
generating: 'Generation in progress…'
empty: 'This person hasn''t created any cards yet.' # TODO
share: '這裡'

View File

@ -1,18 +1,18 @@
<template>
<div v-if="profile">
<section v-if="$isGranted('users') && profile.bannedReason">
<section v-if="$isGranted('users') && user.bannedReason">
<div class="alert alert-warning">
<p class="h4">
<Icon v="ban"/>
{{$t('ban.banned')}}
</p>
<p class="mb-0">{{profile.bannedReason}}</p>
<p class="mb-0">{{user.bannedReason}}</p>
</div>
</section>
<Profile :profile="profile" :terms="terms">
<div v-if="Object.keys(profiles).length > 1">
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="profiles[locale] !== undefined"
<Profile :user="user" :profile="profile" :terms="terms">
<div v-if="Object.keys(user.profiles).length > 1">
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="user.profiles[locale] !== undefined"
:locale="locale" :link="`/@${profile.username}`"
:class="['btn', locale === config.locale ? 'btn-primary disabled' : 'btn-outline-primary', 'btn-sm', 'mb-2 mx-1']">
{{options.name}}
@ -23,7 +23,7 @@
<Icon v="edit"/>
<T>profile.edit</T>
</nuxt-link>
<a :href="`https://pronouns.page/@${profile.username}`" v-if="Object.keys(profiles).length > 1"
<a :href="`https://pronouns.page/@${profile.username}`" v-if="Object.keys(user.profiles).length > 1"
class="btn btn-outline-secondary btn-sm mb-2 mx-1"
>
<Icon v="external-link"/>
@ -51,17 +51,7 @@
</div>
</Profile>
<client-only>
<section v-if="$isGranted('users')">
<div class="alert alert-warning">
<textarea v-model="profile.bannedReason" class="form-control" rows="3" :placeholder="$t('ban.reason') + ' ' + $t('ban.visible')" :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>
</client-only>
<Ban :user="user"/>
<Separator icon="heart"/>
<Support/>
@ -69,14 +59,14 @@
<Share/>
</section>
</div>
<div v-else-if="Object.keys(profiles).length">
<div v-else-if="user.username">
<h2 class="text-nowrap mb-3">
<Avatar :user="profiles[Object.keys(profiles)[0]]"/>
<Avatar :user="user"/>
@{{username}}
</h2>
<div class="list-group">
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="profiles[locale] !== undefined"
<div v-if="Object.keys(user.profiles).length" class="list-group">
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="user.profiles[locale] !== undefined"
:locale="locale" :link="`/@${username}`"
class="list-group-item list-group-item-action list-group-item-hoverable">
<div class="h3">
@ -84,6 +74,14 @@
</div>
</LocaleLink>
</div>
<div v-else class="alert alert-info">
<p class="mb-0">
<Icon v="info-circle"/>
<T>profile.empty</T>
</p>
</div>
<Ban :user="user"/>
</div>
<NotFound v-else/>
</template>
@ -96,13 +94,12 @@
components: { ClientOnly },
data() {
return {
saving: false,
terms: [],
}
},
async asyncData({ app, route }) {
return {
profiles: await app.$axios.$get(`/profile/get/${encodeURIComponent(route.params.pathMatch)}`),
user: await app.$axios.$get(`/profile/get/${encodeURIComponent(route.params.pathMatch)}`),
};
},
async mounted() {
@ -112,9 +109,9 @@
},
computed: {
profile() {
for (let locale in this.profiles) {
for (let locale in this.user.profiles) {
if (locale === this.config.locale) {
return this.profiles[locale];
return this.user.profiles[locale];
}
}
@ -127,31 +124,17 @@
return base;
}
if (this.profile.username !== base && process.client) {
if (this.user.username !== base && process.client) {
history.pushState(
'',
document.title,
'/@' + this.profile.username,
'/@' + this.user.username,
);
}
return this.profile.username;
return this.user.username;
},
},
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() {
return head({
title: `@${this.username}`,

View File

@ -1,9 +1,9 @@
<template>
<Profile v-if="profile" :profile="profile" class="pb-3">
<Profile v-if="profile" :user="user" :profile="profile" class="pb-3">
<nuxt-link to="/">
<h1 class="text-nowrap h5">
<Icon v="tags"/>
<T>title</T><span v-if="profile">/@{{profile.username}}</span>
<T>title</T><span v-if="profile">/@{{user.username}}</span>
</h1>
</nuxt-link>
</Profile>
@ -17,14 +17,14 @@
layout: 'basic',
async asyncData({ app, route }) {
return {
profiles: await app.$axios.$get(`/profile/get/${encodeURIComponent(route.params.pathMatch)}`),
user: await app.$axios.$get(`/profile/get/${encodeURIComponent(route.params.pathMatch)}`),
};
},
computed: {
profile() {
for (let locale in this.profiles) {
for (let locale in this.user.profiles) {
if (locale === this.config.locale) {
return this.profiles[locale];
return this.user.profiles[locale];
}
}

View File

@ -188,9 +188,9 @@
return {};
}
const profiles = await app.$axios.$get(`/profile/get/${encodeURIComponent(store.state.user.username)}`, { headers: {
const profiles = (await app.$axios.$get(`/profile/get/${encodeURIComponent(store.state.user.username)}`, { headers: {
authorization: 'Bearer ' + store.state.token,
} });
} })).profiles;
for (let locale in profiles) {
if (!profiles.hasOwnProperty(locale)) {

View File

@ -27,21 +27,14 @@ const calcAge = birthday => {
const fetchProfiles = async (db, username, self, isAdmin) => {
const profiles = await db.all(SQL`
SELECT profiles.*, users.id, users.username, users.email, users.avatarSource, users.bannedReason, users.roles FROM profiles LEFT JOIN users on users.id == profiles.userId
SELECT profiles.* FROM profiles LEFT JOIN users on users.id == profiles.userId
WHERE usernameNorm = ${normalise(username)}
ORDER BY profiles.locale
`);
const p = {}
for (let profile of profiles) {
if (profile.bannedReason !== null && !isAdmin && !self) {
return {};
}
p[profile.locale] = {
id: profile.id,
userId: profile.userId,
username: profile.username,
emailHash: md5(profile.email),
names: JSON.parse(profile.names),
pronouns: JSON.parse(profile.pronouns),
description: profile.description,
@ -50,13 +43,10 @@ const fetchProfiles = async (db, username, self, isAdmin) => {
flags: JSON.parse(profile.flags),
customFlags: JSON.parse(profile.customFlags),
words: JSON.parse(profile.words),
avatar: await avatar(db, profile),
birthday: self ? profile.birthday : undefined,
teamName: profile.teamName,
footerName: profile.footerName,
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
bannedReason: profile.bannedReason,
team: !!profile.roles,
card: profile.card,
};
}
@ -66,7 +56,34 @@ const fetchProfiles = async (db, username, self, isAdmin) => {
const router = Router();
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, req.isGranted('users')))
const isSelf = req.user && req.user.username === req.params.username;
const isAdmin = req.isGranted('users');
const user = await req.db.get(SQL`
SELECT
users.id,
users.username,
users.email,
users.avatarSource,
users.bannedReason,
users.roles != '' AS team
FROM users
WHERE users.usernameNorm = ${normalise(req.params.username)}
`);
if (!user || (user.bannedReason !== null && !isAdmin && !isSelf)) {
return res.json({
profiles: {},
});
}
user.emailHash = md5(user.email);
delete user.email;
user.avatar = await avatar(req.db, user);
return res.json({
...user,
profiles: await fetchProfiles(req.db, req.params.username, isSelf),
});
}));
router.post('/profile/save', handleErrorAsync(async (req, res) => {