[CORL-155] User Suspending and Banning (#2247)

* feat: suspending, banning, now propogation

* feat: adapting to `now`

* feat: support auth for suspension/banned

* feat: added trace-id to requests

* feat: new mutation api with hooks support

* feat: added user status filtering, current field

* feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3

* fix: typo

* fix: tests

* chore: rename banned status to ban status

* test: feature test + lots of test helper improvements e.g. types

* fix: add translation to ban user modal

* fix: translation

* fix: test
This commit is contained in:
Wyatt Johnson
2019-04-22 22:57:32 +00:00
committed by GitHub
parent b63c00f26f
commit dbbc1af42e
147 changed files with 4609 additions and 1468 deletions
+8 -4
View File
@@ -186,7 +186,8 @@ export function filterDuplicateActions<T extends {}>(actions: T[]): T[] {
export async function createAction(
mongo: Db,
tenantID: string,
input: CreateActionInput
input: CreateActionInput,
now = new Date()
): Promise<CreateActionResultObject> {
const { metadata, additionalDetails, ...filter } = input;
@@ -198,7 +199,7 @@ export async function createAction(
const defaults: Sub<CommentAction, CreateActionInput> = {
id,
tenantID,
createdAt: new Date(),
createdAt: now,
};
// Merge the defaults with the input.
@@ -244,10 +245,13 @@ export async function createAction(
export async function createActions(
mongo: Db,
tenantID: string,
inputs: CreateActionInput[]
inputs: CreateActionInput[],
now = new Date()
): Promise<CreateActionResultObject[]> {
// TODO: (wyattjoh) replace with a batch write.
return Promise.all(inputs.map(input => createAction(mongo, tenantID, input)));
return Promise.all(
inputs.map(input => createAction(mongo, tenantID, input, now))
);
}
export async function retrieveUserAction(
+7 -9
View File
@@ -221,10 +221,9 @@ export type CreateCommentInput = Omit<
export async function createComment(
mongo: Db,
tenantID: string,
input: CreateCommentInput
input: CreateCommentInput,
now = new Date()
) {
const createdAt = new Date();
// Pull out some useful properties from the input.
const { body, actionCounts = {}, ...rest } = input;
@@ -233,7 +232,7 @@ export async function createComment(
id: uuid.v4(),
body,
actionCounts,
createdAt,
createdAt: now,
};
// default are the properties set by the application when a new comment is
@@ -244,7 +243,7 @@ export async function createComment(
replyIDs: [],
replyCount: 0,
revisions: [revision],
createdAt,
createdAt: now,
};
// Merge the defaults and the input together.
@@ -360,10 +359,9 @@ export interface EditComment {
export async function editComment(
mongo: Db,
tenantID: string,
input: EditCommentInput
input: EditCommentInput,
now = new Date()
): Promise<EditComment> {
const createdAt = new Date();
const {
id,
body,
@@ -379,7 +377,7 @@ export async function editComment(
id: uuid.v4(),
body,
actionCounts,
createdAt,
createdAt: now,
};
const update: Record<string, any> = {
+1
View File
@@ -81,6 +81,7 @@ export type Settings = GlobalModerationSettings &
| "editCommentWindowLength"
| "customCSSURL"
| "communityGuidelines"
| "createdAt"
> & {
/**
* auth is the set of configured authentication integrations.
+36 -21
View File
@@ -62,9 +62,9 @@ function collection<T = Story>(mongo: Db) {
export async function recalculateSharedModerationQueueQueueCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
const now = new Date();
const key = commentCountsModerationQueueQueuesKey(tenantID);
const freshKey = freshenKey(key);
@@ -136,9 +136,9 @@ export async function recalculateSharedModerationQueueQueueCounts(
export async function recalculateSharedModerationQueueTotalCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
const now = new Date();
const key = commentCountsModerationQueueTotalKey(tenantID);
const freshKey = freshenKey(key);
@@ -191,9 +191,9 @@ export async function recalculateSharedModerationQueueTotalCounts(
export async function recalculateSharedStatusCommentCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
const now = new Date();
const key = commentCountsStatusKey(tenantID);
const freshKey = freshenKey(key);
@@ -265,9 +265,9 @@ export async function recalculateSharedStatusCommentCounts(
export async function recalculateSharedActionCommentCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
const now = new Date();
const key = commentCountsActionKey(tenantID);
const freshKey = freshenKey(key);
@@ -339,13 +339,14 @@ export async function recalculateSharedActionCommentCounts(
export async function recalculateSharedCommentCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
await Promise.all([
recalculateSharedModerationQueueQueueCounts(mongo, redis, tenantID),
recalculateSharedModerationQueueTotalCounts(mongo, redis, tenantID),
recalculateSharedStatusCommentCounts(mongo, redis, tenantID),
recalculateSharedActionCommentCounts(mongo, redis, tenantID),
recalculateSharedModerationQueueQueueCounts(mongo, redis, tenantID, now),
recalculateSharedModerationQueueTotalCounts(mongo, redis, tenantID, now),
recalculateSharedStatusCommentCounts(mongo, redis, tenantID, now),
recalculateSharedActionCommentCounts(mongo, redis, tenantID, now),
]);
}
@@ -385,7 +386,8 @@ function fillAndConvertStringToNumber<
export async function retrieveSharedActionCommentCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
): Promise<EncodedCommentActionCounts> {
const key = commentCountsActionKey(tenantID);
const freshKey = freshenKey(key);
@@ -400,7 +402,7 @@ export async function retrieveSharedActionCommentCounts(
.get(freshKey)
.exec();
if (!fresh || !actions) {
return recalculateSharedActionCommentCounts(mongo, redis, tenantID);
return recalculateSharedActionCommentCounts(mongo, redis, tenantID, now);
}
return fillAndConvertStringToNumber(
@@ -421,7 +423,8 @@ export async function retrieveSharedActionCommentCounts(
export async function retrieveSharedStatusCommentCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
): Promise<CommentStatusCounts> {
const key = commentCountsStatusKey(tenantID);
const freshKey = freshenKey(key);
@@ -436,7 +439,7 @@ export async function retrieveSharedStatusCommentCounts(
.get(freshKey)
.exec();
if (!fresh || !statuses) {
return recalculateSharedStatusCommentCounts(mongo, redis, tenantID);
return recalculateSharedStatusCommentCounts(mongo, redis, tenantID, now);
}
return fillAndConvertStringToNumber(
@@ -457,7 +460,8 @@ export async function retrieveSharedStatusCommentCounts(
export async function retrieveSharedModerationQueueTotal(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
) {
const key = commentCountsModerationQueueTotalKey(tenantID);
const freshKey = freshenKey(key);
@@ -468,7 +472,12 @@ export async function retrieveSharedModerationQueueTotal(
freshKey
);
if (fresh === null || total === null) {
return recalculateSharedModerationQueueTotalCounts(mongo, redis, tenantID);
return recalculateSharedModerationQueueTotalCounts(
mongo,
redis,
tenantID,
now
);
}
return parseInt(total, 10) || 0;
@@ -486,7 +495,8 @@ export async function retrieveSharedModerationQueueTotal(
export async function retrieveSharedModerationQueueQueuesCounts(
mongo: Db,
redis: AugmentedRedis,
tenantID: string
tenantID: string,
now = new Date()
): Promise<CommentModerationCountsPerQueue> {
const key = commentCountsModerationQueueQueuesKey(tenantID);
const freshKey = freshenKey(key);
@@ -505,7 +515,12 @@ export async function retrieveSharedModerationQueueQueuesCounts(
.exec();
if (!fresh || !queues) {
logger.debug({ tenantID }, "comment moderation counts were not cached");
return recalculateSharedModerationQueueQueueCounts(mongo, redis, tenantID);
return recalculateSharedModerationQueueQueueCounts(
mongo,
redis,
tenantID,
now
);
}
logger.debug({ tenantID }, "comment moderation counts were cached");
+37 -21
View File
@@ -121,10 +121,9 @@ export interface UpsertStoryInput {
export async function upsertStory(
mongo: Db,
tenantID: string,
{ id, url }: UpsertStoryInput
{ id, url }: UpsertStoryInput,
now = new Date()
) {
const now = new Date();
// Create the story, optionally sourcing the id from the input, additionally
// porting in the tenantID.
const update: { $setOnInsert: Story } = {
@@ -171,15 +170,21 @@ export interface FindOrCreateStoryInput {
export async function findOrCreateStory(
mongo: Db,
tenantID: string,
{ id, url }: FindOrCreateStoryInput
{ id, url }: FindOrCreateStoryInput,
now = new Date()
) {
if (id) {
if (url) {
// The URL was specified, this is an upsert operation.
return upsertStory(mongo, tenantID, {
id,
url,
});
return upsertStory(
mongo,
tenantID,
{
id,
url,
},
now
);
}
// The URL was not specified, this is a lookup operation.
@@ -192,7 +197,7 @@ export async function findOrCreateStory(
throw new Error("cannot upsert an story without the url");
}
return upsertStory(mongo, tenantID, { url });
return upsertStory(mongo, tenantID, { url }, now);
}
export type CreateStoryInput = Partial<Pick<Story, "metadata" | "scrapedAt">>;
@@ -202,10 +207,9 @@ export async function createStory(
tenantID: string,
id: string,
url: string,
input: CreateStoryInput
input: CreateStoryInput,
now = new Date()
) {
const now = new Date();
// Create the story.
const story: Story = {
...input,
@@ -289,14 +293,15 @@ export async function updateStory(
mongo: Db,
tenantID: string,
id: string,
input: UpdateStoryInput
input: UpdateStoryInput,
now = new Date()
) {
// Only update fields that have been updated.
const update = {
$set: {
...dotize(input, { embedArrays: true }),
// Always update the updated at time.
updatedAt: new Date(),
updatedAt: now,
},
};
@@ -326,14 +331,15 @@ export async function updateStorySettings(
mongo: Db,
tenantID: string,
id: string,
input: UpdateStorySettingsInput
input: UpdateStorySettingsInput,
now = new Date()
) {
// Only update fields that have been updated.
const update = {
$set: {
...omitBy(dotize({ settings: input }, { embedArrays: true }), isNull),
// Always update the updated at time.
updatedAt: new Date(),
updatedAt: now,
},
};
@@ -348,14 +354,19 @@ export async function updateStorySettings(
return result.value || null;
}
export async function openStory(mongo: Db, tenantID: string, id: string) {
export async function openStory(
mongo: Db,
tenantID: string,
id: string,
now = new Date()
) {
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
{
$set: {
closedAt: false,
// Always update the updated at time.
updatedAt: new Date(),
updatedAt: now,
},
},
// False to return the updated document instead of the original
@@ -366,14 +377,19 @@ export async function openStory(mongo: Db, tenantID: string, id: string) {
return result.value || null;
}
export async function closeStory(mongo: Db, tenantID: string, id: string) {
export async function closeStory(
mongo: Db,
tenantID: string,
id: string,
now = new Date()
) {
const result = await collection(mongo).findOneAndUpdate(
{ id, tenantID },
{
$set: {
closedAt: new Date(),
closedAt: now,
// Always update the updated at time.
updatedAt: new Date(),
updatedAt: now,
},
},
// False to return the updated document instead of the original
+7 -2
View File
@@ -69,7 +69,11 @@ export type CreateTenantInput = Pick<
* @param mongo the MongoDB connection used to create the tenant.
* @param input the customizable parts of the Tenant available during creation
*/
export async function createTenant(mongo: Db, input: CreateTenantInput) {
export async function createTenant(
mongo: Db,
input: CreateTenantInput,
now = new Date()
) {
const defaults: Sub<Tenant, CreateTenantInput> = {
// Create a new ID.
id: uuid.v4(),
@@ -119,7 +123,7 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
stream: true,
},
key: generateSSOKey(),
keyGeneratedAt: new Date(),
keyGeneratedAt: now,
},
oidc: {
enabled: false,
@@ -175,6 +179,7 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
labelActive: "Respected",
icon: "thumb_up",
},
createdAt: now,
};
// Create the new Tenant by merging it together with the defaults.
+536 -16
View File
@@ -2,17 +2,26 @@ import bcrypt from "bcryptjs";
import { Db, MongoError } from "mongodb";
import uuid from "uuid";
import { Omit, Sub } from "talk-common/types";
import { DeepPartial, Omit, Sub } from "talk-common/types";
import { dotize } from "talk-common/utils/dotize";
import {
DuplicateEmailError,
DuplicateUserError,
LocalProfileAlreadySetError,
LocalProfileNotSetError,
TokenNotFoundError,
UserAlreadyBannedError,
UserAlreadySuspendedError,
UsernameAlreadySetError,
UserNotFoundError,
} from "talk-server/errors";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
import {
GQLBanStatus,
GQLSuspensionStatus,
GQLTimeRange,
GQLUSER_ROLE,
} from "talk-server/graph/tenant/schema/__generated__/types";
import logger from "talk-server/logger";
import {
createConnectionOrderVariants,
createIndexFactory,
@@ -57,6 +66,10 @@ export interface GoogleProfile {
id: string;
}
/**
* Profile is all the different profiles that a given User may have associated
* with their account.
*/
export type Profile =
| LocalProfile
| OIDCProfile
@@ -70,15 +83,168 @@ export interface Token {
createdAt: Date;
}
/**
* SuspensionStatusHistory SuspensionStatusHistory is the list of all suspension
* events against a specific User.
*/
export interface SuspensionStatusHistory {
/**
* id is a specific reference for a particular suspension status that will be
* used internally to update suspension records.
*/
id: string;
/**
* from represents a range of time where a user suspension applies.
*/
from: GQLTimeRange;
/**
* createdBy is the ID for the User that suspended the User. If `null`, the
* suspension was created by the system.
*/
createdBy?: string;
/**
* createdAt is the time that the given suspension time frame was created.
*/
createdAt: Date;
/**
* modifiedBy is the ID for the User that modified the suspension for this
* User. If `null`, the suspension has not been edited, or has been edited by
* the system.
*/
modifiedBy?: string;
/**
* modifiedAt is the time that the date that the given suspension time frame
* was edited at.
*/
modifiedAt?: Date;
}
/**
* SuspensionStatus stores the user suspension status as well as the history of
* changes.
*/
export interface SuspensionStatus {
/**
* history is the list of all suspension events against a specific User.
*/
history: SuspensionStatusHistory[];
}
/**
* BanStatusHistory is the list of all ban events against a specific User.
*/
export interface BanStatusHistory {
/**
* id is a specific reference for a particular banned status that will be
* used internally to update banned records.
*/
id: string;
/**
* active, when true, indicates that the user is banned from this status.
*/
active: boolean;
/**
* createdBy is the ID for the User that banned the User. If `null`, the ban
* was created by the system.
*/
createdBy?: string;
/**
* createdAt is the time that the given ban was added.
*/
createdAt: Date;
}
/**
* BanStatus contains information about a ban for a given User.
*/
export interface BanStatus {
/**
* active when true, indicates that the given user is banned.
*/
active: boolean;
/**
* history is the list of all ban events against a specific User.
*/
history: BanStatusHistory[];
}
/**
* UserStatus stores the user status information regarding moderation state.
*/
export interface UserStatus {
/**
* suspension stores the user suspension status as well as the history of
* changes.
*/
suspension: SuspensionStatus;
/**
* ban stores the user ban status as well as the history of changes.
*/
ban: BanStatus;
}
/**
* User is someone that leaves Comments, and logs in.
*/
export interface User extends TenantResource {
/**
* id is the identifier of the User.
*/
readonly id: string;
/**
* username is the name of the User visible to other Users.
*/
username?: string;
/**
* avatar is the url to the avatar for a specific User.
*/
avatar?: string;
/**
* email is the current email address for the User.
*/
email?: string;
/**
* emailVerified when true indicates that the given email address has been verified.
*/
emailVerified?: boolean;
/**
* profiles is the array of profiles assigned to the user.
*/
profiles: Profile[];
/**
* tokens lists the access tokens associated with the account.
*/
tokens: Token[];
/**
* role is the current role of the User.
*/
role: GQLUSER_ROLE;
/**
* status stores the user status information regarding moderation state.
*/
status: UserStatus;
/**
* createdAt is the time that the User was created at.
*/
createdAt: Date;
}
@@ -138,6 +304,19 @@ export async function createUserIndexes(mongo: Db) {
tenantID: 1,
role: 1,
});
// Suspension based User Connection pagination.
await variants(createIndex, {
tenantID: 1,
"status.suspension.history.from.start": 1,
"status.suspension.history.from.finish": 1,
});
// Ban based User Connection pagination.
await variants(createIndex, {
tenantID: 1,
"status.ban.active": 1,
});
}
function hashPassword(password: string): Promise<string> {
@@ -146,16 +325,15 @@ function hashPassword(password: string): Promise<string> {
export type InsertUserInput = Omit<
User,
"id" | "tenantID" | "tokens" | "createdAt"
"id" | "tenantID" | "tokens" | "status" | "createdAt"
>;
export async function insertUser(
mongo: Db,
tenantID: string,
input: InsertUserInput
input: InsertUserInput,
now = new Date()
) {
const now = new Date();
// Create a new ID for the user.
const id = uuid.v4();
@@ -165,6 +343,10 @@ export async function insertUser(
id,
tenantID,
tokens: [],
status: {
suspension: { history: [] },
ban: { active: false, history: [] },
},
createdAt: now,
};
@@ -332,7 +514,7 @@ export async function updateUserPassword(
throw new LocalProfileNotSetError();
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value || null;
@@ -384,7 +566,7 @@ export async function setUserUsername(
throw new UsernameAlreadySetError();
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -430,7 +612,7 @@ export async function updateUserUsername(
throw new UserNotFoundError(id);
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -492,7 +674,7 @@ export async function setUserEmail(
throw new UsernameAlreadySetError();
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -548,7 +730,7 @@ export async function updateUserEmail(
throw new UserNotFoundError(id);
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -595,7 +777,7 @@ export async function updateUserAvatar(
throw new UserNotFoundError(id);
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -672,7 +854,7 @@ export async function setUserLocalProfile(
throw new LocalProfileAlreadySetError();
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
return result.value;
@@ -682,13 +864,14 @@ export async function createUserToken(
mongo: Db,
tenantID: string,
userID: string,
name: string
name: string,
now = new Date()
) {
// Create the Token that we'll be adding to the User.
const token: Readonly<Token> = {
id: uuid.v4(),
name,
createdAt: new Date(),
createdAt: now,
};
const result = await collection(mongo).findOneAndUpdate(
@@ -748,7 +931,7 @@ export async function deactivateUserToken(
throw new TokenNotFoundError();
}
throw new Error("an unexpected error occured");
throw new Error("an unexpected error occurred");
}
// We have to typecast here because we know at this point that the record does
@@ -798,3 +981,340 @@ async function retrieveConnection(
// Return a connection.
return resolveConnection(query, input, user => user.createdAt);
}
/**
* banUser will ban a specific user from interacting with the site.
*
* @param mongo the mongo database handle
* @param tenantID the Tenant's ID where the User exists
* @param id the ID of the user being banned
* @param createdBy the ID of the user banning the above mentioned user
* @param now the current date
*/
export async function banUser(
mongo: Db,
tenantID: string,
id: string,
createdBy: string,
now = new Date()
) {
// Create the new ban.
const banHistory: BanStatusHistory = {
id: uuid(),
active: true,
createdBy,
createdAt: now,
};
// Try to update the user if the user isn't already banned.
const result = await collection(mongo).findOneAndUpdate(
{
id,
tenantID,
"status.ban.active": {
$ne: true,
},
},
{
$set: {
"status.ban.active": true,
},
$push: {
"status.ban.history": banHistory,
},
},
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
if (!result.value) {
// Get the user so we can figure out why the ban operation failed.
const user = await retrieveUser(mongo, tenantID, id);
if (!user) {
throw new UserNotFoundError(id);
}
// Check to see if the user is already banned.
const ban = consolidateUserBanStatus(user.status.ban);
if (ban.active) {
throw new UserAlreadyBannedError();
}
throw new Error("an unexpected error occurred");
}
return result.value;
}
/**
* removeUserBan will lift a user ban from a User allowing them to interact with
* the site again.
*
* @param mongo the mongo database handle
* @param tenantID the Tenant's ID where the User exists
* @param id the ID of the user having their ban lifted
* @param createdBy the ID of the user lifting the ban
* @param now the current date
*/
export async function removeUserBan(
mongo: Db,
tenantID: string,
id: string,
createdBy: string,
now = new Date()
) {
// Create the new ban.
const ban: BanStatusHistory = {
id: uuid(),
active: false,
createdBy,
createdAt: now,
};
// Try to update the user if the user isn't already banned.
const result = await collection(mongo).findOneAndUpdate(
{
id,
tenantID,
$or: [
{
"status.ban.active": {
$ne: false,
},
},
{
"status.ban.history": {
$size: 0,
},
},
],
},
{
$set: {
"status.ban.active": false,
},
$push: {
"status.ban.history": ban,
},
},
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
if (!result.value) {
// Get the user so we can figure out why the ban operation failed.
const user = await retrieveUser(mongo, tenantID, id);
if (!user) {
throw new UserNotFoundError(id);
}
// The user wasn't banned already, so nothing needs to be done!
return user;
}
return result.value;
}
/**
* suspendUser will suspend a user for a specific time range from interacting
* with the site.
*
* @param mongo the mongo database handle
* @param tenantID the Tenant's ID where the User exists
* @param id the ID of the user being suspended
* @param createdBy the ID of the user banning the above mentioned user
* @param from the range of time that the user is being banned for
* @param now the current date
*/
export async function suspendUser(
mongo: Db,
tenantID: string,
id: string,
createdBy: string,
finish: Date,
now = new Date()
) {
// Create the new suspension.
const suspension: SuspensionStatusHistory = {
id: uuid(),
from: {
start: now,
finish,
},
createdBy,
createdAt: now,
};
// Try to update the user if the user isn't already suspended.
const result = await collection(mongo).findOneAndUpdate(
{
id,
tenantID,
"status.suspension.history": {
$not: {
$elemMatch: {
"from.start": {
$lte: now,
},
"from.finish": {
$gt: now,
},
},
},
},
},
{
$push: {
"status.suspension.history": suspension,
},
},
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
if (!result.value) {
// Get the user so we can figure out why the suspend operation failed.
const user = await retrieveUser(mongo, tenantID, id);
if (!user) {
throw new UserNotFoundError(id);
}
// Check to see if the user is already suspended.
const suspended = consolidateUserSuspensionStatus(
user.status.suspension,
now
);
if (suspended.active && suspended.until) {
throw new UserAlreadySuspendedError(suspended.until);
}
throw new Error("an unexpected error occurred");
}
return result.value;
}
/**
* removeUserSuspensions will lift any active suspensions.
*
* @param mongo the mongo database handle
* @param tenantID the Tenant's ID where the User exists
* @param id the ID of the User having their suspension lifted
* @param modifiedBy the ID of the User lifting the suspension
* @param now the current date
*/
export async function removeActiveUserSuspensions(
mongo: Db,
tenantID: string,
id: string,
modifiedBy: string,
now = new Date()
) {
// Prepare the update payload.
const update: DeepPartial<SuspensionStatusHistory> = {
from: {
finish: now,
},
modifiedAt: now,
modifiedBy,
};
// Try to update the user suspension times.
const result = await collection(mongo).findOneAndUpdate(
{ tenantID, id },
{
$set: dotize({
"status.suspension.history.$[active]": update,
}),
},
{
arrayFilters: [
// Change the finish date on all suspension records that indicate their
// active time within our current range.
{
"active.from.start": { $lte: now },
"active.from.finish": { $gt: now },
},
],
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
if (!result.value) {
// Get the user so we can figure out why the suspend operation failed.
const user = await retrieveUser(mongo, tenantID, id);
if (!user) {
throw new UserNotFoundError(id);
}
// The user wasn't already suspended, so nothing needs to be done!
return user;
}
logger.debug({ result }, "finished update operation");
return result.value;
}
export type ConsolidatedBanStatus = Omit<GQLBanStatus, "history"> &
Pick<BanStatus, "history">;
export function consolidateUserBanStatus(
ban: User["status"]["ban"]
): ConsolidatedBanStatus {
return ban;
}
export type ConsolidatedSuspensionStatus = Omit<
GQLSuspensionStatus,
"history"
> &
Pick<SuspensionStatus, "history">;
export function consolidateUserSuspensionStatus(
suspension: User["status"]["suspension"],
now = new Date()
): ConsolidatedSuspensionStatus {
return suspension.history.reduce(
(status: ConsolidatedSuspensionStatus, history) => {
// Check to see if we're currently suspended.
if (history.from.start <= now && history.from.finish > now) {
status.active = true;
// Ensure that we have the furthest suspension finish time.
if (!status.until || status.until < history.from.finish) {
status.until = history.from.finish;
}
}
return status;
},
{
active: false,
history: suspension.history,
}
);
}
export interface ConsolidatedUserStatus {
suspension: ConsolidatedSuspensionStatus;
ban: ConsolidatedBanStatus;
}
export function consolidateUserStatus(
status: User["status"],
now = new Date()
): ConsolidatedUserStatus {
// Return the status.
return {
suspension: consolidateUserSuspensionStatus(status.suspension, now),
ban: consolidateUserBanStatus(status.ban),
};
}