mirror of
https://github.com/wassname/talk.git
synced 2026-07-21 12:51:03 +08:00
* fix: fixed up download handler to improve error handling * fix: fixed invalid expiry date being set * fix: nick found bug
1040 lines
28 KiB
TypeScript
1040 lines
28 KiB
TypeScript
import { DateTime } from "luxon";
|
|
import { Db } from "mongodb";
|
|
|
|
import {
|
|
ALLOWED_USERNAME_CHANGE_FREQUENCY,
|
|
DOWNLOAD_LIMIT_TIMEFRAME,
|
|
} from "coral-common/constants";
|
|
import { SCHEDULED_DELETION_TIMESPAN_DAYS } from "coral-common/constants";
|
|
import { Config } from "coral-server/config";
|
|
import {
|
|
DuplicateEmailError,
|
|
DuplicateUserError,
|
|
EmailAlreadySetError,
|
|
EmailNotSetError,
|
|
LocalProfileAlreadySetError,
|
|
LocalProfileNotSetError,
|
|
PasswordIncorrect,
|
|
TokenNotFoundError,
|
|
UserAlreadyBannedError,
|
|
UserAlreadySuspendedError,
|
|
UserCannotBeIgnoredError,
|
|
UsernameAlreadySetError,
|
|
UsernameUpdatedWithinWindowError,
|
|
UserNotFoundError,
|
|
} from "coral-server/errors";
|
|
import {
|
|
GQLAuthIntegrations,
|
|
GQLUSER_ROLE,
|
|
} from "coral-server/graph/tenant/schema/__generated__/types";
|
|
import logger from "coral-server/logger";
|
|
import { Tenant } from "coral-server/models/tenant";
|
|
import {
|
|
banUser,
|
|
clearDeletionDate,
|
|
consolidateUserBanStatus,
|
|
consolidateUserSuspensionStatus,
|
|
createUserToken,
|
|
deactivateUserToken,
|
|
ignoreUser,
|
|
insertUser,
|
|
InsertUserInput,
|
|
removeActiveUserSuspensions,
|
|
removeUserBan,
|
|
removeUserIgnore,
|
|
retrieveUser,
|
|
retrieveUserWithEmail,
|
|
scheduleDeletionDate,
|
|
setUserEmail,
|
|
setUserLastDownloadedAt,
|
|
setUserLocalProfile,
|
|
setUserUsername,
|
|
suspendUser,
|
|
updateUserAvatar,
|
|
updateUserEmail,
|
|
updateUserPassword,
|
|
updateUserRole,
|
|
updateUserUsername,
|
|
User,
|
|
verifyUserPassword,
|
|
} from "coral-server/models/user";
|
|
import {
|
|
getLocalProfile,
|
|
hasLocalProfile,
|
|
} from "coral-server/models/user/helpers";
|
|
import { userIsStaff } from "coral-server/models/user/helpers";
|
|
import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
|
import { sendConfirmationEmail } from "coral-server/services/users/auth";
|
|
|
|
import { JWTSigningConfig, signPATString } from "coral-server/services/jwt";
|
|
|
|
import {
|
|
generateAdminDownloadLink,
|
|
generateDownloadLink,
|
|
} from "./download/token";
|
|
import { validateEmail, validatePassword, validateUsername } from "./helpers";
|
|
|
|
export type InsertUser = InsertUserInput;
|
|
|
|
/**
|
|
* insert will upsert the User into the database for the Tenant.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be added to
|
|
* @param input the input for creating the User
|
|
*/
|
|
export async function insert(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
input: InsertUser,
|
|
now = new Date()
|
|
) {
|
|
if (input.username) {
|
|
validateUsername(input.username);
|
|
}
|
|
|
|
if (input.email) {
|
|
validateEmail(input.email);
|
|
|
|
// Try to lookup the user to see if this user already has an account if they
|
|
// do, we can short circuit the database index hit.
|
|
const alreadyFoundUser = await retrieveUserWithEmail(
|
|
mongo,
|
|
tenant.id,
|
|
input.email
|
|
);
|
|
if (alreadyFoundUser) {
|
|
throw new DuplicateEmailError(input.email);
|
|
}
|
|
}
|
|
|
|
if (input.id) {
|
|
// Try to check to see if there is a user with the same ID before we try to
|
|
// create the user again.
|
|
const alreadyFoundUser = await retrieveUser(mongo, tenant.id, input.id);
|
|
if (alreadyFoundUser) {
|
|
throw new DuplicateUserError();
|
|
}
|
|
}
|
|
|
|
const localProfile = getLocalProfile(input);
|
|
if (localProfile) {
|
|
validateEmail(localProfile.id);
|
|
validatePassword(localProfile.password);
|
|
|
|
if (input.email !== localProfile.id) {
|
|
throw new Error("email addresses don't match profile");
|
|
}
|
|
}
|
|
|
|
const user = await insertUser(mongo, tenant.id, input, now);
|
|
|
|
// // TODO: (wyattjoh) evaluate the tenant to determine if we should send the verification email.
|
|
// if (localProfile && user.email) {
|
|
// if (mailer) {
|
|
// // // Send the email confirmation email.
|
|
// // await sendConfirmationEmail(mongo, mailer, tenant, user, user.email);
|
|
// } else {
|
|
// // FIXME: (wyattjoh) extract the local profile based inserts into another function.
|
|
// throw new Error("local profile was provided, but the mailer was not");
|
|
// }
|
|
// }
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* setUsername will set the username on the User if they don't already have one
|
|
* associated with them.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user User that should get their username changed
|
|
* @param username the new username for the User
|
|
*/
|
|
export async function setUsername(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
username: string
|
|
) {
|
|
// We require that the username is not defined in order to use this method.
|
|
if (user.username) {
|
|
throw new UsernameAlreadySetError();
|
|
}
|
|
|
|
validateUsername(username);
|
|
|
|
return setUserUsername(mongo, tenant.id, user.id, username);
|
|
}
|
|
|
|
/**
|
|
* setEmail will set the email address on the User if they don't already have
|
|
* one associated with them.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user User that should get their username changed
|
|
* @param email the new email for the User
|
|
*/
|
|
export async function setEmail(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User,
|
|
email: string
|
|
) {
|
|
// We requires that the email address is not defined in order to use this
|
|
// method.
|
|
if (user.email) {
|
|
throw new EmailAlreadySetError();
|
|
}
|
|
|
|
validateEmail(email);
|
|
|
|
const updatedUser = await setUserEmail(mongo, tenant.id, user.id, email);
|
|
|
|
// // FIXME: (wyattjoh) evaluate the tenant to determine if we should send the verification email.
|
|
// // Send the email confirmation email.
|
|
// await sendConfirmationEmail(mailer, tenant, updatedUser, email);
|
|
|
|
return updatedUser;
|
|
}
|
|
|
|
/**
|
|
* setPassword will set the password on the User if they don't already have
|
|
* one associated with them. This will allow the User to sign in with their
|
|
* current email address and new password if email based authentication is
|
|
* enabled. If the User does not have a email address associated with their
|
|
* account, this will fail.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user User that should get their password changed
|
|
* @param password the new password for the User
|
|
*/
|
|
export async function setPassword(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
password: string
|
|
) {
|
|
// We require that the email address for the user be defined for this method.
|
|
if (!user.email) {
|
|
throw new EmailNotSetError();
|
|
}
|
|
|
|
// We also don't allow this method to be used by users that already have a
|
|
// local profile.
|
|
if (hasLocalProfile(user)) {
|
|
throw new LocalProfileAlreadySetError();
|
|
}
|
|
|
|
validatePassword(password);
|
|
|
|
return setUserLocalProfile(mongo, tenant.id, user.id, user.email, password);
|
|
}
|
|
|
|
/**
|
|
* updatePassword will update the password associated with the User. If the User
|
|
* does not already have a password associated with their account, it will fail.
|
|
* If the User does not have an email address associated with the account, this
|
|
* will fail.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user User that should get their password changed
|
|
* @param password the new password for the User
|
|
*/
|
|
export async function updatePassword(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User,
|
|
oldPassword: string,
|
|
newPassword: string
|
|
) {
|
|
// Validate that the new password is valid.
|
|
validatePassword(newPassword);
|
|
|
|
// We require that the email address for the user be defined for this method.
|
|
if (!user.email) {
|
|
throw new EmailNotSetError();
|
|
}
|
|
|
|
// We also don't allow this method to be used by users that don't have a local
|
|
// profile already.
|
|
const profile = getLocalProfile(user, user.email);
|
|
if (!profile) {
|
|
throw new LocalProfileNotSetError();
|
|
}
|
|
|
|
// Verify that the old password is correct. We'll be using the profile's
|
|
// passwordID to ensure we prevent a race.
|
|
const passwordVerified = await verifyUserPassword(
|
|
user,
|
|
oldPassword,
|
|
user.email
|
|
);
|
|
if (!passwordVerified) {
|
|
// We throw a PasswordIncorrect error here instead of an
|
|
// InvalidCredentialsError because the current user is already signed in.
|
|
throw new PasswordIncorrect();
|
|
}
|
|
|
|
const updatedUser = await updateUserPassword(
|
|
mongo,
|
|
tenant.id,
|
|
user.id,
|
|
newPassword,
|
|
profile.passwordID
|
|
);
|
|
|
|
// If the user has an email address associated with their account, send them
|
|
// a ban notification email.
|
|
if (updatedUser.email) {
|
|
// Send the ban user email.
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: updatedUser.email,
|
|
},
|
|
template: {
|
|
name: "password-change",
|
|
context: {
|
|
// TODO: (wyattjoh) possibly reevaluate the use of a required username.
|
|
username: updatedUser.username!,
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
organizationContactEmail: tenant.organization.contactEmail,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
export async function requestAccountDeletion(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User,
|
|
password: string,
|
|
now: Date
|
|
) {
|
|
if (!user.email) {
|
|
throw new EmailNotSetError();
|
|
}
|
|
|
|
const passwordVerified = await verifyUserPassword(user, password, user.email);
|
|
if (!passwordVerified) {
|
|
// We throw a PasswordIncorrect error here instead of an
|
|
// InvalidCredentialsError because the current user is already signed in.
|
|
throw new PasswordIncorrect();
|
|
}
|
|
|
|
const deletionDate = DateTime.fromJSDate(now).plus({
|
|
days: SCHEDULED_DELETION_TIMESPAN_DAYS,
|
|
});
|
|
|
|
const updatedUser = await scheduleDeletionDate(
|
|
mongo,
|
|
tenant.id!,
|
|
user.id,
|
|
deletionDate.toJSDate()
|
|
);
|
|
|
|
// TODO: extract out into a common shared formatter
|
|
// this is being duplicated everywhere
|
|
const formattedDate = Intl.DateTimeFormat(tenant.locale, {
|
|
year: "numeric",
|
|
month: "numeric",
|
|
day: "numeric",
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
second: "numeric",
|
|
}).format(deletionDate.toJSDate());
|
|
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: user.email,
|
|
},
|
|
template: {
|
|
name: "delete-request-confirmation",
|
|
context: {
|
|
requestDate: formattedDate,
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
},
|
|
},
|
|
});
|
|
|
|
return updatedUser;
|
|
}
|
|
|
|
export async function cancelAccountDeletion(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User
|
|
) {
|
|
if (!user.email) {
|
|
throw new EmailNotSetError();
|
|
}
|
|
|
|
const updatedUser = await clearDeletionDate(mongo, tenant.id, user.id);
|
|
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: user.email,
|
|
},
|
|
template: {
|
|
name: "delete-request-cancel",
|
|
context: {
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
},
|
|
},
|
|
});
|
|
|
|
return updatedUser;
|
|
}
|
|
|
|
/**
|
|
* createToken will create a Token for the User as well as return a signed Token
|
|
* that can be used to authenticate.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param config signing configuration to create the signed token
|
|
* @param user User that should get updated
|
|
* @param name name of the Token
|
|
*/
|
|
export async function createToken(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
config: JWTSigningConfig,
|
|
user: User,
|
|
name: string,
|
|
now = new Date()
|
|
) {
|
|
// Create the token for the User!
|
|
const result = await createUserToken(mongo, tenant.id, user.id, name, now);
|
|
|
|
// Sign the token!
|
|
const signedToken = await signPATString(
|
|
config,
|
|
user,
|
|
{
|
|
// Tokens are issued with the token ID as their JWT ID.
|
|
jwtid: result.token.id,
|
|
|
|
// Tokens are issued with the tenant ID.
|
|
issuer: tenant.id,
|
|
|
|
// Tokens are not valid before the creation date.
|
|
notBefore: 0,
|
|
},
|
|
now
|
|
);
|
|
|
|
return { ...result, signedToken };
|
|
}
|
|
|
|
/**
|
|
* deactivateToken will disable the given Token so that it can not be used to
|
|
* authenticate any more.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param config signing configuration to create the signed token
|
|
* @param user User that should get updated
|
|
* @param id of the Token to be deactivated
|
|
*/
|
|
export async function deactivateToken(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
id: string
|
|
) {
|
|
if (!user.tokens.find(t => t.id === id)) {
|
|
throw new TokenNotFoundError();
|
|
}
|
|
|
|
return deactivateUserToken(mongo, tenant.id, user.id, id);
|
|
}
|
|
|
|
/**
|
|
* updateUsername will update the current users username.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param mailer mailer queue instance
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user the User we are updating
|
|
* @param username the username that we are setting on the User
|
|
*/
|
|
export async function updateUsername(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User,
|
|
username: string,
|
|
now: Date
|
|
) {
|
|
// Validate the username.
|
|
validateUsername(username);
|
|
|
|
const canUpdate = canUpdateLocalProfile(tenant, user);
|
|
if (!canUpdate) {
|
|
throw new Error("Cannot update profile due to tenant settings");
|
|
}
|
|
|
|
// Get the earliest date that the username could have been edited before to/
|
|
// allow it now.
|
|
const lastUsernameEditAllowed = DateTime.fromJSDate(now)
|
|
.plus({ seconds: -ALLOWED_USERNAME_CHANGE_FREQUENCY })
|
|
.toJSDate();
|
|
|
|
const { history } = user.status.username;
|
|
if (history.length > 1) {
|
|
// If the last update was made at a date sooner than the earliest edited
|
|
// date, then we know that the last edit was conducted within the time-frame
|
|
// already.
|
|
const lastUpdate = history[history.length - 1];
|
|
if (lastUpdate.createdAt > lastUsernameEditAllowed) {
|
|
throw new UsernameUpdatedWithinWindowError(lastUpdate.createdAt);
|
|
}
|
|
}
|
|
|
|
const updated = await updateUserUsername(
|
|
mongo,
|
|
tenant.id,
|
|
user.id,
|
|
username,
|
|
user.id
|
|
);
|
|
|
|
if (user.email) {
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: user.email,
|
|
},
|
|
template: {
|
|
name: "update-username",
|
|
context: {
|
|
username: user.username!,
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
organizationContactEmail: tenant.organization.contactEmail,
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
logger.warn(
|
|
{ id: user.id },
|
|
"Failed to send email: user does not have email address"
|
|
);
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
/**
|
|
* updateUsernameByID will update a given User's username.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param userID the User's ID that we are updating
|
|
* @param username the username that we are setting on the User
|
|
*/
|
|
export async function updateUsernameByID(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
userID: string,
|
|
username: string,
|
|
createdBy: User
|
|
) {
|
|
// Validate the username.
|
|
validateUsername(username);
|
|
|
|
return updateUserUsername(mongo, tenant.id, userID, username, createdBy.id);
|
|
}
|
|
|
|
/**
|
|
* updateRole will update the given User to the specified role.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param userID the User's ID that we are updating
|
|
* @param role the role that we are setting on the User
|
|
*/
|
|
export async function updateRole(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: Pick<User, "id">,
|
|
userID: string,
|
|
role: GQLUSER_ROLE
|
|
) {
|
|
if (user.id === userID) {
|
|
throw new Error("cannot update your own user role");
|
|
}
|
|
|
|
return updateUserRole(mongo, tenant.id, userID, role);
|
|
}
|
|
|
|
/**
|
|
* enabledAuthenticationIntegrations returns enabled auth integrations for a tenant
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param target whether to filter by stream or admin enabled. defaults to requiring both.
|
|
*/
|
|
function enabledAuthenticationIntegrations(
|
|
tenant: Tenant,
|
|
target?: "stream" | "admin"
|
|
): string[] {
|
|
return Object.keys(tenant.auth.integrations).filter((key: string) => {
|
|
const { enabled, targetFilter } = tenant.auth.integrations[
|
|
key as keyof GQLAuthIntegrations
|
|
];
|
|
if (target) {
|
|
return enabled && targetFilter[target];
|
|
}
|
|
return enabled && targetFilter.admin && targetFilter.stream;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* canUpdateLocalProfile will determine if a user is permitted to update their email address.
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param user the User that we are updating
|
|
*/
|
|
function canUpdateLocalProfile(tenant: Tenant, user: User): boolean {
|
|
if (!tenant.accountFeatures.changeUsername) {
|
|
return false;
|
|
}
|
|
|
|
if (!hasLocalProfile(user)) {
|
|
return false;
|
|
}
|
|
|
|
const streamAuthTypes = enabledAuthenticationIntegrations(tenant, "stream");
|
|
|
|
// user can update email if local auth is enabled or any integration other than sso is enabled
|
|
return (
|
|
streamAuthTypes.includes("local") ||
|
|
!(streamAuthTypes.length === 1 && streamAuthTypes[0] === "sso")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* updateEmail will update the current User's email address.
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param mailer The mailer queue
|
|
* @param config Convict config
|
|
* @param user the User that we are updating
|
|
* @param email the email address that we are setting on the User
|
|
* @param password the users password for confirmation
|
|
*/
|
|
export async function updateEmail(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
mailer: MailerQueue,
|
|
config: Config,
|
|
signingConfig: JWTSigningConfig,
|
|
user: User,
|
|
emailAddress: string,
|
|
password: string,
|
|
now = new Date()
|
|
) {
|
|
const email = emailAddress.toLowerCase();
|
|
validateEmail(email);
|
|
|
|
const canUpdate = canUpdateLocalProfile(tenant, user);
|
|
if (!canUpdate) {
|
|
throw new Error("Cannot update profile due to tenant settings");
|
|
}
|
|
|
|
const passwordVerified = await verifyUserPassword(user, password);
|
|
if (!passwordVerified) {
|
|
// We throw a PasswordIncorrect error here instead of an
|
|
// InvalidCredentialsError because the current user is already signed in.
|
|
throw new PasswordIncorrect();
|
|
}
|
|
|
|
const updated = await updateUserEmail(mongo, tenant.id, user.id, email);
|
|
|
|
await sendConfirmationEmail(
|
|
mongo,
|
|
mailer,
|
|
tenant,
|
|
config,
|
|
signingConfig,
|
|
updated as Required<User>,
|
|
now
|
|
);
|
|
return updated;
|
|
}
|
|
|
|
/**
|
|
* updateUserEmail will update the given User's email address. This should not
|
|
* trigger and email notifications as it's designed to be used by administrators
|
|
* to update a user's email address.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param userID the User's ID that we are updating
|
|
* @param email the email address that we are setting on the User
|
|
*/
|
|
export async function updateEmailByID(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
userID: string,
|
|
email: string
|
|
) {
|
|
// Validate the email address.
|
|
validateEmail(email);
|
|
|
|
return updateUserEmail(mongo, tenant.id, userID, email, true);
|
|
}
|
|
|
|
/**
|
|
* updateAvatar will update the given User's avatar.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be interacted with
|
|
* @param userID the User's ID that we are updating
|
|
* @param avatar the avatar that we are setting on the User
|
|
*/
|
|
export async function updateAvatar(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
userID: string,
|
|
avatar?: string
|
|
) {
|
|
return updateUserAvatar(mongo, tenant.id, userID, avatar);
|
|
}
|
|
|
|
/**
|
|
* ban will ban a specific user from interacting with Coral.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be banned on
|
|
* @param user the User that is banning the User
|
|
* @param userID the ID of the User being banned
|
|
* @param message message to banned user
|
|
* @param now the current time that the ban took effect
|
|
*/
|
|
export async function ban(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
banner: User,
|
|
userID: string,
|
|
message: string,
|
|
now = new Date()
|
|
) {
|
|
// Get the user being banned to check to see if the user already has an
|
|
// existing ban.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
// Check to see if the User is currently banned.
|
|
const banStatus = consolidateUserBanStatus(targetUser.status.ban);
|
|
if (banStatus.active) {
|
|
throw new UserAlreadyBannedError();
|
|
}
|
|
|
|
// Ban the user.
|
|
const user = await banUser(mongo, tenant.id, userID, banner.id, message, now);
|
|
|
|
// If the user has an email address associated with their account, send them
|
|
// a ban notification email.
|
|
if (user.email) {
|
|
// Send the ban user email.
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: user.email,
|
|
},
|
|
template: {
|
|
name: "ban",
|
|
context: {
|
|
// TODO: (wyattjoh) possibly reevaluate the use of a required username.
|
|
username: user.username!,
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
organizationContactEmail: tenant.organization.contactEmail,
|
|
customMessage: (message || "").replace(/\n/g, "<br />"),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* suspend will suspend a give user from interacting with Coral.
|
|
*
|
|
* @param mongo mongo database to interact with
|
|
* @param tenant Tenant where the User will be suspended on
|
|
* @param user the User that is suspending the User
|
|
* @param userID the ID of the user being suspended
|
|
* @param timeout the duration in seconds that the user will suspended for
|
|
* @param message message to suspended user
|
|
* @param now the current time that the suspension will take effect
|
|
*/
|
|
export async function suspend(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
user: User,
|
|
userID: string,
|
|
timeout: number,
|
|
message: string,
|
|
now = new Date()
|
|
) {
|
|
// Convert the timeout to the until time.
|
|
const finishDateTime = DateTime.fromJSDate(now).plus({ seconds: timeout });
|
|
|
|
// Get the user being suspended to check to see if the user already has an
|
|
// existing suspension.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
// Check to see if the User is currently suspended.
|
|
const suspended = consolidateUserSuspensionStatus(
|
|
targetUser.status.suspension,
|
|
now
|
|
);
|
|
if (suspended.active && suspended.until) {
|
|
throw new UserAlreadySuspendedError(suspended.until);
|
|
}
|
|
|
|
const updatedUser = await suspendUser(
|
|
mongo,
|
|
tenant.id,
|
|
userID,
|
|
user.id,
|
|
finishDateTime.toJSDate(),
|
|
message,
|
|
now
|
|
);
|
|
|
|
// If the user has an email address associated with their account, send them
|
|
// a suspend notification email.
|
|
if (updatedUser.email) {
|
|
// Send the suspend user email.
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: updatedUser.email,
|
|
},
|
|
template: {
|
|
name: "suspend",
|
|
context: {
|
|
// TODO: (wyattjoh) possibly reevaluate the use of a required username.
|
|
username: updatedUser.username!,
|
|
until: finishDateTime.toRFC2822(),
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
organizationContactEmail: tenant.organization.contactEmail,
|
|
customMessage: (message || "").replace(/\n/g, "<br />"),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return updatedUser;
|
|
}
|
|
|
|
export async function removeSuspension(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
userID: string,
|
|
now = new Date()
|
|
) {
|
|
// Get the user being suspended to check to see if the user already has an
|
|
// existing suspension.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
// Check to see if the User is currently suspended.
|
|
const suspended = consolidateUserSuspensionStatus(
|
|
targetUser.status.suspension,
|
|
now
|
|
);
|
|
if (!suspended.active) {
|
|
// The user is not suspended currently, just return the user because we
|
|
// don't have to do anything.
|
|
return targetUser;
|
|
}
|
|
|
|
// For each of the suspensions, remove it.
|
|
return removeActiveUserSuspensions(mongo, tenant.id, userID, user.id, now);
|
|
}
|
|
|
|
export async function removeBan(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
userID: string,
|
|
now = new Date()
|
|
) {
|
|
// Get the user being un-banned to check if they are even banned.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
// Check to see if the User is currently banned.
|
|
const banStatus = consolidateUserBanStatus(targetUser.status.ban);
|
|
if (!banStatus.active) {
|
|
// The user is not ban currently, just return the user because we don't
|
|
// have to do anything.
|
|
return targetUser;
|
|
}
|
|
|
|
return removeUserBan(mongo, tenant.id, userID, user.id, now);
|
|
}
|
|
|
|
export async function ignore(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
userID: string,
|
|
now = new Date()
|
|
) {
|
|
// Get the user being ignored to check if they exist.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
const userToBeIgnoredIsStaff = userIsStaff(targetUser);
|
|
if (userToBeIgnoredIsStaff) {
|
|
throw new UserCannotBeIgnoredError(userID);
|
|
}
|
|
|
|
// TODO: extract function
|
|
if (user.ignoredUsers && user.ignoredUsers.some(u => u.id === userID)) {
|
|
// TODO: improve error
|
|
throw new Error("user already ignored");
|
|
}
|
|
|
|
await ignoreUser(mongo, tenant.id, user.id, userID, now);
|
|
|
|
return targetUser;
|
|
}
|
|
|
|
export async function removeIgnore(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
user: User,
|
|
userID: string
|
|
) {
|
|
// Get the user being un-ignored to check if they exist.
|
|
const targetUser = await retrieveUser(mongo, tenant.id, userID);
|
|
if (!targetUser) {
|
|
throw new UserNotFoundError(userID);
|
|
}
|
|
|
|
if (user.ignoredUsers && user.ignoredUsers.every(u => u.id !== userID)) {
|
|
// TODO: improve error
|
|
throw new Error("user already not ignored");
|
|
}
|
|
|
|
await removeUserIgnore(mongo, tenant.id, user.id, userID);
|
|
|
|
return targetUser;
|
|
}
|
|
|
|
export async function requestCommentsDownload(
|
|
mongo: Db,
|
|
mailer: MailerQueue,
|
|
tenant: Tenant,
|
|
config: Config,
|
|
signingConfig: JWTSigningConfig,
|
|
user: User,
|
|
now: Date
|
|
) {
|
|
if (!tenant.accountFeatures.downloadComments) {
|
|
throw new Error("Downloading comments is not enabled");
|
|
}
|
|
// Check to see if the user is allowed to download this now.
|
|
if (
|
|
user.lastDownloadedAt &&
|
|
DateTime.fromJSDate(user.lastDownloadedAt)
|
|
.plus({ seconds: DOWNLOAD_LIMIT_TIMEFRAME })
|
|
.toSeconds() >= DateTime.fromJSDate(now).toSeconds()
|
|
) {
|
|
throw new Error("requested download too early");
|
|
}
|
|
|
|
const downloadUrl = await generateDownloadLink(
|
|
user.id,
|
|
tenant,
|
|
config,
|
|
signingConfig,
|
|
now
|
|
);
|
|
|
|
await setUserLastDownloadedAt(mongo, tenant.id, user.id, now);
|
|
|
|
if (user.email) {
|
|
await mailer.add({
|
|
tenantID: tenant.id,
|
|
message: {
|
|
to: user.email,
|
|
},
|
|
template: {
|
|
name: "download-comments",
|
|
context: {
|
|
username: user.username!,
|
|
date: Intl.DateTimeFormat(tenant.locale).format(now),
|
|
downloadUrl,
|
|
organizationName: tenant.organization.name,
|
|
organizationURL: tenant.organization.url,
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
logger.error(
|
|
{ userID: user.id },
|
|
"could not send download email because the user does not have an email address"
|
|
);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
export async function requestUserCommentsDownload(
|
|
mongo: Db,
|
|
tenant: Tenant,
|
|
config: Config,
|
|
signingConfig: JWTSigningConfig,
|
|
userID: string,
|
|
now: Date
|
|
) {
|
|
const downloadUrl = await generateAdminDownloadLink(
|
|
userID,
|
|
tenant,
|
|
config,
|
|
signingConfig,
|
|
now
|
|
);
|
|
|
|
return downloadUrl;
|
|
}
|