base
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
// plane imports
|
||||
import { SUPPORT_EMAIL } from "@plane/constants";
|
||||
|
||||
export enum EPageTypes {
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
SET_PASSWORD = "SET_PASSWORD",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
|
||||
export enum EAuthModes {
|
||||
SIGN_IN = "SIGN_IN",
|
||||
SIGN_UP = "SIGN_UP",
|
||||
}
|
||||
|
||||
export enum EAuthSteps {
|
||||
EMAIL = "EMAIL",
|
||||
PASSWORD = "PASSWORD",
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
}
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
// Global
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
INVALID_EMAIL = "5005",
|
||||
EMAIL_REQUIRED = "5010",
|
||||
SIGNUP_DISABLED = "5015",
|
||||
MAGIC_LINK_LOGIN_DISABLED = "5016",
|
||||
PASSWORD_LOGIN_DISABLED = "5018",
|
||||
USER_ACCOUNT_DEACTIVATED = "5019",
|
||||
// Password strength
|
||||
INVALID_PASSWORD = "5020",
|
||||
PASSWORD_TOO_WEAK = "5021",
|
||||
SMTP_NOT_CONFIGURED = "5025",
|
||||
// Sign Up
|
||||
USER_ALREADY_EXIST = "5030",
|
||||
AUTHENTICATION_FAILED_SIGN_UP = "5035",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040",
|
||||
INVALID_EMAIL_SIGN_UP = "5045",
|
||||
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
|
||||
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
|
||||
// Sign In
|
||||
USER_DOES_NOT_EXIST = "5060",
|
||||
AUTHENTICATION_FAILED_SIGN_IN = "5065",
|
||||
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
|
||||
INVALID_EMAIL_SIGN_IN = "5075",
|
||||
INVALID_EMAIL_MAGIC_SIGN_IN = "5080",
|
||||
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085",
|
||||
// Both Sign in and Sign up for magic
|
||||
INVALID_MAGIC_CODE_SIGN_IN = "5090",
|
||||
INVALID_MAGIC_CODE_SIGN_UP = "5092",
|
||||
EXPIRED_MAGIC_CODE_SIGN_IN = "5095",
|
||||
EXPIRED_MAGIC_CODE_SIGN_UP = "5097",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN = "5100",
|
||||
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP = "5102",
|
||||
// Oauth
|
||||
OAUTH_NOT_CONFIGURED = "5104",
|
||||
GOOGLE_NOT_CONFIGURED = "5105",
|
||||
GITHUB_NOT_CONFIGURED = "5110",
|
||||
GITLAB_NOT_CONFIGURED = "5111",
|
||||
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
|
||||
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
|
||||
GITLAB_OAUTH_PROVIDER_ERROR = "5121",
|
||||
// Reset Password
|
||||
INVALID_PASSWORD_TOKEN = "5125",
|
||||
EXPIRED_PASSWORD_TOKEN = "5130",
|
||||
// Change password
|
||||
INCORRECT_OLD_PASSWORD = "5135",
|
||||
MISSING_PASSWORD = "5138",
|
||||
INVALID_NEW_PASSWORD = "5140",
|
||||
// set password
|
||||
PASSWORD_ALREADY_SET = "5145",
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5150",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
|
||||
INVALID_ADMIN_EMAIL = "5160",
|
||||
INVALID_ADMIN_PASSWORD = "5165",
|
||||
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
|
||||
ADMIN_AUTHENTICATION_FAILED = "5175",
|
||||
ADMIN_USER_ALREADY_EXIST = "5180",
|
||||
ADMIN_USER_DOES_NOT_EXIST = "5185",
|
||||
ADMIN_USER_DEACTIVATED = "5190",
|
||||
// Rate limit
|
||||
RATE_LIMIT_EXCEEDED = "5900",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthenticationErrorCodes;
|
||||
title: string;
|
||||
message: ReactNode;
|
||||
};
|
||||
|
||||
// TODO: move all error messages to translation files
|
||||
const errorCodeMessages: {
|
||||
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string) => ReactNode };
|
||||
} = {
|
||||
// global
|
||||
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: `Instance not configured`,
|
||||
message: () => `Instance not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
|
||||
title: `Email required`,
|
||||
message: () => `Email required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.SIGNUP_DISABLED]: {
|
||||
title: `Sign up disabled`,
|
||||
message: () => `Sign up disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED]: {
|
||||
title: `Magic link login disabled`,
|
||||
message: () => `Magic link login disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED]: {
|
||||
title: `Password login disabled`,
|
||||
message: () => `Password login disabled. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
|
||||
title: `User account deactivated`,
|
||||
message: () => `User account deactivated. Please contact ${SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD]: {
|
||||
title: `Invalid password`,
|
||||
message: () => `Invalid password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.PASSWORD_TOO_WEAK]: {
|
||||
title: `Password too weak`,
|
||||
message: () => `Password must include upper-case, lower-case, number, special character, and must not be predictable.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED]: {
|
||||
title: `SMTP not configured`,
|
||||
message: () => `SMTP not configured. Please contact your administrator.`,
|
||||
},
|
||||
|
||||
// sign up
|
||||
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
|
||||
title: `User already exists`,
|
||||
message: (email = undefined) => (
|
||||
<div>
|
||||
Your account is already registered.
|
||||
<Link
|
||||
className="font-medium underline underline-offset-4 transition-all hover:font-bold"
|
||||
href={`/sign-in${email ? `?email=${encodeURIComponent(email)}` : ``}`}
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: {
|
||||
title: `Email and code required`,
|
||||
message: () => `Email and code required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
|
||||
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
|
||||
title: `User does not exist`,
|
||||
message: (email = undefined) => (
|
||||
<div>
|
||||
No account found.
|
||||
<Link
|
||||
className="font-medium underline underline-offset-4 transition-all hover:font-bold"
|
||||
href={`/${email ? `?email=${encodeURIComponent(email)}` : ``}`}
|
||||
>
|
||||
Create one
|
||||
</Link>
|
||||
to get started.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: {
|
||||
title: `Email and code required`,
|
||||
message: () => `Email and code required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: {
|
||||
title: `Invalid email`,
|
||||
message: () => `Invalid email. Please try again.`,
|
||||
},
|
||||
|
||||
// Both Sign in and Sign up
|
||||
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Invalid magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Invalid magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP]: {
|
||||
title: `Expired magic code`,
|
||||
message: () => `Expired magic code. Please try again.`,
|
||||
},
|
||||
|
||||
// Oauth
|
||||
[EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED]: {
|
||||
title: `OAuth not configured`,
|
||||
message: () => `OAuth not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
|
||||
title: `Google not configured`,
|
||||
message: () => `Google not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: {
|
||||
title: `GitHub not configured`,
|
||||
message: () => `GitHub not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED]: {
|
||||
title: `GitLab not configured`,
|
||||
message: () => `GitLab not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `Google OAuth provider error`,
|
||||
message: () => `Google OAuth provider error. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `GitHub OAuth provider error`,
|
||||
message: () => `GitHub OAuth provider error. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR]: {
|
||||
title: `GitLab OAuth provider error`,
|
||||
message: () => `GitLab OAuth provider error. Please try again.`,
|
||||
},
|
||||
|
||||
// Reset Password
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
|
||||
title: `Invalid password token`,
|
||||
message: () => `Invalid password token.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: {
|
||||
title: `Expired password token`,
|
||||
message: () => `Expired password token. Please try again.`,
|
||||
},
|
||||
|
||||
// Change password
|
||||
[EAuthenticationErrorCodes.MISSING_PASSWORD]: {
|
||||
title: `Password required`,
|
||||
message: () => `Password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: {
|
||||
title: `Incorrect old password`,
|
||||
message: () => `Incorrect old password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: {
|
||||
title: `Invalid new password`,
|
||||
message: () => `Invalid new password. Please try again.`,
|
||||
},
|
||||
|
||||
// set password
|
||||
[EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: {
|
||||
title: `Password already set`,
|
||||
message: () => `Password already set. Please try again.`,
|
||||
},
|
||||
|
||||
// admin
|
||||
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: `Admin already exists`,
|
||||
message: () => `Admin already exists. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: `Email, password and first name required`,
|
||||
message: () => `Email, password and first name required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||
title: `Invalid admin email`,
|
||||
message: () => `Invalid admin email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||
title: `Invalid admin password`,
|
||||
message: () => `Invalid admin password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
||||
title: `Admin user already exists`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user already exists.
|
||||
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
||||
title: `Admin user does not exist`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user does not exist.
|
||||
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
|
||||
title: `Admin user deactivated`,
|
||||
message: () => <div>Your account is deactivated</div>,
|
||||
},
|
||||
[EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED]: {
|
||||
title: "",
|
||||
message: () => `Rate limit exceeded. Please try again later.`,
|
||||
},
|
||||
};
|
||||
|
||||
export const authErrorHandler = (errorCode: EAuthenticationErrorCodes, email?: string): TAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL,
|
||||
EAuthenticationErrorCodes.EMAIL_REQUIRED,
|
||||
EAuthenticationErrorCodes.SIGNUP_DISABLED,
|
||||
EAuthenticationErrorCodes.MAGIC_LINK_LOGIN_DISABLED,
|
||||
EAuthenticationErrorCodes.PASSWORD_LOGIN_DISABLED,
|
||||
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD,
|
||||
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
|
||||
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
|
||||
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
||||
EAuthenticationErrorCodes.OAUTH_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GITLAB_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.GITLAB_OAUTH_PROVIDER_ERROR,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
|
||||
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
|
||||
EAuthenticationErrorCodes.MISSING_PASSWORD,
|
||||
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
|
||||
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
|
||||
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
|
||||
EAuthenticationErrorCodes.RATE_LIMIT_EXCEEDED,
|
||||
EAuthenticationErrorCodes.PASSWORD_TOO_WEAK,
|
||||
];
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
code: errorCode,
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const passwordErrors = [
|
||||
EAuthenticationErrorCodes.PASSWORD_TOO_WEAK,
|
||||
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
|
||||
];
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { EFileAssetType } from "@plane/types";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
|
||||
import CoverImage1 from "@/app/assets/cover-images/image_1.jpg?url";
|
||||
import CoverImage10 from "@/app/assets/cover-images/image_10.jpg?url";
|
||||
import CoverImage11 from "@/app/assets/cover-images/image_11.jpg?url";
|
||||
import CoverImage12 from "@/app/assets/cover-images/image_12.jpg?url";
|
||||
import CoverImage13 from "@/app/assets/cover-images/image_13.jpg?url";
|
||||
import CoverImage14 from "@/app/assets/cover-images/image_14.jpg?url";
|
||||
import CoverImage15 from "@/app/assets/cover-images/image_15.jpg?url";
|
||||
import CoverImage16 from "@/app/assets/cover-images/image_16.jpg?url";
|
||||
import CoverImage17 from "@/app/assets/cover-images/image_17.jpg?url";
|
||||
import CoverImage18 from "@/app/assets/cover-images/image_18.jpg?url";
|
||||
import CoverImage19 from "@/app/assets/cover-images/image_19.jpg?url";
|
||||
import CoverImage2 from "@/app/assets/cover-images/image_2.jpg?url";
|
||||
import CoverImage20 from "@/app/assets/cover-images/image_20.jpg?url";
|
||||
import CoverImage21 from "@/app/assets/cover-images/image_21.jpg?url";
|
||||
import CoverImage22 from "@/app/assets/cover-images/image_22.jpg?url";
|
||||
import CoverImage23 from "@/app/assets/cover-images/image_23.jpg?url";
|
||||
import CoverImage24 from "@/app/assets/cover-images/image_24.jpg?url";
|
||||
import CoverImage25 from "@/app/assets/cover-images/image_25.jpg?url";
|
||||
import CoverImage26 from "@/app/assets/cover-images/image_26.jpg?url";
|
||||
import CoverImage27 from "@/app/assets/cover-images/image_27.jpg?url";
|
||||
import CoverImage28 from "@/app/assets/cover-images/image_28.jpg?url";
|
||||
import CoverImage29 from "@/app/assets/cover-images/image_29.jpg?url";
|
||||
import CoverImage3 from "@/app/assets/cover-images/image_3.jpg?url";
|
||||
import CoverImage4 from "@/app/assets/cover-images/image_4.jpg?url";
|
||||
import CoverImage5 from "@/app/assets/cover-images/image_5.jpg?url";
|
||||
import CoverImage6 from "@/app/assets/cover-images/image_6.jpg?url";
|
||||
import CoverImage7 from "@/app/assets/cover-images/image_7.jpg?url";
|
||||
import CoverImage8 from "@/app/assets/cover-images/image_8.jpg?url";
|
||||
import CoverImage9 from "@/app/assets/cover-images/image_9.jpg?url";
|
||||
|
||||
import { FileService } from "@/services/file.service";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
/**
|
||||
* Map of all available static cover images
|
||||
* These are pre-loaded images available in the assets/cover-images folder
|
||||
*/
|
||||
export const STATIC_COVER_IMAGES = {
|
||||
IMAGE_1: CoverImage1,
|
||||
IMAGE_2: CoverImage2,
|
||||
IMAGE_3: CoverImage3,
|
||||
IMAGE_4: CoverImage4,
|
||||
IMAGE_5: CoverImage5,
|
||||
IMAGE_6: CoverImage6,
|
||||
IMAGE_7: CoverImage7,
|
||||
IMAGE_8: CoverImage8,
|
||||
IMAGE_9: CoverImage9,
|
||||
IMAGE_10: CoverImage10,
|
||||
IMAGE_11: CoverImage11,
|
||||
IMAGE_12: CoverImage12,
|
||||
IMAGE_13: CoverImage13,
|
||||
IMAGE_14: CoverImage14,
|
||||
IMAGE_15: CoverImage15,
|
||||
IMAGE_16: CoverImage16,
|
||||
IMAGE_17: CoverImage17,
|
||||
IMAGE_18: CoverImage18,
|
||||
IMAGE_19: CoverImage19,
|
||||
IMAGE_20: CoverImage20,
|
||||
IMAGE_21: CoverImage21,
|
||||
IMAGE_22: CoverImage22,
|
||||
IMAGE_23: CoverImage23,
|
||||
IMAGE_24: CoverImage24,
|
||||
IMAGE_25: CoverImage25,
|
||||
IMAGE_26: CoverImage26,
|
||||
IMAGE_27: CoverImage27,
|
||||
IMAGE_28: CoverImage28,
|
||||
IMAGE_29: CoverImage29,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_COVER_IMAGE_URL = STATIC_COVER_IMAGES.IMAGE_1;
|
||||
|
||||
/**
|
||||
* Set of static image URLs for fast O(1) lookup
|
||||
*/
|
||||
const STATIC_COVER_IMAGES_SET = new Set<string>(Object.values(STATIC_COVER_IMAGES));
|
||||
|
||||
export type TCoverImageType = "local_static" | "uploaded_asset" | "unsplash";
|
||||
|
||||
export type TCoverImageResult = {
|
||||
needsUpload: boolean;
|
||||
imageType: TCoverImageType;
|
||||
shouldUpdate: boolean;
|
||||
};
|
||||
|
||||
export type TCoverImagePayload = {
|
||||
cover_image?: string | null;
|
||||
cover_image_url?: string | null;
|
||||
cover_image_asset?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a given URL is a valid static cover image
|
||||
*/
|
||||
export const isStaticCoverImage = (imageUrl: string | null | undefined): boolean => {
|
||||
if (!imageUrl) return false;
|
||||
return STATIC_COVER_IMAGES_SET.has(imageUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the type of cover image URL
|
||||
* Uses explicit validation against known static images for better accuracy
|
||||
*/
|
||||
export const getCoverImageType = (imageUrl: string): TCoverImageType => {
|
||||
// Check against the explicit set of static images
|
||||
if (isStaticCoverImage(imageUrl)) return "local_static";
|
||||
|
||||
// Check if it's an Unsplash image by validating the hostname
|
||||
try {
|
||||
const url = new URL(imageUrl);
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
if (hostname === "unsplash.com" || hostname.endsWith(".unsplash.com")) {
|
||||
return "unsplash";
|
||||
}
|
||||
} catch {
|
||||
// If URL parsing fails (e.g., relative path), fall through to other checks
|
||||
}
|
||||
|
||||
if (imageUrl.startsWith("http")) return "uploaded_asset";
|
||||
|
||||
return "uploaded_asset";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the correct display URL for a cover image
|
||||
* - Local static images: returned as-is (served from assets folder)
|
||||
* - Uploaded assets: processed through getFileURL (adds backend URL)
|
||||
*/
|
||||
export function getCoverImageDisplayURL(imageUrl: string | null | undefined, fallbackUrl: string): string;
|
||||
export function getCoverImageDisplayURL(imageUrl: string | null | undefined, fallbackUrl: null): string | null;
|
||||
export function getCoverImageDisplayURL(
|
||||
imageUrl: string | null | undefined,
|
||||
fallbackUrl: string | null
|
||||
): string | null {
|
||||
if (!imageUrl) {
|
||||
return fallbackUrl;
|
||||
}
|
||||
|
||||
const imageType = getCoverImageType(imageUrl);
|
||||
|
||||
if (imageType === "local_static" || imageType === "unsplash") {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
if (imageType === "uploaded_asset") {
|
||||
return getFileURL(imageUrl) || imageUrl;
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes cover image change and determines what action to take
|
||||
* Merged with isUnsplashImage logic - now detects unsplash images as a separate type
|
||||
*/
|
||||
export const analyzeCoverImageChange = (
|
||||
currentImage: string | null | undefined,
|
||||
newImage: string | null | undefined
|
||||
): TCoverImageResult => {
|
||||
const hasChanged = currentImage !== newImage;
|
||||
|
||||
if (!hasChanged) {
|
||||
return {
|
||||
needsUpload: false,
|
||||
imageType: "uploaded_asset",
|
||||
shouldUpdate: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!newImage) {
|
||||
return {
|
||||
needsUpload: false,
|
||||
imageType: "uploaded_asset",
|
||||
shouldUpdate: true,
|
||||
};
|
||||
}
|
||||
|
||||
const imageType = getCoverImageType(newImage);
|
||||
|
||||
return {
|
||||
needsUpload: imageType === "local_static" || imageType === "unsplash",
|
||||
imageType,
|
||||
shouldUpdate: hasChanged,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a local static image to S3
|
||||
*/
|
||||
export const uploadCoverImage = async (
|
||||
imageUrl: string,
|
||||
uploadConfig: {
|
||||
workspaceSlug?: string;
|
||||
entityIdentifier: string;
|
||||
entityType: EFileAssetType;
|
||||
isUserAsset?: boolean;
|
||||
}
|
||||
): Promise<string> => {
|
||||
const { workspaceSlug, entityIdentifier, entityType, isUserAsset = false } = uploadConfig;
|
||||
|
||||
// Fetch the local image
|
||||
const response = await fetch(imageUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
// Validate it's actually an image
|
||||
if (!blob.type.startsWith("image/")) {
|
||||
throw new Error("Invalid file type. Please select an image.");
|
||||
}
|
||||
|
||||
const fileName = imageUrl.split("/").pop()?.split("?")[0] || "image.jpg";
|
||||
const file = new File([blob], fileName, { type: blob.type });
|
||||
|
||||
// Upload based on context
|
||||
if (isUserAsset) {
|
||||
const uploadResult = await fileService.uploadUserAsset(
|
||||
{
|
||||
entity_identifier: entityIdentifier,
|
||||
entity_type: entityType,
|
||||
},
|
||||
file
|
||||
);
|
||||
return uploadResult.asset_url;
|
||||
} else {
|
||||
if (!workspaceSlug) {
|
||||
throw new Error("Workspace slug is required for workspace asset upload");
|
||||
}
|
||||
|
||||
const uploadResult = await fileService.uploadWorkspaceAsset(
|
||||
workspaceSlug,
|
||||
{
|
||||
entity_identifier: entityIdentifier,
|
||||
entity_type: entityType,
|
||||
},
|
||||
file
|
||||
);
|
||||
return uploadResult.asset_url;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main utility to handle cover image changes with upload
|
||||
*/
|
||||
export const handleCoverImageChange = async (
|
||||
currentImage: string | null | undefined,
|
||||
newImage: string | null | undefined,
|
||||
uploadConfig: {
|
||||
workspaceSlug?: string;
|
||||
entityIdentifier: string;
|
||||
entityType: EFileAssetType;
|
||||
isUserAsset?: boolean;
|
||||
}
|
||||
): Promise<TCoverImagePayload | undefined> => {
|
||||
const analysis = analyzeCoverImageChange(currentImage, newImage);
|
||||
if (!analysis.shouldUpdate) return;
|
||||
|
||||
if (!newImage) {
|
||||
return { cover_image: null, cover_image_url: null, cover_image_asset: null };
|
||||
}
|
||||
|
||||
if (analysis.needsUpload) {
|
||||
await uploadCoverImage(newImage, uploadConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
return { cover_image: newImage };
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random cover image from the STATIC_COVER_IMAGES object
|
||||
* @returns {string} A random cover image URL
|
||||
*/
|
||||
export const getRandomCoverImage = (): string =>
|
||||
Object.values(STATIC_COVER_IMAGES)[Math.floor(Math.random() * Object.keys(STATIC_COVER_IMAGES).length)];
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from "date-fns";
|
||||
// helpers
|
||||
// types
|
||||
import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants";
|
||||
import type { TIssuesListTypes } from "@plane/types";
|
||||
// constants
|
||||
import { renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||
|
||||
// -------------------- DEPRECATED --------------------
|
||||
|
||||
/**
|
||||
* @description returns date range based on the duration filter
|
||||
* @param duration
|
||||
*/
|
||||
export const getCustomDates = (duration: EDurationFilters, customDates: string[]): string => {
|
||||
const today = new Date();
|
||||
let firstDay, lastDay;
|
||||
|
||||
switch (duration) {
|
||||
case EDurationFilters.NONE:
|
||||
return "";
|
||||
case EDurationFilters.TODAY:
|
||||
firstDay = renderFormattedPayloadDate(today);
|
||||
lastDay = renderFormattedPayloadDate(today);
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_WEEK:
|
||||
firstDay = renderFormattedPayloadDate(startOfWeek(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfWeek(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_MONTH:
|
||||
firstDay = renderFormattedPayloadDate(startOfMonth(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfMonth(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.THIS_YEAR:
|
||||
firstDay = renderFormattedPayloadDate(startOfYear(today));
|
||||
lastDay = renderFormattedPayloadDate(endOfYear(today));
|
||||
return `${firstDay};after,${lastDay};before`;
|
||||
case EDurationFilters.CUSTOM:
|
||||
return customDates.join(",");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns redirection filters for the issues list
|
||||
* @param type
|
||||
*/
|
||||
export const getRedirectionFilters = (type: TIssuesListTypes): string => {
|
||||
const today = renderFormattedPayloadDate(new Date());
|
||||
|
||||
const filterParams =
|
||||
type === "pending"
|
||||
? "?state_group=backlog,unstarted,started"
|
||||
: type === "upcoming"
|
||||
? `?target_date=${today};after`
|
||||
: type === "overdue"
|
||||
? `?target_date=${today};before`
|
||||
: "?state_group=completed";
|
||||
|
||||
return filterParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the tab key based on the duration filter
|
||||
* @param duration
|
||||
* @param tab
|
||||
*/
|
||||
export const getTabKey = (duration: EDurationFilters, tab: TIssuesListTypes | undefined): TIssuesListTypes => {
|
||||
if (!tab) return "completed";
|
||||
|
||||
if (tab === "completed") return tab;
|
||||
|
||||
if (duration === "none") return "pending";
|
||||
else {
|
||||
if (["upcoming", "overdue"].includes(tab)) return tab;
|
||||
else return "upcoming";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the label for the duration filter dropdown
|
||||
* @param duration
|
||||
* @param customDates
|
||||
*/
|
||||
export const getDurationFilterDropdownLabel = (duration: EDurationFilters, customDates: string[]): string => {
|
||||
if (duration !== "custom") return DURATION_FILTER_OPTIONS.find((option) => option.key === duration)?.label ?? "";
|
||||
else {
|
||||
const afterDate = customDates.find((date) => date.includes("after"))?.split(";")[0];
|
||||
const beforeDate = customDates.find((date) => date.includes("before"))?.split(";")[0];
|
||||
|
||||
if (afterDate && beforeDate) return `${renderFormattedDate(afterDate)} - ${renderFormattedDate(beforeDate)}`;
|
||||
else if (afterDate) return `After ${renderFormattedDate(afterDate)}`;
|
||||
else if (beforeDate) return `Before ${renderFormattedDate(beforeDate)}`;
|
||||
else return "";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders an emoji or icon
|
||||
* @param {string | { name: string; color: string }} emoji - The emoji or icon to render
|
||||
* @returns {React.ReactNode} The rendered emoji or icon
|
||||
*/
|
||||
export const renderEmoji = (
|
||||
emoji:
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
): React.ReactNode => {
|
||||
if (!emoji) return;
|
||||
|
||||
if (typeof emoji === "object")
|
||||
return (
|
||||
<span style={{ fontSize: "16px", color: emoji.color }} className="material-symbols-rounded">
|
||||
{emoji.name}
|
||||
</span>
|
||||
);
|
||||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// ------------ DEPRECATED (Use re-charts and its helpers instead) ------------
|
||||
|
||||
export const generateYAxisTickValues = (data: number[]) => {
|
||||
if (!data || !Array.isArray(data) || data.length === 0) return [];
|
||||
|
||||
const minValue = 0;
|
||||
const maxValue = Math.max(...data);
|
||||
|
||||
const valueRange = maxValue - minValue;
|
||||
|
||||
let tickInterval = 1;
|
||||
|
||||
if (valueRange < 10) tickInterval = 1;
|
||||
else if (valueRange < 20) tickInterval = 2;
|
||||
else if (valueRange < 50) tickInterval = 5;
|
||||
else tickInterval = (Math.ceil(valueRange / 100) * 100) / 10;
|
||||
|
||||
const tickValues: number[] = [];
|
||||
let tickValue = minValue;
|
||||
while (tickValue <= maxValue) {
|
||||
tickValues.push(tickValue);
|
||||
tickValue += tickInterval;
|
||||
}
|
||||
|
||||
return tickValues;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// types
|
||||
import type { IIssueDisplayProperties } from "@plane/types";
|
||||
// lib
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const shouldRenderColumn = (key: keyof IIssueDisplayProperties): boolean => {
|
||||
const isEstimateEnabled: boolean = store.projectRoot.project.currentProjectDetails?.estimate !== null;
|
||||
switch (key) {
|
||||
case "estimate":
|
||||
return isEstimateEnabled;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { FieldError, FieldValues } from "react-hook-form";
|
||||
|
||||
/**
|
||||
* Get a nested error from a form's errors object
|
||||
* @param errors - The form's errors object
|
||||
* @param path - The path to the error
|
||||
* @returns The error or undefined if not found
|
||||
*/
|
||||
export const getNestedError = <T extends FieldValues>(errors: T, path: string): FieldError | undefined => {
|
||||
const keys = path.split(".");
|
||||
let current: unknown = errors;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === "object" && key in current) {
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the final value is a FieldError
|
||||
if (current && typeof current === "object" && "message" in current) {
|
||||
return current as FieldError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { VIEW_ACCESS_SPECIFIERS as VIEW_ACCESS_SPECIFIERS_CONSTANTS } from "@plane/constants";
|
||||
import { GlobeIcon, LockIcon } from "@plane/propel/icons";
|
||||
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import { EViewAccess } from "@plane/types";
|
||||
|
||||
const VIEW_ACCESS_ICONS = {
|
||||
[EViewAccess.PUBLIC]: GlobeIcon,
|
||||
[EViewAccess.PRIVATE]: LockIcon,
|
||||
};
|
||||
|
||||
export const VIEW_ACCESS_SPECIFIERS: {
|
||||
key: EViewAccess;
|
||||
i18n_label: string;
|
||||
icon: LucideIcon | React.FC<ISvgIcons>;
|
||||
}[] = VIEW_ACCESS_SPECIFIERS_CONSTANTS.map((option) => ({
|
||||
...option,
|
||||
icon: VIEW_ACCESS_ICONS[option.key as keyof typeof VIEW_ACCESS_ICONS],
|
||||
}));
|
||||
Reference in New Issue
Block a user