#219 reporting profiles

This commit is contained in:
Avris 2021-07-24 19:18:39 +02:00
parent 617cf70641
commit c45a8c0bf6
23 changed files with 265 additions and 66 deletions

View File

@ -83,7 +83,7 @@
</template> </template>
<ul v-if="profiles !== undefined" class="list-group"> <ul v-if="profiles !== undefined" class="list-group">
<li v-for="(options, locale) in locales" :key="locale" :class="['list-group-item', locale === config.locale ? 'profile-current' : '']"> <li v-for="(options, locale) in locales" :key="locale" :class="['list-group-item', locale === config.locale ? 'profile-current' : '']">
<ProfileOverview :profile="profiles[locale]" :locale="locale" @update="setProfiles"/> <ProfileOverview :username="username" :profile="profiles[locale]" :locale="locale" @update="setProfiles"/>
</li> </li>
</ul> </ul>
</Loading> </Loading>

View File

@ -1,15 +1,35 @@
<template> <template>
<client-only v-if="config.profile.editorEnabled"> <div v-if="config.profile.editorEnabled">
<section v-if="$user()">
<a v-if="!showReportForm" href="#" @click.prevent="showReportForm = true" class="small">
<Icon v="spider"/>
<T>report.action</T>
</a>
<div v-else-if="!reported">
<textarea v-model="reportComment" class="form-control" rows="3" :placeholder="$t('report.comment')" :disabled="saving" required></textarea>
<button class="btn btn-danger d-block w-100 mt-2" :disabled="saving || !reportComment" @click="report">
<Icon v="spider"/>
<T>report.action</T>
</button>
</div>
<div v-else class="alert alert-success">
<T>report.sent</T>
</div>
</section>
<section v-if="$isGranted('users')"> <section v-if="$isGranted('users')">
<div class="alert alert-warning"> <a v-if="!showBanForm" href="#" @click.prevent="showBanForm = true" class="small">
<Icon v="ban"/>
<T>ban.action</T>
</a>
<div v-else>
<textarea v-model="user.bannedReason" class="form-control" rows="3" :placeholder="$t('ban.reason') + ' ' + $t('ban.visible')" :disabled="saving"></textarea> <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"> <button class="btn btn-danger d-block w-100 mt-2" :disabled="saving" @click="ban">
<Icon v="ban"/> <Icon v="ban"/>
{{$t('ban.action')}} <T>ban.action</T>
</button> </button>
</div> </div>
</section> </section>
</client-only> </div>
</template> </template>
<script> <script>
@ -22,6 +42,12 @@
}, },
data() { data() {
return { return {
showReportForm: false,
reportComment: '',
reported: false,
showBanForm: !!this.user.bannedReason,
saving: false, saving: false,
} }
}, },
@ -37,7 +63,20 @@
} finally { } finally {
this.saving = false; this.saving = false;
} }
} },
async report() {
if (!this.reportComment) { return; }
await this.$confirm(this.$t('report.confirm', {username: this.user.username}), 'danger');
this.saving = true;
try {
await this.$post(`/profile/report/${encodeURIComponent(this.user.username)}`, {
comment: this.reportComment,
});
this.reported = true;
} finally {
this.saving = false;
}
},
}, },
} }
</script> </script>

View File

@ -23,7 +23,7 @@
</div> </div>
</section> </section>
<Table :data="visibleNouns()" columns="3" :marked="(el) => !el.approved" fixed ref="dictionarytable"> <Table :data="visibleNouns()" :columns="3" :marked="(el) => !el.approved" fixed ref="dictionarytable">
<template v-slot:header> <template v-slot:header>
<th class="text-nowrap"> <th class="text-nowrap">
<Icon v="mars"/> <Icon v="mars"/>

View File

@ -31,7 +31,7 @@
</div> </div>
</section> </section>
<Table :data="visibleEntries()" columns="3" :marked="(el) => !el.approved" fixed ref="dictionarytable"> <Table :data="visibleEntries()" :columns="3" :marked="(el) => !el.approved" fixed ref="dictionarytable">
<template v-slot:header> <template v-slot:header>
<th class="text-nowrap"> <th class="text-nowrap">
<Icon v="comment-times"/> <Icon v="comment-times"/>

View File

@ -2,7 +2,7 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
{{ locales[locale].name }} {{ locales[locale].name }}
<span v-if="profile"> <span v-if="profile">
<LocaleLink :locale="locale" :link="`/@${profile.username}`" class="badge bg-primary text-white text-white"> <LocaleLink :locale="locale" :link="`/@${username}`" class="badge bg-primary text-white text-white">
<Icon v="id-card"/> <Icon v="id-card"/>
<T>profile.show</T> <T>profile.show</T>
</LocaleLink> </LocaleLink>
@ -27,6 +27,7 @@
<script> <script>
export default { export default {
props: { props: {
username: { required: true },
profile: { required: true }, profile: { required: true },
locale: { required: true }, locale: { required: true },
}, },

View File

@ -3,7 +3,7 @@
<table :class="['table table-striped table-hover', fixed ? 'table-fixed-' + columns : '']"> <table :class="['table table-striped table-hover', fixed ? 'table-fixed-' + columns : '']">
<thead ref="thead"> <thead ref="thead">
<tr> <tr>
<td :colspan="columns + 1"> <td :colspan="columns">
<nav v-if="pages > 1"> <nav v-if="pages > 1">
<ul class="pagination pagination-sm justify-content-center mb-0"> <ul class="pagination pagination-sm justify-content-center mb-0">
<li v-for="p in pagesRange" :class="['page-item', p.page === page ? 'active' : '', p.enabled ? '' : 'disabled']"> <li v-for="p in pagesRange" :class="['page-item', p.page === page ? 'active' : '', p.enabled ? '' : 'disabled']">

View File

@ -23,7 +23,7 @@
</div> </div>
</section> </section>
<Table :data="visibleEntries()" columns="1" fixed :marked="(el) => !el.approved" ref="dictionarytable"> <Table :data="visibleEntries()" :columns="1" fixed :marked="(el) => !el.approved" ref="dictionarytable">
<template v-slot:header> <template v-slot:header>
<th class="cell-wide"></th> <th class="cell-wide"></th>
<th></th> <th></th>

View File

@ -511,6 +511,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/bedingungen=Terms of Service}:' termsIntro: 'According to our {/bedingungen=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abrosexual: 'Abrosexuell' Abrosexual: 'Abrosexuell'
Achillean: 'Achillean' Achillean: 'Achillean'

View File

@ -608,3 +608,9 @@ ban:
header: 'You''re banned. Your profile will not be shown to anyone.' header: 'You''re banned. Your profile will not be shown to anyone.'
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terms=Terms of Service}:' termsIntro: 'According to our {/terms=Terms of Service}:'
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'

View File

@ -523,6 +523,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:' termsIntro: 'According to our {/terinos=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'Abrorrománti{inflection_c}' Abroromantic: 'Abrorrománti{inflection_c}'
Abrosexual: 'Abrosexual' Abrosexual: 'Abrosexual'

View File

@ -511,6 +511,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terms=Terms of Service}:' termsIntro: 'According to our {/terms=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -518,6 +518,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/voorwaarden=Terms of Service}:' termsIntro: 'According to our {/voorwaarden=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'Abroromantisch' Abroromantic: 'Abroromantisch'
Abrosexual: 'Abroseksueel' Abrosexual: 'Abroseksueel'

View File

@ -1123,6 +1123,12 @@ ban:
banned: 'Zbanowanx' banned: 'Zbanowanx'
termsIntro: 'Zgodnie z naszym {/regulamin=Regulaminem}:' termsIntro: 'Zgodnie z naszym {/regulamin=Regulaminem}:'
report:
action: 'Zgłoś profil'
comment: 'Opisz krótko, w czym problem'
confirm: 'Czy na pewno chcesz zgłosić profil @%username%?'
sent: 'Twoje zgłoszenie zostało wysłane. Dziękujemy za pomoc!'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -521,6 +521,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/termos=Terms of Service}:' termsIntro: 'According to our {/termos=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'Abrorromânti{inflection_c}' Abroromantic: 'Abrorromânti{inflection_c}'
Abrosexual: 'Abrossexual' Abrosexual: 'Abrossexual'

View File

@ -1094,6 +1094,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:' termsIntro: 'According to our {/terinos=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'Abroromantyczn{adjective_n}' Abroromantic: 'Abroromantyczn{adjective_n}'
Abrosexual: 'Abroseksualn{adjective_n}' Abrosexual: 'Abroseksualn{adjective_n}'

View File

@ -535,6 +535,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:' termsIntro: 'According to our {/terinos=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abroromantic: 'אַבראָראָמאַנטיש' Abroromantic: 'אַבראָראָמאַנטיש'
Abrosexual: 'אַבראָסעקסועל' Abrosexual: 'אַבראָסעקסועל'

View File

@ -503,6 +503,13 @@ ban:
banned: 'Banned' banned: 'Banned'
termsIntro: 'According to our {/terinos=Terms of Service}:' termsIntro: 'According to our {/terinos=Terms of Service}:'
# TODO
report:
action: 'Report abuse'
comment: 'Please explain briefly what''s wrong with this profile'
confirm: 'Are you sure you want to report @%username%?'
sent: 'Your report has been sent. Thanks for your help!'
flags: flags:
Abrosexual: '嫩性戀' Abrosexual: '嫩性戀'
Abroromantic: '嫩浪漫傾向' Abroromantic: '嫩浪漫傾向'

View File

@ -0,0 +1,20 @@
-- Up
CREATE TABLE reports (
id TEXT NOT NULL PRIMARY KEY,
userId TEXT NOT NULL,
reporterId TEXT NULL,
comment TEXT NOT NULL,
isAutomatic INTEGER,
isHandled INTEGER,
FOREIGN KEY(userId) REFERENCES users(id),
FOREIGN KEY(reporterId) REFERENCES users(id)
);
CREATE INDEX "reports_isAutomatic" ON "reports" ("isAutomatic");
CREATE INDEX "reports_isHandled" ON "reports" ("isHandled");
-- Down
DROP TABLE reports;

View File

@ -3,6 +3,7 @@ import t from '../src/translator';
import config from '../data/config.suml'; import config from '../data/config.suml';
import {buildDict} from "../src/helpers"; import {buildDict} from "../src/helpers";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {decodeTime} from 'ulid';
export default ({ app, store }) => { export default ({ app, store }) => {
Vue.prototype.$eventHub = new Vue(); Vue.prototype.$eventHub = new Vue();
@ -47,4 +48,8 @@ export default ({ app, store }) => {
const dt = DateTime.fromSeconds(timestamp); const dt = DateTime.fromSeconds(timestamp);
return dt.toFormat('y-MM-dd HH:mm') return dt.toFormat('y-MM-dd HH:mm')
} }
Vue.prototype.$ulidTime = (ulid) => {
return decodeTime(ulid) / 1000;
}
} }

View File

@ -90,33 +90,53 @@
{{ stats.cards * 100 }}% {{ stats.cards * 100 }}%
</section> </section>
<section v-if="$isGranted('users') && suspiciousUsers.length > 0"> <section v-if="$isGranted('users')">
<h3> <h3>
<Icon v="siren-on"/> <Icon v="siren-on"/>
Suspicious accounts Abuse reports
</h3> </h3>
<Table :data="suspiciousUsers" columns="2"> <Table :data="abuseReports" :columns="4">
<template v-slot:header>
<th class="text-nowrap">
Suspicious account
</th>
<th class="text-nowrap">
Reported by
</th>
<th class="text-nowrap">
Comment
</th>
<th class="text-nowrap">
Action
</th>
</template>
<template v-slot:row="s"><template v-if="s"> <template v-slot:row="s"><template v-if="s">
<td> <td>
<LocaleLink :link="`/@${s.el.username}`" :locale="s.el.locale"> <a :href="`https://pronouns.page/${s.el.susUsername}`" target="_blank" rel="noopener">@{{s.el.susUsername}}</a>
{{s.el.username}}
<span class="badge bg-light text-dark">{{s.el.locale}}</span>
</LocaleLink>
</td> </td>
<td> <td>
<a href="#" class="badge bg-light text-success border border-success float-end" <span v-if="s.el.isAutomatic" class="badge bg-info">
@click.prevent="checkedSuspicious(s.el.id)" Keyword found
</span>
<a v-else :href="`https://pronouns.page/${s.el.reporterUsername}`" target="_blank" rel="noopener">@{{s.el.reporterUsername}}</a>
<small>({{$datetime($ulidTime(s.el.id))}})</small>
</td>
<td class="small">
{{ s.el.comment }}
</td>
<td>
<span v-if="s.el.isHandled" class="badge bg-success">
Case closed
</span>
<a v-else href="#" class="badge bg-light text-success border border-success"
@click.prevent="handleReport(s.el.id)"
> >
<Icon v="thumbs-up"/> <Icon v="thumbs-up"/>
I checked the profile, it's OK. I checked the profile, it's OK.
</a> </a>
</td> </td>
</template></template> </template></template>
<template v-slot:empty>
<Icon v="search"/>
<T>nouns.empty</T>
</template>
</Table> </Table>
</section> </section>
@ -190,14 +210,14 @@
stats = await app.$axios.$get(`/admin/stats`); stats = await app.$axios.$get(`/admin/stats`);
} catch {} } catch {}
let suspiciousUsers = []; let abuseReports = [];
try { try {
suspiciousUsers = await app.$axios.$get(`/admin/suspicious`); abuseReports = await app.$axios.$get(`/admin/reports`);
} catch {} } catch {}
return { return {
stats, stats,
suspiciousUsers, abuseReports,
}; };
}, },
methods: { methods: {
@ -206,10 +226,13 @@
this.users = await this.$axios.$get(`/admin/users`); this.users = await this.$axios.$get(`/admin/users`);
} }
}, },
async checkedSuspicious(id) { async handleReport(id) {
await this.$confirm('Are you sure you want to mark this profile as not suspicious?', 'success'); await this.$confirm('Are you sure you want to mark this report as handled?', 'success');
await this.$post(`/admin/suspicious/checked/${id}`); await this.$post(`/admin/reports/handle/${id}`);
this.suspiciousUsers = this.suspiciousUsers.filter(u => u.id !== id); this.abuseReports = this.abuseReports.map(r => {
if (r.id === id) { r.isHandled = true; }
return r;
});
}, },
}, },
computed: { computed: {

View File

@ -18,16 +18,16 @@
{{options.name}} {{options.name}}
</LocaleLink> </LocaleLink>
</div> </div>
<div v-if="$user() && $user().username === profile.username"> <div v-if="$user() && $user().username === user.username">
<nuxt-link to="/editor" class="btn btn-primary btn-sm mb-2 mx-1"> <nuxt-link to="/editor" class="btn btn-primary btn-sm mb-2 mx-1">
<Icon v="edit"/> <Icon v="edit"/>
<T>profile.edit</T> <T>profile.edit</T>
</nuxt-link> </nuxt-link>
<a :href="`https://pronouns.page/@${profile.username}`" v-if="Object.keys(user.profiles).length > 1" <a :href="`https://pronouns.page/@${user.username}`" v-if="Object.keys(user.profiles).length > 1"
class="btn btn-outline-secondary btn-sm mb-2 mx-1" class="btn btn-outline-secondary btn-sm mb-2 mx-1"
> >
<Icon v="external-link"/> <Icon v="external-link"/>
pronouns.page/@{{profile.username}} pronouns.page/@{{user.username}}
</a> </a>
</div> </div>
<div v-if="($user() && $user().username === profile.username) || $isGranted('users')"> <div v-if="($user() && $user().username === profile.username) || $isGranted('users')">

View File

@ -139,50 +139,28 @@ router.post('/admin/ban/:username', handleErrorAsync(async (req, res) => {
return res.json(true); return res.json(true);
})); }));
router.get('/admin/suspicious', handleErrorAsync(async (req, res) => { router.get('/admin/reports', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) { if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'}); return res.status(401).json({error: 'Unauthorised'});
} }
return res.json(await req.db.all(SQL` return res.json(await req.db.all(SQL`
SELECT users.id, users.username, profiles.locale FROM profiles SELECT reports.id, sus.username AS susUsername, reporter.username AS reporterUsername, reports.comment, reports.isAutomatic, reports.isHandled
LEFT JOIN users ON profiles.userId = users.id FROM reports
WHERE users.suspiciousChecked != 1 LEFT JOIN users sus ON reports.userId = sus.id
AND users.bannedReason IS NULL LEFT JOIN users reporter ON reports.reporterId = reporter.id
AND ( ORDER BY reports.isHandled ASC, reports.id ASC
lower(customFlags) LIKE '%superstr%'
OR lower(description) LIKE '%superstr%'
OR lower(customFlags) LIKE '%superhet%'
OR lower(description) LIKE '%superhet%'
OR lower(customFlags) LIKE '%super-%'
OR lower(description) LIKE '%super-%'
OR lower(customFlags) LIKE '%phobe%'
OR lower(description) LIKE '%phobe%'
OR lower(customFlags) LIKE '%phobic%'
OR lower(description) LIKE '%phobic%'
OR lower(customFlags) LIKE '%terf%'
OR lower(description) LIKE '%terf%'
OR lower(customFlags) LIKE '%radfem%'
OR lower(description) LIKE '%radfem%'
OR lower(customFlags) LIKE '%gender critical%'
OR lower(description) LIKE '%gender critical%'
OR lower(customFlags) LIKE '%helicopter%'
OR lower(description) LIKE '%helicopter%'
OR lower(pronouns) LIKE '%helicopter%'
OR lower(pronouns) LIKE '%nor/mal%'
)
ORDER BY users.id DESC
`)); `));
})); }));
router.post('/admin/suspicious/checked/:id', handleErrorAsync(async (req, res) => { router.post('/admin/reports/handle/:id', handleErrorAsync(async (req, res) => {
if (!req.isGranted('users')) { if (!req.isGranted('users')) {
return res.status(401).json({error: 'Unauthorised'}); return res.status(401).json({error: 'Unauthorised'});
} }
await req.db.get(SQL` await req.db.get(SQL`
UPDATE users UPDATE reports
SET suspiciousChecked = 1 SET isHandled = 1
WHERE id=${req.params.id} WHERE id=${req.params.id}
`); `);

View File

@ -53,6 +53,46 @@ const fetchProfiles = async (db, username, self, isAdmin) => {
return p; return p;
}; };
function* isSuspicious(profile) {
const description = profile.description.toLowerCase();
const flags = JSON.stringify(profile.customFlags).toLowerCase();
const pronouns = JSON.stringify(profile.pronouns).toLowerCase();
if (description.includes('superstr') || description.includes('superhet') || description.includes('super-') ||
flags.includes('superstr') || flags.includes('superhet') || flags.includes('super-')
) {
yield 'Superstraight';
}
if (description.includes('phobe') || description.includes('phobic') ||
flags.includes('phobe') || flags.includes('phobic')
) {
yield '-phobic';
}
if (description.includes('terf') || description.includes('radfem') || description.includes('gender critical') ||
flags.includes('terf') || flags.includes('radfem') || flags.includes('gender critical')
) {
yield 'TERF';
}
if (description.includes('helicopter') ||
flags.includes('helicopter') ||
pronouns.includes('helicopter')
) {
yield 'Helicopter';
}
if (pronouns.includes('nor/mal')
) {
yield 'nor/mal';
}
}
const hasAutomatedHandledReports = async (db, id) => {
return (await db.get(SQL`SELECT COUNT(*) AS c FROM reports WHERE userId = ${id} AND isAutomatic = 1 AND isHandled = 1`)).c > 0;
}
const router = Router(); const router = Router();
router.get('/profile/get/:username', handleErrorAsync(async (req, res) => { router.get('/profile/get/:username', handleErrorAsync(async (req, res) => {
@ -121,6 +161,14 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
)`); )`);
} }
const sus = [...isSuspicious(req.body)];
if (sus.length && !await hasAutomatedHandledReports(req.db, req.user.id)) {
await req.db.get(SQL`
INSERT INTO reports (id, userId, reporterId, isAutomatic, comment, isHandled)
VALUES (${ulid()}, ${req.user.id}, null, 1, ${sus.join(', ')}, 0);
`);
}
if (req.body.teamName) { if (req.body.teamName) {
await caches.admins.invalidate(); await caches.admins.invalidate();
await caches.adminsFooter.invalidate(); await caches.adminsFooter.invalidate();
@ -135,4 +183,21 @@ router.post('/profile/delete/:locale', handleErrorAsync(async (req, res) => {
return res.json(await fetchProfiles(req.db, req.user.username, true)); return res.json(await fetchProfiles(req.db, req.user.username, true));
})); }));
router.post('/profile/report/:username', handleErrorAsync(async (req, res) => {
const user = await req.db.get(SQL`SELECT id FROM users WHERE usernameNorm = ${normalise(req.params.username)}`);
if (!user) {
return res.status(400).json({error: 'Missing user'});
}
if (!req.body.comment) {
return res.status(400).json({error: 'Missing comment'});
}
await req.db.get(SQL`
INSERT INTO reports (id, userId, reporterId, isAutomatic, comment, isHandled)
VALUES (${ulid()}, ${user.id}, ${req.user.id}, 0, ${req.body.comment}, 0);
`);
return res.json('OK');
}));
export default router; export default router;