Skip to content

Commit

Permalink
feat(firewall): create route to undo firewall event side-effects
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafatcb committed Mar 8, 2024
1 parent 0ea7578 commit 25cc2b8
Show file tree
Hide file tree
Showing 11 changed files with 1,090 additions and 19 deletions.
1 change: 1 addition & 0 deletions infra/scripts/seed-database.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async function seedDevelopmentUsers() {
'create:recovery_token:username',
'read:votes:others',
'read:user:list',
'undo:firewall',
]);
await insertUser('user', '[email protected]', '$2a$04$v0hvAu/y6pJ17LzeCfcKG.rDStO9x5ficm2HTLZIfeDBG8oR/uQXi', [
'create:session',
Expand Down
1 change: 1 addition & 0 deletions models/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const availableFeatures = new Set([
'update:user:others',
'ban:user',
'create:recovery_token:username',
'undo:firewall',
]);

function can(user, feature, resource) {
Expand Down
54 changes: 43 additions & 11 deletions models/balance.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,51 @@ async function rateContent({ contentId, contentOwnerId, fromUserId, transactionT
};
}

async function undo(operationId, options = {}) {
const balanceOperation = await findOne(operationId, options);

const invertedBalanceOperation = {
balanceType: balanceOperation.balance_type,
recipientId: balanceOperation.recipient_id,
amount: balanceOperation.amount * -1,
originatorType: 'event',
originatorId: options.event.id,
async function undo({ values = {}, where }, options = {}) {
let globalIndex = 1;
const selectFields = getSelectFields();
const whereClause = buildWhereClause();

const query = {
text: `
INSERT INTO balance_operations
(balance_type, recipient_id, amount, originator_type, originator_id)
SELECT
${selectFields.text}
FROM
balance_operations
${whereClause}
;`,
values: [...selectFields.values, ...Object.values(where)],
};

const newBalanceOperation = await create(invertedBalanceOperation, options);
return newBalanceOperation;
await database.query(query, options);

function getSelectFields() {
const text = `balance_type,
recipient_id,
-amount,
${values.originator_type ? `$${globalIndex++}` : 'originator_type'},
${values.originator_id ? `$${globalIndex++}` : 'originator_id'}`;

const selectValues = [];
if (values.originator_type) {
selectValues.push(values.originator_type);
}
if (values.originator_id) {
selectValues.push(values.originator_id);
}

return { text, values: selectValues };
}

function buildWhereClause() {
const conditions = Object.entries(where).map(([key, value]) => {
return Array.isArray(value) ? `(${key} = ANY ($${globalIndex++}))` : `(${key} = $${globalIndex++})`;
});

return `WHERE ${conditions.join(' AND ')}`;
}
}

export default Object.freeze({
Expand Down
12 changes: 9 additions & 3 deletions models/ban.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ async function nuke(userId, options = {}) {
for (const userEvent of userEvents) {
const eventBalanceOperations = await getRatingBalanceOperationsFromEvent(userEvent.id, options);

for (const eventBalanceOperation of eventBalanceOperations) {
await balance.undo(eventBalanceOperation.id, options);
}
const ids = eventBalanceOperations.map((event) => event.id);
await balance.undo(
{
where: {
id: ids,
},
},
options,
);
}

async function getAllRatingEventsFromUser(userId, options = {}) {
Expand Down
16 changes: 16 additions & 0 deletions models/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,21 @@ async function update(contentId, postedContent, options = {}) {
}
}

async function undoDeleted(contentIds, options) {
const query = {
text: `
UPDATE contents SET
status = 'published',
deleted_at = NULL,
updated_at = (now() at time zone 'utc')
WHERE
id = ANY ($1)
;`,
values: [contentIds],
};
await database.query(query, options);
}

function validateUpdateSchema(content) {
const cleanValues = validator(content, {
slug: 'optional',
Expand Down Expand Up @@ -973,5 +988,6 @@ export default Object.freeze({
findWithStrategy,
create,
update,
undoDeleted,
creditOrDebitTabCoins,
});
40 changes: 40 additions & 0 deletions models/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,48 @@ async function findAll() {
return results.rows;
}

async function findOneById(eventId) {
const query = {
text: `
SELECT
*
FROM
events
WHERE
id = $1
LIMIT
1
;`,
values: [eventId],
};

const results = await database.query(query);
return results.rows[0];
}

async function findOneByOriginalEventId(originalEventId) {
const query = {
text: `
SELECT
*
FROM
events
WHERE
metadata->>'original_event_id' = $1
LIMIT
1
;`,
values: [originalEventId],
};

const results = await database.query(query);
return results.rows[0];
}

export default Object.freeze({
create,
updateMetadata,
findAll,
findOneById,
findOneByOriginalEventId,
});
115 changes: 114 additions & 1 deletion models/firewall.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TooManyRequestsError } from 'errors';
import { NotFoundError, TooManyRequestsError, ValidationError } from 'errors';
import database from 'infra/database.js';
import email from 'infra/email.js';
import balance from 'models/balance.js';
import content from 'models/content.js';
import event from 'models/event.js';
import { FirewallEmail } from 'models/transactional';
Expand Down Expand Up @@ -298,6 +299,118 @@ function getFirewallDeletedContentLine(contents) {
return `Identificamos que você está tentando criar ${beforeTitles} ${formattedList} ${afterTitles}.`;
}

async function undoAllFirewallSideEffects(context, eventId) {
const foundEvent = await event.findOneById(eventId);

if (!foundEvent) {
throw new NotFoundError({
message: `O id "${eventId}" não foi encontrado no sistema.`,
action: 'Verifique se o "id" está digitado corretamente.',
stack: new Error().stack,
errorLocationCode: 'MODEL:FIREWALL:UNDO_ALL_FIREWALL_SIDE_EFFECTS:NOT_FOUND',
key: 'id',
});
}

const acceptedFirewallEvents = [
'firewall:block_contents:text_child',
'firewall:block_contents:text_root',
'firewall:block_users',
];

if (!acceptedFirewallEvents.includes(foundEvent.type)) {
throw new ValidationError({
message: 'Você está tentando desfazer um evento inválido.',
action: 'Utilize um "event_id" que aponte para um evento de firewall.',
stack: new Error().stack,
errorLocationCode: 'MODEL:FIREWALL:UNDO_ALL_FIREWALL_SIDE_EFFECTS:INVALID_EVENT_TYPE',
key: 'type',
});
}

const transaction = await database.transaction();

try {
await transaction.query('BEGIN');

const metadata =
foundEvent.type === 'firewall:block_users'
? { users: foundEvent.metadata.users }
: { contents: foundEvent.metadata.contents };
metadata.original_event_id = eventId;

const createdEvent = await event.create(
{
type: `${foundEvent.type}:undo`,
originatorUserId: context.user.id,
originatorIp: context.clientIp,
metadata: metadata,
},
{
transaction: transaction,
},
);

if (foundEvent.type === 'firewall:block_users') {
const users = await user.findAll(
{
id: foundEvent.metadata.users,
},
{
transaction: transaction,
},
);
const inactiveUsers = [];
const activeUsers = [];

for (const userData of users) {
if (userData.features.length === 0) {
inactiveUsers.push(userData.id);
} else {
activeUsers.push(userData.id);
}
}

if (activeUsers.length) {
await user.addFeatures(activeUsers, ['create:session', 'read:session'], {
transaction: transaction,
});
}
if (inactiveUsers.length) {
await user.addFeatures(inactiveUsers, ['read:activation_token'], {
transaction: transaction,
});
}
} else {
await content.undoDeleted(foundEvent.metadata.contents, {
transaction: transaction,
});
await balance.undo(
{
values: {
originator_type: 'event',
originator_id: createdEvent.id,
},
where: {
originator_id: foundEvent.id,
},
},
{
transaction: transaction,
},
);
}

await transaction.query('COMMIT');
} catch (error) {
await transaction.query('ROLLBACK');
throw error;
} finally {
await transaction.release();
}
}

export default Object.freeze({
canRequest,
undoAllFirewallSideEffects,
});
25 changes: 21 additions & 4 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import balance from 'models/balance.js';
import emailConfirmation from 'models/email-confirmation.js';
import validator from 'models/validator.js';

async function findAll() {
async function findAll(where = {}, options) {
const whereClause = buildWhereClause();
const query = {
text: `
SELECT
Expand All @@ -17,12 +18,23 @@ async function findAll() {
get_current_balance('user:tabcoin', users.id) as tabcoins,
get_current_balance('user:tabcash', users.id) as tabcash
) as balance
${whereClause}
ORDER BY
created_at ASC
;`,
values: Object.values(where),
};
const results = await database.query(query);
const results = await database.query(query, options);
return results.rows;

function buildWhereClause() {
const conditions = Object.entries(where).map(([key, value], index) => {
const valueIndex = index + 1;
return Array.isArray(value) ? `(${key} = ANY ($${valueIndex}))` : `(${key} = $${valueIndex})`;
});

return conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
}
}

async function findOneByUsername(username, options = {}) {
Expand Down Expand Up @@ -394,15 +406,15 @@ async function removeFeatures(userId, features, options = {}) {
}

async function addFeatures(userId, features, options) {
const whereClause = buildWhereClause();
const query = {
text: `
UPDATE
users
SET
features = array_cat(features, $1),
updated_at = (now() at time zone 'utc')
WHERE
id = $2
${whereClause}
RETURNING
*
;`,
Expand All @@ -412,6 +424,11 @@ async function addFeatures(userId, features, options) {
const results = await database.query(query, options);

return results.rows[0];

function buildWhereClause() {
const condition = Array.isArray(userId) ? 'id = ANY ($2)' : 'id = $2';
return `WHERE ${condition}`;
}
}

async function updateRewardedAt(userId, options) {
Expand Down
Loading

0 comments on commit 25cc2b8

Please sign in to comment.