Files
talk/src/core/server/errors/index.ts
T
Wyatt Johnson 6bd3e093b3 [CORL-157] Ignore Users (#2328)
* feat: added server impl of ignore user

* feat: added development error wrapping

* feat: Add arror to popover

* fix: dependencies

* fix:  server locals watch paths

* feat: AuthorPopover & Member Since

* chore: refactor stream file organization

* feat: ignore user

* fix: file case

* feat: add support for useLocal hook!

* test: test ignored users
2019-05-31 22:41:20 +00:00

607 lines
14 KiB
TypeScript

// tslint:disable:max-classes-per-file
import { FluentBundle } from "fluent/compat";
import uuid from "uuid";
import { VError } from "verror";
import { ERROR_CODES, ERROR_TYPES } from "coral-common/errors";
import { translate } from "coral-server/services/i18n";
import { Writeable } from "coral-common/types";
import { GQLUSER_AUTH_CONDITIONS } from "coral-server/graph/tenant/schema/__generated__/types";
import { ERROR_TRANSLATIONS } from "./translations";
/**
* CoralErrorExtensions is the different extension data that is associated with
* a given error. This data is surfaced in the GraphQL, REST error response as
* well as via logs.
*/
export interface CoralErrorExtensions {
/**
* id identifies this specific error that was thrown, allowing offline tracing
* to occur.
*/
readonly id: string;
/**
* code is the identifier specific to this Error. No other CoralError should
* share the same code.
*/
readonly code: ERROR_CODES;
/**
* type represents the class of errors that this error is associated with.
*/
readonly type: ERROR_TYPES;
/**
* message is the (optionally translated) message that can be shown to users.
*/
readonly message: string;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
param?: string;
}
export interface CoralErrorContext {
/**
* pub stores information that is used by the translation framework
* to provide context to the error being emitted to pass publicly. Sensitive
* information should not be passed via this method.
*/
pub: Record<string, any>;
/**
* pvt stores information that is logged out by the logging
* framework to provide context for bug reporting software in the event that
* the error is unexpected.
*/
pvt: Record<string, any>;
}
/**
* CoralErrorOptions describes the options used to create a CoralError.
*/
export interface CoralErrorOptions {
/**
* code is the identifier specific to this Error. No other CoralError should
* share the same code.
*/
code: ERROR_CODES;
/**
* context stores the public and private details about the error.
*/
context?: Partial<CoralErrorContext>;
/**
* status is the number sent via the REST error responses. GraphQL responses
* do not involve this number.
*/
status?: number;
/**
* type represents the class of errors that this error is associated with.
*/
type?: ERROR_TYPES;
/**
* cause is the error that provides the root cause of the underlying error
* that is thrown.
*/
cause?: Error;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
param?: string;
}
export class CoralError extends VError {
/**
* id identifies this specific error that was thrown, allowing offline tracing
* to occur.
*/
public readonly id: string;
/**
* code is the identifier specific to this Error. No other CoralError should
* share the same code.
*/
public readonly code: ERROR_CODES;
/**
* status is the number sent via the REST error responses. GraphQL responses
* do not involve this number.
*/
public readonly status: number;
/**
* type represents the class of errors that this error is associated with.
*/
public readonly type: ERROR_TYPES;
/**
* param, if set, references the fieldSpec to which the error is related to.
* If for example an error occurred during email processing, this field could
* be `input.email` to denote the specific input field that caused the error.
*/
public param?: string;
/**
* context stores the public and private details about the error.
*/
public readonly context: Readonly<CoralErrorContext>;
constructor({
code,
context = {},
status = 500,
type = ERROR_TYPES.INVALID_REQUEST_ERROR,
cause,
param,
}: CoralErrorOptions) {
// Call the super method with the right arguments depending on if we're
// supposed to be handling a causal error or not.
if (cause) {
super(cause, code);
} else {
super(code);
}
// Rename the error to have the name of the error that this extends.
this.name = new.target.name;
// Assign a unique ID to this error.
const id = uuid.v1();
this.status = status;
// Capture the context for the error.
const { pub = {}, pvt = {} } = context;
this.context = { pub, pvt };
// Capture the extension parameters.
this.id = id;
this.code = code;
this.type = type;
this.param = param;
}
public serializeExtensions(
bundle: FluentBundle | null
): CoralErrorExtensions {
let message: string;
if (bundle) {
message = translate(
bundle,
this.code,
ERROR_TRANSLATIONS[this.code],
this.context.pub
);
} else {
message = this.code;
}
return {
id: this.id,
code: this.code,
type: this.type,
message,
param: this.param,
};
}
}
export class CommentingDisabledError extends CoralError {
constructor() {
super({
code: ERROR_CODES.COMMENTING_DISABLED,
});
}
}
export class StoryClosedError extends CoralError {
constructor() {
super({
code: ERROR_CODES.STORY_CLOSED,
});
}
}
export class CommentBodyTooShortError extends CoralError {
constructor(min: number) {
super({
code: ERROR_CODES.COMMENT_BODY_TOO_SHORT,
context: { pub: { min } },
});
}
}
export class CommentBodyExceedsMaxLengthError extends CoralError {
constructor(max: number) {
super({
code: ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
context: { pub: { max } },
});
}
}
export class URLInvalidError extends CoralError {
constructor({
url,
...properties
}: {
url: string;
allowedDomains: string[];
tenantDomain?: string;
}) {
super({
code: ERROR_CODES.URL_NOT_PERMITTED,
context: { pvt: properties, pub: { url } },
});
}
}
export class StoryURLInvalidError extends CoralError {
constructor(properties: {
storyURL: string;
allowedDomains: string[];
tenantDomain?: string;
}) {
super({
code: ERROR_CODES.STORY_URL_NOT_PERMITTED,
context: { pvt: properties },
});
}
}
export class DuplicateUserError extends CoralError {
constructor() {
super({ code: ERROR_CODES.DUPLICATE_USER });
}
}
export class EmailNotSetError extends CoralError {
constructor() {
super({ code: ERROR_CODES.EMAIL_NOT_SET });
}
}
export class DuplicateStoryURLError extends CoralError {
constructor(url: string) {
super({ code: ERROR_CODES.DUPLICATE_STORY_URL, context: { pvt: { url } } });
}
}
export class DuplicateEmailError extends CoralError {
constructor(email: string) {
super({ code: ERROR_CODES.DUPLICATE_EMAIL, context: { pvt: { email } } });
}
}
export class UsernameAlreadySetError extends CoralError {
constructor() {
super({ code: ERROR_CODES.USERNAME_ALREADY_SET });
}
}
export class EmailAlreadySetError extends CoralError {
constructor() {
super({ code: ERROR_CODES.EMAIL_ALREADY_SET });
}
}
export class LocalProfileNotSetError extends CoralError {
constructor() {
super({ code: ERROR_CODES.LOCAL_PROFILE_NOT_SET });
}
}
export class LocalProfileAlreadySetError extends CoralError {
constructor() {
super({ code: ERROR_CODES.LOCAL_PROFILE_ALREADY_SET });
}
}
export class UsernameContainsInvalidCharactersError extends CoralError {
constructor() {
super({ code: ERROR_CODES.USERNAME_CONTAINS_INVALID_CHARACTERS });
}
}
export class UsernameExceedsMaxLengthError extends CoralError {
constructor(length: number, max: number) {
super({
code: ERROR_CODES.USERNAME_EXCEEDS_MAX_LENGTH,
context: { pub: { length, max } },
});
}
}
export class UsernameTooShortError extends CoralError {
constructor(length: number, min: number) {
super({
code: ERROR_CODES.USERNAME_TOO_SHORT,
context: { pub: { length, min } },
});
}
}
export class PasswordTooShortError extends CoralError {
constructor(length: number, min: number) {
super({
code: ERROR_CODES.PASSWORD_TOO_SHORT,
context: { pub: { length, min } },
});
}
}
export class EmailInvalidFormatError extends CoralError {
constructor() {
super({ code: ERROR_CODES.EMAIL_INVALID_FORMAT });
}
}
export class EmailExceedsMaxLengthError extends CoralError {
constructor(length: number, max: number) {
super({
code: ERROR_CODES.EMAIL_EXCEEDS_MAX_LENGTH,
context: { pub: { length, max } },
});
}
}
export class TokenNotFoundError extends CoralError {
constructor() {
super({ code: ERROR_CODES.TOKEN_NOT_FOUND });
}
}
export class TokenInvalidError extends CoralError {
constructor(token: string, reason: string, cause?: Error) {
super({
code: ERROR_CODES.TOKEN_INVALID,
cause,
context: { pvt: { token, reason } },
status: 401,
});
}
}
export class UserForbiddenError extends CoralError {
constructor(
reason: string,
resource: string,
operation: string,
userID?: string,
permit?: GQLUSER_AUTH_CONDITIONS[],
conditions?: GQLUSER_AUTH_CONDITIONS[]
) {
super({
code: ERROR_CODES.USER_NOT_ENTITLED,
context: {
pvt: { reason, userID, resource, operation, conditions, permit },
},
status: 403,
});
}
}
export class UserNotFoundError extends CoralError {
constructor(userID: string) {
super({ code: ERROR_CODES.USER_NOT_FOUND, context: { pub: { userID } } });
}
}
export class StoryNotFoundError extends CoralError {
constructor(storyID: string) {
super({ code: ERROR_CODES.STORY_NOT_FOUND, context: { pvt: { storyID } } });
}
}
export class CommentNotFoundError extends CoralError {
constructor(commentID: string) {
super({
code: ERROR_CODES.COMMENT_NOT_FOUND,
context: { pvt: { commentID } },
});
}
}
export class TenantNotFoundError extends CoralError {
constructor(hostname: string) {
super({
code: ERROR_CODES.TENANT_NOT_FOUND,
context: { pub: { hostname } },
});
}
}
export class IntegrationDisabled extends CoralError {
constructor(integrationName: string) {
super({
code: ERROR_CODES.INTEGRATION_DISABLED,
context: { pvt: { integrationName } },
});
}
}
export class InternalError extends CoralError {
constructor(cause: Error, reason: string) {
super({
code: ERROR_CODES.INTERNAL_ERROR,
cause,
context: { pvt: { reason } },
status: 500,
});
}
}
export class InternalDevelopmentError extends CoralError {
constructor(cause: Error, reason: string) {
super({
code: ERROR_CODES.INTERNAL_ERROR,
cause,
context: { pvt: { reason } },
status: 500,
});
}
public serializeExtensions(
bundle: FluentBundle | null
): CoralErrorExtensions {
// Serialize the extensions from the public source.
const extensions = super.serializeExtensions(bundle) as Writeable<
CoralErrorExtensions
>;
// Push in the internal message for this override.
extensions.message = this.cause()!.message;
return extensions;
}
// public get message() {
// return this.cause()!.message;
// }
}
export class NotFoundError extends CoralError {
constructor(method: string, path: string) {
super({
code: ERROR_CODES.NOT_FOUND,
status: 404,
context: { pub: { method, path } },
});
}
}
export class TenantInstalledAlreadyError extends CoralError {
constructor() {
super({ code: ERROR_CODES.TENANT_INSTALLED_ALREADY, status: 400 });
}
}
export class InvalidCredentialsError extends CoralError {
constructor(reason: string) {
super({
code: ERROR_CODES.INVALID_CREDENTIALS,
status: 401,
context: { pvt: { reason } },
});
}
}
export class AuthenticationError extends CoralError {
constructor(reason: string) {
super({
code: ERROR_CODES.AUTHENTICATION_ERROR,
status: 401,
context: { pvt: { reason } },
});
}
}
export class ToxicCommentError extends CoralError {
constructor(model: string, score: number, threshold: number) {
super({
code: ERROR_CODES.TOXIC_COMMENT,
type: ERROR_TYPES.MODERATION_NUDGE_ERROR,
status: 400,
context: { pvt: { model, score, threshold } },
});
}
}
export class SpamCommentError extends CoralError {
constructor() {
super({
code: ERROR_CODES.SPAM_COMMENT,
type: ERROR_TYPES.MODERATION_NUDGE_ERROR,
status: 400,
});
}
}
export class UserAlreadySuspendedError extends CoralError {
constructor(until: Date) {
super({
code: ERROR_CODES.USER_ALREADY_SUSPENDED,
context: {
pub: {
until: until.toISOString(),
},
},
});
}
}
export class UserAlreadyBannedError extends CoralError {
constructor() {
super({
code: ERROR_CODES.USER_ALREADY_BANNED,
});
}
}
export class UserBanned extends CoralError {
constructor(userID: string, resource?: string, operation?: string) {
super({
code: ERROR_CODES.USER_BANNED,
context: { pvt: { resource, operation, userID } },
});
}
}
export class UserSuspended extends CoralError {
constructor(
userID: string,
until: Date,
resource?: string,
operation?: string
) {
super({
code: ERROR_CODES.USER_SUSPENDED,
context: { pvt: { resource, operation, userID }, pub: { until } },
});
}
}
export class PasswordResetTokenExpired extends CoralError {
constructor(reason: string, cause?: Error) {
super({
code: ERROR_CODES.PASSWORD_RESET_TOKEN_EXPIRED,
cause,
status: 400,
context: { pvt: { reason } },
});
}
}
export class ConfirmEmailTokenExpired extends CoralError {
constructor(reason: string, cause?: Error) {
super({
code: ERROR_CODES.EMAIL_CONFIRM_TOKEN_EXPIRED,
cause,
status: 400,
context: { pvt: { reason } },
});
}
}
export class RateLimitExceeded extends CoralError {
constructor(resource: string, max: number, tries: number) {
super({
code: ERROR_CODES.RATE_LIMIT_EXCEEDED,
status: 429,
context: { pvt: { resource, max, tries } },
});
}
}