mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 23:27:26 +01:00
v3: misc fixes
This commit is contained in:
@@ -134,7 +134,6 @@ api.registerUser = function(req, res, next) {
|
|||||||
user.save(function(err, savedUser){
|
user.save(function(err, savedUser){
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
// Clean previous email preferences
|
// Clean previous email preferences
|
||||||
// TODO when emails added to EmailUnsubcription they should use lowercase version
|
|
||||||
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
|
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
|
||||||
utils.txnEmail(savedUser, 'welcome');
|
utils.txnEmail(savedUser, 'welcome');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ api.list = async function(req, res, next) {
|
|||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||||
await Bluebird.all(resChals.map((chal, index) => {
|
await Bluebird.all(resChals.map((chal, index) => {
|
||||||
return Bluebird.all([
|
return Bluebird.all([
|
||||||
User.findById(chal.leader).select(nameFields).exec(),
|
User.findById(chal.leader).select(nameFields).exec(),
|
||||||
@@ -268,8 +268,7 @@ api.update = function(req, res, next){
|
|||||||
async.forEachOf(newTasksObj, function(newTask, taskId, cb2){
|
async.forEachOf(newTasksObj, function(newTask, taskId, cb2){
|
||||||
// some properties can't be changed
|
// some properties can't be changed
|
||||||
newTask = Tasks.Task.sanitize(newTask);
|
newTask = Tasks.Task.sanitize(newTask);
|
||||||
// TODO we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
|
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
|
||||||
// TODO regarding comment above, make sure other models with nested fields are using this trick too
|
|
||||||
_.assign(updatedTasks[taskId], shared.ops.updateTask(updatedTasks[taskId].toObject(), {body: newTask}));
|
_.assign(updatedTasks[taskId], shared.ops.updateTask(updatedTasks[taskId].toObject(), {body: newTask}));
|
||||||
_before.chal.updateTask(updatedTasks[taskId]).then(cb2).catch(cb2);
|
_before.chal.updateTask(updatedTasks[taskId]).then(cb2).catch(cb2);
|
||||||
}, cb1);
|
}, cb1);
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ api.delete = function(req, res, next) {
|
|||||||
return res.status(400).json({err:"You have an active subscription, cancel your plan before deleting your account."});
|
return res.status(400).json({err:"You have an active subscription, cancel your plan before deleting your account."});
|
||||||
}
|
}
|
||||||
|
|
||||||
let types = ['party', 'publicGuilds', 'privateGuilds'];
|
let types = ['party', 'guilds'];
|
||||||
let groupFields = basicGroupFields.concat(' leader memberCount');
|
let groupFields = basicGroupFields.concat(' leader memberCount');
|
||||||
|
|
||||||
Group.getGroups({user, types, groupFields})
|
Group.getGroups({user, types, groupFields})
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ api.unsubscribe = {
|
|||||||
|
|
||||||
res.send(`<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextUsers')}`);
|
res.send(`<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextUsers')}`);
|
||||||
} else {
|
} else {
|
||||||
let unsubscribedEmail = await EmailUnsubscription.findOne({email: data.email});
|
let unsubscribedEmail = await EmailUnsubscription.findOne({email: data.email.toLowerCase()});
|
||||||
let okResponse = `<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextOthers')}`;
|
let okResponse = `<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextOthers')}`;
|
||||||
if (!unsubscribedEmail) await EmailUnsubscription.create({email: data.email});
|
if (!unsubscribedEmail) await EmailUnsubscription.create({email: data.email.toLowerCase()});
|
||||||
res.send(okResponse);
|
res.send(okResponse);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -383,7 +383,6 @@ api.abortQuest = {
|
|||||||
'party._id': groupId,
|
'party._id': groupId,
|
||||||
}, {
|
}, {
|
||||||
$set: {'party.quest': Group.cleanQuestProgress()},
|
$set: {'party.quest': Group.cleanQuestProgress()},
|
||||||
$inc: {_v: 1}, // TODO update middleware
|
|
||||||
}, {multi: true}).exec();
|
}, {multi: true}).exec();
|
||||||
|
|
||||||
let questLeaderUpdate = User.update({
|
let questLeaderUpdate = User.update({
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ function _generateWebhookTaskData (task, direction, delta, stats, user) {
|
|||||||
let extendedStats = _.extend(stats, {
|
let extendedStats = _.extend(stats, {
|
||||||
toNextLevel: common.tnl(user.stats.lvl),
|
toNextLevel: common.tnl(user.stats.lvl),
|
||||||
maxHealth: common.maxHealth,
|
maxHealth: common.maxHealth,
|
||||||
maxMP: user._statsComputed.maxMP, // TODO refactor as method not getter
|
maxMP: common.statsComputed(user).maxMP,
|
||||||
});
|
});
|
||||||
|
|
||||||
let userData = {
|
let userData = {
|
||||||
@@ -428,7 +428,6 @@ api.scoreTask = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// completed todos cannot be moved, they'll be returned ordered by date of completion
|
// completed todos cannot be moved, they'll be returned ordered by date of completion
|
||||||
// TODO support challenges?
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/tasks/:taskId/move/to/:position Move a task to a new position
|
* @api {post} /api/v3/tasks/:taskId/move/to/:position Move a task to a new position
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ api.getUser = {
|
|||||||
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
|
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
|
||||||
delete user.apiToken;
|
delete user.apiToken;
|
||||||
|
|
||||||
// TODO move to model (maybe virtuals, maybe in toJSON)
|
// TODO move to model? (maybe virtuals, maybe in toJSON)
|
||||||
user.stats.toNextLevel = common.tnl(user.stats.lvl);
|
user.stats.toNextLevel = common.tnl(user.stats.lvl);
|
||||||
user.stats.maxHealth = common.maxHealth;
|
user.stats.maxHealth = common.maxHealth;
|
||||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
user.stats.maxMP = common.statsComputed(user).maxMP;
|
||||||
|
|
||||||
return res.respond(200, user);
|
return res.respond(200, user);
|
||||||
},
|
},
|
||||||
@@ -210,7 +210,7 @@ api.deleteUser = {
|
|||||||
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let types = ['party', 'publicGuilds', 'privateGuilds'];
|
let types = ['party', 'guilds'];
|
||||||
let groupFields = basicGroupFields.concat(' leader memberCount');
|
let groupFields = basicGroupFields.concat(' leader memberCount');
|
||||||
|
|
||||||
let groupsUserIsMemberOf = await Group.getGroups({user, types, groupFields});
|
let groupsUserIsMemberOf = await Group.getGroups({user, types, groupFields});
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ api.getFrontPage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO remove api static page
|
let staticPages = ['front', 'privacy', 'terms', 'api-v2', 'features',
|
||||||
let staticPages = ['front', 'privacy', 'terms', 'api', 'features',
|
|
||||||
'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines',
|
'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines',
|
||||||
'old-news', 'press-kit', 'faq', 'overview', 'apps',
|
'old-news', 'press-kit', 'faq', 'overview', 'apps',
|
||||||
'clear-browser-data', 'merch'];
|
'clear-browser-data', 'merch'];
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ module.exports = function baseModel (schema, options = {}) {
|
|||||||
if (!this.isNew) this.updatedAt = Date.now();
|
if (!this.isNew) this.updatedAt = Date.now();
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
schema.pre('update', function preUpdateModel () {
|
||||||
|
this.update({}, { $set: { updatedAt: new Date() } });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let noSetFields = ['createdAt', 'updatedAt'];
|
let noSetFields = ['createdAt', 'updatedAt'];
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ function grantEndOfTheMonthPerks (user, now) {
|
|||||||
plan.dateUpdated = now;
|
plan.dateUpdated = now;
|
||||||
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
|
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
|
||||||
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
|
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
|
||||||
// TODO use month diff instead of ++ / --?
|
// TODO use month diff instead of ++ / --? see https://github.com/HabitRPG/habitrpg/issues/4317
|
||||||
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0}); // FIXME see https://github.com/HabitRPG/habitrpg/issues/4317
|
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0});
|
||||||
|
|
||||||
plan.consecutive.count++;
|
plan.consecutive.count++;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Bluebird from 'bluebird';
|
|
||||||
|
|
||||||
const IS_PROD = nconf.get('IS_PROD');
|
const IS_PROD = nconf.get('IS_PROD');
|
||||||
const IS_TEST = nconf.get('IS_TEST');
|
const IS_TEST = nconf.get('IS_TEST');
|
||||||
@@ -11,8 +10,6 @@ const ENABLE_CONSOLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') ===
|
|||||||
const logger = new winston.Logger();
|
const logger = new winston.Logger();
|
||||||
|
|
||||||
if (IS_PROD) {
|
if (IS_PROD) {
|
||||||
// TODO production logging, use loggly and new relic too
|
|
||||||
|
|
||||||
if (ENABLE_CONSOLE_LOGS_IN_PROD === 'true') {
|
if (ENABLE_CONSOLE_LOGS_IN_PROD === 'true') {
|
||||||
logger.add(winston.transports.Console, {
|
logger.add(winston.transports.Console, {
|
||||||
colorize: true,
|
colorize: true,
|
||||||
@@ -54,15 +51,6 @@ let loggerInterface = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable warnings for missed returns in Bluebird.
|
|
||||||
// See https://github.com/petkaantonov/bluebird/issues/903
|
|
||||||
Bluebird.config({
|
|
||||||
// Enables all warnings except forgotten return statements.
|
|
||||||
warnings: {
|
|
||||||
wForgottenReturn: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logs unhandled promises errors
|
// Logs unhandled promises errors
|
||||||
// when no catch is attached to a promise a unhandledRejection event will be triggered
|
// when no catch is attached to a promise a unhandledRejection event will be triggered
|
||||||
process.on('unhandledRejection', function handlePromiseRejection (reason) {
|
process.on('unhandledRejection', function handlePromiseRejection (reason) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function authWithHeaders (optional = false) {
|
|||||||
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {userId: user._id}));
|
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {userId: user._id}));
|
||||||
|
|
||||||
res.locals.user = user;
|
res.locals.user = user;
|
||||||
// TODO use either session/cookie or headers, not both
|
|
||||||
req.session.userId = user._id;
|
req.session.userId = user._id;
|
||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ module.exports = function cronMiddleware (req, res, next) {
|
|||||||
let ranCron = user.isModified();
|
let ranCron = user.isModified();
|
||||||
let quest = common.content.quests[user.party.quest.key];
|
let quest = common.content.quests[user.party.quest.key];
|
||||||
|
|
||||||
// if (ranCron) res.locals.wasModified = true; // TODO remove after v2 is retired
|
if (ranCron) res.locals.wasModified = true; // TODO remove after v2 is retired
|
||||||
if (!ranCron) return next();
|
if (!ranCron) return next();
|
||||||
|
|
||||||
// Group.tavernBoss(user, progress);
|
// Group.tavernBoss(user, progress);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ module.exports = function attachMiddlewares (app, server) {
|
|||||||
app.use(cookieSession({
|
app.use(cookieSession({
|
||||||
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
|
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
|
||||||
secret: SESSION_SECRET,
|
secret: SESSION_SECRET,
|
||||||
httpOnly: false, // TODO this should be true for security, what about https only?
|
httpOnly: false, // TODO this should be true for security, what about https only (secure) ?
|
||||||
maxAge: TWO_WEEKS,
|
maxAge: TWO_WEEKS,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ schema.methods.canView = function canViewChallenge (user, group) {
|
|||||||
function _syncableAttrs (task) {
|
function _syncableAttrs (task) {
|
||||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||||
// only sync/compare important attrs
|
// only sync/compare important attrs
|
||||||
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes', 'updatedAt'];
|
||||||
if (t.type !== 'reward') omitAttrs.push('value');
|
if (t.type !== 'reward') omitAttrs.push('value');
|
||||||
return _.omit(t, omitAttrs);
|
return _.omit(t, omitAttrs);
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ schema.methods.removeTask = async function challengeRemoveTask (task) {
|
|||||||
'challenge.id': challenge.id,
|
'challenge.id': challenge.id,
|
||||||
'challenge.taskId': task._id,
|
'challenge.taskId': task._id,
|
||||||
}, {
|
}, {
|
||||||
$set: {'challenge.broken': 'TASK_DELETED'}, // TODO what about updatedAt?
|
$set: {'challenge.broken': 'TASK_DELETED'},
|
||||||
}, {multi: true}).exec();
|
}, {multi: true}).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
|
|||||||
|
|
||||||
if (keep === 'keep-all') {
|
if (keep === 'keep-all') {
|
||||||
await Tasks.Task.update(findQuery, {
|
await Tasks.Task.update(findQuery, {
|
||||||
$set: {challenge: {}}, // TODO what about updatedAt?
|
$set: {challenge: {}},
|
||||||
}, {multi: true}).exec();
|
}, {multi: true}).exec();
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
@@ -259,7 +259,7 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO everything here should be moved to a worker
|
// TODO everything here should be moved to a worker
|
||||||
// actually even for a worker it's probably just too big and will kill mongo
|
// actually even for a worker it's probably just too big and will kill mongo, figure out something else
|
||||||
schema.methods.closeChal = async function closeChal (broken = {}) {
|
schema.methods.closeChal = async function closeChal (broken = {}) {
|
||||||
let challenge = this;
|
let challenge = this;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export let schema = new Schema({
|
|||||||
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||||
type: {type: String, enum: ['guild', 'party'], required: true},
|
type: {type: String, enum: ['guild', 'party'], required: true},
|
||||||
privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
|
privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
|
||||||
// _v: {type: Number,'default': 0}, // TODO ?
|
|
||||||
chat: Array,
|
chat: Array,
|
||||||
/*
|
/*
|
||||||
# [{
|
# [{
|
||||||
@@ -94,7 +93,6 @@ schema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) {
|
|||||||
// Basic fields to fetch for populating a group info
|
// Basic fields to fetch for populating a group info
|
||||||
export let basicFields = 'name type privacy';
|
export let basicFields = 'name type privacy';
|
||||||
|
|
||||||
// TODO test
|
|
||||||
schema.pre('remove', true, async function preRemoveGroup (next, done) {
|
schema.pre('remove', true, async function preRemoveGroup (next, done) {
|
||||||
next();
|
next();
|
||||||
try {
|
try {
|
||||||
@@ -179,14 +177,16 @@ schema.statics.getGroups = async function getGroups (options = {}) {
|
|||||||
queries.push(privateGuildsQuery);
|
queries.push(privateGuildsQuery);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// NOTE: when returning publicGuilds we use `.lean()` so all mongoose methods won't be available.
|
||||||
|
// Docs are going to be plain javascript objects
|
||||||
case 'publicGuilds': {
|
case 'publicGuilds': {
|
||||||
let publicGuildsQuery = this.find({
|
let publicGuildsQuery = this.find({
|
||||||
type: 'guild',
|
type: 'guild',
|
||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
}).select(groupFields);
|
}).select(groupFields);
|
||||||
if (populateLeader === true) publicGuildsQuery.populate('leader', nameFields);
|
if (populateLeader === true) publicGuildsQuery.populate('leader', nameFields);
|
||||||
publicGuildsQuery.sort(sort).exec();
|
publicGuildsQuery.sort(sort).lean().exec();
|
||||||
queries.push(publicGuildsQuery); // TODO use lean?
|
queries.push(publicGuildsQuery);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tavern': {
|
case 'tavern': {
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ TaskSchema.statics.fromJSONV2 = function fromJSONV2 (taskObj) {
|
|||||||
return taskObj;
|
return taskObj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// END of API v2 methods
|
// END of API v2 methods
|
||||||
|
|
||||||
export let Task = mongoose.model('Task', TaskSchema);
|
export let Task = mongoose.model('Task', TaskSchema);
|
||||||
|
|||||||
@@ -525,11 +525,8 @@ export let schema = new Schema({
|
|||||||
});
|
});
|
||||||
|
|
||||||
schema.plugin(baseModel, {
|
schema.plugin(baseModel, {
|
||||||
// TODO revisit a lot of things are missing. Given how many attributes we do have here we should white-list the ones that can be updated
|
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
||||||
// This is not really used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
noSet: [],
|
||||||
noSet: ['_id', 'apiToken', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password',
|
|
||||||
'auth.local.salt', 'tasksOrder', 'tags', 'stats', 'challenges', 'guilds', 'party._id', 'party.quest',
|
|
||||||
'invitations', 'balance', 'backer', 'contributor'],
|
|
||||||
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
||||||
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
||||||
plainObj.id = plainObj._id;
|
plainObj.id = plainObj._id;
|
||||||
@@ -638,7 +635,6 @@ function _setProfileName (user) {
|
|||||||
schema.pre('save', true, function preSaveUser (next, done) {
|
schema.pre('save', true, function preSaveUser (next, done) {
|
||||||
next();
|
next();
|
||||||
|
|
||||||
// TODO remove all unnecessary checks
|
|
||||||
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
||||||
this.preferences.dayStart = 0;
|
this.preferences.dayStart = 0;
|
||||||
}
|
}
|
||||||
@@ -697,7 +693,10 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO unit test this?
|
schema.pre('update', function preUpdateUser () {
|
||||||
|
this.update({}, {$inc: {_v: 1}});
|
||||||
|
});
|
||||||
|
|
||||||
schema.methods.isSubscribed = function isSubscribed () {
|
schema.methods.isSubscribed = function isSubscribed () {
|
||||||
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user