feat(Moderation): enhance moderation reporting and user feedback
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Added user blocking checks in authentication and reporting processes, returning appropriate error responses. - Expanded moderation report functionality to include new target types and optional fields for reports. - Implemented a new API endpoint to retrieve the count of open moderation reports. - Enhanced frontend components to allow users to report profiles, images, and guestbook entries, with corresponding UI updates. - Updated internationalization files to include new strings for reporting features in both German and English.
This commit is contained in:
@@ -29,6 +29,8 @@ class AuthController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'credentialsinvalid') {
|
if (error.message === 'credentialsinvalid') {
|
||||||
res.status(404).json({ error: error.message });
|
res.status(404).json({ error: error.message });
|
||||||
|
} else if (error.message === 'userblocked') {
|
||||||
|
res.status(403).json({ error: error.message });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,23 @@ import moderationService from '../services/moderationService.js';
|
|||||||
|
|
||||||
const moderationController = {
|
const moderationController = {
|
||||||
async createReport(req, res) {
|
async createReport(req, res) {
|
||||||
|
const allowedTargetTypes = [
|
||||||
|
'forum_message',
|
||||||
|
'gallery_image',
|
||||||
|
'guestbook_entry',
|
||||||
|
'one_to_one_message',
|
||||||
|
'diary_entry',
|
||||||
|
'user_profile',
|
||||||
|
'blog',
|
||||||
|
'blog_post'
|
||||||
|
];
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
targetType: Joi.string().valid('forum_message').required(),
|
targetType: Joi.string().valid(...allowedTargetTypes).required(),
|
||||||
targetId: Joi.number().integer().min(1).required(),
|
targetId: Joi.number().integer().min(1).optional(),
|
||||||
|
targetRef: Joi.string().trim().max(255).allow('').optional(),
|
||||||
reason: Joi.string().trim().min(3).max(120).required(),
|
reason: Joi.string().trim().min(3).max(120).required(),
|
||||||
details: Joi.string().allow('').max(2000).optional()
|
details: Joi.string().allow('').max(2000).optional()
|
||||||
});
|
}).or('targetId', 'targetRef');
|
||||||
const { error, value } = schema.validate(req.body || {});
|
const { error, value } = schema.validate(req.body || {});
|
||||||
if (error) {
|
if (error) {
|
||||||
return res.status(400).json({ error: error.details[0].message });
|
return res.status(400).json({ error: error.details[0].message });
|
||||||
@@ -51,6 +62,17 @@ const moderationController = {
|
|||||||
console.error('Error in updateReportStatus:', err);
|
console.error('Error in updateReportStatus:', err);
|
||||||
return res.status(400).json({ error: err.message });
|
return res.status(400).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOpenReportCount(req, res) {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const result = await moderationService.getOpenReportCount(userId);
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in getOpenReportCount:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const authenticate = async (req, res, next) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
|
||||||
}
|
}
|
||||||
|
if (!user.active) {
|
||||||
|
return res.status(403).json({ error: 'Unauthorized: User blocked' });
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await updateUserTimestamp(user.id);
|
await updateUserTimestamp(user.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ router.get('/falukant/region-distances', authenticate, adminController.getRegion
|
|||||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||||
router.get('/moderation/reports', authenticate, moderationController.listReports);
|
router.get('/moderation/reports', authenticate, moderationController.listReports);
|
||||||
|
router.get('/moderation/reports/open-count', authenticate, moderationController.getOpenReportCount);
|
||||||
router.post('/moderation/reports/:reportId/status', authenticate, moderationController.updateReportStatus);
|
router.post('/moderation/reports/:reportId/status', authenticate, moderationController.updateReportStatus);
|
||||||
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||||
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||||
|
|||||||
@@ -1663,9 +1663,15 @@ class AdminService {
|
|||||||
}
|
}
|
||||||
if (typeof data.active === 'boolean') {
|
if (typeof data.active === 'boolean') {
|
||||||
updates.active = data.active;
|
updates.active = data.active;
|
||||||
|
if (data.active === false) {
|
||||||
|
updates.authCode = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(updates).length === 0) return { id: user.hashedId, username: user.username, active: user.active };
|
if (Object.keys(updates).length === 0) return { id: user.hashedId, username: user.username, active: user.active };
|
||||||
await user.update(updates);
|
await user.update(updates);
|
||||||
|
if (typeof data.active === 'boolean') {
|
||||||
|
await notifyUser(user.hashedId, 'userAccessChanged', { active: !!data.active });
|
||||||
|
}
|
||||||
return { id: user.hashedId, username: user.username, active: user.active };
|
return { id: user.hashedId, username: user.username, active: user.active };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ export const loginUser = async ({ username, password }) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('credentialsinvalid');
|
throw new Error('credentialsinvalid');
|
||||||
}
|
}
|
||||||
|
if (!user.active) {
|
||||||
|
throw new Error('userblocked');
|
||||||
|
}
|
||||||
const match = await bcrypt.compare(password, user.password);
|
const match = await bcrypt.compare(password, user.password);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error('credentialsinvalid');
|
throw new Error('credentialsinvalid');
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import BaseService from './BaseService.js';
|
import BaseService from './BaseService.js';
|
||||||
import { sequelize } from '../utils/sequelize.js';
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
import { notifyUser } from '../utils/socket.js';
|
||||||
|
|
||||||
const ALLOWED_TARGET_TYPES = new Set(['forum_message']);
|
const ALLOWED_TARGET_TYPES = new Set([
|
||||||
|
'forum_message',
|
||||||
|
'gallery_image',
|
||||||
|
'guestbook_entry',
|
||||||
|
'one_to_one_message',
|
||||||
|
'diary_entry',
|
||||||
|
'user_profile',
|
||||||
|
'blog',
|
||||||
|
'blog_post'
|
||||||
|
]);
|
||||||
const ALLOWED_STATUS = new Set(['open', 'in_review', 'resolved', 'rejected']);
|
const ALLOWED_STATUS = new Set(['open', 'in_review', 'resolved', 'rejected']);
|
||||||
|
|
||||||
class ModerationService extends BaseService {
|
class ModerationService extends BaseService {
|
||||||
@@ -16,7 +26,8 @@ class ModerationService extends BaseService {
|
|||||||
CREATE TABLE IF NOT EXISTS community.moderation_report (
|
CREATE TABLE IF NOT EXISTS community.moderation_report (
|
||||||
id bigserial PRIMARY KEY,
|
id bigserial PRIMARY KEY,
|
||||||
target_type varchar(64) NOT NULL,
|
target_type varchar(64) NOT NULL,
|
||||||
target_id bigint NOT NULL,
|
target_id bigint NULL,
|
||||||
|
target_ref varchar(255) NULL,
|
||||||
reason varchar(120) NOT NULL,
|
reason varchar(120) NOT NULL,
|
||||||
details text NULL,
|
details text NULL,
|
||||||
status varchar(32) NOT NULL DEFAULT 'open',
|
status varchar(32) NOT NULL DEFAULT 'open',
|
||||||
@@ -31,6 +42,14 @@ class ModerationService extends BaseService {
|
|||||||
CREATE INDEX IF NOT EXISTS moderation_report_target_idx
|
CREATE INDEX IF NOT EXISTS moderation_report_target_idx
|
||||||
ON community.moderation_report (target_type, target_id);
|
ON community.moderation_report (target_type, target_id);
|
||||||
`);
|
`);
|
||||||
|
await sequelize.query(`
|
||||||
|
ALTER TABLE community.moderation_report
|
||||||
|
ADD COLUMN IF NOT EXISTS target_ref varchar(255) NULL;
|
||||||
|
`);
|
||||||
|
await sequelize.query(`
|
||||||
|
ALTER TABLE community.moderation_report
|
||||||
|
ALTER COLUMN target_id DROP NOT NULL;
|
||||||
|
`);
|
||||||
await sequelize.query(`
|
await sequelize.query(`
|
||||||
CREATE INDEX IF NOT EXISTS moderation_report_status_idx
|
CREATE INDEX IF NOT EXISTS moderation_report_status_idx
|
||||||
ON community.moderation_report (status, created_at DESC);
|
ON community.moderation_report (status, created_at DESC);
|
||||||
@@ -38,7 +57,7 @@ class ModerationService extends BaseService {
|
|||||||
this._tableEnsured = true;
|
this._tableEnsured = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createReport(hashedUserId, { targetType, targetId, reason, details }) {
|
async createReport(hashedUserId, { targetType, targetId, targetRef, reason, details }) {
|
||||||
await this._ensureTable();
|
await this._ensureTable();
|
||||||
const user = await this.getUserByHashedId(hashedUserId);
|
const user = await this.getUserByHashedId(hashedUserId);
|
||||||
const normalizedTargetType = String(targetType || '').trim();
|
const normalizedTargetType = String(targetType || '').trim();
|
||||||
@@ -46,8 +65,10 @@ class ModerationService extends BaseService {
|
|||||||
throw new Error('Unsupported target type');
|
throw new Error('Unsupported target type');
|
||||||
}
|
}
|
||||||
const numericTargetId = Number(targetId);
|
const numericTargetId = Number(targetId);
|
||||||
if (!Number.isFinite(numericTargetId) || numericTargetId < 1) {
|
const hasTargetId = Number.isFinite(numericTargetId) && numericTargetId >= 1;
|
||||||
throw new Error('Invalid target id');
|
const normalizedTargetRef = String(targetRef || '').trim().slice(0, 255);
|
||||||
|
if (!hasTargetId && !normalizedTargetRef) {
|
||||||
|
throw new Error('Invalid target');
|
||||||
}
|
}
|
||||||
const normalizedReason = String(reason || '').trim().slice(0, 120);
|
const normalizedReason = String(reason || '').trim().slice(0, 120);
|
||||||
if (!normalizedReason) {
|
if (!normalizedReason) {
|
||||||
@@ -58,15 +79,16 @@ class ModerationService extends BaseService {
|
|||||||
const rows = await sequelize.query(
|
const rows = await sequelize.query(
|
||||||
`
|
`
|
||||||
INSERT INTO community.moderation_report
|
INSERT INTO community.moderation_report
|
||||||
(target_type, target_id, reason, details, status, reporter_user_id)
|
(target_type, target_id, target_ref, reason, details, status, reporter_user_id)
|
||||||
VALUES
|
VALUES
|
||||||
(:targetType, :targetId, :reason, :details, 'open', :reporterUserId)
|
(:targetType, :targetId, :targetRef, :reason, :details, 'open', :reporterUserId)
|
||||||
RETURNING id, target_type AS "targetType", target_id AS "targetId", reason, details, status, created_at AS "createdAt"
|
RETURNING id, target_type AS "targetType", target_id AS "targetId", target_ref AS "targetRef", reason, details, status, created_at AS "createdAt"
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
replacements: {
|
replacements: {
|
||||||
targetType: normalizedTargetType,
|
targetType: normalizedTargetType,
|
||||||
targetId: numericTargetId,
|
targetId: hasTargetId ? numericTargetId : null,
|
||||||
|
targetRef: normalizedTargetRef || null,
|
||||||
reason: normalizedReason,
|
reason: normalizedReason,
|
||||||
details: normalizedDetails || null,
|
details: normalizedDetails || null,
|
||||||
reporterUserId: user.id
|
reporterUserId: user.id
|
||||||
@@ -75,7 +97,9 @@ class ModerationService extends BaseService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return rows[0];
|
const created = rows[0];
|
||||||
|
await this._notifyModeratorsAboutChange('created', created.id);
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listReports(hashedUserId, { status = 'open', limit = 50 } = {}) {
|
async listReports(hashedUserId, { status = 'open', limit = 50 } = {}) {
|
||||||
@@ -95,6 +119,7 @@ class ModerationService extends BaseService {
|
|||||||
r.id,
|
r.id,
|
||||||
r.target_type AS "targetType",
|
r.target_type AS "targetType",
|
||||||
r.target_id AS "targetId",
|
r.target_id AS "targetId",
|
||||||
|
r.target_ref AS "targetRef",
|
||||||
fm.title_id AS "topicId",
|
fm.title_id AS "topicId",
|
||||||
ft.forum_id AS "forumId",
|
ft.forum_id AS "forumId",
|
||||||
r.reason,
|
r.reason,
|
||||||
@@ -167,7 +192,66 @@ class ModerationService extends BaseService {
|
|||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
throw new Error('Report not found');
|
throw new Error('Report not found');
|
||||||
}
|
}
|
||||||
return rows[0];
|
const updated = rows[0];
|
||||||
|
await this._notifyModeratorsAboutChange('status_changed', numericReportId);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenReportCount(hashedUserId) {
|
||||||
|
await this._ensureTable();
|
||||||
|
const user = await this.getUserByHashedId(hashedUserId);
|
||||||
|
const isAdmin = await this.hasUserRight(user.id, ['mainadmin', 'forum']);
|
||||||
|
if (!isAdmin) {
|
||||||
|
throw new Error('Access denied');
|
||||||
|
}
|
||||||
|
const rows = await sequelize.query(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS count
|
||||||
|
FROM community.moderation_report
|
||||||
|
WHERE status = 'open'
|
||||||
|
`,
|
||||||
|
{ type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
return { openCount: Number(rows?.[0]?.count || 0) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _notifyModeratorsAboutChange(kind, reportId) {
|
||||||
|
try {
|
||||||
|
const moderatorRows = await sequelize.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT u.hashed_id AS "hashedId"
|
||||||
|
FROM community.user u
|
||||||
|
JOIN community.user_right ur ON ur.user_id = u.id
|
||||||
|
JOIN type.user_right urt ON urt.id = ur.right_type_id
|
||||||
|
WHERE u.active = true
|
||||||
|
AND urt.title IN ('mainadmin', 'forum')
|
||||||
|
`,
|
||||||
|
{ type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const countRows = await sequelize.query(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*)::int AS count
|
||||||
|
FROM community.moderation_report
|
||||||
|
WHERE status = 'open'
|
||||||
|
`,
|
||||||
|
{ type: sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const openCount = Number(countRows?.[0]?.count || 0);
|
||||||
|
await Promise.all(
|
||||||
|
(moderatorRows || [])
|
||||||
|
.map((row) => row?.hashedId)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((hashedId) =>
|
||||||
|
notifyUser(hashedId, 'moderationReportChanged', {
|
||||||
|
kind,
|
||||||
|
reportId,
|
||||||
|
openCount
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to notify moderators about moderation report change:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,17 @@
|
|||||||
:style="`background-image:url('/images/icons/${subitem.icon}')`"
|
:style="`background-image:url('/images/icons/${subitem.icon}')`"
|
||||||
class="submenu-icon"
|
class="submenu-icon"
|
||||||
> </span>
|
> </span>
|
||||||
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
|
<span class="submenu-label-row">
|
||||||
|
<span>{{ subitem?.label || $t(`navigation.m-${key}.${subkey}`) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="key === 'administration' && subkey === 'moderationReports' && moderationOpenCount > 0"
|
||||||
|
class="moderation-alert-badge"
|
||||||
|
:title="$t('navigation.m-administration.moderationBadgeTitle')"
|
||||||
|
>
|
||||||
|
<span class="moderation-alert-icon">⚠</span>
|
||||||
|
<span class="moderation-alert-count">{{ moderationOpenCount }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<span v-if="subitem.disabled" class="menu-lock-badge">18+</span>
|
<span v-if="subitem.disabled" class="menu-lock-badge">18+</span>
|
||||||
<span
|
<span
|
||||||
v-if="hasSecondLevelSubmenu(subitem, subkey)"
|
v-if="hasSecondLevelSubmenu(subitem, subkey)"
|
||||||
@@ -203,7 +213,10 @@ export default {
|
|||||||
_forumsChangedHandler: null,
|
_forumsChangedHandler: null,
|
||||||
_friendLoginChangedHandler: null,
|
_friendLoginChangedHandler: null,
|
||||||
_reloadMenuHandler: null,
|
_reloadMenuHandler: null,
|
||||||
_adultVerificationChangedHandler: null
|
_adultVerificationChangedHandler: null,
|
||||||
|
_moderationReportChangedHandler: null,
|
||||||
|
_userAccessChangedHandler: null,
|
||||||
|
moderationOpenCount: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -213,6 +226,12 @@ export default {
|
|||||||
menuNeedsUpdate(newVal) {
|
menuNeedsUpdate(newVal) {
|
||||||
if (newVal) this.loadMenu();
|
if (newVal) this.loadMenu();
|
||||||
},
|
},
|
||||||
|
menu: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.fetchModerationOpenCount();
|
||||||
|
}
|
||||||
|
},
|
||||||
$route() {
|
$route() {
|
||||||
this.collapseMenus();
|
this.collapseMenus();
|
||||||
},
|
},
|
||||||
@@ -229,6 +248,7 @@ export default {
|
|||||||
this.fetchForums();
|
this.fetchForums();
|
||||||
this.fetchFriends();
|
this.fetchFriends();
|
||||||
this.fetchVocabLanguages();
|
this.fetchVocabLanguages();
|
||||||
|
this.fetchModerationOpenCount();
|
||||||
}
|
}
|
||||||
this.updateViewportState();
|
this.updateViewportState();
|
||||||
window.addEventListener('resize', this.updateViewportState);
|
window.addEventListener('resize', this.updateViewportState);
|
||||||
@@ -263,10 +283,25 @@ export default {
|
|||||||
showInfo(this, this.$t('socialnetwork.erotic.notifications.rejected'));
|
showInfo(this, this.$t('socialnetwork.erotic.notifications.rejected'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this._moderationReportChangedHandler = async (payload = {}) => {
|
||||||
|
if (Number.isFinite(Number(payload.openCount))) {
|
||||||
|
this.moderationOpenCount = Number(payload.openCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.fetchModerationOpenCount();
|
||||||
|
};
|
||||||
|
this._userAccessChangedHandler = async (payload = {}) => {
|
||||||
|
if (payload.active === false) {
|
||||||
|
this.$root?.$refs?.messageDialog?.open?.('Dein Account wurde gesperrt.');
|
||||||
|
await this.logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
sock.on('forumschanged', this._forumsChangedHandler);
|
sock.on('forumschanged', this._forumsChangedHandler);
|
||||||
sock.on('friendloginchanged', this._friendLoginChangedHandler);
|
sock.on('friendloginchanged', this._friendLoginChangedHandler);
|
||||||
sock.on('reloadmenu', this._reloadMenuHandler);
|
sock.on('reloadmenu', this._reloadMenuHandler);
|
||||||
sock.on('adultVerificationChanged', this._adultVerificationChangedHandler);
|
sock.on('adultVerificationChanged', this._adultVerificationChangedHandler);
|
||||||
|
sock.on('moderationReportChanged', this._moderationReportChangedHandler);
|
||||||
|
sock.on('userAccessChanged', this._userAccessChangedHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
unregisterSocketListeners() {
|
unregisterSocketListeners() {
|
||||||
@@ -276,10 +311,14 @@ export default {
|
|||||||
if (this._friendLoginChangedHandler) sock.off('friendloginchanged', this._friendLoginChangedHandler);
|
if (this._friendLoginChangedHandler) sock.off('friendloginchanged', this._friendLoginChangedHandler);
|
||||||
if (this._reloadMenuHandler) sock.off('reloadmenu', this._reloadMenuHandler);
|
if (this._reloadMenuHandler) sock.off('reloadmenu', this._reloadMenuHandler);
|
||||||
if (this._adultVerificationChangedHandler) sock.off('adultVerificationChanged', this._adultVerificationChangedHandler);
|
if (this._adultVerificationChangedHandler) sock.off('adultVerificationChanged', this._adultVerificationChangedHandler);
|
||||||
|
if (this._moderationReportChangedHandler) sock.off('moderationReportChanged', this._moderationReportChangedHandler);
|
||||||
|
if (this._userAccessChangedHandler) sock.off('userAccessChanged', this._userAccessChangedHandler);
|
||||||
this._forumsChangedHandler = null;
|
this._forumsChangedHandler = null;
|
||||||
this._friendLoginChangedHandler = null;
|
this._friendLoginChangedHandler = null;
|
||||||
this._reloadMenuHandler = null;
|
this._reloadMenuHandler = null;
|
||||||
this._adultVerificationChangedHandler = null;
|
this._adultVerificationChangedHandler = null;
|
||||||
|
this._moderationReportChangedHandler = null;
|
||||||
|
this._userAccessChangedHandler = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateViewportState() {
|
updateViewportState() {
|
||||||
@@ -461,6 +500,19 @@ export default {
|
|||||||
this.vocabLanguagesList = [];
|
this.vocabLanguagesList = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async fetchModerationOpenCount() {
|
||||||
|
try {
|
||||||
|
const adminChildren = this.menu?.administration?.children || {};
|
||||||
|
if (!adminChildren.moderationReports) {
|
||||||
|
this.moderationOpenCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await apiClient.get('/api/admin/moderation/reports/open-count');
|
||||||
|
this.moderationOpenCount = Number(res?.data?.openCount || 0);
|
||||||
|
} catch (_) {
|
||||||
|
this.moderationOpenCount = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openForum(forumId) {
|
openForum(forumId) {
|
||||||
this.$router.push({ name: 'Forum', params: { id: forumId } });
|
this.$router.push({ name: 'Forum', params: { id: forumId } });
|
||||||
@@ -800,6 +852,29 @@ a {
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submenu-label-row {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderation-alert-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: #d32f2f;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderation-alert-icon {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
.submenu1 > li:hover {
|
.submenu1 > li:hover {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
background-color: rgba(248, 162, 43, 0.12);
|
background-color: rgba(248, 162, 43, 0.12);
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" v-if="activeTab === 'general'">
|
<div class="tab-content" v-if="activeTab === 'general'">
|
||||||
|
<div class="profile-report-row">
|
||||||
|
<button type="button" class="report-btn" @click="reportProfile">
|
||||||
|
{{ $t('socialnetwork.profile.reportProfile') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tr v-for="(value, key) in userProfile.params" :key="key">
|
<tr v-for="(value, key) in userProfile.params" :key="key">
|
||||||
<td>{{ $t(`socialnetwork.profile.${key}`) }}</td>
|
<td>{{ $t(`socialnetwork.profile.${key}`) }}</td>
|
||||||
@@ -44,6 +49,9 @@
|
|||||||
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
|
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
|
||||||
<img :src="image.url || image.placeholder" :alt="$t('socialnetwork.gallery.imageLoadingAlt')" />
|
<img :src="image.url || image.placeholder" :alt="$t('socialnetwork.gallery.imageLoadingAlt')" />
|
||||||
<p>{{ image.title }}</p>
|
<p>{{ image.title }}</p>
|
||||||
|
<button type="button" class="report-btn" @click.stop="reportGalleryImage(image)">
|
||||||
|
{{ $t('socialnetwork.gallery.reportImage') }}
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,6 +90,19 @@
|
|||||||
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
|
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="entry-actions">
|
||||||
|
<button type="button" class="report-btn" @click="reportGuestbookEntry(entry)">
|
||||||
|
{{ $t('socialnetwork.profile.guestbook.reportEntry') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canDeleteGuestbookEntry(entry)"
|
||||||
|
type="button"
|
||||||
|
class="delete-btn"
|
||||||
|
@click="deleteGuestbookEntry(entry)"
|
||||||
|
>
|
||||||
|
{{ $t('socialnetwork.profile.guestbook.deleteEntry') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<button @click="loadGuestbookEntries(currentPage - 1)" :disabled="currentPage === 1">{{
|
<button @click="loadGuestbookEntries(currentPage - 1)" :disabled="currentPage === 1">{{
|
||||||
@@ -357,6 +378,75 @@ export default {
|
|||||||
console.error('Error fetching image:', error);
|
console.error('Error fetching image:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
canDeleteGuestbookEntry(entry) {
|
||||||
|
const ownUsername = String(this.user?.username || '').toLowerCase();
|
||||||
|
const profileUsername = String(this.userProfile?.username || '').toLowerCase();
|
||||||
|
const sender = String(entry?.sender || '').toLowerCase();
|
||||||
|
return ownUsername && (ownUsername === sender || ownUsername === profileUsername);
|
||||||
|
},
|
||||||
|
async deleteGuestbookEntry(entry) {
|
||||||
|
const ok = window.confirm(this.$t('socialnetwork.profile.guestbook.confirmDeleteEntry'));
|
||||||
|
if (!ok) return;
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/api/socialnetwork/guestbook/entries/${entry.id}`);
|
||||||
|
await this.loadGuestbookEntries(this.currentPage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen des Gästebucheintrags:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reportModerationTarget(payload, successKey, errorKey) {
|
||||||
|
const reason = window.prompt(this.$t('socialnetwork.reporting.reasonPrompt'));
|
||||||
|
if (reason == null) return;
|
||||||
|
const trimmed = String(reason || '').trim();
|
||||||
|
if (trimmed.length < 3) {
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t('socialnetwork.reporting.reasonTooShort'), this.$t('error.title'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/moderation/reports', {
|
||||||
|
...payload,
|
||||||
|
reason: trimmed,
|
||||||
|
details: payload.details || ''
|
||||||
|
});
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t(successKey), this.$t('message.title'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating moderation report:', error);
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t(errorKey), this.$t('error.title'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reportProfile() {
|
||||||
|
await this.reportModerationTarget(
|
||||||
|
{
|
||||||
|
targetType: 'user_profile',
|
||||||
|
targetRef: this.userId,
|
||||||
|
details: `username=${this.userProfile?.username || ''}`
|
||||||
|
},
|
||||||
|
'socialnetwork.reporting.profileReported',
|
||||||
|
'socialnetwork.reporting.reportError'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async reportGalleryImage(image) {
|
||||||
|
await this.reportModerationTarget(
|
||||||
|
{
|
||||||
|
targetType: 'gallery_image',
|
||||||
|
targetId: image.id,
|
||||||
|
details: `profile=${this.userProfile?.username || ''}; imageTitle=${image?.title || ''}`
|
||||||
|
},
|
||||||
|
'socialnetwork.reporting.imageReported',
|
||||||
|
'socialnetwork.reporting.reportError'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async reportGuestbookEntry(entry) {
|
||||||
|
await this.reportModerationTarget(
|
||||||
|
{
|
||||||
|
targetType: 'guestbook_entry',
|
||||||
|
targetId: entry.id,
|
||||||
|
details: `profile=${this.userProfile?.username || ''}; sender=${entry?.sender || ''}`
|
||||||
|
},
|
||||||
|
'socialnetwork.reporting.guestbookReported',
|
||||||
|
'socialnetwork.reporting.reportError'
|
||||||
|
);
|
||||||
|
},
|
||||||
async handleFriendship() {
|
async handleFriendship() {
|
||||||
console.log(this.friendshipState);
|
console.log(this.friendshipState);
|
||||||
if (['none', 'withdrawn'].includes(this.friendshipState)) {
|
if (['none', 'withdrawn'].includes(this.friendshipState)) {
|
||||||
@@ -502,6 +592,10 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-list > li > .report-btn {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.folder-name-text {
|
.folder-name-text {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -541,6 +635,26 @@ export default {
|
|||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-btn,
|
||||||
|
.delete-btn {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-report-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"loading": "Laden…",
|
"loading": "Laden…",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
|
"reportBlog": "Blog melden",
|
||||||
|
"reportPost": "Beitrag melden",
|
||||||
|
"reportBlogSent": "Blog wurde an die Moderation gemeldet.",
|
||||||
|
"reportPostSent": "Beitrag wurde an die Moderation gemeldet.",
|
||||||
"entriesCount": "{count} Einträge",
|
"entriesCount": "{count} Einträge",
|
||||||
"empty": "Keine Einträge vorhanden.",
|
"empty": "Keine Einträge vorhanden.",
|
||||||
"fallbackDescription": "Öffentlicher Community-Blog auf YourPart.",
|
"fallbackDescription": "Öffentlicher Community-Blog auf YourPart.",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"m-administration": {
|
"m-administration": {
|
||||||
"contactrequests": "Kontaktanfragen",
|
"contactrequests": "Kontaktanfragen",
|
||||||
"moderationReports": "Moderationsmeldungen",
|
"moderationReports": "Moderationsmeldungen",
|
||||||
|
"moderationBadgeTitle": "Offene Moderationsmeldungen",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"userrights": "Benutzerrechte",
|
"userrights": "Benutzerrechte",
|
||||||
"m-users": {
|
"m-users": {
|
||||||
|
|||||||
@@ -126,8 +126,12 @@
|
|||||||
"imageUpload": "Bild",
|
"imageUpload": "Bild",
|
||||||
"submit": "Eintrag absenden",
|
"submit": "Eintrag absenden",
|
||||||
"noEntries": "Keine Einträge gefunden",
|
"noEntries": "Keine Einträge gefunden",
|
||||||
"entryImageAlt": "Bild zum Gästebucheintrag"
|
"entryImageAlt": "Bild zum Gästebucheintrag",
|
||||||
|
"reportEntry": "Melden",
|
||||||
|
"deleteEntry": "Löschen",
|
||||||
|
"confirmDeleteEntry": "Diesen Gästebucheintrag wirklich löschen?"
|
||||||
},
|
},
|
||||||
|
"reportProfile": "Benutzername/Profil melden",
|
||||||
"interestedInGender": "Interessiert an",
|
"interestedInGender": "Interessiert an",
|
||||||
"hasChildren": "Hat Kinder",
|
"hasChildren": "Hat Kinder",
|
||||||
"smokes": "Rauchen",
|
"smokes": "Rauchen",
|
||||||
@@ -199,7 +203,8 @@
|
|||||||
"title": "Bild"
|
"title": "Bild"
|
||||||
},
|
},
|
||||||
"imagePreviewAlt": "Bildvorschau",
|
"imagePreviewAlt": "Bildvorschau",
|
||||||
"imageLoadingAlt": "Bild wird geladen"
|
"imageLoadingAlt": "Bild wird geladen",
|
||||||
|
"reportImage": "Bild melden"
|
||||||
},
|
},
|
||||||
"guestbook": {
|
"guestbook": {
|
||||||
"kicker": "Gästebuch",
|
"kicker": "Gästebuch",
|
||||||
@@ -209,6 +214,14 @@
|
|||||||
"nextPage": "Weiter",
|
"nextPage": "Weiter",
|
||||||
"page": "Seite"
|
"page": "Seite"
|
||||||
},
|
},
|
||||||
|
"reporting": {
|
||||||
|
"reasonPrompt": "Kurzer Meldegrund (z. B. Spam, Beleidigung, Hassrede):",
|
||||||
|
"reasonTooShort": "Bitte gib mindestens 3 Zeichen als Meldegrund ein.",
|
||||||
|
"profileReported": "Profil wurde an die Moderation gemeldet.",
|
||||||
|
"imageReported": "Bild wurde an die Moderation gemeldet.",
|
||||||
|
"guestbookReported": "Gästebucheintrag wurde an die Moderation gemeldet.",
|
||||||
|
"reportError": "Meldung konnte nicht gesendet werden."
|
||||||
|
},
|
||||||
"diary": {
|
"diary": {
|
||||||
"kicker": "Persönliche Einträge",
|
"kicker": "Persönliche Einträge",
|
||||||
"intro": "Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.",
|
"intro": "Gedanken, Notizen und kurze Updates in einer ruhigen, persönlichen Ansicht.",
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"reportBlog": "Report blog",
|
||||||
|
"reportPost": "Report post",
|
||||||
|
"reportBlogSent": "Blog has been reported to moderation.",
|
||||||
|
"reportPostSent": "Post has been reported to moderation.",
|
||||||
"entriesCount": "{count} entries",
|
"entriesCount": "{count} entries",
|
||||||
"empty": "No entries available.",
|
"empty": "No entries available.",
|
||||||
"fallbackDescription": "Public community blog on YourPart.",
|
"fallbackDescription": "Public community blog on YourPart.",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"m-administration": {
|
"m-administration": {
|
||||||
"contactrequests": "Contact requests",
|
"contactrequests": "Contact requests",
|
||||||
"moderationReports": "Moderation reports",
|
"moderationReports": "Moderation reports",
|
||||||
|
"moderationBadgeTitle": "Open moderation reports",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"userrights": "User rights",
|
"userrights": "User rights",
|
||||||
"m-users": {
|
"m-users": {
|
||||||
|
|||||||
@@ -126,8 +126,12 @@
|
|||||||
"imageUpload": "Image",
|
"imageUpload": "Image",
|
||||||
"submit": "Submit entry",
|
"submit": "Submit entry",
|
||||||
"noEntries": "No entries found",
|
"noEntries": "No entries found",
|
||||||
"entryImageAlt": "Guestbook entry image"
|
"entryImageAlt": "Guestbook entry image",
|
||||||
|
"reportEntry": "Report",
|
||||||
|
"deleteEntry": "Delete",
|
||||||
|
"confirmDeleteEntry": "Delete this guestbook entry?"
|
||||||
},
|
},
|
||||||
|
"reportProfile": "Report username/profile",
|
||||||
"interestedInGender": "Interested in",
|
"interestedInGender": "Interested in",
|
||||||
"hasChildren": "Has children",
|
"hasChildren": "Has children",
|
||||||
"smokes": "Smoking",
|
"smokes": "Smoking",
|
||||||
@@ -199,7 +203,8 @@
|
|||||||
"title": "Image"
|
"title": "Image"
|
||||||
},
|
},
|
||||||
"imagePreviewAlt": "Image preview",
|
"imagePreviewAlt": "Image preview",
|
||||||
"imageLoadingAlt": "Loading image"
|
"imageLoadingAlt": "Loading image",
|
||||||
|
"reportImage": "Report image"
|
||||||
},
|
},
|
||||||
"guestbook": {
|
"guestbook": {
|
||||||
"kicker": "Guestbook",
|
"kicker": "Guestbook",
|
||||||
@@ -209,6 +214,14 @@
|
|||||||
"nextPage": "Next",
|
"nextPage": "Next",
|
||||||
"page": "Page"
|
"page": "Page"
|
||||||
},
|
},
|
||||||
|
"reporting": {
|
||||||
|
"reasonPrompt": "Short report reason (e.g. spam, insult, hate speech):",
|
||||||
|
"reasonTooShort": "Please enter at least 3 characters as report reason.",
|
||||||
|
"profileReported": "Profile has been reported to moderation.",
|
||||||
|
"imageReported": "Image has been reported to moderation.",
|
||||||
|
"guestbookReported": "Guestbook entry has been reported to moderation.",
|
||||||
|
"reportError": "Report could not be submitted."
|
||||||
|
},
|
||||||
"diary": {
|
"diary": {
|
||||||
"kicker": "Personal entries",
|
"kicker": "Personal entries",
|
||||||
"intro": "Thoughts, notes, and short updates in a calm personal view.",
|
"intro": "Thoughts, notes, and short updates in a calm personal view.",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="$store.getters.isLoggedIn" class="actions">
|
<div v-if="$store.getters.isLoggedIn" class="actions">
|
||||||
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">{{ $t('blog.view.edit') }}</router-link>
|
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">{{ $t('blog.view.edit') }}</router-link>
|
||||||
|
<button type="button" class="report-btn" @click="reportBlog">{{ $t('blog.view.reportBlog') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="blog-content">
|
<div class="blog-content">
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
<article v-for="p in items" :key="p.id" class="post">
|
<article v-for="p in items" :key="p.id" class="post">
|
||||||
<h3>{{ p.title }}</h3>
|
<h3>{{ p.title }}</h3>
|
||||||
<div class="content" v-html="sanitize(p.content)" />
|
<div class="content" v-html="sanitize(p.content)" />
|
||||||
|
<div class="post-actions">
|
||||||
|
<button type="button" class="report-btn" @click="reportPost(p)">{{ $t('blog.view.reportPost') }}</button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div class="pagination" v-if="total > pageSize">
|
<div class="pagination" v-if="total > pageSize">
|
||||||
<button :disabled="page===1" @click="go(page-1)">«</button>
|
<button :disabled="page===1" @click="go(page-1)">«</button>
|
||||||
@@ -44,6 +48,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getBlog, listPosts, createPost } from '@/api/blogApi.js';
|
import { getBlog, listPosts, createPost } from '@/api/blogApi.js';
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import RichTextEditor from './components/RichTextEditor.vue';
|
import RichTextEditor from './components/RichTextEditor.vue';
|
||||||
import {
|
import {
|
||||||
@@ -185,6 +190,41 @@ export default {
|
|||||||
this.applyBlogSeo();
|
this.applyBlogSeo();
|
||||||
},
|
},
|
||||||
async go(p) { if (p>=1 && p<=this.pages) await this.fetchPage(p); },
|
async go(p) { if (p>=1 && p<=this.pages) await this.fetchPage(p); },
|
||||||
|
async submitReport(payload, successKey) {
|
||||||
|
const reason = window.prompt(this.$t('socialnetwork.reporting.reasonPrompt'));
|
||||||
|
if (reason == null) return;
|
||||||
|
const trimmed = String(reason || '').trim();
|
||||||
|
if (trimmed.length < 3) {
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t('socialnetwork.reporting.reasonTooShort'), this.$t('error.title'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/moderation/reports', {
|
||||||
|
...payload,
|
||||||
|
reason: trimmed,
|
||||||
|
details: payload.details || ''
|
||||||
|
});
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t(successKey), this.$t('message.title'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Blog report failed:', error);
|
||||||
|
this.$root.$refs.messageDialog?.open(this.$t('socialnetwork.reporting.reportError'), this.$t('error.title'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reportBlog() {
|
||||||
|
if (!this.blog) return;
|
||||||
|
await this.submitReport({
|
||||||
|
targetType: 'blog',
|
||||||
|
targetId: this.blog.id,
|
||||||
|
details: `title=${this.blog.title || ''}; owner=${this.blog.owner?.username || ''}`
|
||||||
|
}, 'blog.view.reportBlogSent');
|
||||||
|
},
|
||||||
|
async reportPost(post) {
|
||||||
|
await this.submitReport({
|
||||||
|
targetType: 'blog_post',
|
||||||
|
targetId: post.id,
|
||||||
|
details: `blogId=${this.blog?.id || ''}; postTitle=${post?.title || ''}`
|
||||||
|
}, 'blog.view.reportPostSent');
|
||||||
|
},
|
||||||
async addPost() {
|
async addPost() {
|
||||||
if (!this.newPost.title || !this.newPost.content) return;
|
if (!this.newPost.title || !this.newPost.content) return;
|
||||||
const id = this.$route.params.id || this.resolvedId;
|
const id = this.$route.params.id || this.resolvedId;
|
||||||
@@ -263,6 +303,17 @@ export default {
|
|||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-btn {
|
||||||
|
min-height: auto;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user